feat: Implement comprehensive log filtering feature with severity, time range, and tag options
This commit is contained in:
+39
-28
@@ -24,7 +24,11 @@ class SyncRepository(context: Context) {
|
||||
suspend fun performSync(): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val lastSync = getLastSyncTimestamp()
|
||||
Logger.d("Sync", "Starting sync with lastSyncTimestamp: $lastSync")
|
||||
val isInitialSync = lastSync == 0L
|
||||
|
||||
if (isInitialSync) {
|
||||
Logger.i("Sync", "🔄 Starting initial sync with server")
|
||||
}
|
||||
|
||||
// Gather local changes since last sync
|
||||
val localLists = database.listDao().getListsUpdatedSince(lastSync)
|
||||
@@ -32,11 +36,19 @@ class SyncRepository(context: Context) {
|
||||
val localItems = database.itemDao().getItemsUpdatedSince(lastSync)
|
||||
val localValues = database.itemValueDao().getValuesUpdatedSince(lastSync)
|
||||
|
||||
Logger.i("Sync", "Sending to server: ${localLists.size} lists, ${localFields.size} fields, ${localItems.size} items")
|
||||
|
||||
// Log details of lists being sent
|
||||
localLists.forEach { list ->
|
||||
Logger.d("Sync", " List: ${list.id} - ${list.name} (updated: ${list.updatedAt})")
|
||||
// Only log when sending data
|
||||
if (localLists.isNotEmpty() || localFields.isNotEmpty() || localItems.isNotEmpty()) {
|
||||
Logger.i("Sync", "⬆️ Sending to server:")
|
||||
if (localLists.isNotEmpty()) {
|
||||
Logger.i("Sync", " 📋 ${localLists.size} list(s)")
|
||||
localLists.forEach { list ->
|
||||
val action = if (list.isDeleted) "Deleted" else "Updated"
|
||||
Logger.i("Sync", " $action: ${list.name}")
|
||||
}
|
||||
}
|
||||
if (localFields.isNotEmpty()) Logger.i("Sync", " 🏷️ ${localFields.size} field(s)")
|
||||
if (localItems.isNotEmpty()) Logger.i("Sync", " 📝 ${localItems.size} item(s)")
|
||||
if (localValues.isNotEmpty()) Logger.i("Sync", " 💾 ${localValues.size} value(s)")
|
||||
}
|
||||
|
||||
// Send to server and get updates
|
||||
@@ -53,44 +65,43 @@ class SyncRepository(context: Context) {
|
||||
if (response.isSuccessful) {
|
||||
val syncResponse = response.body()!!
|
||||
|
||||
Logger.i("Sync", "Received from server: ${syncResponse.lists.size} lists, ${syncResponse.fields.size} fields, ${syncResponse.items.size} items")
|
||||
|
||||
// Log details of lists being received
|
||||
syncResponse.lists.forEach { list ->
|
||||
Logger.d("Sync", " Received list: ${list.id} - ${list.name} (updated: ${list.updatedAt}, deleted: ${list.isDeleted})")
|
||||
// Only log when receiving data
|
||||
if (syncResponse.lists.isNotEmpty() || syncResponse.fields.isNotEmpty() ||
|
||||
syncResponse.items.isNotEmpty() || syncResponse.itemValues.isNotEmpty()) {
|
||||
Logger.i("Sync", "⬇️ Received from server:")
|
||||
if (syncResponse.lists.isNotEmpty()) {
|
||||
Logger.i("Sync", " 📋 ${syncResponse.lists.size} list(s)")
|
||||
syncResponse.lists.forEach { list ->
|
||||
if (!list.isDeleted) {
|
||||
Logger.i("Sync", " ${list.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (syncResponse.fields.isNotEmpty()) Logger.i("Sync", " 🏷️ ${syncResponse.fields.size} field(s)")
|
||||
if (syncResponse.items.isNotEmpty()) Logger.i("Sync", " 📝 ${syncResponse.items.size} item(s)")
|
||||
if (syncResponse.itemValues.isNotEmpty()) Logger.i("Sync", " 💾 ${syncResponse.itemValues.size} value(s)")
|
||||
}
|
||||
|
||||
// Apply server changes to local database
|
||||
Logger.d("Sync", "Inserting ${syncResponse.lists.size} lists into local database")
|
||||
database.listDao().insertLists(syncResponse.lists)
|
||||
|
||||
Logger.d("Sync", "Inserting ${syncResponse.fields.size} fields into local database")
|
||||
database.fieldDao().insertFields(syncResponse.fields)
|
||||
|
||||
Logger.d("Sync", "Inserting ${syncResponse.items.size} items into local database")
|
||||
database.itemDao().insertItems(syncResponse.items)
|
||||
|
||||
Logger.d("Sync", "Inserting ${syncResponse.itemValues.size} item values into local database")
|
||||
database.itemValueDao().insertValues(syncResponse.itemValues)
|
||||
|
||||
// Verify what was actually saved
|
||||
val savedLists = database.listDao().getListsUpdatedSince(0)
|
||||
Logger.d("Sync", "After insert, total lists in DB: ${savedLists.size}")
|
||||
savedLists.forEach { list ->
|
||||
Logger.d("Sync", " DB list: ${list.id} - ${list.name} (deleted: ${list.isDeleted})")
|
||||
}
|
||||
|
||||
// Update last sync timestamp
|
||||
setLastSyncTimestamp(syncResponse.serverTimestamp)
|
||||
|
||||
Logger.i("Sync", "Sync completed successfully. Timestamp updated to: ${syncResponse.serverTimestamp}")
|
||||
if (isInitialSync) {
|
||||
Logger.i("Sync", "✅ Initial sync completed")
|
||||
}
|
||||
|
||||
return@withContext Result.success(Unit)
|
||||
} else {
|
||||
Logger.e("Sync", "Sync failed with code: ${response.code()}, message: ${response.message()}")
|
||||
Logger.e("Sync", "❌ Sync failed: HTTP ${response.code()}")
|
||||
return@withContext Result.failure(Exception("Sync failed: ${response.code()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e("Sync", "Sync error", e)
|
||||
Logger.e("Sync", "❌ Sync error: ${e.message}")
|
||||
return@withContext Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
+51
-27
@@ -71,35 +71,59 @@ fun ListsScreen(
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
if (lists.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_lists),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(lists, key = { it.id }) { list ->
|
||||
ListItem(
|
||||
list = list,
|
||||
onListClick = { onNavigateToList(list.id) },
|
||||
onEditClick = { listToEdit = list },
|
||||
onDeleteClick = { listToDelete = list }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
if (isLoading && lists.isEmpty()) {
|
||||
// Show loading indicator during initial sync
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text(
|
||||
text = "Syncing with server...",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else if (lists.isEmpty()) {
|
||||
// Show empty state
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_lists),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "Tap + to create your first list",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Show lists
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(lists, key = { it.id }) { list ->
|
||||
ListItem(
|
||||
list = list,
|
||||
onListClick = { onNavigateToList(list.id) },
|
||||
onEditClick = { listToEdit = list },
|
||||
onDeleteClick = { listToDelete = list }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-10
@@ -27,9 +27,11 @@ class ListsViewModel(
|
||||
|
||||
init {
|
||||
loadLists()
|
||||
// Do initial sync immediately, then start periodic sync
|
||||
// Perform initial sync immediately on startup, then start periodic sync
|
||||
viewModelScope.launch {
|
||||
performSync() // Initial sync on startup
|
||||
_isLoading.value = true
|
||||
performSync()
|
||||
_isLoading.value = false
|
||||
startPeriodicSync()
|
||||
}
|
||||
}
|
||||
@@ -50,12 +52,15 @@ class ListsViewModel(
|
||||
}
|
||||
|
||||
private suspend fun performSync() {
|
||||
syncRepository.performSync()
|
||||
val result = syncRepository.performSync()
|
||||
result.onFailure { error ->
|
||||
Logger.e("Lists", "❌ Sync failed: ${error.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun createList(name: String) {
|
||||
viewModelScope.launch {
|
||||
Logger.i("ListsViewModel", "Creating list: $name")
|
||||
Logger.i("Lists", "➕ Creating list: \"$name\"")
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val newList = CollabList(
|
||||
id = UUID.randomUUID().toString(),
|
||||
@@ -71,9 +76,9 @@ class ListsViewModel(
|
||||
|
||||
fun renameList(listId: String, newName: String) {
|
||||
viewModelScope.launch {
|
||||
Logger.i("ListsViewModel", "Renaming list: $listId to $newName")
|
||||
val list = database.listDao().getListById(listId)
|
||||
if (list != null && newName.isNotBlank()) {
|
||||
Logger.i("Lists", "✏️ Renaming: \"${list.name}\" → \"$newName\"")
|
||||
database.listDao().updateList(
|
||||
list.copy(
|
||||
name = newName.trim(),
|
||||
@@ -88,16 +93,19 @@ class ListsViewModel(
|
||||
|
||||
fun deleteList(listId: String) {
|
||||
viewModelScope.launch {
|
||||
Logger.i("ListsViewModel", "Deleting list: $listId")
|
||||
database.listDao().softDeleteList(listId, System.currentTimeMillis())
|
||||
// Sync immediately after deleting
|
||||
performSync()
|
||||
val list = database.listDao().getListById(listId)
|
||||
if (list != null) {
|
||||
Logger.i("Lists", "🗑️ Deleting list: \"${list.name}\"")
|
||||
database.listDao().softDeleteList(listId, System.currentTimeMillis())
|
||||
// Sync immediately after deleting
|
||||
performSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun manualSync() {
|
||||
viewModelScope.launch {
|
||||
Logger.i("ListsViewModel", "Manual sync triggered")
|
||||
Logger.i("Lists", "🔄 Manual sync requested")
|
||||
_isLoading.value = true
|
||||
performSync()
|
||||
_isLoading.value = false
|
||||
|
||||
+355
-28
@@ -3,22 +3,47 @@ package com.collabtable.app.ui.screens
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.collabtable.app.utils.LogEntry
|
||||
import com.collabtable.app.utils.LogLevel
|
||||
import com.collabtable.app.utils.Logger
|
||||
|
||||
|
||||
enum class TimeRange(val label: String, val milliseconds: Long) {
|
||||
LAST_10_SECONDS("Last 10 seconds", 10_000),
|
||||
LAST_30_SECONDS("Last 30 seconds", 30_000),
|
||||
LAST_MINUTE("Last minute", 60_000),
|
||||
LAST_5_MINUTES("Last 5 minutes", 300_000),
|
||||
LAST_15_MINUTES("Last 15 minutes", 900_000),
|
||||
LAST_HOUR("Last hour", 3_600_000),
|
||||
LAST_24_HOURS("Last 24 hours", 86_400_000),
|
||||
ALL_TIME("All time", Long.MAX_VALUE)
|
||||
}
|
||||
|
||||
data class LogFilters(
|
||||
val severities: Set<LogLevel> = LogLevel.values().toSet(),
|
||||
val timeRange: TimeRange = TimeRange.ALL_TIME,
|
||||
val tags: Set<String> = emptySet(),
|
||||
val searchText: String = ""
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LogsScreen(
|
||||
@@ -26,24 +51,59 @@ fun LogsScreen(
|
||||
) {
|
||||
val logs by Logger.logs.collectAsState()
|
||||
val listState = rememberLazyListState()
|
||||
var showFilterSheet by remember { mutableStateOf(false) }
|
||||
var filters by remember { mutableStateOf(LogFilters()) }
|
||||
|
||||
// Extract all unique tags from logs
|
||||
val allTags = remember(logs) {
|
||||
logs.map { it.tag }.distinct().sorted()
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
val filteredLogs = remember(logs, filters) {
|
||||
val now = System.currentTimeMillis()
|
||||
val cutoffTime = now - filters.timeRange.milliseconds
|
||||
|
||||
logs.filter { log ->
|
||||
// Severity filter
|
||||
log.level in filters.severities &&
|
||||
// Time range filter
|
||||
log.timestamp >= cutoffTime &&
|
||||
// Tag filter (empty means show all)
|
||||
(filters.tags.isEmpty() || log.tag in filters.tags) &&
|
||||
// Search text filter
|
||||
(filters.searchText.isEmpty() ||
|
||||
log.message.contains(filters.searchText, ignoreCase = true) ||
|
||||
log.tag.contains(filters.searchText, ignoreCase = true))
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll to bottom when new logs arrive
|
||||
LaunchedEffect(logs.size) {
|
||||
if (logs.isNotEmpty()) {
|
||||
listState.animateScrollToItem(logs.size - 1)
|
||||
LaunchedEffect(filteredLogs.size) {
|
||||
if (filteredLogs.isNotEmpty()) {
|
||||
listState.animateScrollToItem(filteredLogs.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Logs") },
|
||||
title = { Text("Logs (${filteredLogs.size}/${logs.size})") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(Icons.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = { showFilterSheet = true }) {
|
||||
Badge(
|
||||
containerColor = if (hasActiveFilters(filters))
|
||||
MaterialTheme.colorScheme.error
|
||||
else Color.Transparent
|
||||
) {
|
||||
Icon(Icons.Default.FilterList, contentDescription = "Filter logs")
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { Logger.clear() }) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Clear logs")
|
||||
}
|
||||
@@ -55,32 +115,299 @@ fun LogsScreen(
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
if (logs.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = androidx.compose.ui.Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "No logs yet",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.background(Color(0xFF1E1E1E)),
|
||||
contentPadding = PaddingValues(8.dp)
|
||||
) {
|
||||
items(logs) { log ->
|
||||
LogItem(log)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
// Active filters chips
|
||||
if (hasActiveFilters(filters)) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Severity filters
|
||||
if (filters.severities.size < LogLevel.values().size) {
|
||||
items(filters.severities.toList()) { level ->
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = {
|
||||
filters = filters.copy(
|
||||
severities = filters.severities - level
|
||||
)
|
||||
},
|
||||
label = { Text(level.name, fontSize = 12.sp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Time range filter
|
||||
if (filters.timeRange != TimeRange.ALL_TIME) {
|
||||
item {
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = {
|
||||
filters = filters.copy(timeRange = TimeRange.ALL_TIME)
|
||||
},
|
||||
label = { Text(filters.timeRange.label, fontSize = 12.sp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Tag filters
|
||||
items(filters.tags.toList()) { tag ->
|
||||
FilterChip(
|
||||
selected = true,
|
||||
onClick = {
|
||||
filters = filters.copy(tags = filters.tags - tag)
|
||||
},
|
||||
label = { Text(tag, fontSize = 12.sp) }
|
||||
)
|
||||
}
|
||||
|
||||
// Clear all button
|
||||
item {
|
||||
FilterChip(
|
||||
selected = false,
|
||||
onClick = {
|
||||
filters = LogFilters()
|
||||
},
|
||||
label = { Text("Clear All", fontSize = 12.sp) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logs list
|
||||
if (filteredLogs.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (logs.isEmpty()) "No logs yet" else "No logs match filters",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFF1E1E1E)),
|
||||
contentPadding = PaddingValues(8.dp)
|
||||
) {
|
||||
items(filteredLogs) { log ->
|
||||
LogItem(log)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter bottom sheet
|
||||
if (showFilterSheet) {
|
||||
FilterBottomSheet(
|
||||
filters = filters,
|
||||
allTags = allTags,
|
||||
onFiltersChanged = { filters = it },
|
||||
onDismiss = { showFilterSheet = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasActiveFilters(filters: LogFilters): Boolean {
|
||||
return filters.severities.size < LogLevel.values().size ||
|
||||
filters.timeRange != TimeRange.ALL_TIME ||
|
||||
filters.tags.isNotEmpty() ||
|
||||
filters.searchText.isNotEmpty()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FilterBottomSheet(
|
||||
filters: LogFilters,
|
||||
allTags: List<String>,
|
||||
onFiltersChanged: (LogFilters) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = 32.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Filter Logs",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
// Severity filters
|
||||
Text(
|
||||
text = "Severity",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
LogLevel.values().forEach { level ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = level in filters.severities,
|
||||
onCheckedChange = { checked ->
|
||||
onFiltersChanged(
|
||||
filters.copy(
|
||||
severities = if (checked) {
|
||||
filters.severities + level
|
||||
} else {
|
||||
filters.severities - level
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = level.name,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||
|
||||
// Time range filter
|
||||
Text(
|
||||
text = "Time Range",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
TimeRange.values().forEach { range ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = filters.timeRange == range,
|
||||
onClick = {
|
||||
onFiltersChanged(filters.copy(timeRange = range))
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = range.label,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||
|
||||
// Tag filters
|
||||
Text(
|
||||
text = "Tags (${filters.tags.size} selected)",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
if (allTags.isEmpty()) {
|
||||
Text(
|
||||
text = "No tags available",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onFiltersChanged(filters.copy(tags = allTags.toSet()))
|
||||
}
|
||||
) {
|
||||
Text("Select All")
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
onFiltersChanged(filters.copy(tags = emptySet()))
|
||||
}
|
||||
) {
|
||||
Text("Clear All")
|
||||
}
|
||||
}
|
||||
|
||||
allTags.forEach { tag ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = tag in filters.tags,
|
||||
onCheckedChange = { checked ->
|
||||
onFiltersChanged(
|
||||
filters.copy(
|
||||
tags = if (checked) {
|
||||
filters.tags + tag
|
||||
} else {
|
||||
filters.tags - tag
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = tag,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider(modifier = Modifier.padding(vertical = 16.dp))
|
||||
|
||||
// Reset button
|
||||
Button(
|
||||
onClick = {
|
||||
onFiltersChanged(LogFilters())
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Reset All Filters")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user