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