Skip to content

Misc fixes #277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import io.github.inductiveautomation.kindling.utils.FlatScrollPane
import io.github.inductiveautomation.kindling.utils.RendererBase
import io.github.inductiveautomation.kindling.utils.StyledLabel
import io.github.inductiveautomation.kindling.utils.TabStrip
import io.github.inductiveautomation.kindling.utils.attachPopupMenu
import io.github.inductiveautomation.kindling.utils.chooseFiles
import io.github.inductiveautomation.kindling.utils.clipboardString
import io.github.inductiveautomation.kindling.utils.getLogger
Expand Down Expand Up @@ -74,6 +75,7 @@ import javax.swing.JMenu
import javax.swing.JMenuBar
import javax.swing.JMenuItem
import javax.swing.JPanel
import javax.swing.JPopupMenu
import javax.swing.KeyStroke
import javax.swing.SwingConstants.BOTTOM
import javax.swing.SwingConstants.CENTER
Expand Down Expand Up @@ -193,6 +195,23 @@ class MainPanel : JPanel(MigLayout("ins 6, fill, hidemode 3")) {
JButton(openAction).apply {
hideActionText = true
icon = FlatSVGIcon("icons/bx-plus.svg")
attachPopupMenu {
JPopupMenu().apply {
for (tool in Tool.sortedByTitle) {
add(
Action(
name = "Open ${tool.title}",
icon = tool.icon,
) {
fileChooser.fileFilter = tool.filter
fileChooser.chooseFiles(this@MainPanel)?.let { selectedFiles ->
openFiles(selectedFiles, tool)
}
},
)
}
}
}
},
BorderLayout.WEST,
)
Expand Down
183 changes: 68 additions & 115 deletions src/main/kotlin/io/github/inductiveautomation/kindling/idb/IdbView.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package io.github.inductiveautomation.kindling.idb

import com.formdev.flatlaf.extras.FlatSVGIcon
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.extras.components.FlatTabbedPane.TabType
import io.github.inductiveautomation.kindling.core.MultiTool
import io.github.inductiveautomation.kindling.core.Preference
import io.github.inductiveautomation.kindling.core.Preference.Companion.PreferenceCheckbox
import io.github.inductiveautomation.kindling.core.Preference.Companion.preference
import io.github.inductiveautomation.kindling.core.PreferenceCategory
import io.github.inductiveautomation.kindling.core.ToolPanel
import io.github.inductiveautomation.kindling.idb.generic.GenericView
import io.github.inductiveautomation.kindling.idb.metrics.MetricsView
Expand All @@ -19,77 +16,33 @@ import io.github.inductiveautomation.kindling.utils.SQLiteConnection
import io.github.inductiveautomation.kindling.utils.TabStrip
import io.github.inductiveautomation.kindling.utils.get
import io.github.inductiveautomation.kindling.utils.toList
import org.sqlite.SQLiteConnection
import java.nio.file.Path
import java.sql.Connection
import javax.swing.SwingConstants
import kotlin.io.path.name

class IdbView(paths: List<Path>) : ToolPanel() {
private val data = paths.map(::IdbFileData)
private val connections = paths.map(::IdbConnection)

private val tabs = TabStrip().apply {
trailingComponent = null
private val tabs = TabStrip(false).apply {
isTabsClosable = false
tabType = TabType.underlined
tabType = TabType.card
tabHeight = 16
isHideTabAreaWithOneTab = true
tabPlacement = SwingConstants.LEFT
tabRotation = FlatTabbedPane.TabRotation.auto
}

init {
name = paths.first().name
toolTipText = paths.joinToString("\n")

var addedTabs = 0

/*
* Not doing partial subsets of files for now.
* (e.g. Multitool X supports files A and B, Multitool Y supports files C and D)
* i.e. we assume the user is opening multiple IDBs they expect to work together
*/
val supportedMultiTools = MultiIdbTool.entries.filter { tool ->
data.all { tool.supports(it.tables) }
val addedTabs = IdbTool.entries.filter { tool ->
tool.supports(connections)
}.map { tool ->
tabs.addLazyTab(tool.tabName) { tool.open(connections) }
}

if (supportedMultiTools.isNotEmpty()) {
if (IdbViewer.ShowGenericViewWithMultiTools.currentValue || paths.size == 1) {
for ((path, connection, _) in data) {
tabs.addTab(
tabName = path.name,
component = GenericView(connection),
tabTooltip = null,
select = true,
)
}
}
for (tool in supportedMultiTools) {
tabs.addLazyTab(tabName = tool.tabName) { tool.open(data) }
}
addedTabs = supportedMultiTools.size
} else {
for ((_, connection) in data) {
tabs.addTab(
tabName = "Tables",
component = GenericView(connection),
tabTooltip = null,
select = true,
)
}

for (tool in SingleIdbTool.entries) {
for (idbFile in data) {
if (tool.supports(idbFile.tables)) {
tabs.addLazyTab(
tabName = tool.tabName,
) {
tool.open(idbFile)
}
addedTabs += 1
}
}
}
}

if (addedTabs > 0) {
if (addedTabs.isNotEmpty()) {
tabs.selectedIndex = tabs.indices.last
}

Expand All @@ -100,78 +53,90 @@ class IdbView(paths: List<Path>) : ToolPanel() {

override fun removeNotify() {
super.removeNotify()
data.forEach { it.connection.close() }
}

companion object {
fun Connection.getAllTableNames(): List<String> {
if (this !is SQLiteConnection) return emptyList()
return metaData
.getTables("", "", "", null)
.toList { rs -> rs[3] }
for (connection in connections) {
connection.close()
}
}
}

private class IdbConnection(
val path: Path,
) : AutoCloseable {
val connection = SQLiteConnection(path)

internal class IdbFileData(val path: Path) {
val connection = SQLiteConnection(path)
val tables = connection.getAllTableNames()
val tables = connection.metaData
.getTables("", "", "", null)
.toList<String> { rs -> rs["TABLE_NAME"] }

operator fun component1() = path
operator fun component2() = connection
operator fun component3() = tables
override fun close() {
connection.close()
}
}

private sealed interface IdbTool {
fun supports(tables: List<String>): Boolean

fun open(fileData: IdbView.IdbFileData): ToolPanel
private enum class IdbTool {
Generic {
override fun supports(connections: List<IdbConnection>): Boolean {
return connections.size == 1
}

val tabName: String
}
override fun open(connections: List<IdbConnection>): ToolPanel {
return GenericView(connections.single().connection)
}

private enum class SingleIdbTool : IdbTool {
override val tabName: String = "Tables"
},
Metrics {
override fun supports(tables: List<String>): Boolean = "SYSTEM_METRICS" in tables
override fun open(fileData: IdbView.IdbFileData): ToolPanel = MetricsView(fileData.connection)
override fun supports(connections: List<IdbConnection>): Boolean {
return connections.singleOrNull { "SYSTEM_METRICS" in it.tables } != null
}
override fun open(connections: List<IdbConnection>): ToolPanel {
return MetricsView(connections.single().connection)
}
},
Images {
override fun supports(tables: List<String>): Boolean = "IMAGES" in tables
override fun open(fileData: IdbView.IdbFileData): ToolPanel = ImagesPanel(fileData.connection)
override fun supports(connections: List<IdbConnection>): Boolean {
return connections.singleOrNull { "IMAGES" in it.tables } != null
}
override fun open(connections: List<IdbConnection>): ToolPanel {
return ImagesPanel(connections.single().connection)
}
},
TagConfig {
override fun supports(tables: List<String>): Boolean = "TAGCONFIG" in tables
override fun open(fileData: IdbView.IdbFileData): ToolPanel = TagConfigView(fileData.connection)
override fun supports(connections: List<IdbConnection>): Boolean {
return connections.singleOrNull { "TAGCONFIG" in it.tables } != null
}
override fun open(connections: List<IdbConnection>): ToolPanel {
return TagConfigView(connections.single().connection)
}

override val tabName: String = "Tag Config"
},
;
Logs {
override fun supports(connections: List<IdbConnection>): Boolean {
return connections.all { conn -> "logging_event" in conn.tables }
}

override val tabName: String = name
}
override fun open(connections: List<IdbConnection>): ToolPanel {
val paths = connections.map { it.path }

private enum class MultiIdbTool : IdbTool {
Logs {
override fun supports(tables: List<String>): Boolean = "logging_event" in tables
override fun open(fileData: List<IdbView.IdbFileData>): ToolPanel {
val paths = fileData.map { it.path }

val logFiles = fileData.map { (_, connection, _) ->
LogFile(
connection.parseLogs(),
)
val logFiles = connections.map { it ->
LogFile(it.connection.parseLogs())
}

return SystemLogPanel(paths, logFiles)
}
},
;

abstract fun open(fileData: List<IdbView.IdbFileData>): ToolPanel
override fun open(fileData: IdbView.IdbFileData): ToolPanel = open(listOf(fileData))
override val tabName = name
open val tabName: String = name

abstract fun supports(connections: List<IdbConnection>): Boolean

abstract fun open(connections: List<IdbConnection>): ToolPanel
}

data object IdbViewer : MultiTool, PreferenceCategory {
data object IdbViewer : MultiTool {
override val serialKey = "idb-viewer"
override val title = "SQLite Database"
override val description = "SQLite Database (.idb)"
Expand All @@ -181,16 +146,4 @@ data object IdbViewer : MultiTool, PreferenceCategory {
override fun open(path: Path): ToolPanel = IdbView(listOf(path))

override fun open(paths: List<Path>): ToolPanel = IdbView(paths)

override val displayName = "IDB Viewer"

val ShowGenericViewWithMultiTools: Preference<Boolean> = preference(
name = "Include Generic IDB Tabs with Multiple Files",
default = false,
editor = {
PreferenceCheckbox("Include tabs for Generic IDB browser when opening multiple IDB Files")
},
)

override val preferences: List<Preference<*>> = listOf(ShowGenericViewWithMultiTools)
}
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ class GenericView(connection: Connection) : ToolPanel("ins 0, fill, hidemode 3")
override val icon: Icon? = null

companion object {
private val TIMESTAMP_COLUMN_NAMES = setOf("timestamp", "timestmp", "t_stamp", "tstamp")
private val TIMESTAMP_COLUMN_NAMES = setOf(
"timestamp",
"timestmp",
"t_stamp",
"tstamp",
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.inductiveautomation.kindling.utils

import org.intellij.lang.annotations.Language
import org.sqlite.SQLiteConnection
import org.sqlite.SQLiteDataSource
import java.math.BigDecimal
import java.nio.file.Path
Expand Down Expand Up @@ -78,17 +79,16 @@ inline fun <reified T> sqliteCoercion(raw: Any?): T {
} as T
}

@Suppress("FunctionName")
fun SQLiteConnection(
path: Path,
readOnly: Boolean = true,
): Connection {
): SQLiteConnection {
return SQLiteDataSource().apply {
url = "jdbc:sqlite:file:$path"
setReadOnly(readOnly)
setJournalMode("OFF")
setSynchronous("OFF")
}.connection
}.connection as SQLiteConnection
}

val JDBCType.javaType: Class<*>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ open class TabStrip(val tabsEditable: Boolean = false) : DnDTabbedPane() {
tabLayoutPolicy = SCROLL_TAB_LAYOUT
tabAlignment = TabAlignment.leading
isTabsClosable = true
maximumTabWidth = 250

setTabCloseCallback { _, i ->
removeTabAt(i)
Expand Down Expand Up @@ -140,7 +141,7 @@ open class TabStrip(val tabsEditable: Boolean = false) : DnDTabbedPane() {
icon: Icon? = component.icon,
select: Boolean = true,
) where T : Container, T : FloatableComponent {
addTab(tabName.truncate(30), icon, component, tabTooltip)
addTab(tabName, icon, component, tabTooltip)
if (select) {
selectedIndex = indices.last
}
Expand Down