git.iko.soy/parseable-android
on 3b7ea33 help sign in
255 lines 9.4 kB history raw
1package com.parseable.android.ui.screens.logviewer
3import androidx.compose.foundation.layout.*
4import androidx.compose.foundation.rememberScrollState
5import androidx.compose.foundation.verticalScroll
6import androidx.compose.material.icons.Icons
7import androidx.compose.material.icons.automirrored.filled.ArrowBack
8import androidx.compose.material.icons.filled.*
9import androidx.compose.material3.*
10import androidx.compose.runtime.*
11import androidx.compose.ui.Alignment
12import androidx.compose.ui.Modifier
13import androidx.compose.ui.text.font.FontFamily
14import androidx.compose.ui.text.font.FontWeight
15import androidx.compose.ui.unit.dp
16import androidx.hilt.navigation.compose.hiltViewModel
17import androidx.lifecycle.Lifecycle
18import androidx.lifecycle.compose.LocalLifecycleOwner
19import androidx.lifecycle.compose.collectAsStateWithLifecycle
20import androidx.lifecycle.repeatOnLifecycle
21import com.parseable.android.data.formatBytes
22import kotlinx.serialization.json.Json
23import kotlinx.serialization.json.JsonObject
25private val prettyJson = Json { prettyPrint = true }
27@OptIn(ExperimentalMaterial3Api::class)
28@Composable
29fun StreamInfoScreen(
30 streamName: String,
31 onBack: () -> Unit,
32 onStreamDeleted: () -> Unit,
33 viewModel: StreamInfoViewModel = hiltViewModel(),
34) {
35 val state by viewModel.state.collectAsStateWithLifecycle()
36 var showDeleteConfirmation by remember { mutableStateOf(false) }
38 val lifecycleOwner = LocalLifecycleOwner.current
39 LaunchedEffect(streamName, lifecycleOwner) {
40 lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
41 viewModel.load(streamName)
42 }
43 }
45 LaunchedEffect(state.deleteSuccess) {
46 if (state.deleteSuccess) {
47 viewModel.consumeDeleteSuccess()
48 onStreamDeleted()
49 }
50 }
52 Scaffold(
53 topBar = {
54 TopAppBar(
55 title = { Text("Stream: $streamName") },
56 navigationIcon = {
57 IconButton(onClick = onBack) {
58 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
59 }
60 },
61 actions = {
62 IconButton(
63 onClick = { showDeleteConfirmation = true },
64 enabled = !state.isDeleting && !state.isLoading,
65 ) {
66 Icon(
67 Icons.Filled.Delete,
68 contentDescription = "Delete stream",
69 tint = MaterialTheme.colorScheme.error,
70 )
71 }
72 },
73 )
74 },
75 ) { padding ->
76 if (state.isLoading) {
77 Box(
78 modifier = Modifier
79 .fillMaxSize()
80 .padding(padding),
81 contentAlignment = Alignment.Center,
82 ) {
83 CircularProgressIndicator()
84 }
85 } else {
86 Column(
87 modifier = Modifier
88 .fillMaxSize()
89 .padding(padding)
90 .verticalScroll(rememberScrollState())
91 .padding(16.dp),
92 verticalArrangement = Arrangement.spacedBy(16.dp),
93 ) {
94 // Stats section
95 state.stats?.let { stats ->
96 InfoSection(title = "Statistics") {
97 InfoRow("Event Count", stats.ingestion?.count?.toString() ?: stats.ingestion?.lifetimeCount?.toString() ?: "N/A")
98 InfoRow("Ingestion Size", formatBytes(stats.ingestion?.size) ?: formatBytes(stats.ingestion?.lifetimeSize) ?: "N/A")
99 InfoRow("Storage Size", formatBytes(stats.storage?.size) ?: formatBytes(stats.storage?.lifetimeSize) ?: "N/A")
100 if (stats.ingestion?.lifetimeCount != null) {
101 InfoRow("Lifetime Events", stats.ingestion.lifetimeCount.toString())
102 }
103 if (stats.ingestion?.deletedCount != null) {
104 InfoRow("Deleted Events", stats.ingestion.deletedCount.toString())
105 }
106 }
107 }
109 // Schema section
110 if (state.schema.isNotEmpty()) {
111 InfoSection(title = "Schema (${state.schema.size} fields)") {
112 state.schema.forEach { field ->
113 InfoRow(
114 field.name,
115 field.dataType?.toString()?.trim('"') ?: "Unknown",
116 )
117 }
118 }
119 } else if (state.schemaFailed) {
120 InfoSection(title = "Schema") {
121 Text(
122 text = "Schema unavailable",
123 style = MaterialTheme.typography.bodyMedium,
124 color = MaterialTheme.colorScheme.onSurfaceVariant,
125 )
126 }
127 }
129 // Retention section
130 if (state.retention.isNotEmpty()) {
131 InfoSection(title = "Retention") {
132 state.retention.forEach { r ->
133 InfoRow("Duration", r.duration ?: "N/A")
134 InfoRow("Action", r.action ?: "N/A")
135 if (r.description != null) {
136 InfoRow("Description", r.description)
137 }
138 }
139 }
140 }
142 // Raw info section
143 state.rawInfo?.let { rawInfo ->
144 val prettyInfo = remember(rawInfo) {
145 prettyJson.encodeToString(JsonObject.serializer(), rawInfo)
146 }
147 InfoSection(title = "Stream Info") {
148 Text(
149 text = prettyInfo,
150 style = MaterialTheme.typography.bodySmall,
151 fontFamily = FontFamily.Monospace,
152 )
153 }
154 }
156 state.error?.let { errorText ->
157 Card(
158 colors = CardDefaults.cardColors(
159 containerColor = MaterialTheme.colorScheme.errorContainer,
160 ),
161 ) {
162 Text(
163 text = errorText,
164 modifier = Modifier.padding(16.dp),
165 color = MaterialTheme.colorScheme.onErrorContainer,
166 )
167 }
168 }
169 }
170 }
171 }
173 if (showDeleteConfirmation) {
174 AlertDialog(
175 onDismissRequest = { showDeleteConfirmation = false },
176 icon = {
177 Icon(
178 Icons.Filled.Warning,
179 contentDescription = "Warning",
180 tint = MaterialTheme.colorScheme.error,
181 )
182 },
183 title = { Text("Delete Stream") },
184 text = {
185 Text(
186 "Are you sure you want to delete \"$streamName\"? " +
187 "This will permanently remove all log data and cannot be undone."
188 )
189 },
190 confirmButton = {
191 Button(
192 onClick = {
193 showDeleteConfirmation = false
194 viewModel.deleteStream()
195 },
196 colors = ButtonDefaults.buttonColors(
197 containerColor = MaterialTheme.colorScheme.error,
198 ),
199 ) {
200 Text("Delete")
201 }
202 },
203 dismissButton = {
204 TextButton(onClick = { showDeleteConfirmation = false }) {
205 Text("Cancel")
206 }
207 },
208 )
209 }
212@Composable
213fun InfoSection(
214 title: String,
215 content: @Composable ColumnScope.() -> Unit,
216) {
217 Card(
218 modifier = Modifier.fillMaxWidth(),
219 elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
220 ) {
221 Column(modifier = Modifier.padding(16.dp)) {
222 Text(
223 text = title,
224 style = MaterialTheme.typography.titleMedium,
225 fontWeight = FontWeight.Bold,
226 )
227 Spacer(modifier = Modifier.height(12.dp))
228 content()
229 }
230 }
233@Composable
234fun InfoRow(label: String, value: String) {
235 Row(
236 modifier = Modifier
237 .fillMaxWidth()
238 .padding(vertical = 4.dp),
239 horizontalArrangement = Arrangement.SpaceBetween,
240 ) {
241 Text(
242 text = label,
243 style = MaterialTheme.typography.bodyMedium,
244 color = MaterialTheme.colorScheme.onSurfaceVariant,
245 modifier = Modifier.weight(1f),
246 )
247 Text(
248 text = value,
249 style = MaterialTheme.typography.bodyMedium,
250 fontWeight = FontWeight.Medium,
251 modifier = Modifier.weight(1f),
252 fontFamily = FontFamily.Monospace,
253 )
254 }