addTo(temporal: R, amount: Long): R =
20 | duration.multipliedBy(amount).addTo(temporal) as R
21 |
22 | override fun between(temporal1Inclusive: Temporal, temporal2Exclusive: Temporal): Long {
23 | return Duration.between(temporal1Inclusive, temporal2Exclusive).dividedBy(duration)
24 | }
25 |
26 | override fun toString(): String = duration.toString()
27 |
28 | companion object {
29 | private const val SECONDS_PER_DAY = 86_400
30 | private const val NANOS_PER_SECOND = 1_000_000_000L
31 | private const val NANOS_PER_DAY = NANOS_PER_SECOND * SECONDS_PER_DAY
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/deser/support/ClassField.java:
--------------------------------------------------------------------------------
1 | package deser.support;
2 |
3 | /***********************************************************
4 | * Support class for serialization data parsing that holds
5 | * details of a class field to enable the field value to
6 | * be read from the stream.
7 | *
8 | * Written by Nicky Bloor (@NickstaDB).
9 | **********************************************************/
10 | public class ClassField {
11 | /**
12 | * The field type code
13 | */
14 | private final byte typeCode;
15 | /**
16 | * The field name
17 | */
18 | private String name;
19 |
20 | /*******************
21 | * Construct the ClassField object.
22 | *
23 | * @param typeCode The field type code.
24 | ******************/
25 | public ClassField(byte typeCode) {
26 | this.typeCode = typeCode;
27 | this.name = "";
28 | }
29 |
30 | /*******************
31 | * Get the field type code.
32 | *
33 | * @return The field type code.
34 | ******************/
35 | public byte getTypeCode() {
36 | return this.typeCode;
37 | }
38 |
39 | /*******************
40 | * Set the field name.
41 | *
42 | * @param name The field name.
43 | ******************/
44 | public void setName(String name) {
45 | this.name = name;
46 | }
47 |
48 | /*******************
49 | * Get the field name.
50 | *
51 | * @return The field name.
52 | ******************/
53 | public String getName() {
54 | return this.name;
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/internal/DetailsIcon.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.internal
2 |
3 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
4 | import org.jdesktop.swingx.JXTable
5 | import org.jdesktop.swingx.decorator.HighlighterFactory
6 | import java.awt.event.MouseAdapter
7 | import java.awt.event.MouseEvent
8 | import javax.swing.JLabel
9 | import javax.swing.Popup
10 | import javax.swing.PopupFactory
11 |
12 | class DetailsIcon(details: Map) : JLabel(DETAILS_ICON) {
13 | private val table = JXTable(DetailsModel(details.entries.toList())).apply {
14 | addHighlighter(HighlighterFactory.createSimpleStriping())
15 | packAll()
16 | }
17 |
18 | init {
19 | alignmentY = 0.7F
20 |
21 | addMouseListener(
22 | object : MouseAdapter() {
23 | var popup: Popup? = null
24 |
25 | override fun mouseEntered(e: MouseEvent) {
26 | popup = PopupFactory.getSharedInstance().getPopup(
27 | this@DetailsIcon,
28 | table,
29 | locationOnScreen.x + DETAILS_ICON.iconWidth,
30 | locationOnScreen.y,
31 | ).also {
32 | it.show()
33 | }
34 | }
35 |
36 | override fun mouseExited(e: MouseEvent) {
37 | popup?.hide()
38 | }
39 | },
40 | )
41 | }
42 |
43 | companion object {
44 | private val DETAILS_ICON = FlatActionIcon("icons/bx-search.svg")
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/core/LinkHandlingStrategy.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.core
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.launch
6 | import kotlinx.serialization.Serializable
7 | import java.awt.Desktop
8 | import java.net.URI
9 | import javax.swing.event.HyperlinkEvent
10 |
11 | @Serializable
12 | @Suppress("ktlint:standard:trailing-comma-on-declaration-site")
13 | enum class LinkHandlingStrategy(val description: String) {
14 | OpenInBrowser("Open links in default browser") {
15 | override fun handleEvent(event: HyperlinkEvent) {
16 | Desktop.getDesktop().browse(event.url.toURI())
17 | }
18 | },
19 | OpenInIde("Open links in IntelliJ (requires Youtrack plugin)") {
20 | private val scope = CoroutineScope(Dispatchers.IO)
21 |
22 | override fun handleEvent(event: HyperlinkEvent) {
23 | for (port in 63330..63339) {
24 | scope.launch {
25 | try {
26 | URI.create("http://localhost:$port/file?${event.url.query}").toURL().openConnection().getInputStream().use { input ->
27 | input.readAllBytes()
28 | }
29 | } catch (e: Exception) {
30 | // ignored - the Youtrack plugin listens on any of the 10 ports it can,
31 | // so we have to blindly broadcast to them all
32 | }
33 | }
34 | }
35 | }
36 | };
37 |
38 | abstract fun handleEvent(event: HyperlinkEvent)
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/Action.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import java.awt.event.ActionEvent
4 | import javax.swing.AbstractAction
5 | import javax.swing.Icon
6 | import javax.swing.KeyStroke
7 | import kotlin.properties.ReadWriteProperty
8 | import kotlin.reflect.KProperty
9 |
10 | /**
11 | * More idiomatic Kotlin wrapper for AbstractAction.
12 | */
13 | open class Action(
14 | name: String? = null,
15 | description: String? = null,
16 | icon: Icon? = null,
17 | accelerator: KeyStroke? = null,
18 | selected: Boolean = false,
19 | private val action: Action.(e: ActionEvent) -> Unit,
20 | ) : AbstractAction() {
21 | var name: String? by actionValue(NAME, name)
22 | var description: String? by actionValue(SHORT_DESCRIPTION, description)
23 | var icon: Icon? by actionValue(SMALL_ICON, icon)
24 | var accelerator: KeyStroke? by actionValue(ACCELERATOR_KEY, accelerator)
25 | var selected: Boolean by actionValue(SELECTED_KEY, selected)
26 |
27 | protected fun actionValue(name: String, initialValue: V) = object : ReadWriteProperty {
28 | init {
29 | putValue(name, initialValue)
30 | }
31 |
32 | @Suppress("UNCHECKED_CAST")
33 | override fun getValue(thisRef: AbstractAction, property: KProperty<*>): V {
34 | return thisRef.getValue(name) as V
35 | }
36 |
37 | override fun setValue(thisRef: AbstractAction, property: KProperty<*>, value: V) {
38 | return thisRef.putValue(name, value)
39 | }
40 | }
41 |
42 | override fun actionPerformed(e: ActionEvent) = action(this, e)
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/thread/model/Thread.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.thread.model
2 |
3 | import io.github.inductiveautomation.kindling.utils.StackTrace
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 | import java.lang.Thread.State
7 |
8 | @Serializable
9 | data class Thread(
10 | val id: Long,
11 | val name: String,
12 | val state: State,
13 | @SerialName("daemon")
14 | val isDaemon: Boolean,
15 | @Serializable(with = NoneAsNullStringSerializer::class)
16 | val system: String? = null,
17 | val scope: String? = null,
18 | val cpuUsage: Double? = null,
19 | val lockedMonitors: List = emptyList(),
20 | val lockedSynchronizers: List = emptyList(),
21 | @SerialName("waitingFor")
22 | val blocker: Blocker? = null,
23 | val stacktrace: StackTrace = emptyList(),
24 | ) {
25 | var marked: Boolean = false
26 |
27 | val pool: String? = extractPool(name)
28 |
29 | @Serializable
30 | data class Monitors(
31 | val lock: String,
32 | val frame: String? = null,
33 | )
34 |
35 | @Serializable
36 | data class Blocker(
37 | val lock: String,
38 | val owner: Long? = null,
39 | ) {
40 | override fun toString(): String = if (owner != null) {
41 | "$lock (owned by $owner)"
42 | } else {
43 | lock
44 | }
45 | }
46 |
47 | companion object {
48 | private val threadPoolRegex = "(?.+)-\\d+\$".toRegex()
49 |
50 | internal fun extractPool(name: String): String? = threadPoolRegex.find(name)?.groups?.get("pool")?.value
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/diff/LongestCommonSequence.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils.diff
2 |
3 | import kotlin.math.max
4 |
5 | class LongestCommonSequence> private constructor(
6 | private val a: List,
7 | private val b: List,
8 | private val equalityPredicate: (T, T) -> Boolean,
9 | ) {
10 | private val lengthMatrix = Array(a.size + 1) { Array(b.size + 1) { -1 } }
11 |
12 | private fun buildLengthMatrix(
13 | i: Int,
14 | j: Int,
15 | ): Int {
16 | if (i == 0 || j == 0) {
17 | lengthMatrix[i][j] = 0
18 | return 0
19 | }
20 |
21 | if (lengthMatrix[i][j] != -1) return lengthMatrix[i][j]
22 |
23 | val result: Int = if (equalityPredicate(a[i - 1], b[j - 1])) {
24 | 1 + buildLengthMatrix(i - 1, j - 1)
25 | } else {
26 | max(buildLengthMatrix(i - 1, j), buildLengthMatrix(i, j - 1))
27 | }
28 |
29 | lengthMatrix[i][j] = result
30 | return result
31 | }
32 |
33 | fun calculateLcs(): List {
34 | var i = a.size
35 | val j = b.size
36 | buildLengthMatrix(i, j)
37 |
38 | return buildList {
39 | for (n in j.downTo(1)) {
40 | if (lengthMatrix[i][n] == lengthMatrix[i][n - 1]) {
41 | continue
42 | } else {
43 | add(0, b[n - 1])
44 | i--
45 | }
46 | }
47 | }
48 | }
49 |
50 | companion object {
51 | fun > of(
52 | a: List,
53 | b: List,
54 | equalizer: (U, U) -> Boolean = { l, r -> compareValues(l, r) == 0 },
55 | ) = LongestCommonSequence(a, b, equalizer).calculateLcs()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/log/Model.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.log
2 |
3 | import io.github.inductiveautomation.kindling.utils.FileFilterableCollection
4 | import io.github.inductiveautomation.kindling.utils.StackTrace
5 | import java.time.Instant
6 |
7 | data class LogFile(
8 | override val items: List,
9 | ) : FileFilterableCollection
10 |
11 | sealed interface LogEvent {
12 | var marked: Boolean
13 | val timestamp: Instant
14 | val message: String
15 | val logger: String
16 | val level: Level?
17 | val stacktrace: List
18 | }
19 |
20 | data class MDC(
21 | val key: String,
22 | val value: String?,
23 | ) {
24 | fun toPair() = Pair(key, value)
25 | }
26 |
27 | data class WrapperLogEvent(
28 | override val timestamp: Instant,
29 | override val message: String,
30 | override val logger: String,
31 | override val level: Level? = null,
32 | override val stacktrace: StackTrace = emptyList(),
33 | override var marked: Boolean = false,
34 | ) : LogEvent {
35 | companion object {
36 | const val STDOUT = "STDOUT"
37 | }
38 | }
39 |
40 | data class SystemLogEvent(
41 | override val timestamp: Instant,
42 | override val message: String,
43 | override val logger: String,
44 | val thread: String,
45 | override val level: Level,
46 | val mdc: List,
47 | override val stacktrace: List,
48 | override var marked: Boolean = false,
49 | ) : LogEvent
50 |
51 | @Suppress("ktlint:standard:trailing-comma-on-declaration-site")
52 | enum class Level {
53 | TRACE,
54 | DEBUG,
55 | INFO,
56 | WARN,
57 | ERROR;
58 |
59 | companion object {
60 | private val firstChars = entries.associateBy { it.name.first() }
61 |
62 | fun valueOf(char: Char): Level = firstChars.getValue(char)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/test/resources/io/github/inductiveautomation/kindling/thread/legacyScriptThreadDump.txt:
--------------------------------------------------------------------------------
1 | Ignition version: 8.1.1 (b2020120808)
2 |
3 | "AsyncAppender-Worker-DBAsync"
4 | CPU: 0.00%
5 | java.lang.Thread.State: WAITING
6 | at java.base@11.0.7/jdk.internal.misc.Unsafe.park(Native Method)
7 | at java.base@11.0.7/java.util.concurrent.locks.LockSupport.park(Unknown Source)
8 | at java.base@11.0.7/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(Unknown Source)
9 | at java.base@11.0.7/java.util.concurrent.ArrayBlockingQueue.take(Unknown Source)
10 | at app//ch.qos.logback.core.AsyncAppenderBase$Worker.run(AsyncAppenderBase.java:264)
11 |
12 | "AsyncAppender-Worker-SysoutAsync"
13 | CPU: 0.00%
14 | java.lang.Thread.State: WAITING
15 | at java.base@11.0.7/jdk.internal.misc.Unsafe.park(Native Method)
16 | at java.base@11.0.7/java.util.concurrent.locks.LockSupport.park(Unknown Source)
17 | at java.base@11.0.7/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(Unknown Source)
18 | at java.base@11.0.7/java.util.concurrent.ArrayBlockingQueue.take(Unknown Source)
19 | at app//ch.qos.logback.core.AsyncAppenderBase$Worker.run(AsyncAppenderBase.java:264)
20 |
21 | "AsyncSocketIOSession[I/O]-1"
22 | CPU: 0.27%
23 | java.lang.Thread.State: RUNNABLE
24 | at java.base@11.0.7/java.net.SocketInputStream.socketRead0(Native Method)
25 | at java.base@11.0.7/java.net.SocketInputStream.socketRead(Unknown Source)
26 | at java.base@11.0.7/java.net.SocketInputStream.read(Unknown Source)
27 | at java.base@11.0.7/java.net.SocketInputStream.read(Unknown Source)
28 | at java.base@11.0.7/java.net.SocketInputStream.read(Unknown Source)
29 | at com.inductiveautomation.iosession.socket.AsyncSocketIOSession.run(AsyncSocketIOSession.java:71)
30 | at java.base@11.0.7/java.lang.Thread.run(Unknown Source)
31 |
32 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/ImageView.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import com.jidesoft.swing.SimpleScrollPane
5 | import io.github.inductiveautomation.kindling.core.ToolOpeningException
6 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
7 | import java.nio.file.Path
8 | import java.nio.file.spi.FileSystemProvider
9 | import javax.imageio.ImageIO
10 | import javax.swing.ImageIcon
11 | import javax.swing.JLabel
12 | import javax.swing.SwingConstants.CENTER
13 | import kotlin.io.path.extension
14 |
15 | class ImageView(override val provider: FileSystemProvider, override val path: Path) : SinglePathView() {
16 | init {
17 | val image = try {
18 | ImageIO.createImageInputStream(provider.newInputStream(path)).use { iis ->
19 | val reader = ImageIO.getImageReaders(iis).next()
20 | reader.input = iis
21 | reader.read(0)
22 | }
23 | } catch (e: Exception) {
24 | throw ToolOpeningException("Unable to open ${path.fileName} as an image", e)
25 | }
26 |
27 | add(
28 | SimpleScrollPane(
29 | JLabel().apply {
30 | horizontalAlignment = CENTER
31 | verticalAlignment = CENTER
32 | icon = ImageIcon(image)
33 | },
34 | ),
35 | "center",
36 | )
37 | }
38 |
39 | override val icon: FlatSVGIcon = FlatActionIcon("icons/bx-image.svg")
40 |
41 | companion object {
42 | private val KNOWN_EXTENSIONS = setOf(
43 | "png",
44 | "bmp",
45 | "gif",
46 | "jpg",
47 | "jpeg",
48 | )
49 |
50 | fun isImageFile(path: Path) = path.extension.lowercase() in KNOWN_EXTENSIONS
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/gwbk/MetaStatisticsRenderer.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views.gwbk
2 |
3 | import com.jidesoft.swing.StyledLabelBuilder
4 | import io.github.inductiveautomation.kindling.statistics.categories.MetaStatistics
5 | import java.awt.BorderLayout
6 | import java.awt.Font
7 | import javax.swing.Icon
8 | import javax.swing.JComponent
9 | import javax.swing.JPanel
10 |
11 | class MetaStatisticsRenderer : StatisticRenderer {
12 | override var title: String = "Meta"
13 | override val icon: Icon? = null
14 |
15 | override fun MetaStatistics.render(): JComponent {
16 | title = "Gateway: $gatewayName"
17 |
18 | return JPanel(BorderLayout()).apply {
19 | add(
20 | displayedStatistics.fold(StyledLabelBuilder()) { acc, (field, suffix, value) ->
21 | acc.add("$field: ", Font.BOLD)
22 | acc.add(value(this@render))
23 | acc.add(suffix)
24 | acc.add("\n")
25 | acc
26 | }.createLabel(),
27 | BorderLayout.NORTH,
28 | )
29 | }
30 | }
31 |
32 | private data class MetaStatistic(
33 | val label: String,
34 | val suffix: String = "",
35 | val value: (stats: MetaStatistics) -> String,
36 | )
37 |
38 | companion object {
39 | private val displayedStatistics: List = listOf(
40 | MetaStatistic("Version") { it.version },
41 | MetaStatistic("Role") { it.role ?: "Independent" },
42 | MetaStatistic("Edition") { it.edition },
43 | MetaStatistic("UUID") { it.uuid.orEmpty() },
44 | MetaStatistic("Init Memory", suffix = "mB") { it.initMemory.toString() },
45 | MetaStatistic("Max Memory", suffix = "mB") { it.maxMemory.toString() },
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/thread/ThreadDumpCheckboxList.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.thread
2 |
3 | import com.jidesoft.swing.CheckBoxList
4 | import io.github.inductiveautomation.kindling.utils.NoSelectionModel
5 | import io.github.inductiveautomation.kindling.utils.listCellRenderer
6 | import java.nio.file.Path
7 | import javax.swing.AbstractListModel
8 | import javax.swing.JList
9 | import javax.swing.ListModel
10 | import kotlin.io.path.name
11 |
12 | class ThreadDumpListModel(private val values: List) : AbstractListModel() {
13 | override fun getSize(): Int = values.size + 1
14 | override fun getElementAt(index: Int): Any? = when (index) {
15 | 0 -> CheckBoxList.ALL_ENTRY
16 | else -> values[index - 1]
17 | }
18 | }
19 |
20 | class ThreadDumpCheckboxList(data: List) : CheckBoxList(ThreadDumpListModel(data)) {
21 | init {
22 | layoutOrientation = JList.HORIZONTAL_WRAP
23 | visibleRowCount = 0
24 | isClickInCheckBoxOnly = false
25 | selectionModel = NoSelectionModel()
26 |
27 | cellRenderer = listCellRenderer { _, value, index, _, _ ->
28 | text = when (index) {
29 | 0 -> "All"
30 | else -> index.toString()
31 | }
32 | toolTipText = when (value) {
33 | is Path -> value.name
34 | else -> null
35 | }
36 | }
37 | selectAll()
38 | }
39 |
40 | override fun getModel() = super.getModel() as ThreadDumpListModel
41 |
42 | override fun setModel(model: ListModel<*>) {
43 | require(model is ThreadDumpListModel)
44 | val selection = checkBoxListSelectedValues
45 | checkBoxListSelectionModel.valueIsAdjusting = true
46 | super.setModel(model)
47 | addCheckBoxListSelectedValues(selection)
48 | checkBoxListSelectionModel.valueIsAdjusting = false
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/log/LevelPanel.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.log
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import io.github.inductiveautomation.kindling.utils.Action
5 | import io.github.inductiveautomation.kindling.utils.Column
6 | import io.github.inductiveautomation.kindling.utils.FileFilterResponsive
7 | import io.github.inductiveautomation.kindling.utils.FilterListPanel
8 | import io.github.inductiveautomation.kindling.utils.FilterModel
9 | import javax.swing.JPopupMenu
10 |
11 | internal class LevelPanel(
12 | rawData: List,
13 | ) : FilterListPanel("Levels"),
14 | FileFilterResponsive {
15 | override val icon = FlatSVGIcon("icons/bx-bar-chart-alt.svg")
16 |
17 | override fun setModelData(data: List) {
18 | filterList.setModel(FilterModel.fromRawData(data, filterList.comparator) { it.level?.name })
19 | }
20 |
21 | init {
22 | setModelData(rawData)
23 | filterList.selectAll()
24 | }
25 |
26 | override fun filter(item: T): Boolean = item.level?.name in filterList.checkBoxListSelectedValues
27 |
28 | override fun customizePopupMenu(
29 | menu: JPopupMenu,
30 | column: Column,
31 | event: T,
32 | ) {
33 | val level = event.level
34 | if ((column == WrapperLogColumns.Level || column == SystemLogColumns.Level) && level != null) {
35 | val levelIndex = filterList.model.indexOf(level.name)
36 | menu.add(
37 | Action("Show only $level events") {
38 | filterList.checkBoxListSelectedIndex = levelIndex
39 | filterList.ensureIndexIsVisible(levelIndex)
40 | },
41 | )
42 | menu.add(
43 | Action("Exclude $level events") {
44 | filterList.removeCheckBoxListSelectedIndex(levelIndex)
45 | },
46 | )
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/NOTICES:
--------------------------------------------------------------------------------
1 | NOTICES
2 |
3 | This repository incorporates material as listed below or described in the code.
4 |
5 | ### Build Dependencies
6 | - Java - GPL license 2.0
7 | - Kotlin - Apache License 2.0 - https://github.com/JetBrains/kotlin/blob/master/license/LICENSE.txt
8 | - Kotlin Coroutines - Apache License 2.0 - https://github.com/Kotlin/kotlinx.coroutines/blob/master/LICENSE.txt
9 | - Kotlin Serialization - Apache License 2.0 - https://github.com/Kotlin/kotlinx.serialization/blob/master/LICENSE.txt
10 | - Gradle - Apache License 2.0 - https://docs.gradle.org/current/userguide/licenses.html
11 | - Ktlint Gradle Plugin - MIT License - https://github.com/JLLeitschuh/ktlint-gradle/blob/master/LICENSE.txt
12 | - Ktlint - MIT License - https://github.com/pinterest/ktlint/blob/master/LICENSE
13 |
14 | ### Core Dependencies
15 | - FlatLaf - Apache License 2.0 - https://github.com/JFormDesigner/FlatLaf/blob/main/LICENSE
16 | - SQLite - Public Domain - https://www.sqlite.org/copyright.html
17 | - Xerial JDBC Driver - Apache License 2.0 - https://github.com/xerial/sqlite-jdbc/blob/master/LICENSE
18 | - Logback - Eclipse Public License 1.0 - https://logback.qos.ch/license.html
19 | - HyperSQL - Modified BSD License - https://hsqldb.org/web/hsqlLicense.html
20 | - ExcelKt - MIT License - https://github.com/evanrupert/ExcelKt/blob/master/LICENSE
21 | - MigLayout - BSD License - http://miglayout.com/
22 | - JSVG - MIT License - https://github.com/weisJ/jsvg/LICENSE
23 | - Jide Common Layer - GPL with classpath exception - http://www.jidesoft.com/products/oss.htm
24 | - RSyntaxTextArea - BSD 3-Clause - https://github.com/bobbylight/RSyntaxTextArea/blob/master/LICENSE.md
25 |
26 | ### Test Dependencies
27 | - Kotest - Apache License 2.0 - https://github.com/kotest/kotest/blob/master/LICENSE
28 |
29 | ### Assets
30 | - BoxIcons - MIT License - https://boxicons.com/usage#license
31 | - QuestDB - CC by SA 4.0 - https://en.wikipedia.org/wiki/File:Questdb-logo.svg
32 | - Modifications: Monochrome version of original, redistributed CC-by-SA
33 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/statistics/categories/DeviceStatistics.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.statistics.categories
2 |
3 | import io.github.inductiveautomation.kindling.statistics.GatewayBackup
4 | import io.github.inductiveautomation.kindling.statistics.Statistic
5 | import io.github.inductiveautomation.kindling.statistics.StatisticCalculator
6 | import io.github.inductiveautomation.kindling.utils.executeQuery
7 | import io.github.inductiveautomation.kindling.utils.get
8 | import io.github.inductiveautomation.kindling.utils.toList
9 |
10 | data class DeviceStatistics(
11 | val devices: List,
12 | ) : Statistic {
13 | data class Device(
14 | val name: String,
15 | val type: String,
16 | val description: String?,
17 | val enabled: Boolean,
18 | )
19 |
20 | val total = devices.size
21 | val enabled = devices.count { it.enabled }
22 |
23 | @Suppress("SqlResolve")
24 | companion object Calculator : StatisticCalculator {
25 | private val DEVICES =
26 | """
27 | SELECT
28 | name,
29 | type,
30 | description,
31 | enabled
32 | FROM
33 | devicesettings
34 | """.trimIndent()
35 |
36 | override suspend fun calculate(backup: GatewayBackup): DeviceStatistics? {
37 | val devices =
38 | backup.configDb.executeQuery(DEVICES)
39 | .toList { rs ->
40 | Device(
41 | name = rs[1],
42 | type = rs[2],
43 | description = rs[3],
44 | enabled = rs[4],
45 | )
46 | }
47 |
48 | if (devices.isEmpty()) {
49 | return null
50 | }
51 |
52 | return DeviceStatistics(devices)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/gwbk/OpcConnectionsStatisticsRenderer.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views.gwbk
2 |
3 | import io.github.inductiveautomation.kindling.statistics.categories.OpcServerStatistics
4 | import io.github.inductiveautomation.kindling.utils.ColumnList
5 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
6 | import io.github.inductiveautomation.kindling.utils.FlatScrollPane
7 | import io.github.inductiveautomation.kindling.utils.ReifiedJXTable
8 | import io.github.inductiveautomation.kindling.utils.ReifiedLabelProvider.Companion.setDefaultRenderer
9 | import io.github.inductiveautomation.kindling.utils.ReifiedListTableModel
10 | import javax.swing.Icon
11 | import javax.swing.SortOrder
12 |
13 | class OpcConnectionsStatisticsRenderer : StatisticRenderer {
14 | override val title: String = "OPC Server Connections"
15 | override val icon: Icon = FlatActionIcon("icons/bx-purchase-tag.svg")
16 |
17 | override fun OpcServerStatistics.subtitle() = "$uaServers UA, $comServers COM"
18 |
19 | override fun OpcServerStatistics.render() = FlatScrollPane(
20 | ReifiedJXTable(ReifiedListTableModel(servers, GanColumns)).apply {
21 | setDefaultRenderer(
22 | getText = { it?.name },
23 | getTooltip = { it?.description ?: it?.name },
24 | )
25 | setSortOrder(Name, SortOrder.ASCENDING)
26 | },
27 | )
28 |
29 | @Suppress("unused")
30 | companion object GanColumns : ColumnList() {
31 | val Name by column { it }
32 | val Type by column {
33 | when (it.type) {
34 | OpcServerStatistics.UA_SERVER_TYPE -> "OPC UA"
35 | OpcServerStatistics.COM_SERVER_TYPE -> "OPC COM"
36 | else -> it.type
37 | }
38 | }
39 | val Enabled by column(value = OpcServerStatistics.OpcServer::enabled)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/resources/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
26 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/NoSelectionModel.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import javax.swing.DefaultListSelectionModel
4 | import javax.swing.ListSelectionModel
5 | import javax.swing.tree.DefaultTreeSelectionModel
6 | import javax.swing.tree.TreePath
7 | import javax.swing.tree.TreeSelectionModel
8 |
9 | /**
10 | * A simple [ListSelectionModel]/[TreeSelectionModel] implementation that never allows selecting any elements.
11 | */
12 | class NoSelectionModel :
13 | ListSelectionModel by DefaultListSelectionModel(),
14 | TreeSelectionModel by DefaultTreeSelectionModel() {
15 | override fun setSelectionInterval(index0: Int, index1: Int) = Unit
16 | override fun addSelectionInterval(index0: Int, index1: Int) = Unit
17 | override fun removeSelectionInterval(index0: Int, index1: Int) = Unit
18 | override fun getMinSelectionIndex(): Int = -1
19 | override fun getMaxSelectionIndex(): Int = -1
20 | override fun isSelectedIndex(index: Int): Boolean = false
21 | override fun getAnchorSelectionIndex(): Int = -1
22 | override fun setAnchorSelectionIndex(index: Int) = Unit
23 | override fun getLeadSelectionIndex(): Int = -1
24 | override fun setLeadSelectionIndex(index: Int) = Unit
25 | override fun clearSelection() = Unit
26 | override fun isSelectionEmpty(): Boolean = true
27 | override fun insertIndexInterval(index: Int, length: Int, before: Boolean) = Unit
28 | override fun removeIndexInterval(index0: Int, index1: Int) = Unit
29 |
30 | override fun getSelectionMode(): Int = 0
31 | override fun setSelectionMode(selectionMode: Int) = Unit
32 | override fun getSelectionPath(): TreePath? = null
33 | override fun getSelectionCount(): Int = 0
34 | override fun isPathSelected(path: TreePath): Boolean = false
35 | override fun isRowSelected(row: Int): Boolean = false
36 | override fun getMinSelectionRow(): Int = -1
37 | override fun getMaxSelectionRow(): Int = -1
38 | override fun getLeadSelectionRow(): Int = -1
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/MultiToolView.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import io.github.inductiveautomation.kindling.core.MultiTool
5 | import io.github.inductiveautomation.kindling.core.Tool
6 | import io.github.inductiveautomation.kindling.core.ToolPanel
7 | import io.github.inductiveautomation.kindling.utils.transferTo
8 | import java.nio.file.Files
9 | import java.nio.file.Path
10 | import java.nio.file.spi.FileSystemProvider
11 | import javax.swing.JPopupMenu
12 | import kotlin.io.path.extension
13 | import kotlin.io.path.name
14 | import kotlin.io.path.nameWithoutExtension
15 | import kotlin.io.path.outputStream
16 |
17 | class MultiToolView(
18 | override val provider: FileSystemProvider,
19 | override val paths: List,
20 | ) : PathView("ins 0, fill") {
21 | private val multiTool: MultiTool
22 | private val toolPanel: ToolPanel
23 |
24 | override val tabName by lazy {
25 | val roots = paths.mapTo(mutableSetOf()) { path ->
26 | path.nameWithoutExtension.trimEnd { it.isDigit() || it == '-' || it == '.' }
27 | }
28 | "[${paths.size}] ${roots.joinToString()}.${paths.first().extension}"
29 | }
30 | override val tabTooltip by lazy { paths.joinToString("\n") { it.toString().substring(1) } }
31 |
32 | override fun toString(): String = "MultiToolView(paths=$paths)"
33 |
34 | init {
35 | val tempFiles = paths.map { path ->
36 | Files.createTempFile("kindling", path.name).also { tempFile ->
37 | provider.newInputStream(path) transferTo tempFile.outputStream()
38 | }
39 | }
40 |
41 | multiTool = Tool[tempFiles.first()] as MultiTool
42 | toolPanel = multiTool.open(tempFiles)
43 |
44 | add(toolPanel, "push, grow")
45 | }
46 |
47 | override val icon: FlatSVGIcon = toolPanel.icon as FlatSVGIcon
48 |
49 | override fun customizePopupMenu(menu: JPopupMenu) = toolPanel.customizePopupMenu(menu)
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/log/ThreadPanel.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.log
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import io.github.inductiveautomation.kindling.utils.Action
5 | import io.github.inductiveautomation.kindling.utils.Column
6 | import io.github.inductiveautomation.kindling.utils.FileFilterResponsive
7 | import io.github.inductiveautomation.kindling.utils.FilterListPanel
8 | import io.github.inductiveautomation.kindling.utils.FilterModel
9 | import javax.swing.JPopupMenu
10 |
11 | internal class ThreadPanel(
12 | events: List,
13 | ) : FilterListPanel("Threads"),
14 | FileFilterResponsive {
15 | override val icon = FlatSVGIcon("icons/bx-chip.svg")
16 |
17 | init {
18 | filterList.apply {
19 | setModel(FilterModel.fromRawData(events, filterList.comparator) { it.thread })
20 | selectAll()
21 | }
22 | }
23 |
24 | override fun setModelData(data: List) {
25 | filterList.model = FilterModel.fromRawData(data, filterList.comparator) { it.thread }
26 | }
27 |
28 | override fun filter(item: SystemLogEvent) = item.thread in filterList.checkBoxListSelectedValues
29 |
30 | override fun customizePopupMenu(
31 | menu: JPopupMenu,
32 | column: Column,
33 | event: SystemLogEvent,
34 | ) {
35 | if (column == SystemLogColumns.Thread) {
36 | val threadIndex = filterList.model.indexOf(event.thread)
37 | menu.add(
38 | Action("Show only ${event.thread} events") {
39 | filterList.checkBoxListSelectedIndex = threadIndex
40 | filterList.ensureIndexIsVisible(threadIndex)
41 | },
42 | )
43 | menu.add(
44 | Action("Exclude ${event.thread} events") {
45 | filterList.removeCheckBoxListSelectedIndex(threadIndex)
46 | },
47 | )
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/statistics/categories/MetaStatistics.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.statistics.categories
2 |
3 | import io.github.inductiveautomation.kindling.statistics.GatewayBackup
4 | import io.github.inductiveautomation.kindling.statistics.Statistic
5 | import io.github.inductiveautomation.kindling.statistics.StatisticCalculator
6 | import io.github.inductiveautomation.kindling.utils.asScalarMap
7 | import io.github.inductiveautomation.kindling.utils.executeQuery
8 |
9 | data class MetaStatistics(
10 | val uuid: String?,
11 | val gatewayName: String,
12 | val edition: String,
13 | val role: String?,
14 | val version: String,
15 | val initMemory: Int,
16 | val maxMemory: Int,
17 | ) : Statistic {
18 | @Suppress("SqlResolve")
19 | companion object Calculator : StatisticCalculator {
20 | private val SYS_PROPS =
21 | """
22 | SELECT *
23 | FROM
24 | sysprops
25 | """.trimIndent()
26 |
27 | override suspend fun calculate(backup: GatewayBackup): MetaStatistics {
28 | val sysPropsMap = backup.configDb.executeQuery(SYS_PROPS).asScalarMap()
29 |
30 | val edition = backup.info.getElementsByTagName("edition").item(0)?.textContent
31 | val version = backup.info.getElementsByTagName("version").item(0).textContent
32 |
33 | return MetaStatistics(
34 | uuid = sysPropsMap["SYSTEMUID"] as String?,
35 | gatewayName = sysPropsMap.getValue("SYSTEMNAME") as String,
36 | edition = edition.takeUnless { it.isNullOrEmpty() } ?: "Standard",
37 | role = backup.redundancyInfo.getProperty("redundancy.noderole"),
38 | version = version,
39 | initMemory = backup.ignitionConf.getProperty("wrapper.java.initmemory").takeWhile { it.isDigit() }.toInt(),
40 | maxMemory = backup.ignitionConf.getProperty("wrapper.java.maxmemory").takeWhile { it.isDigit() }.toInt(),
41 | )
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/thread/StatePanel.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.thread
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import io.github.inductiveautomation.kindling.core.FilterChangeListener
5 | import io.github.inductiveautomation.kindling.core.FilterPanel
6 | import io.github.inductiveautomation.kindling.thread.model.Thread
7 | import io.github.inductiveautomation.kindling.utils.Column
8 | import io.github.inductiveautomation.kindling.utils.FileFilterResponsive
9 | import io.github.inductiveautomation.kindling.utils.FilterList
10 | import io.github.inductiveautomation.kindling.utils.FilterModel
11 | import io.github.inductiveautomation.kindling.utils.FlatScrollPane
12 | import io.github.inductiveautomation.kindling.utils.getAll
13 | import javax.swing.JPopupMenu
14 |
15 | class StatePanel :
16 | FilterPanel(),
17 | FileFilterResponsive {
18 | override val icon = FlatSVGIcon("icons/bx-check-circle.svg")
19 |
20 | val stateList = FilterList()
21 | override val tabName = "State"
22 |
23 | override val component = FlatScrollPane(stateList)
24 |
25 | init {
26 | stateList.selectAll()
27 |
28 | stateList.checkBoxListSelectionModel.addListSelectionListener { e ->
29 | if (!e.valueIsAdjusting) {
30 | listeners.getAll().forEach(FilterChangeListener::filterChanged)
31 | }
32 | }
33 | }
34 |
35 | override fun setModelData(data: List) {
36 | stateList.model = FilterModel.fromRawData(data.filterNotNull(), stateList.comparator) { it.state.name }
37 | }
38 |
39 | override fun isFilterApplied(): Boolean = stateList.checkBoxListSelectedValues.size != stateList.model.size - 1
40 |
41 | override fun reset() = stateList.selectAll()
42 |
43 | override fun filter(item: Thread?): Boolean = item?.state?.name in stateList.checkBoxListSelectedValues
44 |
45 | override fun customizePopupMenu(
46 | menu: JPopupMenu,
47 | column: Column,
48 | event: Thread?,
49 | ) = Unit
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/ToolView.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import io.github.inductiveautomation.kindling.core.Tool
5 | import io.github.inductiveautomation.kindling.core.ToolOpeningException
6 | import io.github.inductiveautomation.kindling.core.ToolPanel
7 | import io.github.inductiveautomation.kindling.utils.ACTION_ICON_SCALE_FACTOR
8 | import io.github.inductiveautomation.kindling.utils.transferTo
9 | import java.nio.file.Files
10 | import java.nio.file.Path
11 | import java.nio.file.spi.FileSystemProvider
12 | import java.util.zip.ZipException
13 | import javax.swing.JPopupMenu
14 | import kotlin.io.path.extension
15 | import kotlin.io.path.name
16 | import kotlin.io.path.outputStream
17 |
18 | class ToolView(
19 | override val provider: FileSystemProvider,
20 | override val path: Path,
21 | ) : SinglePathView("ins 0, fill") {
22 | private val toolPanel: ToolPanel
23 |
24 | init {
25 | val tempFile = Files.createTempFile("kindling", path.name)
26 | try {
27 | provider.newInputStream(path) transferTo tempFile.outputStream()
28 | /* Tool.get() throws exception if tool not found, but this check is already done with isTool() */
29 | toolPanel = Tool.find(path)?.open(tempFile)
30 | ?: throw ToolOpeningException("No tool for files of type .${path.extension}")
31 | add(toolPanel, "push, grow")
32 | } catch (e: ZipException) {
33 | throw ToolOpeningException("Unable to open $path .${path.extension}")
34 | }
35 | }
36 |
37 | override val icon: FlatSVGIcon = (toolPanel.icon as FlatSVGIcon).derive(ACTION_ICON_SCALE_FACTOR)
38 |
39 | override fun customizePopupMenu(menu: JPopupMenu) = toolPanel.customizePopupMenu(menu)
40 |
41 | companion object {
42 | fun maybeToolPath(path: Path): Boolean = Tool.find(path) != null
43 |
44 | fun safelyCreate(provider: FileSystemProvider, path: Path): ToolView? = runCatching { ToolView(provider, path) }.getOrNull()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/ColumnList.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import org.jdesktop.swingx.table.ColumnFactory
4 | import org.jdesktop.swingx.table.TableColumnExt
5 | import javax.swing.table.TableModel
6 | import kotlin.properties.PropertyDelegateProvider
7 | import kotlin.properties.ReadOnlyProperty
8 |
9 | abstract class ColumnList private constructor(
10 | @PublishedApi internal val list: MutableList>,
11 | ) : List> by list {
12 | constructor() : this(mutableListOf())
13 |
14 | /**
15 | * Defines a new column (type T). Uses the name of the property if [name] isn't provided.
16 | */
17 | // This is some real Kotlin 'magic', but makes it very easy to define JTable models that can be used type-safely
18 | protected inline fun column(
19 | name: String? = null,
20 | noinline column: (TableColumnExt.(model: TableModel) -> Unit)? = null,
21 | noinline value: (R) -> T,
22 | ): PropertyDelegateProvider, ReadOnlyProperty, Column>> = PropertyDelegateProvider { thisRef, prop ->
23 | val actual = Column(
24 | header = name ?: prop.name,
25 | getValue = value,
26 | columnCustomization = column,
27 | clazz = T::class.java,
28 | )
29 | thisRef.add(actual)
30 | ReadOnlyProperty { _, _ -> actual }
31 | }
32 |
33 | fun add(column: Column) {
34 | list.add(column)
35 | }
36 |
37 | fun removeAt(index: Int) {
38 | list.removeAt(index)
39 | }
40 |
41 | operator fun get(column: Column<*, *>): Int = indexOf(column)
42 |
43 | fun toColumnFactory() = object : ColumnFactory() {
44 | override fun configureTableColumn(model: TableModel, columnExt: TableColumnExt) {
45 | super.configureTableColumn(model, columnExt)
46 | val column = list[columnExt.modelIndex]
47 | columnExt.toolTipText = column.header
48 | columnExt.identifier = column
49 | column.columnCustomization?.invoke(columnExt, model)
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/tagconfig/model/AbstractTagProvider.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.tagconfig.model
2 |
3 | import io.github.inductiveautomation.kindling.tagconfig.TagConfigView
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.async
8 | import kotlinx.serialization.ExperimentalSerializationApi
9 | import kotlinx.serialization.json.encodeToStream
10 | import java.nio.file.Path
11 | import kotlin.io.path.outputStream
12 | import kotlin.uuid.ExperimentalUuidApi
13 | import kotlin.uuid.Uuid
14 |
15 | @OptIn(ExperimentalSerializationApi::class, ExperimentalUuidApi::class)
16 | sealed class AbstractTagProvider(
17 | val name: String,
18 | val uuid: Uuid,
19 | val description: String?,
20 | val enabled: Boolean,
21 | ) {
22 | abstract val providerStatistics: ProviderStatistics
23 | abstract val loadProvider: Job
24 |
25 | protected open val typesNode: Node = Node(
26 | config = TagConfig(name = "_types_", tagType = "Folder"),
27 | isMeta = true,
28 | )
29 |
30 | protected lateinit var providerNode: Node
31 |
32 | val isInitialized: Boolean
33 | get() = ::providerNode.isInitialized
34 |
35 | protected abstract val Node.parentType: Node?
36 |
37 | val Node.tagPath: String
38 | get() {
39 | if (this === providerNode) return ""
40 | val p = checkNotNull(getParent()) { "Parent is null! $this" }
41 | return when (p.name) {
42 | "" -> "[${this@AbstractTagProvider.name}]$name"
43 | else -> "${p.tagPath}/$name"
44 | }
45 | }
46 |
47 | fun exportToJson(path: Path) {
48 | path.outputStream().use {
49 | TagConfigView.TagExportJson.encodeToStream(providerNode, it)
50 | }
51 | }
52 |
53 | fun getProviderNode() = CoroutineScope(Dispatchers.Default).async {
54 | loadProvider.join()
55 | providerNode
56 | }
57 |
58 | protected abstract fun Node.resolveInheritance()
59 | protected abstract fun Node.resolveNestedUdtInstances()
60 | protected abstract fun Node.copyChildrenFrom(other: Node)
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/cache/SchemaFilterList.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.cache
2 |
3 | import com.jidesoft.swing.CheckBoxList
4 | import io.github.inductiveautomation.kindling.utils.listCellRenderer
5 | import java.awt.Font
6 | import java.awt.Font.MONOSPACED
7 | import javax.swing.AbstractListModel
8 | import javax.swing.DefaultListSelectionModel
9 |
10 | class SchemaModel(data: List) : AbstractListModel() {
11 | private val comparator: Comparator = compareBy(nullsFirst()) { it.id }
12 | private val values = data.sortedWith(comparator)
13 |
14 | override fun getSize(): Int {
15 | return values.size + 1
16 | }
17 |
18 | override fun getElementAt(index: Int): Any {
19 | return if (index == 0) {
20 | CheckBoxList.ALL_ENTRY
21 | } else {
22 | values[index - 1]
23 | }
24 | }
25 | }
26 |
27 | class SchemaFilterList(modelData: List) : CheckBoxList(SchemaModel(modelData)) {
28 | init {
29 | selectionModel = DefaultListSelectionModel()
30 | isClickInCheckBoxOnly = true
31 | visibleRowCount = 0
32 |
33 | val txGroupRegex = """(.*)\{.*}""".toRegex()
34 |
35 | cellRenderer = listCellRenderer { _, schemaEntry, _, _, _ ->
36 | text = when (schemaEntry) {
37 | is SchemaRecord -> {
38 | buildString {
39 | append("%4d".format(schemaEntry.id))
40 | val name = txGroupRegex.find(schemaEntry.name)?.groups?.get(1)?.value ?: schemaEntry.name
41 | append(": ").append(name)
42 |
43 | when (val size = schemaEntry.errors.size) {
44 | 0 -> Unit
45 | 1 -> append(" ($size error. Click to view.)")
46 | else -> append(" ($size errors. Click to view.)")
47 | }
48 | }
49 | }
50 | else -> schemaEntry.toString()
51 | }
52 | font = Font(MONOSPACED, Font.PLAIN, 14)
53 | }
54 | selectAll()
55 | }
56 |
57 | override fun getModel() = super.getModel() as SchemaModel
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/core/db/DBMetaDataTree.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.core.db
2 |
3 | import com.formdev.flatlaf.extras.components.FlatTree
4 | import com.jidesoft.swing.TreeSearchable
5 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
6 | import io.github.inductiveautomation.kindling.utils.toFileSizeLabel
7 | import io.github.inductiveautomation.kindling.utils.treeCellRenderer
8 | import javax.swing.tree.TreeModel
9 | import javax.swing.tree.TreePath
10 | import javax.swing.tree.TreeSelectionModel
11 |
12 | class DBMetaDataTree(treeModel: TreeModel) : FlatTree() {
13 | init {
14 | model = treeModel
15 | isRootVisible = false
16 | setShowsRootHandles(true)
17 | selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
18 | setCellRenderer(
19 | treeCellRenderer { _, value, _, _, _, _, _ ->
20 | when (value) {
21 | is Table -> {
22 | text = buildString {
23 | append(value.name)
24 | append(" ")
25 | append("(${value.size.toFileSizeLabel()})")
26 | append(" ")
27 | append("[${value.rowCount} rows]")
28 | }
29 | icon = FlatActionIcon("icons/bx-table.svg")
30 | }
31 |
32 | is Column -> {
33 | text = buildString {
34 | append(value.name)
35 | append(" ")
36 | append(value.type.takeIf { it.isNotEmpty() } ?: "UNKNOWN")
37 | }
38 | icon = FlatActionIcon("icons/bx-column.svg")
39 | }
40 | }
41 | this
42 | },
43 | )
44 |
45 | object : TreeSearchable(this) {
46 | init {
47 | isRecursive = true
48 | isRepeats = true
49 | }
50 |
51 | override fun convertElementToString(element: Any?): String = when (val node = (element as? TreePath)?.lastPathComponent) {
52 | is Table -> node.name
53 | is Column -> node.name
54 | else -> ""
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/Trees.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import com.jidesoft.swing.CheckBoxTree
4 | import java.util.Collections
5 | import java.util.Enumeration
6 | import javax.swing.JTree
7 | import javax.swing.tree.TreeNode
8 | import javax.swing.tree.TreePath
9 |
10 | abstract class AbstractTreeNode : TreeNode {
11 | open val children: MutableList = object : ArrayList() {
12 | override fun add(element: TreeNode): Boolean {
13 | element as AbstractTreeNode
14 | element.parent = this@AbstractTreeNode
15 | return super.add(element)
16 | }
17 | }
18 | var parent: AbstractTreeNode? = null
19 |
20 | override fun getAllowsChildren(): Boolean = true
21 | override fun getChildCount(): Int = children.size
22 | override fun isLeaf(): Boolean = children.isEmpty()
23 | override fun getChildAt(childIndex: Int): TreeNode = children[childIndex]
24 | override fun getIndex(node: TreeNode?): Int = children.indexOf(node)
25 | override fun getParent(): TreeNode? = this.parent
26 | override fun children(): Enumeration = Collections.enumeration(children)
27 |
28 | fun depthFirstChildren(): Sequence = sequence {
29 | for (child in children) {
30 | yield(child as AbstractTreeNode)
31 | yieldAll(child.depthFirstChildren())
32 | }
33 | }
34 |
35 | fun sortWith(comparator: Comparator, recursive: Boolean = false) {
36 | children.sortWith(comparator)
37 | if (recursive) {
38 | for (child in children) {
39 | (child as? AbstractTreeNode)?.sortWith(comparator, recursive = true)
40 | }
41 | }
42 | }
43 | }
44 |
45 | abstract class TypedTreeNode : AbstractTreeNode() {
46 | abstract val userObject: T
47 | }
48 |
49 | fun JTree.expandAll() {
50 | var i = 0
51 | while (i < rowCount) {
52 | expandRow(i)
53 | i += 1
54 | }
55 | }
56 |
57 | fun JTree.collapseAll() {
58 | var i = rowCount - 1 // Skip the root node
59 | while (i > 0) {
60 | collapseRow(i)
61 | i -= 1
62 | }
63 | }
64 |
65 | fun CheckBoxTree.selectAll() {
66 | checkBoxTreeSelectionModel.addSelectionPath(TreePath(model.root))
67 | }
68 |
69 | fun CheckBoxTree.unselectAll() {
70 | checkBoxTreeSelectionModel.clearSelection()
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/Sparkline.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.idb.metrics
2 |
3 | import io.github.inductiveautomation.kindling.core.Kindling.Preferences.UI.Theme
4 | import io.github.inductiveautomation.kindling.core.Theme.Companion.theme
5 | import io.github.inductiveautomation.kindling.core.Timezone
6 | import org.jfree.chart.ChartFactory
7 | import org.jfree.chart.JFreeChart
8 | import org.jfree.chart.axis.NumberAxis
9 | import org.jfree.chart.ui.RectangleInsets
10 | import org.jfree.data.time.FixedMillisecond
11 | import org.jfree.data.time.TimeSeries
12 | import org.jfree.data.time.TimeSeriesCollection
13 | import java.text.NumberFormat
14 | import java.time.Instant
15 |
16 | fun sparkline(data: List, formatter: NumberFormat): JFreeChart = ChartFactory.createTimeSeriesChart(
17 | /* title = */
18 | null,
19 | /* timeAxisLabel = */
20 | null,
21 | /* valueAxisLabel = */
22 | null,
23 | /* dataset = */
24 | TimeSeriesCollection(
25 | TimeSeries("Series").apply {
26 | for ((value, timestamp) in data) {
27 | add(FixedMillisecond(timestamp), value, false)
28 | }
29 | },
30 | ),
31 | /* legend = */
32 | false,
33 | /* tooltips = */
34 | true,
35 | /* urls = */
36 | false,
37 | ).apply {
38 | xyPlot.apply {
39 | domainAxis.isPositiveArrowVisible = true
40 | rangeAxis.apply {
41 | isPositiveArrowVisible = true
42 | (this as NumberAxis).numberFormatOverride = formatter
43 | }
44 | val updateTooltipGenerator = {
45 | renderer.setDefaultToolTipGenerator { dataset, series, item ->
46 | val time = Instant.ofEpochMilli(dataset.getXValue(series, item).toLong())
47 | "${Timezone.Default.format(time)} - ${formatter.format(dataset.getYValue(series, item))}"
48 | }
49 | }
50 |
51 | updateTooltipGenerator()
52 |
53 | Timezone.Default.addChangeListener {
54 | updateTooltipGenerator()
55 | }
56 |
57 | isDomainGridlinesVisible = false
58 | isRangeGridlinesVisible = false
59 | isOutlineVisible = false
60 | }
61 |
62 | padding = RectangleInsets(10.0, 10.0, 10.0, 10.0)
63 | isBorderVisible = false
64 |
65 | theme = Theme.currentValue
66 | Theme.addChangeListener { newTheme ->
67 | theme = newTheme
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/resources/io/github/inductiveautomation/kindling/thread/threadDump.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "Dev",
3 | "threads": [
4 | {
5 | "name": "HSQLDB Timer @4551b699",
6 | "id": 93,
7 | "state": "TIMED_WAITING",
8 | "daemon": true,
9 | "system": "None",
10 | "scope": "Gateway",
11 | "cpuUsage": 0,
12 | "waitingFor": {
13 | "lock": "org.hsqldb.lib.HsqlTimer$TaskQueue@135e1e67"
14 | },
15 | "stacktrace": [
16 | "java.base@11.0.11/java.lang.Object.wait(Native Method)",
17 | "app//org.hsqldb.lib.HsqlTimer$TaskQueue.park(Unknown Source)",
18 | "app//org.hsqldb.lib.HsqlTimer.nextTask(Unknown Source)",
19 | "app//org.hsqldb.lib.HsqlTimer$TaskRunner.run(Unknown Source)",
20 | "java.base@11.0.11/java.lang.Thread.run(Thread.java:829)"
21 | ]
22 | },
23 | {
24 | "name": "HttpClient-1-SelectorManager",
25 | "id": 46,
26 | "state": "RUNNABLE",
27 | "daemon": true,
28 | "system": "None",
29 | "scope": "Gateway",
30 | "cpuUsage": 0,
31 | "lockedMonitors": [
32 | {
33 | "lock": "sun.nio.ch.Util$2@6150057e",
34 | "frame": "java.base@11.0.11/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:124)"
35 | },
36 | {
37 | "lock": "sun.nio.ch.WindowsSelectorImpl@48522463",
38 | "frame": "java.base@11.0.11/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:124)"
39 | }
40 | ],
41 | "stacktrace": [
42 | "java.base@11.0.11/sun.nio.ch.WindowsSelectorImpl$SubSelector.poll0(Native Method)",
43 | "java.base@11.0.11/sun.nio.ch.WindowsSelectorImpl$SubSelector.poll(WindowsSelectorImpl.java:357)",
44 | "java.base@11.0.11/sun.nio.ch.WindowsSelectorImpl.doSelect(WindowsSelectorImpl.java:182)",
45 | "java.base@11.0.11/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:124)",
46 | "java.base@11.0.11/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:136)",
47 | "platform/java.net.http@11.0.11/jdk.internal.net.http.HttpClientImpl$SelectorManager.run(HttpClientImpl.java:867)"
48 | ]
49 | }
50 | ]
51 | }
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/gwbk/GatewayNetworkStatisticsRenderer.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views.gwbk
2 |
3 | import com.formdev.flatlaf.extras.components.FlatTabbedPane
4 | import io.github.inductiveautomation.kindling.statistics.categories.GatewayNetworkStatistics
5 | import io.github.inductiveautomation.kindling.statistics.categories.GatewayNetworkStatistics.IncomingConnection
6 | import io.github.inductiveautomation.kindling.statistics.categories.GatewayNetworkStatistics.OutgoingConnection
7 | import io.github.inductiveautomation.kindling.utils.ColumnList
8 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
9 | import io.github.inductiveautomation.kindling.utils.FlatScrollPane
10 | import io.github.inductiveautomation.kindling.utils.ReifiedJXTable
11 | import io.github.inductiveautomation.kindling.utils.ReifiedListTableModel
12 | import javax.swing.Icon
13 | import javax.swing.JTabbedPane
14 | import javax.swing.SortOrder
15 |
16 | class GatewayNetworkStatisticsRenderer : StatisticRenderer {
17 | override val title: String = "Gateway Network"
18 | override val icon: Icon = FlatActionIcon("icons/bx-sitemap.svg")
19 |
20 | override fun GatewayNetworkStatistics.render() = FlatTabbedPane().apply {
21 | tabLayoutPolicy = JTabbedPane.SCROLL_TAB_LAYOUT
22 | tabType = FlatTabbedPane.TabType.underlined
23 |
24 | addTab(
25 | "${outgoing.size} Outgoing",
26 | FlatScrollPane(
27 | ReifiedJXTable(ReifiedListTableModel(outgoing, OutgoingColumns)).apply {
28 | setSortOrder(OutgoingColumns.Identifier, SortOrder.ASCENDING)
29 | },
30 | ),
31 | )
32 | addTab(
33 | "${incoming.size} Incoming",
34 | FlatScrollPane(
35 | ReifiedJXTable(ReifiedListTableModel(incoming, IncomingColumns)).apply {
36 | setSortOrder(IncomingColumns.Identifier, SortOrder.ASCENDING)
37 | },
38 | ),
39 | )
40 | }
41 |
42 | object IncomingColumns : ColumnList() {
43 | val Identifier by column(value = IncomingConnection::uuid)
44 | }
45 |
46 | @Suppress("unused")
47 | object OutgoingColumns : ColumnList() {
48 | val Identifier by column {
49 | "${it.host}:${it.port}"
50 | }
51 | val Enabled by column { it.enabled }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/statistics/categories/GatewayNetworkStatistics.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.statistics.categories
2 |
3 | import io.github.inductiveautomation.kindling.statistics.GatewayBackup
4 | import io.github.inductiveautomation.kindling.statistics.Statistic
5 | import io.github.inductiveautomation.kindling.statistics.StatisticCalculator
6 | import io.github.inductiveautomation.kindling.utils.executeQuery
7 | import io.github.inductiveautomation.kindling.utils.get
8 | import io.github.inductiveautomation.kindling.utils.toList
9 |
10 | data class GatewayNetworkStatistics(
11 | val outgoing: List,
12 | val incoming: List,
13 | ) : Statistic {
14 | data class OutgoingConnection(
15 | val host: String,
16 | val port: Int,
17 | val enabled: Boolean,
18 | )
19 |
20 | data class IncomingConnection(
21 | val uuid: String,
22 | )
23 |
24 | @Suppress("SqlResolve")
25 | companion object Calculator : StatisticCalculator {
26 | private val OUTGOING_CONNECTIONS =
27 | """
28 | SELECT
29 | host,
30 | port,
31 | enabled
32 | FROM
33 | wsconnectionsettings
34 | """.trimIndent()
35 |
36 | private val INCOMING_CONNECTIONS =
37 | """
38 | SELECT
39 | connectionid
40 | FROM
41 | wsincomingconnection
42 | """.trimIndent()
43 |
44 | override suspend fun calculate(backup: GatewayBackup): GatewayNetworkStatistics? {
45 | val outgoing =
46 | backup.configDb.executeQuery(OUTGOING_CONNECTIONS)
47 | .toList { rs ->
48 | OutgoingConnection(
49 | host = rs[1],
50 | port = rs[2],
51 | enabled = rs[3],
52 | )
53 | }
54 |
55 | val incoming =
56 | backup.configDb.executeQuery(INCOMING_CONNECTIONS)
57 | .toList { rs ->
58 | IncomingConnection(rs[1])
59 | }
60 |
61 | if (outgoing.isEmpty() && incoming.isEmpty()) {
62 | return null
63 | }
64 |
65 | return GatewayNetworkStatistics(outgoing, incoming)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/statistics/GatewayBackup.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.statistics
2 |
3 | import io.github.inductiveautomation.kindling.utils.Properties
4 | import io.github.inductiveautomation.kindling.utils.SQLiteConnection
5 | import io.github.inductiveautomation.kindling.utils.XML_FACTORY
6 | import io.github.inductiveautomation.kindling.utils.parse
7 | import io.github.inductiveautomation.kindling.utils.transferTo
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.runBlocking
12 | import org.w3c.dom.Document
13 | import java.nio.file.FileSystems
14 | import java.nio.file.Path
15 | import java.sql.Connection
16 | import java.util.Properties
17 | import kotlin.io.path.createTempFile
18 | import kotlin.io.path.inputStream
19 | import kotlin.io.path.outputStream
20 |
21 | class GatewayBackup(path: Path) {
22 | private val zipFile = FileSystems.newFileSystem(path)
23 | private val root: Path = zipFile.rootDirectories.first()
24 |
25 | val info: Document = root.resolve(BACKUP_INFO).inputStream().use(XML_FACTORY::parse)
26 |
27 | val projectsDirectory: Path = root.resolve(PROJECTS)
28 |
29 | val configDirectory: Path = root.resolve(CONFIG)
30 |
31 | private val tempFile: Path = createTempFile("gwbk-stats", "idb")
32 |
33 | // eagerly copy out the IDB, since we're always building the statistics view anyways
34 | private val dbCopyJob =
35 | CoroutineScope(Dispatchers.IO).launch {
36 | root.resolve(IDB).inputStream() transferTo tempFile.outputStream()
37 | }
38 |
39 | val configDb: Connection by lazy {
40 | // ensure the file copy is complete
41 | runBlocking { dbCopyJob.join() }
42 |
43 | SQLiteConnection(tempFile)
44 | }
45 |
46 | val ignitionConf: Properties by lazy {
47 | Properties((root.resolve(IGNITION_CONF)).inputStream())
48 | }
49 |
50 | val redundancyInfo: Properties by lazy {
51 | Properties(root.resolve(REDUNDANCY).inputStream(), Properties::loadFromXML)
52 | }
53 |
54 | companion object {
55 | private const val IDB = "db_backup_sqlite.idb"
56 | private const val BACKUP_INFO = "backupinfo.xml"
57 | private const val REDUNDANCY = "redundancy.xml"
58 | private const val IGNITION_CONF = "ignition.conf"
59 | private const val PROJECTS = "projects"
60 | private const val CONFIG = "config"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/gwbk/DeviceStatisticsRenderer.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views.gwbk
2 |
3 | import io.github.inductiveautomation.kindling.statistics.categories.DeviceStatistics
4 | import io.github.inductiveautomation.kindling.utils.ColumnList
5 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
6 | import io.github.inductiveautomation.kindling.utils.FlatScrollPane
7 | import io.github.inductiveautomation.kindling.utils.ReifiedJXTable
8 | import io.github.inductiveautomation.kindling.utils.ReifiedLabelProvider.Companion.setDefaultRenderer
9 | import io.github.inductiveautomation.kindling.utils.ReifiedListTableModel
10 | import javax.swing.Icon
11 | import javax.swing.SortOrder
12 |
13 | class DeviceStatisticsRenderer : StatisticRenderer {
14 | override val title: String = "Devices"
15 | override val icon: Icon = FlatActionIcon("icons/bx-chip.svg")
16 |
17 | override fun DeviceStatistics.subtitle() = "$enabled enabled, $total total"
18 |
19 | override fun DeviceStatistics.render() = FlatScrollPane(
20 | ReifiedJXTable(ReifiedListTableModel(devices, DeviceColumns)).apply {
21 | setDefaultRenderer(
22 | getText = { it?.name },
23 | getTooltip = { it?.description },
24 | )
25 |
26 | setSortOrder(Name, SortOrder.ASCENDING)
27 | },
28 | )
29 |
30 | @Suppress("unused")
31 | companion object DeviceColumns : ColumnList() {
32 | val Name by column { it }
33 | val Type by column {
34 | when (it.type) {
35 | "Dnp3Driver" -> "DNP3"
36 | "LogixDriver" -> "Logix"
37 | "ProgrammableSimulatorDevice" -> "Simulator"
38 | "TCPDriver" -> "TCP"
39 | "UDPDriver" -> "UDP"
40 | "com.inductiveautomation.BacnetIpDeviceType" -> "BACnet"
41 | "com.inductiveautomation.FinsTcpDeviceType" -> "FinsTCP"
42 | "com.inductiveautomation.FinsUdpDeviceType" -> "FinsUDP"
43 | "com.inductiveautomation.Iec61850DeviceType" -> "IEC61850"
44 | "com.inductiveautomation.MitsubishiTcpDeviceType" -> "MitsubishiTCP"
45 | "com.inductiveautomation.omron.NjDriver" -> "OmronNJ"
46 | else -> it.type.substringAfterLast('.')
47 | }
48 | }
49 | val Enabled by column { it.enabled }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/cache/model/AlarmJournalData.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.cache.model
2 |
3 | import com.inductiveautomation.ignition.common.alarming.EventData
4 | import com.inductiveautomation.ignition.common.alarming.evaluation.EventPropertyType
5 | import io.github.inductiveautomation.kindling.core.Detail
6 | import java.io.Serializable
7 | import java.util.EnumSet
8 |
9 | class AlarmJournalData(
10 | private val profileName: String?,
11 | private val tableName: String?,
12 | private val dataTableName: String?,
13 | private val source: String?,
14 | private val dispPath: String?,
15 | private val uuid: String?,
16 | private val priority: Int,
17 | private val eventType: Int,
18 | private val eventFlags: Int,
19 | val data: EventData,
20 | private val storedProps: EnumSet,
21 | ) : Serializable {
22 | val details by lazy {
23 | mapOf(
24 | "profile" to profileName.toString(),
25 | "table" to tableName.toString(),
26 | "dataTable" to dataTableName.toString(),
27 | "source" to source.toString(),
28 | "displayPath" to dispPath.toString(),
29 | "uuid" to uuid.toString(),
30 | "priority" to priority.toString(),
31 | "eventType" to eventType.toString(),
32 | "eventFlags" to eventFlags.toString(),
33 | "storedProps" to storedProps.joinToString(),
34 | )
35 | }
36 |
37 | val body by lazy {
38 | data.properties.map { property ->
39 | "${property.name} (${property.type.simpleName}) = ${data.getOrDefault(property)}"
40 | }
41 | }
42 |
43 | fun toDetail() = Detail(
44 | title = "Alarm Journal Data",
45 | details = details,
46 | body = body,
47 | )
48 |
49 | companion object {
50 | @JvmStatic
51 | private val serialVersionUID = 1L
52 | }
53 | }
54 |
55 | class AlarmJournalSFGroup(
56 | private val groupId: String,
57 | private val entries: List,
58 | ) : Serializable {
59 | fun toDetail() = Detail(
60 | title = "Grouped Alarm Journal Data ($groupId)",
61 | details = entries.fold(mutableMapOf()) { acc, nextData ->
62 | acc.putAll(nextData.details)
63 | acc
64 | },
65 | body = entries.flatMap {
66 | it.data.timestamp
67 | it.body
68 | },
69 | )
70 |
71 | companion object {
72 | @JvmStatic
73 | private val serialVersionUID = -1199203578454144713L
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/statistics/categories/DatabaseStatistics.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.statistics.categories
2 |
3 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor
4 | import io.github.inductiveautomation.kindling.statistics.GatewayBackup
5 | import io.github.inductiveautomation.kindling.statistics.Statistic
6 | import io.github.inductiveautomation.kindling.statistics.StatisticCalculator
7 | import io.github.inductiveautomation.kindling.utils.executeQuery
8 | import io.github.inductiveautomation.kindling.utils.get
9 | import io.github.inductiveautomation.kindling.utils.toList
10 |
11 | data class DatabaseStatistics(
12 | val connections: List,
13 | ) : Statistic {
14 | val enabled: Int = connections.count { it.enabled }
15 |
16 | data class Connection(
17 | val name: String,
18 | val description: String?,
19 | val vendor: DatabaseVendor,
20 | val enabled: Boolean,
21 | val sfEnabled: Boolean,
22 | val bufferSize: Long,
23 | val cacheSize: Long,
24 | )
25 |
26 | @Suppress("SqlResolve")
27 | companion object Calculator : StatisticCalculator {
28 | private val DATABASE_STATS =
29 | """
30 | SELECT
31 | ds.name,
32 | ds.description,
33 | jdbc.dbtype,
34 | ds.enabled,
35 | sf.enablediskstore,
36 | sf.buffersize,
37 | sf.storemaxrecords
38 | FROM
39 | datasources ds
40 | JOIN storeandforwardsyssettings sf ON ds.datasources_id = sf.storeandforwardsyssettings_id
41 | JOIN jdbcdrivers jdbc ON ds.driverid = jdbc.jdbcdrivers_id
42 | """.trimIndent()
43 |
44 | override suspend fun calculate(backup: GatewayBackup): DatabaseStatistics? {
45 | val connections =
46 | backup.configDb.executeQuery(DATABASE_STATS).toList { rs ->
47 | Connection(
48 | name = rs[1],
49 | description = rs[2],
50 | vendor = DatabaseVendor.valueOf(rs[3]),
51 | enabled = rs[4],
52 | sfEnabled = rs[5],
53 | bufferSize = rs[6],
54 | cacheSize = rs[7],
55 | )
56 | }
57 |
58 | if (connections.isEmpty()) {
59 | return null
60 | }
61 |
62 | return DatabaseStatistics(connections)
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/serial/SerialViewer.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.serial
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import deser.SerializationDumper
5 | import io.github.inductiveautomation.kindling.core.Tool
6 | import io.github.inductiveautomation.kindling.core.ToolPanel
7 | import io.github.inductiveautomation.kindling.utils.FileFilter
8 | import io.github.inductiveautomation.kindling.utils.FlatScrollPane
9 | import io.github.inductiveautomation.kindling.utils.HorizontalSplitPane
10 | import io.github.inductiveautomation.kindling.utils.toHumanReadableBinary
11 | import java.awt.Font
12 | import java.nio.file.Path
13 | import javax.swing.Icon
14 | import javax.swing.JLabel
15 | import javax.swing.JTextArea
16 | import kotlin.io.path.inputStream
17 | import kotlin.io.path.name
18 | import kotlin.io.path.readBytes
19 |
20 | class SerialViewPanel(private val path: Path) : ToolPanel() {
21 | private val serialDump = JTextArea().apply {
22 | font = Font(Font.MONOSPACED, Font.PLAIN, 12)
23 | isEditable = false
24 | }
25 |
26 | private val rawBytes = JTextArea().apply {
27 | font = Font(Font.MONOSPACED, Font.PLAIN, 12)
28 | isEditable = false
29 | }
30 |
31 | init {
32 | val data = path.readBytes()
33 | serialDump.text = SerializationDumper(data).parseStream()
34 | rawBytes.text = path.inputStream().toHumanReadableBinary()
35 | name = path.name
36 |
37 | add(
38 | JLabel("Java Serialized Data: ${data.size} bytes").apply {
39 | putClientProperty("FlatLaf.styleClass", "h3.regular")
40 | },
41 | "wrap",
42 | )
43 | add(
44 | HorizontalSplitPane(
45 | FlatScrollPane(serialDump),
46 | FlatScrollPane(rawBytes),
47 | resizeWeight = 0.8,
48 | ) {
49 | },
50 | "push, grow",
51 | )
52 | }
53 |
54 | override val icon: Icon = SerialViewer.icon
55 |
56 | override fun getToolTipText(): String? = path.toString()
57 | }
58 |
59 | data object SerialViewer : Tool {
60 | override val title: String = "Java Serialization Viewer"
61 | override val description: String = "Serial files"
62 | override val icon: FlatSVGIcon = FlatSVGIcon("icons/bx-code.svg")
63 |
64 | override fun open(path: Path): ToolPanel = SerialViewPanel(path)
65 |
66 | override val extensions: Array = arrayOf("bin")
67 | override val filter: FileFilter = FileFilter("Java Serialized File", *extensions)
68 | override val serialKey: String = "serial-viewer"
69 |
70 | override val isAdvanced: Boolean = true
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/StackTrace.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import io.github.inductiveautomation.kindling.core.Detail.BodyLine
4 | import io.github.inductiveautomation.kindling.core.Kindling.Preferences.Advanced.HyperlinkStrategy
5 | import io.github.inductiveautomation.kindling.core.LinkHandlingStrategy.OpenInIde
6 | import java.util.Properties
7 |
8 | typealias StackElement = String
9 | typealias StackTrace = List
10 |
11 | private val classnameRegex = """(.*/)?(?[^\s\d$]*)[.$].*\(((?.*\..*):(?\d+)|.*)\)""".toRegex()
12 |
13 | fun StackElement.toBodyLine(version: String): BodyLine = MajorVersion.lookup(version)?.let {
14 | val escapedLine = this.escapeHtml()
15 | val matchResult = classnameRegex.find(this)
16 |
17 | if (matchResult != null) {
18 | val path by matchResult.groups
19 | if (HyperlinkStrategy.currentValue == OpenInIde) {
20 | val file = matchResult.groups["file"]?.value
21 | val line = matchResult.groups["line"]?.value?.toIntOrNull()
22 | if (file != null && line != null) {
23 | BodyLine(escapedLine, "http://localhost/file?file=$file&line=$line")
24 | } else {
25 | BodyLine(escapedLine)
26 | }
27 | } else {
28 | val url = it.classMap?.get(path.value) as String?
29 | BodyLine(escapedLine, url)
30 | }
31 | } else {
32 | BodyLine(escapedLine)
33 | }
34 | } ?: BodyLine(this)
35 |
36 | @Suppress("ktlint:standard:trailing-comma-on-declaration-site")
37 | enum class MajorVersion(val version: String) {
38 | SevenNine("7.9"),
39 | EightZero("8.0"),
40 | EightOne("8.1");
41 |
42 | val classMap: Properties? by lazy {
43 | Properties().also { properties ->
44 | this::class.java.getResourceAsStream("/$version/links.properties")?.use(properties::load)
45 | }
46 | }
47 |
48 | companion object {
49 | private val versionCache = LinkedHashMap().apply {
50 | put("dev", EightOne)
51 | repeat(22) { patch ->
52 | put("7.9.$patch", SevenNine)
53 | }
54 | repeat(18) { patch ->
55 | put("8.0.$patch", EightZero)
56 | }
57 | repeat(33) { patch ->
58 | put("8.1.$patch", EightOne)
59 | }
60 | }
61 |
62 | fun lookup(version: String): MajorVersion? = versionCache.getOrPut(version) {
63 | entries.firstOrNull { majorVersion ->
64 | version.startsWith(majorVersion.version)
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/core/Timezone.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.core
2 |
3 | import io.github.inductiveautomation.kindling.core.Kindling.Preferences
4 | import java.time.ZoneId
5 | import java.time.temporal.TemporalAccessor
6 | import java.time.format.DateTimeFormatter as JavaFormatter
7 |
8 | object Timezone {
9 | object Default : AbstractDateTimeFormatter() {
10 | override val zoneId: ZoneId
11 | get() = Preferences.General.DefaultTimezone.currentValue
12 |
13 | init {
14 | Preferences.General.DefaultTimezone.addChangeListener { newValue ->
15 | formatter = createFormatter(newValue)
16 | listeners.forEach { it.invoke(this) } // notify listeners when timezone changes
17 | }
18 | }
19 | }
20 | }
21 |
22 | interface DateTimeFormatter {
23 | val zoneId: ZoneId
24 |
25 | /**
26 | * Format [time] using [zoneId] automatically.
27 | */
28 | fun format(time: TemporalAccessor): String
29 |
30 | /**
31 | * Format [date] using [zoneId] automatically.
32 | *
33 | * This function is overloaded to also accept [java.util.Date] types, including [java.sql.Date]
34 | * and [java.sql.Timestamp].
35 | * - [java.sql.Date] is converted via [java.sql.Date.toLocalDate] at the start of the day
36 | * in the selected timezone.
37 | * - [java.sql.Timestamp] and [java.util.Date] preserve full time-of-day precision.
38 | */
39 | fun format(date: java.util.Date): String
40 |
41 | fun addChangeListener(listener: (DateTimeFormatter) -> Unit)
42 |
43 | fun removeChangeListener(listener: (DateTimeFormatter) -> Unit)
44 | }
45 |
46 | abstract class AbstractDateTimeFormatter : DateTimeFormatter {
47 | protected var formatter = createFormatter(zoneId)
48 |
49 | protected val listeners = mutableListOf<(DateTimeFormatter) -> Unit>()
50 |
51 | abstract override val zoneId: ZoneId
52 |
53 | protected open fun createFormatter(id: ZoneId): JavaFormatter = JavaFormatter.ofPattern("uuuu-MM-dd HH:mm:ss:SSS").withZone(id)
54 |
55 | override fun addChangeListener(listener: (DateTimeFormatter) -> Unit) {
56 | listeners += listener
57 | }
58 |
59 | override fun removeChangeListener(listener: (DateTimeFormatter) -> Unit) {
60 | listeners -= listener
61 | }
62 |
63 | override fun format(time: TemporalAccessor): String = formatter.format(time)
64 |
65 | override fun format(date: java.util.Date): String = when (date) {
66 | is java.sql.Date -> format(
67 | date.toLocalDate().atStartOfDay(zoneId),
68 | )
69 |
70 | is java.sql.Timestamp -> format(date.toInstant())
71 | else -> format(date.toInstant())
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/Tables.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import io.github.evanrupert.excelkt.workbook
4 | import java.io.File
5 | import javax.swing.JTable
6 | import javax.swing.event.TableModelEvent
7 | import javax.swing.table.AbstractTableModel
8 | import javax.swing.table.TableModel
9 |
10 | fun JTable.selectedRowIndices(): IntArray = selectionModel.selectedIndices
11 | .filter { isRowSelected(it) }
12 | .map { convertRowIndexToModel(it) }
13 | .toIntArray()
14 |
15 | fun JTable.selectedOrAllRowIndices(): IntArray = if (selectionModel.isSelectionEmpty) {
16 | IntArray(model.rowCount) { it }
17 | } else {
18 | selectedRowIndices()
19 | }
20 |
21 | val TableModel.rowIndices get() = 0 until rowCount
22 | val TableModel.columnIndices get() = 0 until columnCount
23 |
24 | /**
25 | * A custom [TableModelEvent] which is fired when an unspecified number of row data has changed for a single column.
26 | *
27 | * @param column The column index.
28 | */
29 | fun AbstractTableModel.fireTableColumnDataChanged(column: Int) {
30 | fireTableChanged(
31 | object : TableModelEvent(this) {
32 | override fun getColumn(): Int = column
33 | },
34 | )
35 | }
36 |
37 | fun TableModel.exportToCSV(file: File) {
38 | file.printWriter().use { out ->
39 | columnIndices.joinTo(buffer = out, separator = ",") { col ->
40 | getColumnName(col)
41 | }
42 | out.println()
43 | for (row in rowIndices) {
44 | columnIndices.joinTo(buffer = out, separator = ",") { col ->
45 | "\"${getValueAt(row, col)?.toString().orEmpty()}\""
46 | }
47 | out.println()
48 | }
49 | }
50 | }
51 |
52 | fun TableModel.exportToXLSX(file: File) = file.outputStream().use { fos ->
53 | workbook {
54 | sheet("Sheet 1") {
55 | // TODO: Some way to pipe in a more useful sheet name (or multiple sheets?)
56 | row {
57 | for (col in columnIndices) {
58 | cell(getColumnName(col))
59 | }
60 | }
61 | for (row in rowIndices) {
62 | row {
63 | for (col in columnIndices) {
64 | when (val value = getValueAt(row, col)) {
65 | is Double -> cell(
66 | value,
67 | createCellStyle {
68 | dataFormat = xssfWorkbook.createDataFormat().getFormat("0.00")
69 | },
70 | )
71 |
72 | else -> cell(value ?: "")
73 | }
74 | }
75 | }
76 | }
77 | }
78 | }.xssfWorkbook.write(fos)
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/gwbk/DatabaseStatisticsRenderer.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views.gwbk
2 |
3 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor.DB2
4 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor.FIREBIRD
5 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor.GENERIC
6 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor.MSSQL
7 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor.MYSQL
8 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor.ORACLE
9 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor.POSTGRES
10 | import com.inductiveautomation.ignition.common.datasource.DatabaseVendor.SQLITE
11 | import io.github.inductiveautomation.kindling.statistics.categories.DatabaseStatistics
12 | import io.github.inductiveautomation.kindling.utils.ColumnList
13 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
14 | import io.github.inductiveautomation.kindling.utils.FlatScrollPane
15 | import io.github.inductiveautomation.kindling.utils.ReifiedJXTable
16 | import io.github.inductiveautomation.kindling.utils.ReifiedLabelProvider.Companion.setDefaultRenderer
17 | import io.github.inductiveautomation.kindling.utils.ReifiedListTableModel
18 | import javax.swing.Icon
19 | import javax.swing.JComponent
20 | import javax.swing.SortOrder
21 |
22 | class DatabaseStatisticsRenderer : StatisticRenderer {
23 | override val title: String = "Databases"
24 | override val icon: Icon = FlatActionIcon("icons/bx-data.svg")
25 |
26 | override fun DatabaseStatistics.subtitle(): String = "$enabled enabled, ${connections.size} total"
27 |
28 | override fun DatabaseStatistics.render(): JComponent = FlatScrollPane(
29 | ReifiedJXTable(ReifiedListTableModel(connections, ConnectionColumns)).apply {
30 | setDefaultRenderer(
31 | getText = { it?.name },
32 | getTooltip = { it?.description },
33 | )
34 | setSortOrder(Name, SortOrder.ASCENDING)
35 | },
36 | )
37 |
38 | @Suppress("unused")
39 | companion object ConnectionColumns : ColumnList() {
40 | val Name by column { it }
41 | val Vendor by column { conn ->
42 | when (conn.vendor) {
43 | MYSQL -> "MySQL/MariaDB"
44 | POSTGRES -> "PostgreSQL"
45 | MSSQL -> "SQL Server"
46 | ORACLE -> "Oracle"
47 | DB2 -> "DB2"
48 | FIREBIRD -> "Firebird"
49 | SQLITE -> "SQLite"
50 | GENERIC -> "Other"
51 | }
52 | }
53 | val Enabled by column { it.enabled }
54 | val sfEnabled by column("S+F") { it.sfEnabled }
55 | val bufferSize by column("Memory Buffer") { it.bufferSize }
56 | val cacheSize by column("Disk Cache") { it.cacheSize }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/ProjectView.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.zip.views
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import io.github.inductiveautomation.kindling.core.Kindling.Preferences.General.HomeLocation
5 | import io.github.inductiveautomation.kindling.core.Kindling.Preferences.UI.Theme
6 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
7 | import java.nio.file.FileVisitResult
8 | import java.nio.file.Path
9 | import java.nio.file.spi.FileSystemProvider
10 | import java.util.zip.ZipEntry
11 | import java.util.zip.ZipOutputStream
12 | import javax.swing.JButton
13 | import javax.swing.JFileChooser
14 | import javax.swing.filechooser.FileNameExtensionFilter
15 | import kotlin.io.path.ExperimentalPathApi
16 | import kotlin.io.path.div
17 | import kotlin.io.path.isDirectory
18 | import kotlin.io.path.name
19 | import kotlin.io.path.outputStream
20 | import kotlin.io.path.readBytes
21 | import kotlin.io.path.visitFileTree
22 |
23 | @OptIn(ExperimentalPathApi::class)
24 | class ProjectView(override val provider: FileSystemProvider, override val path: Path) : SinglePathView() {
25 | private val exportButton = JButton("Export Project")
26 |
27 | init {
28 | exportButton.addActionListener {
29 | exportZipFileChooser.selectedFile = HomeLocation.currentValue.resolve("${path.name}.zip").toFile()
30 | if (exportZipFileChooser.showSaveDialog(this@ProjectView) == JFileChooser.APPROVE_OPTION) {
31 | val exportLocation = exportZipFileChooser.selectedFile.toPath()
32 |
33 | ZipOutputStream(exportLocation.outputStream()).use { zos ->
34 | path.visitFileTree {
35 | onVisitFile { file, _ ->
36 | zos.run {
37 | putNextEntry(ZipEntry(path.relativize(file).toString()))
38 | write(file.readBytes())
39 | closeEntry()
40 | FileVisitResult.CONTINUE
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }
47 |
48 | add(exportButton, "north")
49 | add(FileView(provider, path / "project.json"), "push, grow")
50 | }
51 |
52 | override val icon: FlatSVGIcon = FlatActionIcon("icons/bx-box.svg")
53 |
54 | companion object {
55 | internal val exportZipFileChooser by lazy {
56 | JFileChooser(HomeLocation.currentValue.toFile()).apply {
57 | isMultiSelectionEnabled = false
58 | isAcceptAllFileFilterUsed = false
59 | fileSelectionMode = JFileChooser.FILES_ONLY
60 | fileFilter = FileNameExtensionFilter("ZIP Files", "zip")
61 |
62 | Theme.addChangeListener {
63 | updateUI()
64 | }
65 | }
66 | }
67 |
68 | fun isProjectDirectory(path: Path) = path.parent?.name == "projects" && path.isDirectory()
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricsView.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.idb.metrics
2 |
3 | import io.github.inductiveautomation.kindling.core.ToolPanel
4 | import io.github.inductiveautomation.kindling.utils.EDT_SCOPE
5 | import io.github.inductiveautomation.kindling.utils.FlatScrollPane
6 | import io.github.inductiveautomation.kindling.utils.toList
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.launch
10 | import net.miginfocom.swing.MigLayout
11 | import java.sql.Connection
12 | import javax.swing.Icon
13 | import javax.swing.JPanel
14 |
15 | class MetricsView(connection: Connection) : ToolPanel("ins 0, fill, hidemode 3") {
16 | @Suppress("SqlResolve")
17 | private val metrics: List =
18 | connection.createStatement().executeQuery(
19 | //language=sql
20 | """
21 | SELECT DISTINCT
22 | metric_name
23 | FROM
24 | system_metrics
25 | """.trimIndent(),
26 | ).toList { rs ->
27 | Metric(rs.getString(1))
28 | }
29 |
30 | private val metricTree = MetricTree(metrics)
31 |
32 | @Suppress("SqlResolve")
33 | private val metricDataQuery =
34 | connection.prepareStatement(
35 | //language=sql
36 | """
37 | SELECT DISTINCT
38 | value,
39 | timestamp
40 | FROM
41 | system_metrics
42 | WHERE
43 | metric_name = ?
44 | ORDER BY
45 | timestamp
46 | """.trimIndent(),
47 | )
48 |
49 | private val metricCards: List =
50 | metrics.map { metric ->
51 | val metricData =
52 | metricDataQuery.apply {
53 | setString(1, metric.name)
54 | }
55 | .executeQuery()
56 | .toList { rs ->
57 | MetricData(rs.getDouble(1), rs.getTimestamp(2))
58 | }
59 |
60 | MetricCard(metric, metricData)
61 | }
62 |
63 | private val cardPanel =
64 | JPanel(MigLayout("wrap 2, fillx, gap 20, hidemode 3, ins 6")).apply {
65 | for (card in metricCards) {
66 | add(card, "pushx, growx")
67 | }
68 | }
69 |
70 | init {
71 | add(FlatScrollPane(metricTree), "grow, w 200::20%")
72 | add(FlatScrollPane(cardPanel), "push, grow, span")
73 |
74 | metricTree.checkBoxTreeSelectionModel.addTreeSelectionListener { updateData() }
75 | }
76 |
77 | private fun updateData() {
78 | BACKGROUND.launch {
79 | val selectedMetricNames = metricTree.selectedLeafNodes.map { it.name }
80 | EDT_SCOPE.launch {
81 | for (card in metricCards) {
82 | card.isVisible = card.metric.name in selectedMetricNames
83 | }
84 | }
85 | }
86 | }
87 |
88 | override val icon: Icon? = null
89 |
90 | companion object {
91 | private val BACKGROUND = CoroutineScope(Dispatchers.Default)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/xml/quarantine/QuarantineViewer.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.xml.quarantine
2 |
3 | import deser.SerializationDumper
4 | import io.github.inductiveautomation.kindling.core.Detail
5 | import io.github.inductiveautomation.kindling.core.DetailsPane
6 | import io.github.inductiveautomation.kindling.utils.FlatScrollPane
7 | import io.github.inductiveautomation.kindling.utils.HorizontalSplitPane
8 | import io.github.inductiveautomation.kindling.utils.XML_FACTORY
9 | import io.github.inductiveautomation.kindling.utils.deserializeStoreAndForward
10 | import io.github.inductiveautomation.kindling.utils.parse
11 | import io.github.inductiveautomation.kindling.utils.toDetail
12 | import io.github.inductiveautomation.kindling.xml.XmlTool
13 | import net.miginfocom.swing.MigLayout
14 | import javax.swing.JList
15 | import javax.swing.JPanel
16 | import javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
17 | import com.inductiveautomation.ignition.common.Base64 as IaBase64
18 |
19 | internal class QuarantineViewer(data: List) : JPanel(MigLayout("ins 6, fill, hidemode 3")) {
20 | private val list = JList(Array(data.size) { i -> i + 1 }).apply {
21 | selectionMode = MULTIPLE_INTERVAL_SELECTION
22 | }
23 |
24 | private val detailsPane = DetailsPane()
25 |
26 | init {
27 | list.addListSelectionListener {
28 | detailsPane.events = list.selectedIndices.map { i ->
29 | data[i].detail
30 | }
31 | }
32 |
33 | add(
34 | HorizontalSplitPane(
35 | FlatScrollPane(list),
36 | detailsPane,
37 | 0.1,
38 | ),
39 | "push, grow",
40 | )
41 | }
42 |
43 | internal data class QuarantineRow(
44 | val b64data: String,
45 | ) {
46 | private val binaryData: ByteArray by lazy {
47 | IaBase64.decodeAndGunzip(b64data)
48 | }
49 |
50 | val detail by lazy {
51 | try {
52 | binaryData.deserializeStoreAndForward().toDetail()
53 | } catch (e: Exception) {
54 | XmlTool.logger.error("Unable to deserialize quarantine data", e)
55 | val serializedData = SerializationDumper(binaryData).parseStream().lines()
56 | Detail(
57 | title = "Error",
58 | message = "Failed to deserialize: ${e.message}",
59 | body = serializedData,
60 | )
61 | }
62 | }
63 | }
64 |
65 | companion object {
66 | operator fun invoke(file: List): QuarantineViewer? {
67 | val document = XML_FACTORY.parse(file.joinToString("\n").byteInputStream())
68 | val cacheEntries = document.getElementsByTagName("base64")
69 |
70 | val data = (0..
72 | QuarantineRow(cacheEntries.item(i).textContent)
73 | }
74 |
75 | return if (data.isNotEmpty()) {
76 | QuarantineViewer(data)
77 | } else {
78 | null
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/tagconfig/TagBrowseTree.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.tagconfig
2 |
3 | import com.jidesoft.swing.TreeSearchable
4 | import io.github.inductiveautomation.kindling.tagconfig.model.AbstractTagProvider
5 | import io.github.inductiveautomation.kindling.tagconfig.model.Node
6 | import io.github.inductiveautomation.kindling.utils.EDT_SCOPE
7 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
8 | import io.github.inductiveautomation.kindling.utils.tag
9 | import io.github.inductiveautomation.kindling.utils.treeCellRenderer
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.launch
12 | import kotlinx.coroutines.withContext
13 | import javax.swing.JTree
14 | import javax.swing.tree.DefaultMutableTreeNode
15 | import javax.swing.tree.DefaultTreeModel
16 | import javax.swing.tree.TreePath
17 | import kotlin.properties.Delegates
18 |
19 | class TagBrowseTree : JTree(NO_SELECTION) {
20 | var provider: AbstractTagProvider? by Delegates.observable(null) { _, _, newValue ->
21 | if (newValue == null) {
22 | model = NO_SELECTION
23 | } else {
24 | EDT_SCOPE.launch {
25 | model = NO_SELECTION
26 | val providerNode = withContext(Dispatchers.Default) {
27 | newValue.getProviderNode().await()
28 | }
29 | model = DefaultTreeModel(providerNode)
30 | }
31 | }
32 | }
33 |
34 | init {
35 | isRootVisible = false
36 | setShowsRootHandles(true)
37 |
38 | setCellRenderer(
39 | treeCellRenderer { _, value, _, _, _, _, _ ->
40 | val actualValue = value as? Node
41 |
42 | text = if (actualValue?.inferred == true) {
43 | buildString {
44 | tag("html") {
45 | tag("i") {
46 | append("${actualValue.name}*")
47 | }
48 | }
49 | }
50 | } else {
51 | actualValue?.name
52 | }
53 |
54 | when (actualValue?.config?.tagType) {
55 | "AtomicTag" -> {
56 | icon = FlatActionIcon("icons/bx-purchase-tag.svg")
57 | }
58 | "UdtInstance", "UdtType" -> {
59 | icon = FlatActionIcon("icons/bx-vector.svg")
60 | }
61 | }
62 |
63 | this
64 | },
65 | )
66 |
67 | object : TreeSearchable(this) {
68 | init {
69 | isRecursive = true
70 | isRepeats = true
71 | }
72 |
73 | // Returns full tag path without provider name. (path/to/tag)
74 | override fun convertElementToString(element: Any?): String {
75 | val path = (element as? TreePath)?.path ?: return ""
76 | return (1..path.lastIndex).joinToString("/") {
77 | (path[it] as Node).name
78 | }
79 | }
80 | }
81 | }
82 |
83 | companion object {
84 | private val NO_SELECTION = DefaultTreeModel(DefaultMutableTreeNode("Select a Tag Provider to Browse"))
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/quest/Utils.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.quest
2 |
3 | import io.questdb.cairo.CairoEngine
4 | import io.questdb.cairo.ColumnType
5 | import io.questdb.cairo.sql.Record
6 | import io.questdb.cairo.sql.RecordMetadata
7 | import io.questdb.griffin.SqlExecutionContext
8 | import java.util.Date
9 | import kotlin.reflect.KClass
10 | import kotlin.uuid.ExperimentalUuidApi
11 | import kotlin.uuid.Uuid
12 |
13 | context(sqlExec: SqlExecutionContext)
14 | internal fun CairoEngine.select(
15 | query: String,
16 | transform: context(RecordMetadata) (Record) -> T,
17 | ): List = buildList {
18 | select(query, sqlExec).use { stmt ->
19 | stmt.getCursor(sqlExec).use { cursor ->
20 | while (cursor.hasNext()) {
21 | add(transform(stmt.metadata, cursor.record))
22 | }
23 | }
24 | }
25 | }
26 |
27 | context(meta: RecordMetadata)
28 | internal inline operator fun Record.get(name: String): T? = get(meta.getColumnIndex(name))
29 |
30 | @OptIn(ExperimentalUuidApi::class)
31 | context(meta: RecordMetadata)
32 | internal inline operator fun Record.get(index: Int): T? {
33 | val type = meta.getColumnType(index)
34 | val clazz = meta.getColumnClass(index)
35 |
36 | if (T::class != Any::class && T::class != clazz) return null
37 |
38 | return when (type.toShort()) {
39 | ColumnType.SYMBOL -> getSymA(index)?.toString()
40 | ColumnType.STRING -> getStrA(index)?.toString()
41 | ColumnType.VARCHAR -> getVarcharA(index)?.toString()
42 | ColumnType.BYTE -> getByte(index)
43 | ColumnType.SHORT -> getShort(index)
44 | ColumnType.INT -> getInt(index)
45 | ColumnType.LONG -> getLong(index)
46 | ColumnType.FLOAT -> getFloat(index)
47 | ColumnType.DOUBLE -> getDouble(index)
48 | ColumnType.CHAR -> getChar(index)
49 | ColumnType.BOOLEAN -> getBool(index)
50 | ColumnType.TIMESTAMP -> getTimestamp(index).takeIf { it >= 0 }?.let {
51 | Date(it / 1000)
52 | }
53 | ColumnType.UUID -> Uuid.parse(getStrA(index).toString())
54 | ColumnType.BINARY -> getBin(index)?.let { seq ->
55 | ByteArray(seq.length().toInt()) { i ->
56 | seq.byteAt(i.toLong())
57 | }
58 | }
59 |
60 | else -> error("Unable to parse column type: ${ColumnType.nameOf(type)}")
61 | } as T
62 | }
63 |
64 | @OptIn(ExperimentalUuidApi::class)
65 | internal fun RecordMetadata.getColumnClass(index: Int): KClass<*>? {
66 | val type = getColumnType(index)
67 | return when (type.toShort()) {
68 | ColumnType.SYMBOL,
69 | ColumnType.STRING,
70 | ColumnType.VARCHAR,
71 | -> String::class
72 |
73 | ColumnType.BYTE -> Byte::class
74 | ColumnType.SHORT -> Short::class
75 | ColumnType.INT -> Int::class
76 | ColumnType.LONG -> Long::class
77 | ColumnType.TIMESTAMP -> Date::class
78 | ColumnType.FLOAT -> Float::class
79 | ColumnType.DOUBLE -> Double::class
80 | ColumnType.CHAR -> Char::class
81 | ColumnType.BOOLEAN -> Boolean::class
82 | ColumnType.BINARY -> ByteArray::class
83 | ColumnType.UUID -> Uuid::class
84 | else -> error("Unknown column type: ${ColumnType.nameOf(type)}")
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/xml/logback/SelectedLoggerCard.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.xml.logback
2 |
3 | import com.formdev.flatlaf.extras.FlatSVGIcon
4 | import net.miginfocom.swing.MigLayout
5 | import java.awt.event.ItemEvent
6 | import javax.swing.JButton
7 | import javax.swing.JCheckBox
8 | import javax.swing.JComboBox
9 | import javax.swing.JLabel
10 | import javax.swing.JPanel
11 | import javax.swing.JTextField
12 | import javax.swing.SwingConstants.RIGHT
13 | import javax.swing.UIManager
14 | import javax.swing.border.LineBorder
15 |
16 | internal class SelectedLoggerCard(
17 | val logger: SelectedLogger,
18 | private val callback: () -> Unit,
19 | ) : JPanel(MigLayout("fill, ins 5, hidemode 3")) {
20 | val loggerLevelSelector = JComboBox(loggingLevels).apply {
21 | selectedItem = logger.level
22 | }
23 |
24 | val loggerSeparateOutput = JCheckBox("Output to separate location?").apply {
25 | isSelected = logger.separateOutput
26 | }
27 |
28 | val closeButton = JButton(FlatSVGIcon("icons/bx-x.svg")).apply {
29 | border = null
30 | background = null
31 | }
32 |
33 | val loggerOutputFolder = JTextField(logger.outputFolder).apply {
34 | addActionListener { callback() }
35 | }
36 |
37 | val loggerFilenamePattern = JTextField(logger.filenamePattern).apply {
38 | addActionListener { callback() }
39 | }
40 |
41 | val maxFileSize = sizeEntryField(logger.maxFileSize, "MB", callback)
42 | val totalSizeCap = sizeEntryField(logger.totalSizeCap, "MB", callback)
43 | val maxDays = sizeEntryField(logger.maxDaysHistory, "Days", callback)
44 |
45 | private val redirectOutputPanel = JPanel(MigLayout("ins 0, fill")).apply {
46 | isVisible = loggerSeparateOutput.isSelected
47 | loggerSeparateOutput.addItemListener {
48 | isVisible = it.stateChange == ItemEvent.SELECTED
49 | }
50 |
51 | add(JLabel("Output Folder", RIGHT), "split 2, spanx, sgx a")
52 | add(loggerOutputFolder, "growx")
53 | add(JLabel("Filename Pattern", RIGHT), "split 2, spanx, sgx a")
54 | add(loggerFilenamePattern, "growx")
55 |
56 | add(JLabel("Max File Size", RIGHT), "sgx a")
57 | add(maxFileSize, "sgx e, growx")
58 | add(JLabel("Total Size Cap", RIGHT), "sgx a")
59 | add(totalSizeCap, "sgx e, growx")
60 | add(JLabel("Max Days", RIGHT), "sgx a")
61 | add(maxDays, "sgx e, growx")
62 | }
63 |
64 | init {
65 | name = logger.name
66 | border = LineBorder(UIManager.getColor("Component.borderColor"), 3, true)
67 |
68 | loggerLevelSelector.addActionListener { callback() }
69 | loggerSeparateOutput.addActionListener { callback() }
70 |
71 | add(
72 | JLabel(logger.name).apply {
73 | putClientProperty("FlatLaf.styleClass", "h3")
74 | },
75 | )
76 | add(closeButton, "right, wrap")
77 | add(loggerLevelSelector)
78 | add(loggerSeparateOutput, "right, wrap")
79 | add(redirectOutputPanel, "growx, span")
80 | }
81 |
82 | companion object {
83 | private val loggingLevels =
84 | arrayOf(
85 | "OFF",
86 | "ERROR",
87 | "WARN",
88 | "INFO",
89 | "DEBUG",
90 | "TRACE",
91 | "ALL",
92 | )
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricTree.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.idb.metrics
2 |
3 | import com.jidesoft.swing.CheckBoxTree
4 | import io.github.inductiveautomation.kindling.utils.AbstractTreeNode
5 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon
6 | import io.github.inductiveautomation.kindling.utils.TypedTreeNode
7 | import io.github.inductiveautomation.kindling.utils.expandAll
8 | import io.github.inductiveautomation.kindling.utils.selectAll
9 | import io.github.inductiveautomation.kindling.utils.treeCellRenderer
10 | import javax.swing.tree.DefaultTreeModel
11 |
12 | data class MetricNode(override val userObject: List) : TypedTreeNode>() {
13 | constructor(vararg parts: String) : this(parts.toList())
14 |
15 | val name by lazy { userObject.joinToString(".") }
16 |
17 | override fun toString(): String = name
18 | }
19 |
20 | class RootNode(metrics: List) : AbstractTreeNode() {
21 | init {
22 | val legacy = MetricNode("Legacy")
23 | val modern = MetricNode("New")
24 |
25 | val seen = mutableMapOf, MetricNode>()
26 | for (metric in metrics) {
27 | var lastSeen = if (metric.isLegacy) legacy else modern
28 | val currentLeadingPath = mutableListOf(lastSeen.name)
29 | for (part in metric.name.split('.')) {
30 | currentLeadingPath.add(part)
31 | val next = seen.getOrPut(currentLeadingPath.toList()) {
32 | val newChild = MetricNode(currentLeadingPath.drop(1))
33 | lastSeen.children.add(newChild)
34 | newChild
35 | }
36 | lastSeen = next
37 | }
38 | }
39 |
40 | when {
41 | legacy.childCount == 0 && modern.childCount > 0 -> {
42 | for (zoomer in modern.children) {
43 | children.add(zoomer)
44 | }
45 | }
46 |
47 | modern.childCount == 0 && legacy.childCount > 0 -> {
48 | for (boomer in legacy.children) {
49 | children.add(boomer)
50 | }
51 | }
52 |
53 | else -> {
54 | children.add(legacy)
55 | children.add(modern)
56 | }
57 | }
58 | }
59 |
60 | private val Metric.isLegacy: Boolean
61 | get() = name.first().isUpperCase()
62 | }
63 |
64 | class MetricTree(metrics: List) : CheckBoxTree(DefaultTreeModel(RootNode(metrics))) {
65 | init {
66 | isRootVisible = false
67 | setShowsRootHandles(true)
68 |
69 | expandAll()
70 | selectAll()
71 |
72 | setCellRenderer(
73 | treeCellRenderer { _, value, selected, _, _, _, _ ->
74 | if (value is MetricNode) {
75 | val path = value.userObject
76 | text = path.last()
77 | toolTipText = value.name
78 | icon = FlatActionIcon("icons/bx-line-chart.svg")
79 | } else {
80 | icon = null
81 | }
82 | this
83 | },
84 | )
85 | }
86 |
87 | val selectedLeafNodes: List
88 | get() = checkBoxTreeSelectionModel.selectionPaths.flatMap {
89 | (it.lastPathComponent as MetricNode).depthFirstChildren()
90 | }.filterIsInstance()
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/core/ToolPanel.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.core
2 |
3 | import io.github.inductiveautomation.kindling.core.Kindling.Preferences.General.HomeLocation
4 | import io.github.inductiveautomation.kindling.core.Kindling.Preferences.UI.Theme
5 | import io.github.inductiveautomation.kindling.utils.Action
6 | import io.github.inductiveautomation.kindling.utils.FileFilter
7 | import io.github.inductiveautomation.kindling.utils.FloatableComponent
8 | import io.github.inductiveautomation.kindling.utils.PopupMenuCustomizer
9 | import io.github.inductiveautomation.kindling.utils.exportToCSV
10 | import io.github.inductiveautomation.kindling.utils.exportToXLSX
11 | import net.miginfocom.swing.MigLayout
12 | import java.io.File
13 | import javax.swing.Icon
14 | import javax.swing.JFileChooser
15 | import javax.swing.JMenu
16 | import javax.swing.JPanel
17 | import javax.swing.JPopupMenu
18 | import javax.swing.table.TableModel
19 |
20 | abstract class ToolPanel(
21 | layoutConstraints: String = "ins 6, fill, hidemode 3",
22 | ) : JPanel(MigLayout(layoutConstraints)),
23 | FloatableComponent,
24 | PopupMenuCustomizer {
25 | abstract override val icon: Icon?
26 | override val tabName: String get() = name ?: this.paramString()
27 | override val tabTooltip: String? get() = toolTipText
28 |
29 | override fun customizePopupMenu(menu: JPopupMenu) = Unit
30 |
31 | protected fun exportMenu(defaultFileName: String = "", modelSupplier: () -> TableModel): JMenu = JMenu("Export").apply {
32 | for (format in ExportFormat.entries) {
33 | add(
34 | Action("Export as ${format.extension.uppercase()}") {
35 | exportFileChooser.apply {
36 | selectedFile = File(defaultFileName)
37 | resetChoosableFileFilters()
38 | fileFilter = format.fileFilter
39 | if (showSaveDialog(this@ToolPanel) == JFileChooser.APPROVE_OPTION) {
40 | val selectedFile =
41 | if (selectedFile.absolutePath.endsWith(format.extension)) {
42 | selectedFile
43 | } else {
44 | File(selectedFile.absolutePath + ".${format.extension}")
45 | }
46 | format.action.invoke(modelSupplier(), selectedFile)
47 | }
48 | }
49 | },
50 | )
51 | }
52 | }
53 |
54 | companion object {
55 | val exportFileChooser = JFileChooser(HomeLocation.currentValue.toFile()).apply {
56 | isMultiSelectionEnabled = false
57 | isAcceptAllFileFilterUsed = false
58 | fileView = CustomIconView()
59 |
60 | Theme.addChangeListener {
61 | updateUI()
62 | }
63 | }
64 |
65 | @Suppress("ktlint:standard:trailing-comma-on-declaration-site")
66 | private enum class ExportFormat(
67 | description: String,
68 | val extension: String,
69 | val action: (TableModel, File) -> Unit,
70 | ) {
71 | CSV("Comma Separated Values", "csv", TableModel::exportToCSV),
72 | EXCEL("Excel Workbook", "xlsx", TableModel::exportToXLSX);
73 |
74 | val fileFilter = FileFilter(description, extension)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/FilterSidebar.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import com.formdev.flatlaf.extras.components.FlatTabbedPane
4 | import io.github.inductiveautomation.kindling.core.FilterPanel
5 | import java.awt.Dimension
6 | import java.awt.Insets
7 | import java.awt.Point
8 | import java.awt.event.MouseEvent
9 | import javax.swing.JPopupMenu
10 | import javax.swing.JToolTip
11 | import javax.swing.UIManager
12 |
13 | open class FilterSidebar(
14 | private val filterPanels: List>,
15 | ) : FlatTabbedPane(),
16 | List> by filterPanels {
17 |
18 | override fun createToolTip(): JToolTip = JToolTip().apply {
19 | font = UIManager.getFont("h3.regular.font")
20 | minimumSize = Dimension(1, tabHeight)
21 | }
22 |
23 | override fun getToolTipLocation(event: MouseEvent): Point? = if (event.x <= tabHeight && event.y <= tabHeight * tabCount) {
24 | Point(
25 | event.x.coerceAtLeast(tabHeight),
26 | event.y.floorDiv(tabHeight) * tabHeight,
27 | )
28 | } else {
29 | null
30 | }
31 |
32 | init {
33 | tabAreaAlignment = TabAreaAlignment.leading
34 | tabPlacement = LEFT
35 | tabInsets = Insets(1, 1, 1, 1)
36 | tabLayoutPolicy = SCROLL_TAB_LAYOUT
37 | tabsPopupPolicy = TabsPopupPolicy.asNeeded
38 | scrollButtonsPolicy = ScrollButtonsPolicy.never
39 | tabWidthMode = TabWidthMode.compact
40 | tabType = TabType.underlined
41 | isShowContentSeparators = false
42 |
43 | preferredSize = Dimension(250, 100)
44 |
45 | filterPanels.forEach { filterPanel ->
46 | addTab(
47 | null,
48 | filterPanel.icon,
49 | filterPanel.component,
50 | filterPanel.formattedTabName,
51 | )
52 | filterPanel.addFilterChangeListener {
53 | filterPanel.updateTabState()
54 | }
55 | }
56 |
57 | attachPopupMenu { event ->
58 | val tabIndex = indexAtLocation(event.x, event.y)
59 | if (tabIndex !in filterPanels.indices) return@attachPopupMenu null
60 |
61 | JPopupMenu().apply {
62 | val filterPanel = filterPanels[tabIndex]
63 | add(
64 | Action("Reset") {
65 | filterPanel.reset()
66 | },
67 | )
68 | if (filterPanel is PopupMenuCustomizer) {
69 | filterPanel.customizePopupMenu(this)
70 | }
71 | }
72 | }
73 | selectedIndex = 0
74 | }
75 |
76 | protected fun FilterPanel<*>.updateTabState() {
77 | val index = indexOfComponent(component)
78 | if (isFilterApplied()) {
79 | setBackgroundAt(index, UIManager.getColor("TabbedPane.focusColor"))
80 | } else {
81 | setBackgroundAt(index, UIManager.getColor("TabbedPane.background"))
82 | }
83 | }
84 |
85 | override fun updateUI() {
86 | super.updateUI()
87 | @Suppress("UNNECESSARY_SAFE_CALL")
88 | filterPanels?.forEach {
89 | it.updateTabState()
90 | }
91 | }
92 |
93 | companion object {
94 | @JvmStatic
95 | protected val FilterPanel<*>.formattedTabName
96 | get() = buildString {
97 | tag("html") {
98 | tag("p", "style" to "margin: 3px;") {
99 | append(tabName)
100 | }
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/Serializers.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import io.github.inductiveautomation.kindling.cache.CacheViewer
4 | import io.github.inductiveautomation.kindling.core.Theme
5 | import io.github.inductiveautomation.kindling.core.Tool
6 | import io.github.inductiveautomation.kindling.idb.IdbViewer
7 | import io.github.inductiveautomation.kindling.thread.MultiThreadViewer
8 | import io.github.inductiveautomation.kindling.zip.ZipViewer
9 | import kotlinx.serialization.KSerializer
10 | import kotlinx.serialization.SerializationException
11 | import kotlinx.serialization.descriptors.PrimitiveKind
12 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
13 | import kotlinx.serialization.descriptors.SerialDescriptor
14 | import kotlinx.serialization.encoding.Decoder
15 | import kotlinx.serialization.encoding.Encoder
16 | import java.nio.charset.Charset
17 | import java.nio.file.Path
18 | import java.time.ZoneId
19 | import kotlin.io.path.Path
20 | import kotlin.io.path.pathString
21 |
22 | data object PathSerializer : KSerializer {
23 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(Path::class.java.name, PrimitiveKind.STRING)
24 |
25 | val Path.serializedForm: String get() = pathString
26 |
27 | fun fromString(string: String): Path = Path(string)
28 |
29 | override fun deserialize(decoder: Decoder): Path = fromString(decoder.decodeString())
30 |
31 | override fun serialize(encoder: Encoder, value: Path) = encoder.encodeString(value.serializedForm)
32 | }
33 |
34 | data object ThemeSerializer : KSerializer {
35 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(Theme::class.java.name, PrimitiveKind.STRING)
36 |
37 | override fun serialize(encoder: Encoder, value: Theme) = encoder.encodeString(value.name)
38 | override fun deserialize(decoder: Decoder): Theme = Theme.themes.getValue(decoder.decodeString())
39 | }
40 |
41 | data object ToolSerializer : KSerializer {
42 | private val bySerialKey: Map by lazy {
43 | Tool.tools.associateBy(Tool::serialKey)
44 | }
45 |
46 | // we used to store keys by their 'title' instead of their serial key
47 | // so to be nice on _de_serialization, we'll map those old values over
48 | private val aliases = mapOf(
49 | "Thread Viewer" to MultiThreadViewer.serialKey,
50 | "Ignition Archive" to ZipViewer.serialKey,
51 | "Cache Dump" to CacheViewer.serialKey,
52 | "Idb File" to IdbViewer.serialKey,
53 | )
54 |
55 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(Tool::class.java.name, PrimitiveKind.STRING)
56 |
57 | override fun deserialize(decoder: Decoder): Tool {
58 | val storedKey = decoder.decodeString()
59 | val actualKey = aliases[storedKey] ?: storedKey
60 | return bySerialKey[actualKey] ?: throw SerializationException("No tool found for key $storedKey")
61 | }
62 |
63 | override fun serialize(encoder: Encoder, value: Tool) = encoder.encodeString(value.serialKey)
64 | }
65 |
66 | data object ZoneIdSerializer : KSerializer {
67 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(ZoneId::class.java.name, PrimitiveKind.STRING)
68 |
69 | override fun deserialize(decoder: Decoder): ZoneId = ZoneId.of(decoder.decodeString())
70 |
71 | override fun serialize(encoder: Encoder, value: ZoneId) = encoder.encodeString(value.id)
72 | }
73 |
74 | data object CharsetSerializer : KSerializer {
75 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(Charset::class.java.name, PrimitiveKind.STRING)
76 |
77 | override fun deserialize(decoder: Decoder): Charset = Charset.forName(decoder.decodeString())
78 |
79 | override fun serialize(encoder: Encoder, value: Charset) = encoder.encodeString(value.name())
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/ZipFileTree.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import com.jidesoft.comparator.AlphanumComparator
4 | import com.jidesoft.swing.TreeSearchable
5 | import io.github.inductiveautomation.kindling.core.Tool
6 | import java.nio.file.FileSystem
7 | import java.nio.file.Path
8 | import javax.swing.JTree
9 | import javax.swing.tree.DefaultTreeModel
10 | import javax.swing.tree.TreeModel
11 | import javax.swing.tree.TreeNode
12 | import javax.swing.tree.TreePath
13 | import kotlin.io.path.ExperimentalPathApi
14 | import kotlin.io.path.PathWalkOption.INCLUDE_DIRECTORIES
15 | import kotlin.io.path.div
16 | import kotlin.io.path.isDirectory
17 | import kotlin.io.path.isRegularFile
18 | import kotlin.io.path.name
19 | import kotlin.io.path.walk
20 |
21 | data class PathNode(override val userObject: Path) : TypedTreeNode() {
22 | override fun isLeaf(): Boolean = super.isLeaf() || !userObject.isDirectory()
23 | }
24 |
25 | @OptIn(ExperimentalPathApi::class)
26 | class RootNode(zipFile: FileSystem) : AbstractTreeNode() {
27 | init {
28 | val zipFilePaths = zipFile.rootDirectories.asSequence()
29 | .flatMap { it.walk(INCLUDE_DIRECTORIES) }
30 |
31 | val seen = mutableMapOf()
32 | for (path in zipFilePaths) {
33 | var lastSeen: AbstractTreeNode = this
34 | var currentDepth = zipFile.getPath("/")
35 | for (part in path) {
36 | currentDepth /= part
37 | val next = seen.getOrPut(currentDepth) {
38 | val newChild = PathNode(currentDepth)
39 | lastSeen.children.add(newChild)
40 | newChild
41 | }
42 | lastSeen = next
43 | }
44 | }
45 |
46 | sortWith(comparator, recursive = true)
47 | }
48 |
49 | companion object {
50 | private val comparator = compareBy { node ->
51 | node as AbstractTreeNode
52 | val isDir = node.children.isNotEmpty() || (node as? PathNode)?.userObject?.isDirectory() == true
53 | !isDir
54 | }.thenBy(AlphanumComparator(false)) { node ->
55 | val path = (node as? PathNode)?.userObject
56 | path?.name.orEmpty()
57 | }
58 | }
59 | }
60 |
61 | class ZipFileModel(fileSystem: FileSystem) : DefaultTreeModel(RootNode(fileSystem))
62 |
63 | class ZipFileTree(fileSystem: FileSystem) : JTree(ZipFileModel(fileSystem)) {
64 | init {
65 | isRootVisible = false
66 | setShowsRootHandles(true)
67 |
68 | setCellRenderer(
69 | treeCellRenderer { _, value, _, _, _, _, _ ->
70 | if (value is PathNode) {
71 | val path = value.userObject
72 | toolTipText = path.toString()
73 | text = path.name
74 | icon = if (path.isRegularFile()) {
75 | Tool.find(path)?.icon?.derive(ACTION_ICON_SCALE_FACTOR) ?: icon
76 | } else {
77 | icon
78 | }
79 | }
80 | this
81 | },
82 | )
83 |
84 | object : TreeSearchable(this) {
85 | init {
86 | isRecursive = true
87 | isRepeats = true
88 | }
89 |
90 | override fun convertElementToString(element: Any?): String = when (val node = (element as? TreePath)?.lastPathComponent) {
91 | is PathNode -> node.userObject.name
92 | else -> ""
93 | }
94 | }
95 | }
96 |
97 | override fun getModel(): ZipFileModel? = super.getModel() as ZipFileModel?
98 | override fun setModel(newModel: TreeModel?) {
99 | newModel as ZipFileModel
100 | super.setModel(newModel)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/cache/model/Dataset.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.cache.model
2 |
3 | import com.inductiveautomation.ignition.common.Dataset
4 | import com.inductiveautomation.ignition.common.model.values.QualityCode
5 | import com.inductiveautomation.ignition.common.sqltags.model.types.DataQuality
6 | import java.io.ObjectInputStream
7 |
8 | @Suppress("PropertyName")
9 | abstract class AbstractDataset @JvmOverloads constructor(
10 | @JvmField
11 | protected var columnNames: List = emptyList(),
12 | @JvmField
13 | protected var columnTypes: List> = emptyList(),
14 | @JvmField
15 | protected var qualityCodes: Array>? = null,
16 | ) : Dataset {
17 | @JvmField
18 | protected var _columnNamesLowercase = columnNames.map { it.lowercase() }
19 |
20 | override fun getColumnNames(): List = columnNames
21 | override fun getColumnTypes(): List> = columnTypes
22 | override fun getColumnCount(): Int = columnNames.size
23 | abstract override fun getRowCount(): Int
24 | override fun getColumnIndex(columnName: String): Int = columnNames.indexOf(columnName)
25 | override fun getColumnName(columnIndex: Int): String = columnNames[columnIndex]
26 | override fun getColumnType(columnIndex: Int): Class<*> = columnTypes[columnIndex]
27 |
28 | @Suppress("UNCHECKED_CAST")
29 | private fun readObject(ois: ObjectInputStream) {
30 | this._columnNamesLowercase = ois.readObject() as List
31 | columnNames = ois.readObject() as List
32 | columnTypes = ois.readObject() as List>
33 | val qualities = ois.readObject()
34 | qualityCodes = when {
35 | qualities is Array<*> && qualities.isArrayOf>() -> qualities as Array>
36 | qualities is Array<*> && qualities.isArrayOf>() -> (qualities as Array>).convertToQualityCodes()
37 | else -> null
38 | }
39 | }
40 |
41 | companion object {
42 | @JvmStatic
43 | private val serialVersionUID = -6392821391144181995L
44 |
45 | private fun Array>.convertToQualityCodes(): Array> {
46 | val columns = size
47 | val rows = firstOrNull()?.size ?: 0
48 | return Array(columns) { column ->
49 | Array(rows) { row ->
50 | this[column][row].qualityCode
51 | }
52 | }
53 | }
54 | }
55 | }
56 |
57 | class BasicDataset @JvmOverloads constructor(
58 | private val data: Array> = emptyArray(),
59 | columnNames: List = emptyList(),
60 | columnTypes: List> = emptyList(),
61 | qualityCodes: Array>? = null,
62 | ) : AbstractDataset(columnNames, columnTypes, qualityCodes) {
63 | override fun getColumnNames(): List = columnNames
64 | override fun getColumnTypes(): List> = columnTypes
65 | override fun getColumnCount(): Int = columnNames.size
66 | override fun getRowCount(): Int = data.firstOrNull()?.size ?: 0
67 | override fun getColumnIndex(columnName: String): Int = columnNames.indexOf(columnName)
68 | override fun getColumnName(columnIndex: Int): String = columnNames[columnIndex]
69 | override fun getColumnType(columnIndex: Int): Class<*> = columnTypes[columnIndex]
70 | override fun getValueAt(rowIndex: Int, columnIndex: Int): Any = data[rowIndex][columnIndex]
71 | override fun getValueAt(rowIndex: Int, columnName: String): Any = getValueAt(rowIndex, getColumnIndex(columnName))
72 | override fun getQualityAt(rowIndex: Int, columnIndex: Int): QualityCode = QualityCode.Good
73 | override fun getPrimitiveValueAt(rowIndex: Int, columnIndex: Int): Double = Double.NaN
74 |
75 | companion object {
76 | @JvmStatic
77 | private val serialVersionUID = 3264911947104906591L
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/utils/TableHeaderCheckbox.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.utils
2 |
3 | import java.awt.Component
4 | import java.awt.EventQueue
5 | import java.awt.event.MouseEvent
6 | import java.awt.event.MouseListener
7 | import javax.swing.JCheckBox
8 | import javax.swing.JTable
9 | import javax.swing.event.TableModelEvent
10 | import javax.swing.event.TableModelListener
11 | import javax.swing.table.JTableHeader
12 | import javax.swing.table.TableCellRenderer
13 |
14 | class TableHeaderCheckbox(
15 | selected: Boolean = true,
16 | ) : JCheckBox(),
17 | TableCellRenderer,
18 | MouseListener,
19 | TableModelListener {
20 | private lateinit var table: JTable
21 | private var targetColumn: Int? = null
22 | private var valueIsAdjusting = false
23 |
24 | init {
25 | isSelected = selected
26 | toolTipText = "Select All"
27 |
28 | addActionListener { handleClick() }
29 | }
30 |
31 | private val isAllDataSelected: Boolean
32 | get() {
33 | val column = targetColumn
34 | if (!this::table.isInitialized || column == null) return false
35 |
36 | val columnModelIndex = table.convertColumnIndexToModel(column)
37 | val columnClass = table.getColumnClass(column)
38 |
39 | if (columnClass != java.lang.Boolean::class.java) return false
40 |
41 | return table.model.rowIndices.all {
42 | table.model.getValueAt(it, columnModelIndex) as? Boolean ?: return false
43 | }
44 | }
45 |
46 | private fun handleClick() {
47 | valueIsAdjusting = true
48 |
49 | val column = targetColumn
50 | if (!this::table.isInitialized || column == null) return
51 |
52 | val columnModelIndex = table.convertColumnIndexToModel(column)
53 | val columnClass = table.getColumnClass(column)
54 |
55 | if (columnClass != java.lang.Boolean::class.java) return
56 |
57 | for (row in table.model.rowIndices) {
58 | table.model.setValueAt(isSelected, row, columnModelIndex)
59 | }
60 |
61 | valueIsAdjusting = false
62 | }
63 |
64 | override fun tableChanged(e: TableModelEvent?) {
65 | if (e == null || !::table.isInitialized) return
66 |
67 | val viewColumn = table.convertColumnIndexToView(e.column)
68 |
69 | if ((viewColumn == targetColumn || e.column == TableModelEvent.ALL_COLUMNS) && !valueIsAdjusting) {
70 | isSelected = isAllDataSelected
71 |
72 | EventQueue.invokeLater {
73 | table.tableHeader.repaint()
74 | }
75 | }
76 | }
77 |
78 | override fun getTableCellRendererComponent(
79 | table: JTable?,
80 | value: Any?,
81 | isSelected: Boolean,
82 | hasFocus: Boolean,
83 | row: Int,
84 | column: Int,
85 | ): Component {
86 | if (!this::table.isInitialized && table != null) {
87 | this.table = table
88 |
89 | this.table.model.addTableModelListener(this)
90 | this.table.tableHeader.addMouseListener(this)
91 | }
92 |
93 | targetColumn = column
94 | return this
95 | }
96 |
97 | override fun mouseClicked(e: MouseEvent?) {
98 | val tableHeader = e?.source as? JTableHeader ?: return
99 |
100 | val viewColumn = tableHeader.columnModel.getColumnIndexAtX(e.x)
101 | val modelColumn = tableHeader.table.convertColumnIndexToModel(viewColumn)
102 |
103 | if (viewColumn == targetColumn && modelColumn != -1) {
104 | doClick()
105 | }
106 |
107 | tableHeader.repaint()
108 | }
109 |
110 | override fun mousePressed(e: MouseEvent?) = Unit
111 | override fun mouseReleased(e: MouseEvent?) = Unit
112 | override fun mouseEntered(e: MouseEvent?) = Unit
113 | override fun mouseExited(e: MouseEvent?) = Unit
114 | }
115 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/inductiveautomation/kindling/tagconfig/model/Node.kt:
--------------------------------------------------------------------------------
1 | package io.github.inductiveautomation.kindling.tagconfig.model
2 |
3 | import com.jidesoft.comparator.AlphanumComparator
4 | import kotlinx.serialization.ExperimentalSerializationApi
5 | import kotlinx.serialization.KSerializer
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.descriptors.SerialDescriptor
8 | import kotlinx.serialization.encoding.Decoder
9 | import kotlinx.serialization.encoding.Encoder
10 | import java.util.Collections
11 | import java.util.Enumeration
12 | import javax.swing.tree.TreeNode
13 | import kotlin.collections.forEach
14 |
15 | @Serializable(with = NodeDelegateSerializer::class)
16 | open class Node(
17 | val config: TagConfig,
18 | val isMeta: Boolean = false,
19 | val inferredFrom: Node? = null,
20 | var resolved: Boolean = false,
21 | ) : TreeNode {
22 | val inferred: Boolean
23 | get() = inferredFrom != null
24 |
25 | open val name: String
26 | get() = config.name!!
27 |
28 | val statistics = NodeStatistics(this)
29 | private var parent: Node? = null
30 |
31 | init {
32 | for (child in children()) {
33 | child.parent = this
34 | }
35 | }
36 |
37 | fun addChildTag(node: Node) {
38 | config.tags.add(node)
39 | node.parent = this
40 | config.tags.sortWith(nodeChildComparator)
41 | }
42 |
43 | fun addChildTags(children: Collection) {
44 | children.forEach {
45 | config.tags.add(it)
46 | it.parent = this
47 | }
48 | config.tags.sortWith(nodeChildComparator)
49 | }
50 |
51 | operator fun div(childName: String): Node? = config.tags.find { it.name == childName }
52 |
53 | override fun getChildAt(childIndex: Int) = config.tags[childIndex]
54 | override fun getChildCount() = config.tags.size
55 | override fun getParent(): Node? = parent
56 | override fun getIndex(node: TreeNode?) = config.tags.indexOf(node)
57 | override fun getAllowsChildren() = !statistics.isAtomicTag
58 | override fun isLeaf() = config.tags.isEmpty()
59 | override fun children(): Enumeration = Collections.enumeration(config.tags)
60 |
61 | companion object {
62 | private val nodeChildComparator = compareByDescending { it.isMeta }
63 | .thenBy { it.config.tagType }
64 | .thenBy(AlphanumComparator(false)) { it.name }
65 | }
66 | }
67 |
68 | class IdbNode(
69 | val id: String,
70 | val providerId: Int,
71 | val folderId: String?,
72 | val rank: Int,
73 | val idbName: String?,
74 | config: TagConfig,
75 | // Improves parsing efficiency a bit.
76 | resolved: Boolean = false,
77 | // "Inferred" means that there is no config entry for this node, but it will exist at runtime
78 | inferredFrom: Node? = null,
79 | // Used for sorting. A "meta" node is either the _types_ folder or the orphaned tags folder
80 | isMeta: Boolean = false,
81 | ) : Node(config, isMeta, inferredFrom, resolved) {
82 | override val name: String = idbName ?: config.name ?: "NULL"
83 | }
84 |
85 | /**
86 | * The JSON serialization of a Node is simply its config. The Node class represents an entry in the IDB.
87 | * Here, we delegate the serialization of a node to just use the TagConfig serializer.
88 | *
89 | * Serializing a node will recursively serialize all child tags, creating the complete json export.
90 | */
91 | object NodeDelegateSerializer : KSerializer {
92 | @OptIn(ExperimentalSerializationApi::class)
93 | override val descriptor: SerialDescriptor = TagConfigSerializer.descriptor
94 |
95 | override fun deserialize(decoder: Decoder): Node {
96 | val config = decoder.decodeSerializableValue(TagConfigSerializer)
97 |
98 | return Node(config)
99 | }
100 |
101 | override fun serialize(encoder: Encoder, value: Node) {
102 | encoder.encodeSerializableValue(TagConfigSerializer, value.config)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------