mirror of
https://github.com/openflocon/Flocon.git
synced 2026-04-28 09:49:32 +00:00
Feat log db query (#471)
Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
parent
20b7b7b29c
commit
24bb623ebd
48 changed files with 8659 additions and 129 deletions
|
|
@ -19,6 +19,15 @@ fun floconRegisterDatabase(displayName: String, absolutePath: String) {
|
|||
)
|
||||
}
|
||||
|
||||
fun floconLogDatabaseQuery(dbName: String, sqlQuery: String, bindArgs: List<Any?>) {
|
||||
FloconApp.instance?.client?.databasePlugin?.logQuery(
|
||||
dbName = dbName,
|
||||
sqlQuery = sqlQuery,
|
||||
bindArgs = bindArgs,
|
||||
)
|
||||
}
|
||||
|
||||
interface FloconDatabasePlugin {
|
||||
fun register(floconDatabaseModel: FloconDatabaseModel)
|
||||
fun logQuery(dbName: String, sqlQuery: String, bindArgs: List<Any?>)
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ object Protocol {
|
|||
object Method {
|
||||
const val Query = "query"
|
||||
const val GetDatabases = "getDatabases"
|
||||
const val LogQuery = "logQuery"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import io.github.openflocon.flocon.plugins.database.model.fromdevice.QueryResult
|
|||
import io.github.openflocon.flocon.plugins.database.model.fromdevice.listDeviceDataBaseDataModelToJson
|
||||
import io.github.openflocon.flocon.plugins.database.model.fromdevice.toJson
|
||||
import io.github.openflocon.flocon.plugins.database.model.todevice.DatabaseQueryMessage
|
||||
import io.github.openflocon.flocon.plugins.database.model.fromdevice.DatabaseQueryLogModel
|
||||
import io.github.openflocon.flocon.utils.currentTimeMillis
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
|
|
@ -94,4 +96,21 @@ internal class FloconDatabasePluginImpl(
|
|||
registeredDatabases.update { it + floconDatabaseModel }
|
||||
sendAllDatabases(sender)
|
||||
}
|
||||
|
||||
override fun logQuery(dbName: String, sqlQuery: String, bindArgs: List<Any?>) {
|
||||
try {
|
||||
sender.send(
|
||||
plugin = Protocol.FromDevice.Database.Plugin,
|
||||
method = Protocol.FromDevice.Database.Method.LogQuery,
|
||||
body = DatabaseQueryLogModel(
|
||||
dbName = dbName,
|
||||
sqlQuery = sqlQuery,
|
||||
bindArgs = bindArgs.map { it.toString() },
|
||||
timestamp = currentTimeMillis(),
|
||||
).toJson(),
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
FloconLogger.logError("Database logging error", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package io.github.openflocon.flocon.plugins.database.model.fromdevice
|
||||
|
||||
import io.github.openflocon.flocon.core.FloconEncoder
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
@Serializable
|
||||
internal data class DatabaseQueryLogModel(
|
||||
val dbName: String,
|
||||
val sqlQuery: String,
|
||||
val bindArgs: List<String>?,
|
||||
val timestamp: Long,
|
||||
) {
|
||||
fun toJson(): String {
|
||||
return FloconEncoder.json.encodeToString<DatabaseQueryLogModel>(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromJson(json: String): DatabaseQueryLogModel {
|
||||
return FloconEncoder.json.decodeFromString<DatabaseQueryLogModel>(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,9 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.openflocon.flocon.Flocon
|
||||
import io.github.openflocon.flocon.FloconLogger
|
||||
|
|
@ -24,6 +26,7 @@ import io.github.openflocon.flocon.myapplication.dashboard.initializeDashboard
|
|||
import io.github.openflocon.flocon.myapplication.database.DogDatabase
|
||||
import io.github.openflocon.flocon.myapplication.database.initializeDatabases
|
||||
import io.github.openflocon.flocon.myapplication.database.initializeInMemoryDatabases
|
||||
import io.github.openflocon.flocon.myapplication.database.model.DogEntity
|
||||
import io.github.openflocon.flocon.myapplication.deeplinks.initializeDeeplinks
|
||||
import io.github.openflocon.flocon.myapplication.graphql.GraphQlTester
|
||||
import io.github.openflocon.flocon.myapplication.grpc.GrpcController
|
||||
|
|
@ -95,6 +98,9 @@ class MainActivity : ComponentActivity() {
|
|||
setContent {
|
||||
MyApplicationTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(Modifier.fillMaxSize().padding(innerPadding)) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
|
|
@ -188,6 +194,23 @@ class MainActivity : ComponentActivity() {
|
|||
) {
|
||||
Text("send analytics event")
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
DogDatabase.getDatabase(context).dogDao().insertDog(
|
||||
DogEntity(
|
||||
id = System.currentTimeMillis(),
|
||||
name = "Flocon",
|
||||
breed = "Golden Retriever ${System.currentTimeMillis()}",
|
||||
age = 6,
|
||||
pictureUrl = "https://picsum.photos/501/500.jpg",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Insert dog in DB")
|
||||
}
|
||||
}
|
||||
|
||||
ImagesListView(modifier = Modifier.fillMaxSize())
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import io.github.openflocon.flocon.myapplication.database.dao.DogDao
|
|||
import io.github.openflocon.flocon.myapplication.database.model.DogEntity
|
||||
import io.github.openflocon.flocon.myapplication.database.model.HumanEntity
|
||||
import io.github.openflocon.flocon.myapplication.database.model.HumanWithDogEntity
|
||||
import io.github.openflocon.flocon.plugins.database.floconLogDatabaseQuery
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
|
|
@ -26,12 +28,17 @@ abstract class DogDatabase : RoomDatabase() {
|
|||
private var INSTANCE: DogDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): DogDatabase {
|
||||
val dbName = "dogs_database"
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
DogDatabase::class.java,
|
||||
"dogs_database" // Nom du fichier de la base de données Dogs
|
||||
).fallbackToDestructiveMigration().build()
|
||||
dbName
|
||||
).setQueryCallback({ sqlQuery, bindArgs -> floconLogDatabaseQuery(
|
||||
dbName = dbName, sqlQuery = sqlQuery, bindArgs = bindArgs
|
||||
) }, Executors.newSingleThreadExecutor())
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,29 @@ package io.github.openflocon.flocon.myapplication.multi
|
|||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import io.github.openflocon.flocon.myapplication.multi.database.DogDatabase
|
||||
import io.github.openflocon.flocon.myapplication.multi.database.FoodDatabase
|
||||
import io.github.openflocon.flocon.plugins.database.floconLogDatabaseQuery
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object Databases {
|
||||
@Volatile
|
||||
private var dogDatabase: DogDatabase? = null
|
||||
|
||||
fun getDogDatabase(context: Context): DogDatabase {
|
||||
val dbName = "dogs_database"
|
||||
return dogDatabase ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
DogDatabase::class.java,
|
||||
"dogs_database"
|
||||
).fallbackToDestructiveMigration().build()
|
||||
)
|
||||
.setQueryCallback({ sqlQuery, bindArgs -> floconLogDatabaseQuery(
|
||||
dbName = dbName, sqlQuery = sqlQuery, bindArgs = bindArgs
|
||||
) }, Executors.newSingleThreadExecutor())
|
||||
.fallbackToDestructiveMigration().build()
|
||||
dogDatabase = instance
|
||||
instance
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,13 @@ import io.github.openflocon.flocon.myapplication.multi.Databases.getDogDatabase
|
|||
import io.github.openflocon.flocon.myapplication.multi.Databases.getFoodDatabase
|
||||
import io.github.openflocon.flocon.myapplication.multi.database.FoodDatabase
|
||||
import io.github.openflocon.flocon.myapplication.multi.database.initializeDatabases
|
||||
import io.github.openflocon.flocon.myapplication.multi.database.model.DogEntity
|
||||
import io.github.openflocon.flocon.myapplication.multi.sharedpreferences.initializeSharedPreferences
|
||||
import io.github.openflocon.flocon.myapplication.multi.ui.App
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -43,8 +46,11 @@ class MainActivity : ComponentActivity() {
|
|||
DummyHttpKtorCaller.initialize(ktorClient)
|
||||
|
||||
initializeSharedPreferences(applicationContext)
|
||||
|
||||
val dogDatabase = getDogDatabase(this)
|
||||
|
||||
initializeDatabases(
|
||||
dogDatabase = getDogDatabase(this),
|
||||
dogDatabase = dogDatabase,
|
||||
foodDatabase = getFoodDatabase(this),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,11 +16,14 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.github.openflocon.flocon.myapplication.multi.DummyHttpKtorCaller
|
||||
import io.github.openflocon.flocon.myapplication.multi.dashboard.initializeDashboard
|
||||
import io.github.openflocon.flocon.myapplication.multi.database.model.DogEntity
|
||||
import io.github.openflocon.flocon.plugins.analytics.floconAnalytics
|
||||
import io.github.openflocon.flocon.plugins.analytics.model.AnalyticsEvent
|
||||
import io.github.openflocon.flocon.plugins.analytics.model.analyticsProperty
|
||||
import io.github.openflocon.flocon.plugins.tables.floconTable
|
||||
import io.github.openflocon.flocon.plugins.tables.model.toParam
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -19,6 +19,8 @@ import io.github.openflocon.data.local.database.dao.TablesDao
|
|||
import io.github.openflocon.data.local.database.models.DatabaseTableEntity
|
||||
import io.github.openflocon.data.local.database.models.FavoriteQueryEntity
|
||||
import io.github.openflocon.data.local.database.models.SuccessQueryEntity
|
||||
import io.github.openflocon.data.local.database.dao.DatabaseQueryLogDao
|
||||
import io.github.openflocon.data.local.database.models.DatabaseQueryLogEntity
|
||||
import io.github.openflocon.data.local.deeplink.dao.FloconDeeplinkDao
|
||||
import io.github.openflocon.data.local.deeplink.models.DeeplinkEntity
|
||||
import io.github.openflocon.data.local.device.datasource.dao.DevicesDao
|
||||
|
|
@ -48,7 +50,7 @@ import io.github.openflocon.flocondesktop.common.db.converters.MapStringsConvert
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Database(
|
||||
version = 74,
|
||||
version = 78,
|
||||
entities = [
|
||||
FloconNetworkCallEntity::class,
|
||||
FileEntity::class,
|
||||
|
|
@ -72,6 +74,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
DeviceAppEntity::class,
|
||||
DatabaseTableEntity::class,
|
||||
CrashReportEntity::class,
|
||||
DatabaseQueryLogEntity::class,
|
||||
],
|
||||
)
|
||||
@TypeConverters(
|
||||
|
|
@ -96,6 +99,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||
abstract val devicesDao: DevicesDao
|
||||
abstract val tablesDao: TablesDao
|
||||
abstract val crashReportDao: CrashReportDao
|
||||
abstract val databaseQueryLogDao: DatabaseQueryLogDao
|
||||
}
|
||||
|
||||
fun getRoomDatabase(): AppDatabase = getDatabaseBuilder()
|
||||
|
|
|
|||
|
|
@ -55,4 +55,7 @@ val roomModule =
|
|||
single {
|
||||
get<AppDatabase>().crashReportDao
|
||||
}
|
||||
single {
|
||||
get<AppDatabase>().databaseQueryLogDao
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package io.github.openflocon.flocondesktop.features.database
|
||||
|
||||
import io.github.openflocon.domain.database.usecase.GetDatabaseQueryLogsUseCase
|
||||
import io.github.openflocon.flocondesktop.features.database.delegate.DatabaseSelectorDelegate
|
||||
import io.github.openflocon.flocondesktop.features.database.processor.ExportDatabaseQueryLogsToCsvProcessor
|
||||
import io.github.openflocon.flocondesktop.features.database.processor.ExportDatabaseQueryLogsToMarkdownProcessor
|
||||
import io.github.openflocon.flocondesktop.features.database.processor.ExportDatabaseResultToCsvProcessor
|
||||
import io.github.openflocon.flocondesktop.features.database.processor.ImportSqlQueryProcessor
|
||||
import org.koin.core.module.dsl.factoryOf
|
||||
|
|
@ -13,4 +16,8 @@ internal val databaseModule = module {
|
|||
factoryOf(::DatabaseSelectorDelegate)
|
||||
factoryOf(::ImportSqlQueryProcessor)
|
||||
factoryOf(::ExportDatabaseResultToCsvProcessor)
|
||||
factoryOf(::ExportDatabaseQueryLogsToCsvProcessor)
|
||||
factoryOf(::ExportDatabaseQueryLogsToMarkdownProcessor)
|
||||
factoryOf(::GetDatabaseQueryLogsUseCase)
|
||||
viewModelOf(::DatabaseQueryLogsViewModel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
package io.github.openflocon.flocondesktop.features.database
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.map
|
||||
import io.github.openflocon.domain.common.DispatcherProvider
|
||||
import io.github.openflocon.domain.common.combines
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.usecase.GetDatabaseQueryLogsUseCase
|
||||
import io.github.openflocon.domain.database.usecase.ObserveDatabaseQueryLogsUseCase
|
||||
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
|
||||
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdAndPackageNameUseCase
|
||||
import io.github.openflocon.domain.feedback.FeedbackDisplayer
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DatabaseQueryUiModel
|
||||
import io.github.openflocon.flocondesktop.features.database.model.FilterChipUiModel
|
||||
import io.github.openflocon.flocondesktop.features.database.model.toDomain
|
||||
import io.github.openflocon.flocondesktop.features.database.processor.ExportDatabaseQueryLogsToCsvProcessor
|
||||
import io.github.openflocon.flocondesktop.features.database.processor.ExportDatabaseQueryLogsToMarkdownProcessor
|
||||
import io.github.openflocon.library.designsystem.common.copyToClipboard
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
import io.github.openflocon.domain.database.utils.injectSqlArgs
|
||||
|
||||
class DatabaseQueryLogsViewModel(
|
||||
private val dbName: String,
|
||||
private val observeDatabaseQueryLogsUseCase: ObserveDatabaseQueryLogsUseCase,
|
||||
private val getDatabaseQueryLogsUseCase: GetDatabaseQueryLogsUseCase,
|
||||
private val observeCurrentDeviceIdAndPackageNameUseCase: ObserveCurrentDeviceIdAndPackageNameUseCase,
|
||||
private val feedbackDisplayer: FeedbackDisplayer,
|
||||
private val exportDatabaseQueryLogsToCsvProcessor: ExportDatabaseQueryLogsToCsvProcessor,
|
||||
private val exportDatabaseQueryLogsToMarkdownProcessor: ExportDatabaseQueryLogsToMarkdownProcessor,
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _showTransactions = MutableStateFlow(false)
|
||||
val showTransactions = _showTransactions.asStateFlow()
|
||||
|
||||
private val _filterChips = MutableStateFlow<List<FilterChipUiModel>>(emptyList())
|
||||
val filterChips = _filterChips.asStateFlow()
|
||||
|
||||
private val _searchQuery = MutableStateFlow("")
|
||||
val searchQuery = _searchQuery.asStateFlow()
|
||||
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
val logs: Flow<PagingData<DatabaseQueryUiModel>> =
|
||||
combines(
|
||||
_showTransactions,
|
||||
_filterChips.map { it.map { it.toDomain() } }.distinctUntilChanged(),
|
||||
observeCurrentDeviceIdAndPackageNameUseCase(),
|
||||
).flatMapLatest { (showTransactions, filterChips, currentDeviceAndPackage) ->
|
||||
observeDatabaseQueryLogsUseCase(
|
||||
dbName = dbName,
|
||||
showTransactions = showTransactions,
|
||||
filters = filterChips,
|
||||
).map {
|
||||
it.map {
|
||||
it.toUi(currentDeviceAndPackage = currentDeviceAndPackage)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(dispatcherProvider.viewModel)
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
fun toggleShowTransactions() {
|
||||
_showTransactions.update { !it }
|
||||
}
|
||||
|
||||
fun onSearchQueryChanged(query: String) {
|
||||
_searchQuery.value = query
|
||||
}
|
||||
|
||||
fun addIncludeFilter() {
|
||||
val query = _searchQuery.value.trim()
|
||||
if (query.isNotEmpty() && !_filterChips.value.any { it.text == query }) {
|
||||
_filterChips.update { it + FilterChipUiModel(query, FilterChipUiModel.FilterType.INCLUDE) }
|
||||
_searchQuery.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
fun addExcludeFilter() {
|
||||
val query = _searchQuery.value.trim()
|
||||
if (query.isNotEmpty() && !_filterChips.value.any { it.text == query }) {
|
||||
_filterChips.update { it + FilterChipUiModel(query, FilterChipUiModel.FilterType.EXCLUDE) }
|
||||
_searchQuery.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFilterType(chip: FilterChipUiModel) {
|
||||
_filterChips.update { list ->
|
||||
list.map {
|
||||
if (it == chip) {
|
||||
it.copy(type = if (it.type == FilterChipUiModel.FilterType.INCLUDE)
|
||||
FilterChipUiModel.FilterType.EXCLUDE
|
||||
else
|
||||
FilterChipUiModel.FilterType.INCLUDE
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addFilter(text: String, type: FilterChipUiModel.FilterType) {
|
||||
if (text.isNotEmpty() && !_filterChips.value.any { it.text == text }) {
|
||||
_filterChips.update { it + FilterChipUiModel(text, type) }
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFilterChip(chip: FilterChipUiModel) {
|
||||
_filterChips.update { it - chip }
|
||||
}
|
||||
|
||||
fun copyQuery(query: String) {
|
||||
copyToClipboard(query)
|
||||
feedbackDisplayer.displayMessage("Query copied to clipboard")
|
||||
}
|
||||
|
||||
fun copyArgs(args: List<String>?) {
|
||||
val argsString = args?.toString() ?: "[]"
|
||||
copyToClipboard(argsString)
|
||||
feedbackDisplayer.displayMessage("Arguments copied to clipboard")
|
||||
}
|
||||
|
||||
fun copyAsSql(query: String, args: List<String>?) {
|
||||
val fullSql = injectSqlArgs(query, args)
|
||||
copyToClipboard(fullSql)
|
||||
feedbackDisplayer.displayMessage("SQL with arguments copied to clipboard")
|
||||
}
|
||||
|
||||
fun exportToCsv() {
|
||||
viewModelScope.launch {
|
||||
val logs = getDatabaseQueryLogsUseCase(
|
||||
dbName = dbName,
|
||||
showTransactions = _showTransactions.value,
|
||||
filters = _filterChips.value.map { it.toDomain() }
|
||||
)
|
||||
exportDatabaseQueryLogsToCsvProcessor(logs).fold(
|
||||
doOnSuccess = {
|
||||
feedbackDisplayer.displayMessage("Logs exported to $it")
|
||||
},
|
||||
doOnFailure = {
|
||||
feedbackDisplayer.displayMessage("Export failed: ${it.message}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun exportToMarkdown() {
|
||||
viewModelScope.launch {
|
||||
val logs = getDatabaseQueryLogsUseCase(
|
||||
dbName = dbName,
|
||||
showTransactions = _showTransactions.value,
|
||||
filters = _filterChips.value.map { it.toDomain() }
|
||||
)
|
||||
exportDatabaseQueryLogsToMarkdownProcessor(logs).fold(
|
||||
doOnSuccess = {
|
||||
feedbackDisplayer.displayMessage("Logs exported to $it")
|
||||
},
|
||||
doOnFailure = {
|
||||
feedbackDisplayer.displayMessage("Export failed: ${it.message}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DatabaseQueryLogDomainModel.toUi(currentDeviceAndPackage: DeviceIdAndPackageNameDomainModel?) = DatabaseQueryUiModel(
|
||||
sqlQuery = sqlQuery,
|
||||
bindArgs = bindArgs,
|
||||
dateFormatted = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp)),
|
||||
isTransaction = isTransaction,
|
||||
isFromOldSession = currentDeviceAndPackage?.appInstance != appInstance,
|
||||
type = findType(this),
|
||||
)
|
||||
|
||||
|
||||
private fun findType(model: DatabaseQueryLogDomainModel): DatabaseQueryUiModel.Type? {
|
||||
return if(model.isTransaction) {
|
||||
DatabaseQueryUiModel.Type.Transaction
|
||||
} else if(model.sqlQuery.startsWith("SELECT")) {
|
||||
DatabaseQueryUiModel.Type.Select
|
||||
} else if(model.sqlQuery.startsWith("INSERT")) {
|
||||
DatabaseQueryUiModel.Type.Insert
|
||||
} else if(model.sqlQuery.startsWith("UPDATE")) {
|
||||
DatabaseQueryUiModel.Type.Update
|
||||
} else if(model.sqlQuery.startsWith("DELETE")) {
|
||||
DatabaseQueryUiModel.Type.Delete
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -135,6 +135,11 @@ class DatabaseViewModel(
|
|||
table = action.table,
|
||||
query = generateInsertQuery(action.table)
|
||||
)
|
||||
|
||||
is DatabaseScreenAction.OnSeeAllQueriesClicked -> createTabForQueryLogs(
|
||||
databaseId = action.databaseId,
|
||||
dbName = action.dbName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +159,7 @@ class DatabaseViewModel(
|
|||
val generatedName = table.name
|
||||
createTab(
|
||||
databaseId = databaseId,
|
||||
databaseName = "", // not used here
|
||||
tableName = null,
|
||||
generatedName = generatedName,
|
||||
favoriteId = null,
|
||||
|
|
@ -161,6 +167,18 @@ class DatabaseViewModel(
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun createTabForQueryLogs(databaseId: DeviceDataBaseId, dbName: String) {
|
||||
createTab(
|
||||
databaseId = databaseId,
|
||||
databaseName = dbName, // only used here
|
||||
tableName = null,
|
||||
generatedName = "Logs ($dbName)",
|
||||
favoriteId = null,
|
||||
query = null,
|
||||
isQueryLogs = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onTableColumnClicked(column: TableUiModel.ColumnUiModel) {
|
||||
copyToClipboard(column.name)
|
||||
feedbackDisplayer.displayMessage("copied: ${column.name}")
|
||||
|
|
@ -275,6 +293,8 @@ class DatabaseViewModel(
|
|||
favoriteId: Long?,
|
||||
generatedName: String,
|
||||
query: String?,
|
||||
isQueryLogs: Boolean = false,
|
||||
databaseName: String? = null, // only used for isQueryLogs
|
||||
) {
|
||||
val deviceIdAndPackageName = getCurrentDeviceIdAndPackageNameUseCase() ?: return
|
||||
|
||||
|
|
@ -291,12 +311,14 @@ class DatabaseViewModel(
|
|||
|
||||
val addedTab = DatabaseTabState(
|
||||
databaseId = databaseId,
|
||||
databaseName = databaseName ?: "",
|
||||
tableName = tableName,
|
||||
generatedName = generatedName,
|
||||
index = index,
|
||||
favoriteId = favoriteId,
|
||||
query = query,
|
||||
id = UUID.randomUUID().toString(),
|
||||
isQueryLogs = isQueryLogs,
|
||||
)
|
||||
|
||||
val newList = list.toMutableList().apply {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package io.github.openflocon.flocondesktop.features.database.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class DatabaseQueryUiModel(
|
||||
val sqlQuery: String,
|
||||
val bindArgs: List<String>?,
|
||||
val dateFormatted: String,
|
||||
val isTransaction: Boolean,
|
||||
val isFromOldSession: Boolean,
|
||||
val type: Type?,
|
||||
) {
|
||||
enum class Type {
|
||||
Select,
|
||||
Insert,
|
||||
Update,
|
||||
Delete,
|
||||
Transaction,
|
||||
}
|
||||
}
|
||||
|
|
@ -12,4 +12,5 @@ sealed interface DatabaseScreenAction {
|
|||
data class DeleteFavorite(val favoriteQuery: DatabaseFavoriteQueryUiModel) : DatabaseScreenAction
|
||||
data class OnDeleteContentClicked(val databaseId: DeviceDataBaseId, val table: TableUiModel) : DatabaseScreenAction
|
||||
data class OnInsertContentClicked(val databaseId: DeviceDataBaseId, val table: TableUiModel) : DatabaseScreenAction
|
||||
data class OnSeeAllQueriesClicked(val databaseId: DeviceDataBaseId, val dbName: String) : DatabaseScreenAction
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import androidx.compose.runtime.Immutable
|
|||
@Immutable
|
||||
data class DatabaseTabState(
|
||||
val databaseId: String,
|
||||
val databaseName: String,
|
||||
val tableName: String?,
|
||||
val generatedName: String,
|
||||
val index: Int,
|
||||
val favoriteId: Long?,
|
||||
val query: String?,
|
||||
val isQueryLogs: Boolean = false,
|
||||
val id: String,
|
||||
) {
|
||||
val displayName: String = generatedName + if (index > 0) " ($index)" else ""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package io.github.openflocon.flocondesktop.features.database.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.github.openflocon.domain.database.models.FilterQueryLogDomainModel
|
||||
|
||||
@Immutable
|
||||
data class FilterChipUiModel(
|
||||
val text: String,
|
||||
val type: FilterType
|
||||
) {
|
||||
enum class FilterType {
|
||||
INCLUDE,
|
||||
EXCLUDE
|
||||
}
|
||||
}
|
||||
|
||||
fun FilterChipUiModel.toDomain() = FilterQueryLogDomainModel(
|
||||
text = text,
|
||||
type = when (type) {
|
||||
FilterChipUiModel.FilterType.INCLUDE -> FilterQueryLogDomainModel.FilterType.INCLUDE
|
||||
FilterChipUiModel.FilterType.EXCLUDE -> FilterQueryLogDomainModel.FilterType.EXCLUDE
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package io.github.openflocon.flocondesktop.features.database.processor
|
||||
|
||||
import io.github.openflocon.domain.common.Either
|
||||
import io.github.openflocon.domain.common.Failure
|
||||
import io.github.openflocon.domain.common.Success
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.toFullSql
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class ExportDatabaseQueryLogsToCsvProcessor {
|
||||
|
||||
suspend operator fun invoke(
|
||||
logs: List<DatabaseQueryLogDomainModel>
|
||||
): Either<Throwable, String> {
|
||||
val fileName = "database_logs_${System.currentTimeMillis()}.csv"
|
||||
|
||||
val file = showSaveFileDialog(defaultFileName = fileName, dialogName = "Export database logs as CSV") ?: return Failure(
|
||||
Throwable("no file selected")
|
||||
)
|
||||
|
||||
exportToCsv(
|
||||
file = file,
|
||||
logs = logs,
|
||||
)
|
||||
return Success(file.absolutePath)
|
||||
}
|
||||
|
||||
private fun showSaveFileDialog(dialogName: String, defaultFileName: String): File? {
|
||||
val parentFrame = Frame()
|
||||
val dialog = FileDialog(parentFrame, dialogName, FileDialog.SAVE).apply {
|
||||
file = defaultFileName
|
||||
}
|
||||
|
||||
dialog.isVisible = true
|
||||
|
||||
val file = dialog.file
|
||||
val directory = dialog.directory
|
||||
|
||||
parentFrame.dispose()
|
||||
|
||||
return if (file != null && directory != null) {
|
||||
File(directory, file)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportToCsv(
|
||||
file: File,
|
||||
logs: List<DatabaseQueryLogDomainModel>,
|
||||
) {
|
||||
val columns = listOf("Date", "SQL Query", "Arguments", "Full SQL")
|
||||
file.writeText(columns.joinToString(separator = ",", postfix = "\n"))
|
||||
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
logs.forEach { log ->
|
||||
val row = listOf(
|
||||
dateFormat.format(Date(log.timestamp)),
|
||||
log.sqlQuery,
|
||||
log.bindArgs?.toString() ?: "[]",
|
||||
log.toFullSql()
|
||||
)
|
||||
val escapedRow = row.map { csvEscape(it) }
|
||||
file.appendText(escapedRow.joinToString(separator = ",", postfix = "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun csvEscape(text: String?): String {
|
||||
val nonNullText = text ?: ""
|
||||
val containsSpecialChars = nonNullText.contains(',') || nonNullText.contains('"') || nonNullText.contains('\n')
|
||||
return if (containsSpecialChars) {
|
||||
val escapedText = nonNullText.replace("\"", "\"\"")
|
||||
"\"$escapedText\""
|
||||
} else {
|
||||
nonNullText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package io.github.openflocon.flocondesktop.features.database.processor
|
||||
|
||||
import io.github.openflocon.domain.common.Either
|
||||
import io.github.openflocon.domain.common.Failure
|
||||
import io.github.openflocon.domain.common.Success
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.toFullSql
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class ExportDatabaseQueryLogsToMarkdownProcessor {
|
||||
|
||||
suspend operator fun invoke(
|
||||
logs: List<DatabaseQueryLogDomainModel>
|
||||
): Either<Throwable, String> {
|
||||
val fileName = "database_logs_${System.currentTimeMillis()}.md"
|
||||
|
||||
val file = showSaveFileDialog(defaultFileName = fileName, dialogName = "Export database logs as Markdown") ?: return Failure(
|
||||
Throwable("no file selected")
|
||||
)
|
||||
|
||||
exportToMarkdown(
|
||||
file = file,
|
||||
logs = logs,
|
||||
)
|
||||
return Success(file.absolutePath)
|
||||
}
|
||||
|
||||
private fun showSaveFileDialog(dialogName: String, defaultFileName: String): File? {
|
||||
val parentFrame = Frame()
|
||||
val dialog = FileDialog(parentFrame, dialogName, FileDialog.SAVE).apply {
|
||||
file = defaultFileName
|
||||
}
|
||||
|
||||
dialog.isVisible = true
|
||||
|
||||
val file = dialog.file
|
||||
val directory = dialog.directory
|
||||
|
||||
parentFrame.dispose()
|
||||
|
||||
return if (file != null && directory != null) {
|
||||
File(directory, file)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportToMarkdown(
|
||||
file: File,
|
||||
logs: List<DatabaseQueryLogDomainModel>,
|
||||
) {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
val markdown = buildString {
|
||||
logs.forEach { log ->
|
||||
val date = dateFormat.format(Date(log.timestamp))
|
||||
val sql = log.sqlQuery.replace("|", "\\|").replace("\n", " ")
|
||||
val args = (log.bindArgs?.toString() ?: "[]").replace("|", "\\|")
|
||||
val fullSql = log.toFullSql().replace("|", "\\|").replace("\n", " ")
|
||||
|
||||
appendLine("# $date")
|
||||
appendLine()
|
||||
appendLine("### query")
|
||||
appendLine(sql)
|
||||
appendLine()
|
||||
appendLine("### args")
|
||||
appendLine(args)
|
||||
appendLine()
|
||||
appendLine("### SQL")
|
||||
appendLine(fullSql)
|
||||
appendLine()
|
||||
appendLine("--------------------------------------------------------------------------------")
|
||||
appendLine()
|
||||
}
|
||||
}
|
||||
|
||||
file.writeText(markdown)
|
||||
}
|
||||
}
|
||||
|
|
@ -154,9 +154,6 @@ fun DatabaseQueryView(
|
|||
isQueryEmpty = query.isBlank(),
|
||||
)
|
||||
|
||||
val highlightedText = remember(query) {
|
||||
}
|
||||
|
||||
FloconTextField(
|
||||
value = query,
|
||||
onValueChange = updateQuery,
|
||||
|
|
|
|||
|
|
@ -1,108 +1,25 @@
|
|||
package io.github.openflocon.flocondesktop.features.database.view
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.github.openflocon.flocondesktop.features.database.DatabaseTabViewModel
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DatabaseScreenState
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DatabaseTabAction
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DatabaseTabState
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.FloconPageTopBar
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import io.github.openflocon.flocondesktop.features.database.view.logs.DatabaseQueryLogsView
|
||||
|
||||
@Composable
|
||||
fun DatabaseTabView(
|
||||
tab: DatabaseTabState,
|
||||
favoritesTitles: Set<String>,
|
||||
) {
|
||||
val viewModel: DatabaseTabViewModel = koinViewModel(
|
||||
key = tab.id,
|
||||
parameters = {
|
||||
parametersOf(
|
||||
DatabaseTabViewModel.Params(
|
||||
databaseId = tab.databaseId,
|
||||
tableName = tab.tableName,
|
||||
favoriteId = tab.favoriteId,
|
||||
query = tab.query,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
DisposableEffect(viewModel) {
|
||||
viewModel.onVisible()
|
||||
onDispose {
|
||||
viewModel.onNotVisible()
|
||||
}
|
||||
}
|
||||
|
||||
val state: DatabaseScreenState by viewModel.state.collectAsStateWithLifecycle()
|
||||
val autoUpdate by viewModel.isAutoUpdateEnabled.collectAsStateWithLifecycle()
|
||||
val lastQueries by viewModel.lastQueries.collectAsStateWithLifecycle()
|
||||
|
||||
DatabaseTabViewContent(
|
||||
query = viewModel.query.value,
|
||||
autoUpdate = autoUpdate,
|
||||
updateQuery = viewModel::updateQuery,
|
||||
onAction = viewModel::onAction,
|
||||
state = state,
|
||||
lastQueries = lastQueries,
|
||||
favoritesTitles = favoritesTitles,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DatabaseTabViewContent(
|
||||
query: String,
|
||||
autoUpdate: Boolean,
|
||||
favoritesTitles: Set<String>,
|
||||
updateQuery: (String) -> Unit,
|
||||
onAction: (action: DatabaseTabAction) -> Unit,
|
||||
state: DatabaseScreenState,
|
||||
lastQueries: List<String>,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FloconPageTopBar(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.clip(FloconTheme.shapes.medium)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = FloconTheme.colorPalette.secondary,
|
||||
shape = FloconTheme.shapes.medium
|
||||
),
|
||||
) { contentPadding ->
|
||||
DatabaseQueryView(
|
||||
query = query,
|
||||
updateQuery = updateQuery,
|
||||
autoUpdate = autoUpdate,
|
||||
onAction = onAction,
|
||||
favoritesTitles = favoritesTitles,
|
||||
lastQueries = lastQueries,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseContentView(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onExportCsvClicked = {
|
||||
onAction(DatabaseTabAction.ExportCsv)
|
||||
},
|
||||
if (tab.isQueryLogs) {
|
||||
DatabaseQueryLogsView(
|
||||
dbName = tab.databaseName,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
DatabaseTabQueryView(
|
||||
tab = tab,
|
||||
favoritesTitles = favoritesTitles,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package io.github.openflocon.flocondesktop.features.database.view
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.github.openflocon.flocondesktop.features.database.DatabaseTabViewModel
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DatabaseScreenState
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DatabaseTabAction
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DatabaseTabState
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.components.FloconPageTopBar
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
fun DatabaseTabQueryView(
|
||||
tab: DatabaseTabState,
|
||||
favoritesTitles: Set<String>,
|
||||
) {
|
||||
val viewModel: DatabaseTabViewModel = koinViewModel(
|
||||
key = tab.id,
|
||||
parameters = {
|
||||
parametersOf(
|
||||
DatabaseTabViewModel.Params(
|
||||
databaseId = tab.databaseId,
|
||||
tableName = tab.tableName,
|
||||
favoriteId = tab.favoriteId,
|
||||
query = tab.query,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
DisposableEffect(viewModel) {
|
||||
viewModel.onVisible()
|
||||
onDispose {
|
||||
viewModel.onNotVisible()
|
||||
}
|
||||
}
|
||||
|
||||
val state: DatabaseScreenState by viewModel.state.collectAsStateWithLifecycle()
|
||||
val autoUpdate by viewModel.isAutoUpdateEnabled.collectAsStateWithLifecycle()
|
||||
val lastQueries by viewModel.lastQueries.collectAsStateWithLifecycle()
|
||||
|
||||
DatabaseTabViewContent(
|
||||
query = viewModel.query.value,
|
||||
autoUpdate = autoUpdate,
|
||||
updateQuery = viewModel::updateQuery,
|
||||
onAction = viewModel::onAction,
|
||||
state = state,
|
||||
lastQueries = lastQueries,
|
||||
favoritesTitles = favoritesTitles,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
private fun DatabaseTabViewContent(
|
||||
query: String,
|
||||
autoUpdate: Boolean,
|
||||
favoritesTitles: Set<String>,
|
||||
updateQuery: (String) -> Unit,
|
||||
onAction: (action: DatabaseTabAction) -> Unit,
|
||||
state: DatabaseScreenState,
|
||||
lastQueries: List<String>,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FloconPageTopBar(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.clip(FloconTheme.shapes.medium)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = FloconTheme.colorPalette.secondary,
|
||||
shape = FloconTheme.shapes.medium
|
||||
),
|
||||
) { contentPadding ->
|
||||
DatabaseQueryView(
|
||||
query = query,
|
||||
updateQuery = updateQuery,
|
||||
autoUpdate = autoUpdate,
|
||||
onAction = onAction,
|
||||
favoritesTitles = favoritesTitles,
|
||||
lastQueries = lastQueries,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseContentView(
|
||||
state = state,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onExportCsvClicked = {
|
||||
onAction(DatabaseTabAction.ExportCsv)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
@file:OptIn(ExperimentalFoundationApi::class)
|
||||
|
||||
package io.github.openflocon.flocondesktop.features.database.view.databases_tables
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -12,6 +17,8 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Dataset
|
||||
import androidx.compose.material.icons.outlined.Preview
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -23,9 +30,12 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseId
|
||||
import io.github.openflocon.flocondesktop.common.ui.ContextualView
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DeviceDataBaseUiModel
|
||||
import io.github.openflocon.flocondesktop.features.database.model.TableUiModel
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.common.FloconContextMenuItem
|
||||
import io.github.openflocon.library.designsystem.components.WithTooltip
|
||||
|
||||
@Composable
|
||||
internal fun DatabaseItemView(
|
||||
|
|
@ -36,6 +46,7 @@ internal fun DatabaseItemView(
|
|||
onTableColumnClicked: (TableUiModel.ColumnUiModel) -> Unit,
|
||||
onDeleteContentClicked: (databaseId: DeviceDataBaseId, TableUiModel) -> Unit,
|
||||
onInsertContentClicked: (databaseId: DeviceDataBaseId, TableUiModel) -> Unit,
|
||||
onSeeAllQueriesClicked: (DeviceDataBaseId, String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
|
|
@ -44,6 +55,7 @@ internal fun DatabaseItemView(
|
|||
onSelect = onSelect,
|
||||
state = state,
|
||||
onDatabaseDoubleClicked = onDatabaseDoubleClicked,
|
||||
onSeeAllQueriesClicked = onSeeAllQueriesClicked,
|
||||
)
|
||||
state.tables?.let { tables ->
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
|
|
@ -73,6 +85,7 @@ private fun DatabaseView(
|
|||
onSelect: (DeviceDataBaseId) -> Unit,
|
||||
state: DeviceDataBaseUiModel,
|
||||
onDatabaseDoubleClicked: (DeviceDataBaseUiModel) -> Unit,
|
||||
onSeeAllQueriesClicked: (DeviceDataBaseId, String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (background, textColor) = if (state.isSelected) {
|
||||
|
|
@ -81,32 +94,58 @@ private fun DatabaseView(
|
|||
Color.Transparent to FloconTheme.colorPalette.onSurface
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(background)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onSelect(state.id)
|
||||
}, onDoubleClick = {
|
||||
onDatabaseDoubleClicked(state)
|
||||
}
|
||||
).padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
ContextualView(
|
||||
listOf(
|
||||
FloconContextMenuItem.Item(
|
||||
label = "See all queries",
|
||||
onClick = {
|
||||
onSeeAllQueriesClicked(state.id, state.name)
|
||||
}
|
||||
))
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.width(14.dp),
|
||||
imageVector = Icons.Outlined.Dataset,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(textColor),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
state.name,
|
||||
color = textColor,
|
||||
style = FloconTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(background)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
onSelect(state.id)
|
||||
}, onDoubleClick = {
|
||||
onDatabaseDoubleClicked(state)
|
||||
}
|
||||
).padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.width(14.dp),
|
||||
imageVector = Icons.Outlined.Dataset,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(textColor),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
state.name,
|
||||
color = textColor,
|
||||
style = FloconTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
WithTooltip("See logs") {
|
||||
Box(
|
||||
modifier = Modifier.width(20.dp)
|
||||
.clickable(onClick = {
|
||||
onSeeAllQueriesClicked(state.id, state.name)
|
||||
})
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.width(16.dp),
|
||||
imageVector = Icons.Outlined.Preview,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(textColor),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,9 @@ fun DatabasesAndTablesView(
|
|||
},
|
||||
onInsertContentClicked = { id, table ->
|
||||
onAction(DatabaseScreenAction.OnInsertContentClicked(id, table))
|
||||
},
|
||||
onSeeAllQueriesClicked = { id, dbName ->
|
||||
onAction(DatabaseScreenAction.OnSeeAllQueriesClicked(id, dbName))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package io.github.openflocon.flocondesktop.features.database.view.logs
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.openflocon.flocondesktop.features.database.model.DatabaseQueryUiModel
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
|
||||
@Composable
|
||||
fun DatabaseQueryLogItemView(
|
||||
log: DatabaseQueryUiModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.then(
|
||||
if (log.isFromOldSession) {
|
||||
Modifier.alpha(0.8f)
|
||||
} else Modifier
|
||||
), horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.width(60.dp),
|
||||
text = log.dateFormatted,
|
||||
style = FloconTheme.typography.bodySmall,
|
||||
textAlign = TextAlign.Center,
|
||||
color = FloconTheme.colorPalette.onSurface
|
||||
.copy(alpha = 0.6f)
|
||||
)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = log.sqlQuery,
|
||||
style = FloconTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
|
||||
color = findColor(log.type)
|
||||
)
|
||||
log.bindArgs?.let {
|
||||
Text(
|
||||
text = "Args: ${log.bindArgs}",
|
||||
style = FloconTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
|
||||
color = FloconTheme.colorPalette.onSurface.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun findColor(type: DatabaseQueryUiModel.Type?): Color {
|
||||
return when (type) {
|
||||
DatabaseQueryUiModel.Type.Select -> Color(0xFF28A745)
|
||||
DatabaseQueryUiModel.Type.Insert -> Color(0xFF007BFF)
|
||||
DatabaseQueryUiModel.Type.Update -> Color(0xFFFFC107)
|
||||
DatabaseQueryUiModel.Type.Delete -> Color(0xFFDC3545)
|
||||
DatabaseQueryUiModel.Type.Transaction,
|
||||
null -> FloconTheme.colorPalette.onSurface
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
package io.github.openflocon.flocondesktop.features.database.view.logs
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Upload
|
||||
import androidx.compose.material.icons.outlined.UploadFile
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.InputChip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import io.github.openflocon.flocondesktop.common.ui.ContextualView
|
||||
import io.github.openflocon.flocondesktop.features.database.DatabaseQueryLogsViewModel
|
||||
import io.github.openflocon.flocondesktop.features.database.model.FilterChipUiModel
|
||||
import io.github.openflocon.flocondesktop.features.network.list.model.NetworkAction
|
||||
import io.github.openflocon.library.designsystem.FloconTheme
|
||||
import io.github.openflocon.library.designsystem.common.FloconContextMenuItem
|
||||
import io.github.openflocon.library.designsystem.components.FloconDropdownMenuItem
|
||||
import io.github.openflocon.library.designsystem.components.FloconIcon
|
||||
import io.github.openflocon.library.designsystem.components.FloconIconButton
|
||||
import io.github.openflocon.library.designsystem.components.FloconIconToggleButton
|
||||
import io.github.openflocon.library.designsystem.components.FloconOverflow
|
||||
import io.github.openflocon.library.designsystem.components.FloconPageTopBar
|
||||
import io.github.openflocon.library.designsystem.components.FloconTextFieldWithoutM3
|
||||
import io.github.openflocon.library.designsystem.components.defaultPlaceHolder
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
@Composable
|
||||
fun DatabaseQueryLogsView(
|
||||
dbName: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val viewModel: DatabaseQueryLogsViewModel = koinViewModel(
|
||||
key = dbName,
|
||||
parameters = { parametersOf(dbName) }
|
||||
)
|
||||
|
||||
val logs = viewModel.logs.collectAsLazyPagingItems()
|
||||
val showTransactions by viewModel.showTransactions.collectAsStateWithLifecycle()
|
||||
val filterChips by viewModel.filterChips.collectAsStateWithLifecycle()
|
||||
val searchQuery by viewModel.searchQuery.collectAsStateWithLifecycle()
|
||||
|
||||
Column(modifier = modifier) {
|
||||
DatabaseLogsHeader(
|
||||
modifier = Modifier.fillMaxWidth().border(
|
||||
width = 1.dp,
|
||||
color = FloconTheme.colorPalette.secondary,
|
||||
shape = FloconTheme.shapes.medium
|
||||
),
|
||||
searchQuery = searchQuery,
|
||||
onSearchQueryChanged = viewModel::onSearchQueryChanged,
|
||||
toggleShowTransactions = viewModel::toggleShowTransactions,
|
||||
removeFilterChip = viewModel::removeFilterChip,
|
||||
addIncludeFilter = viewModel::addIncludeFilter,
|
||||
addExcludeFilter = viewModel::addExcludeFilter,
|
||||
toggleFilterType = viewModel::toggleFilterType,
|
||||
showTransactions = showTransactions,
|
||||
filterChips = filterChips,
|
||||
exportToCsv = viewModel::exportToCsv,
|
||||
exportToMarkdown = viewModel::exportToMarkdown,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().padding(top = 12.dp)
|
||||
.clip(FloconTheme.shapes.medium)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = FloconTheme.colorPalette.secondary,
|
||||
shape = FloconTheme.shapes.medium
|
||||
)
|
||||
.background(
|
||||
color = FloconTheme.colorPalette.primary,
|
||||
shape = FloconTheme.shapes.medium
|
||||
)
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(logs.itemCount) { index ->
|
||||
val log = logs[index]
|
||||
if (log != null) {
|
||||
ContextualView(
|
||||
items = listOf(
|
||||
FloconContextMenuItem.Item("Copy Query") {
|
||||
viewModel.copyQuery(log.sqlQuery)
|
||||
},
|
||||
FloconContextMenuItem.Item("Copy Args") {
|
||||
viewModel.copyArgs(log.bindArgs)
|
||||
},
|
||||
FloconContextMenuItem.Item("Copy as SQL") {
|
||||
viewModel.copyAsSql(log.sqlQuery, log.bindArgs)
|
||||
},
|
||||
FloconContextMenuItem.Separator(),
|
||||
FloconContextMenuItem.Item("Filter In") {
|
||||
viewModel.addFilter(
|
||||
log.sqlQuery,
|
||||
FilterChipUiModel.FilterType.INCLUDE
|
||||
)
|
||||
},
|
||||
FloconContextMenuItem.Item("Filter Out") {
|
||||
viewModel.addFilter(
|
||||
log.sqlQuery,
|
||||
FilterChipUiModel.FilterType.EXCLUDE
|
||||
)
|
||||
}
|
||||
)
|
||||
) {
|
||||
DatabaseQueryLogItemView(
|
||||
log = log,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
HorizontalDivider(color = FloconTheme.colorPalette.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DatabaseLogsHeader(
|
||||
searchQuery: String,
|
||||
showTransactions: Boolean,
|
||||
onSearchQueryChanged: (String) -> Unit,
|
||||
addIncludeFilter: () -> Unit,
|
||||
addExcludeFilter: () -> Unit,
|
||||
toggleShowTransactions: () -> Unit,
|
||||
toggleFilterType: (FilterChipUiModel) -> Unit,
|
||||
removeFilterChip: (FilterChipUiModel) -> Unit,
|
||||
exportToCsv: () -> Unit,
|
||||
exportToMarkdown: () -> Unit,
|
||||
filterChips: List<FilterChipUiModel>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FloconPageTopBar(modifier) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FloconTextFieldWithoutM3(
|
||||
value = searchQuery,
|
||||
onValueChange = onSearchQueryChanged,
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { defaultPlaceHolder("Filter logs...") },
|
||||
containerColor = FloconTheme.colorPalette.secondary,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
addIncludeFilter()
|
||||
}
|
||||
),
|
||||
leadingComponent = {
|
||||
FloconIcon(
|
||||
imageVector = Icons.Outlined.Search,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
androidx.compose.material3.IconButton(onClick = addIncludeFilter) {
|
||||
Icon(imageVector = Icons.Default.Add, contentDescription = "Include")
|
||||
}
|
||||
androidx.compose.material3.IconButton(onClick = addExcludeFilter) {
|
||||
Icon(imageVector = Icons.Default.Remove, contentDescription = "Exclude")
|
||||
}
|
||||
}
|
||||
|
||||
FilterChip(
|
||||
selected = !showTransactions,
|
||||
onClick = {
|
||||
toggleShowTransactions()
|
||||
},
|
||||
label = {
|
||||
Text("Hide transactions", style = FloconTheme.typography.bodySmall)
|
||||
},
|
||||
leadingIcon = {
|
||||
if (!showTransactions) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
FloconOverflow {
|
||||
FloconDropdownMenuItem(
|
||||
text = "Export CSV",
|
||||
leadingIcon = Icons.Outlined.Upload,
|
||||
onClick = { exportToCsv() }
|
||||
)
|
||||
FloconDropdownMenuItem(
|
||||
text = "Export Markdown",
|
||||
leadingIcon = Icons.Outlined.UploadFile,
|
||||
onClick = { exportToMarkdown() }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
FilterChips(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
filterChips = filterChips,
|
||||
toggleFilterType = toggleFilterType,
|
||||
removeFilterChip = removeFilterChip,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FilterChips(
|
||||
modifier: Modifier = Modifier,
|
||||
filterChips: List<FilterChipUiModel>,
|
||||
toggleFilterType: (FilterChipUiModel) -> Unit,
|
||||
removeFilterChip: (FilterChipUiModel) -> Unit
|
||||
) {
|
||||
val itemsColor = FloconTheme.colorPalette.onSecondary
|
||||
if (filterChips.isNotEmpty()) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
filterChips.fastForEach { chip ->
|
||||
InputChip(
|
||||
selected = true,
|
||||
onClick = { toggleFilterType(chip) },
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
label = {
|
||||
Text(
|
||||
chip.text,
|
||||
style = FloconTheme.typography.bodySmall,
|
||||
color = itemsColor,
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = if (chip.type == FilterChipUiModel.FilterType.INCLUDE) Icons.Default.Add else Icons.Default.Remove,
|
||||
contentDescription = null,
|
||||
tint = itemsColor,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
tint = itemsColor,
|
||||
contentDescription = "Remove",
|
||||
modifier = Modifier.clip(CircleShape).size(14.dp)
|
||||
.clickable {
|
||||
removeFilterChip(chip)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
package io.github.openflocon.data.core.database.datasource
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import io.github.openflocon.domain.common.Either
|
||||
import io.github.openflocon.domain.database.models.DatabaseFavoriteQueryDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseTableDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseId
|
||||
import io.github.openflocon.domain.database.models.FilterQueryLogDomainModel
|
||||
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -58,4 +61,23 @@ interface LocalDatabaseDataSource {
|
|||
databaseId: String,
|
||||
id: Long
|
||||
): DatabaseFavoriteQueryDomainModel?
|
||||
|
||||
suspend fun saveQueryLog(
|
||||
log: DatabaseQueryLogDomainModel,
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
)
|
||||
|
||||
fun observeQueryLogs(
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>
|
||||
): Flow<PagingData<DatabaseQueryLogDomainModel>>
|
||||
|
||||
suspend fun getQueryLogs(
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>
|
||||
): List<DatabaseQueryLogDomainModel>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package io.github.openflocon.data.core.database.datasource
|
|||
|
||||
import io.github.openflocon.domain.common.Either
|
||||
import io.github.openflocon.domain.database.models.DatabaseExecuteSqlResponseDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseId
|
||||
import io.github.openflocon.domain.database.models.ResponseAndRequestIdDomainModel
|
||||
|
|
@ -23,4 +24,6 @@ interface QueryDatabaseRemoteDataSource {
|
|||
fun getDeviceDatabases(message: FloconIncomingMessageDomainModel): List<DeviceDataBaseDomainModel>
|
||||
|
||||
fun getReceiveQuery(message: FloconIncomingMessageDomainModel): ResponseAndRequestIdDomainModel?
|
||||
|
||||
fun getQueryLogs(message: FloconIncomingMessageDomainModel): DatabaseQueryLogDomainModel?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package io.github.openflocon.data.core.database.repository
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import io.github.openflocon.data.core.database.datasource.DeviceDatabasesRemoteDataSource
|
||||
import io.github.openflocon.data.core.database.datasource.LocalDatabaseDataSource
|
||||
import io.github.openflocon.data.core.database.datasource.QueryDatabaseRemoteDataSource
|
||||
|
|
@ -8,9 +9,11 @@ import io.github.openflocon.domain.common.DispatcherProvider
|
|||
import io.github.openflocon.domain.common.Either
|
||||
import io.github.openflocon.domain.database.models.DatabaseExecuteSqlResponseDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseFavoriteQueryDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseTableDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseId
|
||||
import io.github.openflocon.domain.database.models.FilterQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.repository.DatabaseRepository
|
||||
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
|
||||
import io.github.openflocon.domain.messages.models.FloconIncomingMessageDomainModel
|
||||
|
|
@ -91,6 +94,17 @@ class DatabaseRepositoryImpl(
|
|||
databases = items,
|
||||
)
|
||||
}
|
||||
|
||||
Protocol.FromDevice.Database.Method.LogQuery -> {
|
||||
queryDatabaseDataSource.getQueryLogs(
|
||||
message,
|
||||
)?.let {
|
||||
localDatabaseDataSource.saveQueryLog(
|
||||
it,
|
||||
deviceIdAndPackageName = deviceIdAndPackageName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -191,4 +205,32 @@ class DatabaseRepositoryImpl(
|
|||
id = id,
|
||||
)
|
||||
}
|
||||
|
||||
override fun observeQueryLogs(
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>,
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
): Flow<PagingData<DatabaseQueryLogDomainModel>> {
|
||||
return localDatabaseDataSource.observeQueryLogs(
|
||||
dbName = dbName,
|
||||
showTransactions = showTransactions,
|
||||
filters = filters,
|
||||
deviceIdAndPackageName = deviceIdAndPackageName,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getQueryLogs(
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>,
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
): List<DatabaseQueryLogDomainModel> {
|
||||
return localDatabaseDataSource.getQueryLogs(
|
||||
dbName = dbName,
|
||||
showTransactions = showTransactions,
|
||||
filters = filters,
|
||||
deviceIdAndPackageName = deviceIdAndPackageName,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package io.github.openflocon.data.local.database.dao
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.RoomRawQuery
|
||||
import io.github.openflocon.data.local.database.models.DatabaseQueryLogEntity
|
||||
|
||||
@Dao
|
||||
interface DatabaseQueryLogDao {
|
||||
@Insert
|
||||
suspend fun insert(entity: DatabaseQueryLogEntity)
|
||||
|
||||
@androidx.room.RawQuery(observedEntities = [DatabaseQueryLogEntity::class])
|
||||
fun getPagingSource(
|
||||
query: RoomRawQuery,
|
||||
): PagingSource<Int, DatabaseQueryLogEntity>
|
||||
|
||||
@androidx.room.RawQuery(observedEntities = [DatabaseQueryLogEntity::class])
|
||||
suspend fun getLogs(
|
||||
query: RoomRawQuery,
|
||||
): List<DatabaseQueryLogEntity>
|
||||
}
|
||||
|
|
@ -1,26 +1,37 @@
|
|||
package io.github.openflocon.data.local.database.datasource
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import io.github.openflocon.data.core.database.datasource.LocalDatabaseDataSource
|
||||
import io.github.openflocon.data.local.database.dao.DatabaseQueryLogDao
|
||||
import io.github.openflocon.data.local.database.dao.QueryDao
|
||||
import io.github.openflocon.data.local.database.dao.TablesDao
|
||||
import io.github.openflocon.data.local.database.mapper.toDomain
|
||||
import io.github.openflocon.data.local.database.mapper.toEntity
|
||||
import androidx.paging.map
|
||||
import androidx.room.RoomRawQuery
|
||||
import io.github.openflocon.data.local.database.models.DatabaseQueryLogEntity
|
||||
import io.github.openflocon.data.local.database.models.FavoriteQueryEntity
|
||||
import io.github.openflocon.data.local.database.models.SuccessQueryEntity
|
||||
import io.github.openflocon.domain.common.Either
|
||||
import io.github.openflocon.domain.common.Success
|
||||
import io.github.openflocon.domain.database.models.DatabaseFavoriteQueryDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseTableDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseId
|
||||
import io.github.openflocon.domain.database.models.FilterQueryLogDomainModel
|
||||
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.let
|
||||
|
||||
internal class LocalDatabaseDataSourceRoom(
|
||||
private val queryDao: QueryDao,
|
||||
private val tablesDao: TablesDao,
|
||||
private val databaseQueryLogDao: DatabaseQueryLogDao,
|
||||
private val json: Json,
|
||||
) : LocalDatabaseDataSource {
|
||||
|
||||
|
|
@ -172,4 +183,146 @@ internal class LocalDatabaseDataSourceRoom(
|
|||
packageName = deviceIdAndPackageName.packageName,
|
||||
id = id,
|
||||
)?.toDomain()
|
||||
|
||||
override suspend fun saveQueryLog(
|
||||
log: DatabaseQueryLogDomainModel,
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
) {
|
||||
databaseQueryLogDao.insert(
|
||||
DatabaseQueryLogEntity(
|
||||
dbName = log.dbName,
|
||||
sqlQuery = log.sqlQuery,
|
||||
bindArgs = log.bindArgs?.let { json.encodeToString(it) },
|
||||
timestamp = log.timestamp,
|
||||
isTransaction = log.isTransaction,
|
||||
deviceId = deviceIdAndPackageName.deviceId,
|
||||
packageName = deviceIdAndPackageName.packageName,
|
||||
appInstance = log.appInstance
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun observeQueryLogs(
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>
|
||||
): Flow<PagingData<DatabaseQueryLogDomainModel>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 20,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
val query = buildQuery(
|
||||
dbName = dbName,
|
||||
showTransactions = showTransactions,
|
||||
filters = filters,
|
||||
deviceIdAndPackageName = deviceIdAndPackageName
|
||||
)
|
||||
|
||||
databaseQueryLogDao.getPagingSource(
|
||||
query = query,
|
||||
)
|
||||
}
|
||||
).flow
|
||||
.map { pagingData ->
|
||||
pagingData.map { entity ->
|
||||
DatabaseQueryLogDomainModel(
|
||||
dbName = entity.dbName,
|
||||
sqlQuery = entity.sqlQuery,
|
||||
bindArgs = entity.bindArgs?.let {
|
||||
json.decodeFromString(it)
|
||||
} ?: emptyList(),
|
||||
timestamp = entity.timestamp,
|
||||
isTransaction = entity.isTransaction,
|
||||
appInstance = entity.appInstance
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getQueryLogs(
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>
|
||||
): List<DatabaseQueryLogDomainModel> {
|
||||
val query = buildQuery(
|
||||
dbName = dbName,
|
||||
showTransactions = showTransactions,
|
||||
filters = filters,
|
||||
deviceIdAndPackageName = deviceIdAndPackageName
|
||||
)
|
||||
return databaseQueryLogDao.getLogs(query).map { entity ->
|
||||
DatabaseQueryLogDomainModel(
|
||||
dbName = entity.dbName,
|
||||
sqlQuery = entity.sqlQuery,
|
||||
bindArgs = entity.bindArgs?.let {
|
||||
json.decodeFromString(it)
|
||||
} ?: emptyList(),
|
||||
timestamp = entity.timestamp,
|
||||
isTransaction = entity.isTransaction,
|
||||
appInstance = entity.appInstance
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildQuery(
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>,
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
): RoomRawQuery {
|
||||
val queryParams = ArrayList<Any>()
|
||||
var queryString = "SELECT * FROM DatabaseQueryLogEntity WHERE deviceId = ? AND packageName = ? AND dbName = ?"
|
||||
queryParams.add(deviceIdAndPackageName.deviceId)
|
||||
queryParams.add(deviceIdAndPackageName.packageName)
|
||||
queryParams.add(dbName)
|
||||
|
||||
if (!showTransactions) {
|
||||
queryString += " AND isTransaction = 0"
|
||||
}
|
||||
|
||||
val includes = filters.filter { it.type == FilterQueryLogDomainModel.FilterType.INCLUDE }
|
||||
val excludes = filters.filter { it.type == FilterQueryLogDomainModel.FilterType.EXCLUDE }
|
||||
|
||||
if (includes.isNotEmpty()) {
|
||||
queryString += " AND ("
|
||||
includes.forEachIndexed { index, filter ->
|
||||
if (index > 0) queryString += " OR "
|
||||
queryString += "(sqlQuery LIKE ? OR bindArgs LIKE ?)"
|
||||
queryParams.add("%${filter.text}%")
|
||||
queryParams.add("%${filter.text}%")
|
||||
}
|
||||
queryString += ")"
|
||||
}
|
||||
|
||||
if (excludes.isNotEmpty()) {
|
||||
excludes.forEach { filter ->
|
||||
queryString += " AND NOT (sqlQuery LIKE ? OR bindArgs LIKE ?)"
|
||||
queryParams.add("%${filter.text}%")
|
||||
queryParams.add("%${filter.text}%")
|
||||
}
|
||||
}
|
||||
|
||||
queryString += " ORDER BY timestamp DESC"
|
||||
|
||||
val query = RoomRawQuery(
|
||||
sql = queryString,
|
||||
onBindStatement = { statement ->
|
||||
queryParams.forEachIndexed { index, arg ->
|
||||
when (arg) {
|
||||
is String -> statement.bindText(index + 1, arg)
|
||||
is Long -> statement.bindLong(index + 1, arg)
|
||||
is Int -> statement.bindLong(index + 1, arg.toLong())
|
||||
is Boolean -> statement.bindLong(index + 1, if (arg) 1L else 0L)
|
||||
is Double -> statement.bindDouble(index + 1, arg)
|
||||
is Float -> statement.bindDouble(index + 1, arg.toDouble())
|
||||
else -> statement.bindText(index + 1, arg.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package io.github.openflocon.data.local.database.models
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class DatabaseQueryLogEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
val dbName: String,
|
||||
val sqlQuery: String,
|
||||
val bindArgs: String?,
|
||||
val timestamp: Long,
|
||||
val isTransaction: Boolean,
|
||||
|
||||
val deviceId: String,
|
||||
val packageName: String,
|
||||
val appInstance: Long,
|
||||
)
|
||||
|
|
@ -5,6 +5,7 @@ import com.flocon.data.remote.database.mapper.decodeDeviceDatabases
|
|||
import com.flocon.data.remote.database.mapper.decodeReceivedQuery
|
||||
import com.flocon.data.remote.database.models.DatabaseExecuteSqlResponseDataModel
|
||||
import com.flocon.data.remote.database.models.DatabaseOutgoingQueryMessage
|
||||
import com.flocon.data.remote.database.models.DatabaseQueryLogModel
|
||||
import com.flocon.data.remote.database.models.ResponseAndRequestIdDataModel
|
||||
import com.flocon.data.remote.database.models.toDeviceDatabasesDomain
|
||||
import com.flocon.data.remote.models.FloconOutgoingMessageDataModel
|
||||
|
|
@ -17,6 +18,7 @@ import io.github.openflocon.domain.common.Either
|
|||
import io.github.openflocon.domain.common.Failure
|
||||
import io.github.openflocon.domain.common.Success
|
||||
import io.github.openflocon.domain.database.models.DatabaseExecuteSqlResponseDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseId
|
||||
import io.github.openflocon.domain.database.models.ResponseAndRequestIdDomainModel
|
||||
|
|
@ -84,6 +86,30 @@ class QueryDatabaseRemoteDataSourceImpl(
|
|||
override fun getDeviceDatabases(message: FloconIncomingMessageDomainModel): List<DeviceDataBaseDomainModel> = toDeviceDatabasesDomain(json.decodeDeviceDatabases(message.body).orEmpty())
|
||||
|
||||
override fun getReceiveQuery(message: FloconIncomingMessageDomainModel): ResponseAndRequestIdDomainModel? = json.decodeReceivedQuery(message.body)?.toDomain()
|
||||
|
||||
override fun getQueryLogs(
|
||||
message: FloconIncomingMessageDomainModel,
|
||||
): DatabaseQueryLogDomainModel? {
|
||||
return json.decodeFromString<DatabaseQueryLogModel>(message.body)?.let {
|
||||
|
||||
val upperQuery = it.sqlQuery.trim().uppercase()
|
||||
val isTransaction = upperQuery == "BEGIN TRANSACTION" ||
|
||||
upperQuery == "COMMIT" ||
|
||||
upperQuery == "ROLLBACK" ||
|
||||
upperQuery == "END TRANSACTION" ||
|
||||
upperQuery == "TRANSACTION SUCCESSFUL" ||
|
||||
upperQuery == "BEGIN IMMEDIATE TRANSACTION"
|
||||
|
||||
DatabaseQueryLogDomainModel(
|
||||
dbName = it.dbName,
|
||||
sqlQuery = it.sqlQuery,
|
||||
bindArgs = it.bindArgs.orEmpty(),
|
||||
timestamp = it.timestamp,
|
||||
isTransaction = isTransaction,
|
||||
appInstance = message.appInstance,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO internal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.flocon.data.remote.database.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DatabaseQueryLogModel(
|
||||
val dbName: String,
|
||||
val sqlQuery: String,
|
||||
val bindArgs: List<String>?,
|
||||
val timestamp: Long,
|
||||
)
|
||||
|
|
@ -35,6 +35,7 @@ object Protocol {
|
|||
object Method {
|
||||
const val Query = "query"
|
||||
const val GetDatabases = "getDatabases"
|
||||
const val LogQuery = "logQuery"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package io.github.openflocon.domain.database
|
|||
import io.github.openflocon.domain.database.usecase.AskForDeviceDatabasesUseCase
|
||||
import io.github.openflocon.domain.database.usecase.ExecuteDatabaseQueryUseCase
|
||||
import io.github.openflocon.domain.database.usecase.GetDatabaseByIdUseCase
|
||||
import io.github.openflocon.domain.database.usecase.ObserveDatabaseQueryLogsUseCase
|
||||
import io.github.openflocon.domain.database.usecase.GetDeviceDatabaseTablesUseCase
|
||||
import io.github.openflocon.domain.database.usecase.GetTableColumnsUseCase
|
||||
import io.github.openflocon.domain.database.usecase.ObserveCurrentDeviceSelectedDatabaseAndTablesUseCase
|
||||
|
|
@ -28,4 +29,5 @@ internal val databaseModule = module {
|
|||
factoryOf(::ObserveFavoriteQueriesUseCase)
|
||||
factoryOf(::SaveQueryAsFavoriteDatabaseUseCase)
|
||||
factoryOf(::GetFavoriteQueryByIdDatabaseUseCase)
|
||||
factoryOf(::ObserveDatabaseQueryLogsUseCase)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package io.github.openflocon.domain.database.models
|
||||
|
||||
import io.github.openflocon.domain.database.utils.injectSqlArgs
|
||||
|
||||
data class DatabaseQueryLogDomainModel(
|
||||
val dbName: String,
|
||||
val sqlQuery: String,
|
||||
val bindArgs: List<String>?,
|
||||
val timestamp: Long,
|
||||
val isTransaction: Boolean,
|
||||
val appInstance: Long,
|
||||
)
|
||||
|
||||
fun DatabaseQueryLogDomainModel.toFullSql(): String {
|
||||
return injectSqlArgs(sqlQuery, bindArgs)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package io.github.openflocon.domain.database.models
|
||||
|
||||
data class FilterQueryLogDomainModel(
|
||||
val text: String,
|
||||
val type: FilterType,
|
||||
) {
|
||||
enum class FilterType {
|
||||
INCLUDE,
|
||||
EXCLUDE
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,14 @@
|
|||
package io.github.openflocon.domain.database.repository
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import io.github.openflocon.domain.common.Either
|
||||
import io.github.openflocon.domain.database.models.DatabaseExecuteSqlResponseDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseFavoriteQueryDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.DatabaseTableDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseDomainModel
|
||||
import io.github.openflocon.domain.database.models.DeviceDataBaseId
|
||||
import io.github.openflocon.domain.database.models.FilterQueryLogDomainModel
|
||||
import io.github.openflocon.domain.device.models.DeviceIdAndPackageNameDomainModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -72,6 +75,20 @@ interface DatabaseRepository {
|
|||
suspend fun getFavorite(
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
databaseId: String,
|
||||
id: Long
|
||||
id: Long,
|
||||
): DatabaseFavoriteQueryDomainModel?
|
||||
|
||||
fun observeQueryLogs(
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>,
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
): Flow<PagingData<DatabaseQueryLogDomainModel>>
|
||||
|
||||
suspend fun getQueryLogs(
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>,
|
||||
deviceIdAndPackageName: DeviceIdAndPackageNameDomainModel,
|
||||
): List<DatabaseQueryLogDomainModel>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package io.github.openflocon.domain.database.usecase
|
||||
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.FilterQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.repository.DatabaseRepository
|
||||
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdAndPackageNameUseCase
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class GetDatabaseQueryLogsUseCase(
|
||||
private val databaseRepository: DatabaseRepository,
|
||||
private val observeCurrentDeviceIdAndPackageNameUseCase: ObserveCurrentDeviceIdAndPackageNameUseCase,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>
|
||||
): List<DatabaseQueryLogDomainModel> {
|
||||
val current = observeCurrentDeviceIdAndPackageNameUseCase().first() ?: return emptyList()
|
||||
return databaseRepository.getQueryLogs(
|
||||
dbName = dbName,
|
||||
showTransactions = showTransactions,
|
||||
filters = filters,
|
||||
deviceIdAndPackageName = current,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package io.github.openflocon.domain.database.usecase
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import io.github.openflocon.domain.database.models.DatabaseQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.models.FilterQueryLogDomainModel
|
||||
import io.github.openflocon.domain.database.repository.DatabaseRepository
|
||||
import io.github.openflocon.domain.device.usecase.ObserveCurrentDeviceIdAndPackageNameUseCase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class ObserveDatabaseQueryLogsUseCase(
|
||||
private val databaseRepository: DatabaseRepository,
|
||||
private val observeCurrentDeviceIdAndPackageNameUseCase: ObserveCurrentDeviceIdAndPackageNameUseCase,
|
||||
) {
|
||||
operator fun invoke(
|
||||
dbName: String,
|
||||
showTransactions: Boolean,
|
||||
filters: List<FilterQueryLogDomainModel>
|
||||
): Flow<PagingData<DatabaseQueryLogDomainModel>> {
|
||||
return observeCurrentDeviceIdAndPackageNameUseCase().flatMapLatest { current ->
|
||||
if (current == null) {
|
||||
return@flatMapLatest flowOf(PagingData.empty())
|
||||
} else {
|
||||
databaseRepository.observeQueryLogs(
|
||||
dbName = dbName,
|
||||
showTransactions = showTransactions,
|
||||
filters = filters,
|
||||
deviceIdAndPackageName = current,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package io.github.openflocon.domain.database.utils
|
||||
|
||||
fun injectSqlArgs(sql: String, args: List<String>?): String {
|
||||
return if (args.isNullOrEmpty()) {
|
||||
sql
|
||||
} else {
|
||||
var result = sql
|
||||
args.forEach { arg ->
|
||||
result = result.replaceFirst("?", "'$arg'")
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,25 @@
|
|||
package io.github.openflocon.library.designsystem.common
|
||||
|
||||
import androidx.compose.foundation.ContextMenuItem
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class FloconContextMenuItem(
|
||||
label: String,
|
||||
onClick: () -> Unit
|
||||
) : ContextMenuItem(label = label, onClick = onClick) {
|
||||
|
||||
// TODO Add icon
|
||||
@Immutable
|
||||
class Item(label: String, onClick: () -> Unit) : FloconContextMenuItem(label, onClick)
|
||||
|
||||
@Immutable
|
||||
class SubMenu(
|
||||
label: String,
|
||||
val items: List<FloconContextMenuItem>
|
||||
) : FloconContextMenuItem(label, onClick = {})
|
||||
|
||||
@Immutable
|
||||
class Separator : FloconContextMenuItem(label = "", onClick = {})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue