diff --git a/src/main/kotlin/models/LogItem.kt b/src/main/kotlin/models/LogItem.kt index e3a62f7..cc30329 100644 --- a/src/main/kotlin/models/LogItem.kt +++ b/src/main/kotlin/models/LogItem.kt @@ -17,6 +17,7 @@ data class LogItem( companion object { private const val serialVersionUID = 1L val EVENT_NAME = attribute("eventName", LogItem::eventName) + val ATTR_TIME = attribute("localTime", LogItem::localTime) fun noContent(msg: String) = LogItem(SourceInternalContent, "No Logs", internalContent = NoLogsContent(msg)) fun errorContent(error: String) = LogItem(SourceInternalContent, "Error", internalContent = ErrorContent(error)) diff --git a/src/main/kotlin/models/ParameterFormats.kt b/src/main/kotlin/models/ParameterFormats.kt index aa15037..0b6ed35 100644 --- a/src/main/kotlin/models/ParameterFormats.kt +++ b/src/main/kotlin/models/ParameterFormats.kt @@ -1,8 +1,21 @@ package models -sealed class ParameterFormats(val key: String, val text: String) -object FormatJsonPretty : ParameterFormats("jsonpretty", "Json with pretty print") -object FormatJsonCompact : ParameterFormats("json", "Compact Json") -object FormatYaml : ParameterFormats("yaml", "Yaml") +sealed class ParameterFormats(val key: String, val text: String, val subText: String) +object FormatJsonPretty : + ParameterFormats( + "jsonpretty", "Json with pretty print", + "Long string with multiple lines with json format" + ) + +object FormatJsonCompact : + ParameterFormats( + "json", "Compact Json", + "Small json string without any formatting in a single line" + ) + +object FormatYaml : ParameterFormats( + "yaml", "Yaml", + "Human friendly formatted string with indentation as formatting and multi-lines" +) val DefaultFormats = listOf(FormatJsonPretty, FormatJsonCompact, FormatYaml) diff --git a/src/main/kotlin/models/SocialIcons.kt b/src/main/kotlin/models/SocialIcons.kt new file mode 100644 index 0000000..f316fbd --- /dev/null +++ b/src/main/kotlin/models/SocialIcons.kt @@ -0,0 +1,24 @@ +package models + +enum class SocialIcons(val icon: String, val url: String) { + Twitter( + "icons/social/social_twitter.svg", + "https://twitter.com/Aman22Kapoor" + ), + Github( + "icons/social/social_github.svg", + "https://github.com/amank22/LogVue" + ), + GithubIssues( + "icons/social/social_github.svg", + "https://github.com/amank22/LogVue/issues/new" + ), + Linkedin("icons/social/social_linkedIn.svg", "https://www.linkedin.com/in/amank22/"), + Email("icons/ico-email.svg", "mailto://kapoor.aman22@gmail.com"), + ; + + companion object { + val DefaultIcons = listOf(Twitter, Github, Linkedin, Email) + } + //object SocialFacebook : SocialIcons("icons/social/social_facebook.svg", "") +} \ No newline at end of file diff --git a/src/main/kotlin/processor/MainProcessor.kt b/src/main/kotlin/processor/MainProcessor.kt index d0c86ff..dc23438 100644 --- a/src/main/kotlin/processor/MainProcessor.kt +++ b/src/main/kotlin/processor/MainProcessor.kt @@ -16,6 +16,7 @@ import utils.Helpers import utils.failureOrNull import utils.getOrNull + class MainProcessor { private val streamer = AndroidLogStreamer() @@ -118,8 +119,8 @@ class MainProcessor { logItemStream.collect { list -> val filterResult = if (fQuery.isNullOrBlank() || fQuery == QUERY_PREFIX) { - registerPropertiesInParser(list, parser) - list + registerPropertiesInParser(list, parser, indexedCollection) + list.sortedBy { it.localTime } } else { if (!isNewStream) { indexedCollection.clear() diff --git a/src/main/kotlin/processor/ParameterizedAttribute.kt b/src/main/kotlin/processor/ParameterizedAttribute.kt index 1dcd177..8f9cbfa 100644 --- a/src/main/kotlin/processor/ParameterizedAttribute.kt +++ b/src/main/kotlin/processor/ParameterizedAttribute.kt @@ -11,9 +11,13 @@ class ParameterizedAttribute(private val mapKey: String, private val clazz: C override fun getValue(logItem: LogItem, queryOptions: QueryOptions?): T? { val result = getNestedValue(logItem) if (result == null || attributeType.isAssignableFrom(clazz)) { - return clazz.cast(result) + try { + return clazz.cast(result) + } catch (cl: ClassCastException) { + //ignore + } } - throw ClassCastException("Cannot cast " + result.javaClass.name + " to " + attributeType.name + " for map key: " + mapKey) + throw ClassCastException("Cannot cast " + result?.javaClass?.name + " to " + attributeType.name + " for map key: " + mapKey) } private fun getNestedValue(logItem: LogItem): Any? { diff --git a/src/main/kotlin/processor/QueryHelper.kt b/src/main/kotlin/processor/QueryHelper.kt index e4ebd12..2ec2838 100644 --- a/src/main/kotlin/processor/QueryHelper.kt +++ b/src/main/kotlin/processor/QueryHelper.kt @@ -4,6 +4,7 @@ import com.googlecode.cqengine.ConcurrentIndexedCollection import com.googlecode.cqengine.ObjectLockingIndexedCollection import com.googlecode.cqengine.attribute.support.FunctionalSimpleAttribute import com.googlecode.cqengine.index.hash.HashIndex +import com.googlecode.cqengine.index.navigable.NavigableIndex import com.googlecode.cqengine.index.radix.RadixTreeIndex import com.googlecode.cqengine.index.radixinverted.InvertedRadixTreeIndex import com.googlecode.cqengine.index.radixreversed.ReversedRadixTreeIndex @@ -26,12 +27,15 @@ fun queryCollection(): ConcurrentIndexedCollection { addIndex(RadixTreeIndex.onAttribute(LogItem.EVENT_NAME)) addIndex(InvertedRadixTreeIndex.onAttribute(LogItem.EVENT_NAME)) addIndex(ReversedRadixTreeIndex.onAttribute(LogItem.EVENT_NAME)) + addIndex(HashIndex.onAttribute(LogItem.ATTR_TIME)) + addIndex(NavigableIndex.onAttribute(LogItem.ATTR_TIME)) } } fun sqlParser(): SQLParser { return SQLParser.forPojo(LogItem::class.java).apply { registerAttribute(LogItem.EVENT_NAME) + registerAttribute(LogItem.ATTR_TIME) } } @@ -43,29 +47,26 @@ fun filterLogs( filterQuery: String? ): List { indexedCollection.addAll(list) - registerPropertiesInParser(list, parser) + registerPropertiesInParser(list, parser, indexedCollection) println("Filtering logs") val filterResult = measureTimedValue { parser.retrieve(indexedCollection, filterQuery) } if (filterResult.duration.inWholeSeconds > 2) { AppLog.d( - "filtering", "Time taken: ${filterResult.duration} , " + - "Retrieval Cost: ${filterResult.value.retrievalCost}" + "filtering", + "Time taken: ${filterResult.duration} , " + "Retrieval Cost: ${filterResult.value.retrievalCost}" ) } - val toList = filterResult.value.toList() - AppLog.d("filterResult", filterResult.value.toList().toString()) - return toList.sortedBy { it.localTime } + return filterResult.value.toList() } fun registerPropertiesInParser( - list: List, - parser: SQLParser + list: List, parser: SQLParser, indexedCollection: ConcurrentIndexedCollection ) { val propertySet = hashSetOf() list.forEach { - registerMapPropertiesInParser(it.properties, propertySet, parser) + registerMapPropertiesInParser(it.properties, propertySet, parser, indexedCollection) } } @@ -73,20 +74,19 @@ private fun registerMapPropertiesInParser( properties: Map, propertySet: HashSet, parser: SQLParser, + indexedCollection: ConcurrentIndexedCollection, parentKey: String = "" ) { properties.forEach { (k, v) -> if (!propertySet.contains(k)) { - val att: ParameterizedAttribute = ParameterizedAttribute("$parentKey$k", v.javaClass) if (v is Map<*, *>) { - @Suppress("UNCHECKED_CAST") - registerMapPropertiesInParser( - v as Map, propertySet, - parser, "$parentKey$k." + @Suppress("UNCHECKED_CAST") registerMapPropertiesInParser( + v as Map, propertySet, parser, indexedCollection, "$parentKey$k." ) } else { // println("Attribute : ${att.attributeName} with first value = $v and v class = ${v.javaClass.name}") - parser.registerAttribute(att) + val key = "$parentKey$k" + registerAndAddIndex(v, key, parser, indexedCollection) propertySet.add(k) } } else { @@ -94,3 +94,58 @@ private fun registerMapPropertiesInParser( } } } + +fun registerAndAddIndex( + value: Any, key: String, + parser: SQLParser, + indexedCollection: ConcurrentIndexedCollection +) { + with(indexedCollection) { + if (key.isBlank()) { + addGenericAttribute(key, value, parser) + } + when (value) { + is String -> { + if (value.isBlank()) { + addGenericAttribute(key, value, parser) + return + } + val att: ParameterizedAttribute = ParameterizedAttribute(key, value.javaClass) + try { + addIndex(HashIndex.onAttribute(att)) + addIndex(NavigableIndex.onAttribute(att)) + addIndex(RadixTreeIndex.onAttribute(att)) + addIndex(InvertedRadixTreeIndex.onAttribute(att)) + addIndex(ReversedRadixTreeIndex.onAttribute(att)) + parser.registerAttribute(att) + } catch (e: Exception) { + // TODO: Make sure to log these exceptions somewhere so that we can analyse them + addGenericAttribute(key, value, parser) + } + } + is Comparable<*> -> { + try { + val att: ParameterizedAttribute> = ParameterizedAttribute(key, value.javaClass) + addIndex(HashIndex.onAttribute(att)) + addIndex(NavigableIndex.onAttribute(att)) + parser.registerAttribute(att) + } catch (e: Exception) { + addGenericAttribute(key, value, parser) + } + } + else -> { + addGenericAttribute(key, value, parser) + } + } + } +} + +private fun ConcurrentIndexedCollection.addGenericAttribute( + key: String, + value: Any, + parser: SQLParser +) { + val att: ParameterizedAttribute = ParameterizedAttribute(key, value.javaClass) + addIndex(HashIndex.onAttribute(att)) + parser.registerAttribute(att) +} diff --git a/src/main/kotlin/ui/components/BasicComponents.kt b/src/main/kotlin/ui/components/BasicComponents.kt index 89043e1..5ad5006 100644 --- a/src/main/kotlin/ui/components/BasicComponents.kt +++ b/src/main/kotlin/ui/components/BasicComponents.kt @@ -1,11 +1,15 @@ package ui.components +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.ClickableText import androidx.compose.material.Icon +import androidx.compose.material.RadioButton import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.AnnotatedString @@ -69,10 +73,7 @@ fun SwitchItem( @Composable fun SimpleListItem( - title: String?, - modifier: Modifier = Modifier, - subTitle: String? = null, - icon: Painter? = null + title: String?, modifier: Modifier = Modifier, subTitle: String? = null, icon: Painter? = null ) { Row(modifier, horizontalArrangement = Arrangement.spacedBy(16.dp)) { if (icon != null) { @@ -108,8 +109,7 @@ fun SimpleListItem( Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { if (title != null) { Text( - title.text, style = CustomTheme.typography.headings.h6Medium, - lineHeight = 19.sp + title.text, style = CustomTheme.typography.headings.h6Medium, lineHeight = 19.sp ) } if (subTitle != null) { @@ -142,8 +142,40 @@ fun ClickableListItem( } if (subTitle != null) { ClickableText( + subTitle, style = CustomTheme.typography.headings.semiText, onClick = onClick + ) + } + } + } +} + +@Composable +fun MultiLineRadioButton( + selected: Boolean, + title: String?, + modifier: Modifier = Modifier, + subTitle: String? = null, + spacing: Dp = 8.dp, + onClick: () -> Unit +) { + Row( + modifier.clickable(MutableInteractionSource(), null, onClick = { onClick() }), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(spacing) + ) { + RadioButton(selected, onClick) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) { + if (title != null) { + Text( + title, style = CustomTheme.typography.headings.h6Medium, lineHeight = 19.sp + ) + } + if (subTitle != null) { + Text( subTitle, - style = CustomTheme.typography.headings.semiText, onClick = onClick + style = CustomTheme.typography.headings.caption, + color = CustomTheme.colors.mediumContrast, + lineHeight = 19.sp ) } } diff --git a/src/main/kotlin/ui/components/ExportDialog.kt b/src/main/kotlin/ui/components/ExportDialog.kt index 6af76bf..3ed60f7 100644 --- a/src/main/kotlin/ui/components/ExportDialog.kt +++ b/src/main/kotlin/ui/components/ExportDialog.kt @@ -1,11 +1,10 @@ package ui.components -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.capitalize @@ -16,7 +15,6 @@ import kotlinx.coroutines.launch import models.* import processor.Exporter import storage.Db -import ui.CustomTheme import utils.Helpers import java.nio.file.Path import kotlin.io.path.absolutePathString @@ -27,45 +25,45 @@ fun ExportDialog( logItems: List, onDismissRequest: () -> Unit ) { - StyledCustomVerticalDialog(onDismissRequest) { - Column(Modifier.fillMaxHeight().padding(16.dp)) { - var exportFilteredLogs by remember { mutableStateOf(true) } - var isFileSelectorOpen by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() - Text("Export Session", style = CustomTheme.typography.headings.h2) - Spacer(Modifier.height(24.dp)) - SelectCheckBox(exportFilteredLogs, "Export filtered logs") { - exportFilteredLogs = !exportFilteredLogs - } - Spacer(Modifier.height(16.dp)) - Text("Parameter formats:") - Spacer(Modifier.height(8.dp)) - var selectedFormat by remember { mutableStateOf(FormatJsonPretty) } - ParameterFormats(selectedFormat) { - selectedFormat = it - } - Spacer(Modifier.height(24.dp)) - Button({ - isFileSelectorOpen = true - }) { - Icon(painterResource("icons/ico-share.svg"), "Export session") - Text("Export") - } - if (isFileSelectorOpen) { - val fileName = sessionInfo.description.replace(" ", "_").capitalize(Locale.current) - val appended = if (selectedFormat == FormatYaml) { - "_yaml" - } else "_json" - ExportFile("$fileName$appended.txt") { path -> - isFileSelectorOpen = false - scope.launch(Dispatchers.IO) { - val logs = getListForExport(exportFilteredLogs, logItems) - Exporter.exportList(sessionInfo, logs, path, selectedFormat) - Db.configs["lastExportFolder"] = path.parent.absolutePathString() - Helpers.openFileExplorer(path.parent) - Helpers.openFileExplorer(path) - onDismissRequest() - } + SimpleVerticalDialog("Export Session", onDismissRequest, PaddingValues()) { + var exportFilteredLogs by remember { mutableStateOf(true) } + var isFileSelectorOpen by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + SwitchItem( + exportFilteredLogs, "Export filtered logs", Modifier.fillMaxWidth() + .padding(16.dp), + "Only filtered logs or full session including all the logs", + painterResource("icons/ico_filter.svg") + ) { + exportFilteredLogs = it + } + Text("Parameter formats:", Modifier.padding(horizontal = 16.dp)) + Spacer(Modifier.height(8.dp)) + var selectedFormat by remember { mutableStateOf(FormatJsonPretty) } + ParameterFormats(selectedFormat, modifier = Modifier.padding(horizontal = 6.dp)) { + selectedFormat = it + } + Spacer(Modifier.height(24.dp)) + Button({ + isFileSelectorOpen = true + }, Modifier.padding(horizontal = 16.dp)) { + Icon(painterResource("icons/ico-share.svg"), "Export session") + Text("Export") + } + if (isFileSelectorOpen) { + val fileName = sessionInfo.description.replace(" ", "_").capitalize(Locale.current) + val appended = if (selectedFormat == FormatYaml) { + "_yaml" + } else "_json" + ExportFile("$fileName$appended.txt") { path -> + isFileSelectorOpen = false + scope.launch(Dispatchers.IO) { + val logs = getListForExport(exportFilteredLogs, logItems) + Exporter.exportList(sessionInfo, logs, path, selectedFormat) + Db.configs["lastExportFolder"] = path.parent.absolutePathString() + Helpers.openFileExplorer(path.parent) + Helpers.openFileExplorer(path) + onDismissRequest() } } } @@ -98,44 +96,16 @@ private fun ParameterFormats( modifier: Modifier = Modifier, onSelected: (format: ParameterFormats) -> Unit ) { - Column(modifier) { + Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { formats.forEach { - SelectRadioButton(selected == it, it.text) { + MultiLineRadioButton( + selected = selected == it, title = it.text, + modifier = Modifier.fillMaxWidth(), + subTitle = it.subText, + spacing = 0.dp + ) { onSelected(it) } } } } - -@Composable -private fun SelectRadioButton(selected: Boolean, text: String, modifier: Modifier = Modifier, onClick: () -> Unit) { - Row( - modifier.clickable(MutableInteractionSource(), null, onClick = { onClick() }), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton(selected, onClick) - Text( - text = text, - modifier = Modifier.padding(horizontal = 4.dp) - ) - } -} - -@Composable -private fun SelectCheckBox( - selected: Boolean, - text: String, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - Row( - modifier.clickable(MutableInteractionSource(), null, onClick = { onClick() }), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox(selected, null) - Text( - text = text, - modifier = Modifier.padding(horizontal = 4.dp) - ) - } -} diff --git a/src/main/kotlin/ui/components/FaqDialog.kt b/src/main/kotlin/ui/components/FaqDialog.kt index 23e086e..10382dd 100644 --- a/src/main/kotlin/ui/components/FaqDialog.kt +++ b/src/main/kotlin/ui/components/FaqDialog.kt @@ -75,6 +75,12 @@ private fun buildFilterFaqs(): List { "\n\nEx: ‘analytics.request_id’ = ‘123456’" ) ) + list.add( + Faq( + "Can I use order the results?", + "Yes, you can use order by query to order data like this : `order by localTime desc` " + ) + ) list.add( Faq( "I am getting issue with query, what should I do?", diff --git a/src/main/kotlin/ui/components/SettingsDialog.kt b/src/main/kotlin/ui/components/SettingsDialog.kt index d7e715d..3f58af5 100644 --- a/src/main/kotlin/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/ui/components/SettingsDialog.kt @@ -1,11 +1,9 @@ package ui.components -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.IconButton +import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString @@ -14,6 +12,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import models.SocialIcons import ui.CustomTheme import utils.AppSettings import utils.Helpers @@ -58,13 +57,21 @@ fun GeneralSettingBlock(modifier: Modifier = Modifier) { fun OtherSettingBlock(modifier: Modifier = Modifier) { Column(modifier, verticalArrangement = Arrangement.spacedBy(16.dp)) { ItemHeader("Other") - SimpleListItem( - "Feedback / Issues", Modifier.fillMaxWidth().clickable { - openBrowser("mailto://kapoor.aman22@gmail.com") - }, - "If you have any feedback or issues, we would love to hear it from you", - painterResource("icons/ico-email.svg") - ) + Column { + SimpleListItem( + "Feedback / Issues", Modifier.fillMaxWidth(), + "If you have any feedback or issues, we would love to hear it from you", + painterResource("icons/ico-email.svg") + ) + Row( + Modifier.padding(start = 32.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + WebLinkButton(SocialIcons.GithubIssues, "Create new issue") + WebLinkButton(SocialIcons.Email, "Mail Us") + } + } val aboutUsText = buildAnnotatedString { withStyle(SpanStyle(color = CustomTheme.colors.mediumContrast)) { append("This ") @@ -81,42 +88,44 @@ fun OtherSettingBlock(modifier: Modifier = Modifier) { append(" is created by Aman Kapoor. Connect with him below.") } } - ClickableListItem( - AnnotatedString("About us"), Modifier.fillMaxWidth(), - aboutUsText, - painterResource("icons/ico_info.svg") - ) { offset -> - aboutUsText.getStringAnnotations( - tag = "gitProjectLink", start = offset, - end = offset - ).firstOrNull()?.let { - openBrowser(it.item) + Column { + ClickableListItem( + AnnotatedString("About us"), Modifier.fillMaxWidth(), + aboutUsText, + painterResource("icons/ico_info.svg") + ) { offset -> + aboutUsText.getStringAnnotations( + tag = "gitProjectLink", start = offset, + end = offset + ).firstOrNull()?.let { + openBrowser(it.item) + } } - } - Row(Modifier.padding(start = 32.dp)) { - SocialIcons.DefaultIcons.forEach { - SocialIcon(it) + Row(Modifier.padding(start = 32.dp)) { + SocialIcons.DefaultIcons.forEach { + println(it) + SocialIcon(it) + } } } } } @Composable -private fun SocialIcon(icon: SocialIcons) { - IconButton({ openBrowser(icon.url) }) { - Icon(painterResource(icon.icon), "social") +private fun WebLinkButton(socialIcons: SocialIcons, text: String) { + val buttonColors = ButtonDefaults.textButtonColors(contentColor = CustomTheme.colors.mediumContrast) + TextButton({ openBrowser(socialIcons.url) }, colors = buttonColors) { + Icon(painterResource(socialIcons.icon), socialIcons.name) + Spacer(Modifier.width(4.dp)) + Text(text, style = CustomTheme.typography.bodySmall) } } -sealed class SocialIcons(val icon: String, val url: String) { - companion object { - val DefaultIcons = listOf(SocialTwitter, SocialGithub, SocialLinkedin) +@Composable +private fun SocialIcon(icon: SocialIcons) { + IconButton({ openBrowser(icon.url) }) { + Icon(painterResource(icon.icon), "social") } } -object SocialTwitter : SocialIcons("icons/social/social_twitter.svg", "https://twitter.com/Aman22Kapoor") -object SocialGithub : SocialIcons("icons/social/social_github.svg", "https://github.com/amank22") -object SocialLinkedin : SocialIcons("icons/social/social_linkedIn.svg", "https://www.linkedin.com/in/amank22/") -object SocialFacebook : SocialIcons("icons/social/social_facebook.svg", "") - fun openBrowser(url: String) = Helpers.openInBrowser(url)