├── .git-blame-ignore-revs ├── .gitignore ├── gradle.properties ├── gradle ├── gradle-daemon-jvm.properties └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── inductiveautomation │ │ │ └── kindling │ │ │ ├── statistics │ │ │ ├── Statistic.kt │ │ │ ├── StatisticCalculator.kt │ │ │ ├── categories │ │ │ │ ├── DeviceStatistics.kt │ │ │ │ ├── MetaStatistics.kt │ │ │ │ ├── GatewayNetworkStatistics.kt │ │ │ │ └── DatabaseStatistics.kt │ │ │ └── GatewayBackup.kt │ │ │ ├── idb │ │ │ └── metrics │ │ │ │ ├── Metric.kt │ │ │ │ ├── Sparkline.kt │ │ │ │ ├── MetricsView.kt │ │ │ │ └── MetricTree.kt │ │ │ ├── cache │ │ │ ├── SchemaModel.kt │ │ │ ├── CacheEntry.kt │ │ │ ├── model │ │ │ │ ├── ScriptedSFData.kt │ │ │ │ ├── AuditProfileData.kt │ │ │ │ ├── AlarmJournalData.kt │ │ │ │ └── Dataset.kt │ │ │ ├── CacheColumns.kt │ │ │ ├── AliasingObjectInputStream.kt │ │ │ └── SchemaFilterList.kt │ │ │ ├── core │ │ │ ├── KindlingSerializable.kt │ │ │ ├── CustomIconView.kt │ │ │ ├── db │ │ │ │ ├── Column.kt │ │ │ │ ├── Table.kt │ │ │ │ ├── QueryResult.kt │ │ │ │ └── DBMetaDataTree.kt │ │ │ ├── Detail.kt │ │ │ ├── Filtering.kt │ │ │ ├── LinkHandlingStrategy.kt │ │ │ ├── Timezone.kt │ │ │ └── ToolPanel.kt │ │ │ ├── alarm │ │ │ └── model │ │ │ │ ├── PersistedAlarmInfo.kt │ │ │ │ └── AlarmEventColumnList.kt │ │ │ ├── thread │ │ │ ├── comparison │ │ │ │ └── BlockerButton.kt │ │ │ ├── model │ │ │ │ ├── NoneAsNullStringSerializer.kt │ │ │ │ └── Thread.kt │ │ │ ├── PoolPanel.kt │ │ │ ├── SystemPanel.kt │ │ │ ├── ThreadDumpCheckboxList.kt │ │ │ └── StatePanel.kt │ │ │ ├── utils │ │ │ ├── Xml.kt │ │ │ ├── Column.kt │ │ │ ├── NumericEntryField.kt │ │ │ ├── ReifiedTableModel.kt │ │ │ ├── FileFilter.kt │ │ │ ├── FilterListPanel.kt │ │ │ ├── Action.kt │ │ │ ├── diff │ │ │ │ └── LongestCommonSequence.kt │ │ │ ├── NoSelectionModel.kt │ │ │ ├── ColumnList.kt │ │ │ ├── Trees.kt │ │ │ ├── StackTrace.kt │ │ │ ├── Tables.kt │ │ │ ├── FilterSidebar.kt │ │ │ ├── Serializers.kt │ │ │ ├── ZipFileTree.kt │ │ │ └── TableHeaderCheckbox.kt │ │ │ ├── gatewaynetwork │ │ │ └── DiagramModel.kt │ │ │ ├── tagconfig │ │ │ ├── model │ │ │ │ ├── ValueStoreEntry.kt │ │ │ │ ├── AbstractTagProvider.kt │ │ │ │ └── Node.kt │ │ │ └── TagBrowseTree.kt │ │ │ ├── xml │ │ │ ├── XmlViewer.kt │ │ │ ├── quarantine │ │ │ │ └── QuarantineViewer.kt │ │ │ └── logback │ │ │ │ └── SelectedLoggerCard.kt │ │ │ ├── internal │ │ │ ├── DetailsModel.kt │ │ │ ├── FileTransferHandler.kt │ │ │ └── DetailsIcon.kt │ │ │ ├── zip │ │ │ └── views │ │ │ │ ├── PathView.kt │ │ │ │ ├── ImageView.kt │ │ │ │ ├── gwbk │ │ │ │ ├── MetaStatisticsRenderer.kt │ │ │ │ ├── OpcConnectionsStatisticsRenderer.kt │ │ │ │ ├── GatewayNetworkStatisticsRenderer.kt │ │ │ │ ├── DeviceStatisticsRenderer.kt │ │ │ │ └── DatabaseStatisticsRenderer.kt │ │ │ │ ├── MultiToolView.kt │ │ │ │ ├── ToolView.kt │ │ │ │ └── ProjectView.kt │ │ │ ├── log │ │ │ ├── DurationUnit.kt │ │ │ ├── Model.kt │ │ │ ├── LevelPanel.kt │ │ │ └── ThreadPanel.kt │ │ │ ├── serial │ │ │ └── SerialViewer.kt │ │ │ └── quest │ │ │ └── Utils.kt │ ├── resources │ │ ├── icons │ │ │ ├── bx-minus.svg │ │ │ ├── bx-plus.svg │ │ │ ├── bx-bar-chart-alt.svg │ │ │ ├── bx-subdirectory-left.svg │ │ │ ├── bx-chevron-left.svg │ │ │ ├── bx-sort-down.svg │ │ │ ├── bx-chevron-right.svg │ │ │ ├── bx-sort-up.svg │ │ │ ├── bx-arrow-up.svg │ │ │ ├── bx-arrow-down.svg │ │ │ ├── bx-download.svg │ │ │ ├── bx-code.svg │ │ │ ├── bx-x.svg │ │ │ ├── bx-collapse-vertical.svg │ │ │ ├── bx-expand-vertical.svg │ │ │ ├── bx-folder.svg │ │ │ ├── bx-clipboard.svg │ │ │ ├── bx-chevrons-up.svg │ │ │ ├── bx-box.svg │ │ │ ├── bx-chevrons-down.svg │ │ │ ├── bx-column.svg │ │ │ ├── bx-detail.svg │ │ │ ├── bx-link-external.svg │ │ │ ├── bx-save.svg │ │ │ ├── bxs-eraser.svg │ │ │ ├── bx-key.svg │ │ │ ├── bx-table.svg │ │ │ ├── bx-sitemap.svg │ │ │ ├── bx-book.svg │ │ │ ├── bx-line-chart.svg │ │ │ ├── bx-purchase-tag.svg │ │ │ ├── bx-search.svg │ │ │ ├── bx-time-five.svg │ │ │ ├── bx-chip.svg │ │ │ ├── bx-image.svg │ │ │ ├── bx-spreadsheet.svg │ │ │ ├── bx-check-circle.svg │ │ │ ├── bx-book-add.svg │ │ │ ├── bx-highlight.svg │ │ │ ├── bx-block.svg │ │ │ ├── bx-sort-a-z.svg │ │ │ ├── bx-sort-z-a.svg │ │ │ ├── bx-archive.svg │ │ │ ├── bx-folder-open.svg │ │ │ ├── bx-error.svg │ │ │ ├── bx-hdd.svg │ │ │ ├── null.svg │ │ │ ├── bx-file-find.svg │ │ │ ├── bx-loader-circle.svg │ │ │ ├── bx-show.svg │ │ │ ├── bx-bell.svg │ │ │ ├── bx-data.svg │ │ │ ├── bx-file.svg │ │ │ ├── Questdb-logo.svg │ │ │ ├── bx-star.svg │ │ │ ├── bx-vector.svg │ │ │ ├── bx-globe.svg │ │ │ └── bx-cog.svg │ │ ├── io │ │ │ └── github │ │ │ │ └── inductiveautomation │ │ │ │ └── kindling │ │ │ │ └── gatewaynetwork │ │ │ │ ├── favicon.ico │ │ │ │ ├── favicon-160x160.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon-48x48.png │ │ │ │ ├── README.txt │ │ │ │ └── index.html │ │ ├── logback.xml │ │ └── logo.svg │ └── java │ │ └── deser │ │ └── support │ │ └── ClassField.java └── test │ └── resources │ └── io │ └── github │ └── inductiveautomation │ └── kindling │ └── thread │ ├── legacyScriptThreadDump.txt │ └── threadDump.json ├── .editorconfig ├── settings.gradle.kts ├── .github ├── renovate.json └── workflows │ ├── pr-build.yml │ ├── dokka.yml │ └── codeql.yml ├── ci.conveyor.conf ├── LICENSE ├── conveyor.conf ├── NOTICES └── gradlew.bat /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 160673cfd69e047d61dbb349350e3b2f97dbdf93 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .gradle/ 3 | .idea/ 4 | 5 | **build/ 6 | 7 | output/ 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=1.4.1-SNAPSHOT 2 | org.gradle.jvmargs=-Xmx2G 3 | org.gradle.caching=true 4 | 5 | -------------------------------------------------------------------------------- /gradle/gradle-daemon-jvm.properties: -------------------------------------------------------------------------------- 1 | #This file is generated by updateDaemonJvm 2 | toolchainVersion=21 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inductiveautomation/kindling/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/statistics/Statistic.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.statistics 2 | 3 | interface Statistic 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inductiveautomation/kindling/HEAD/src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/icons/bx-bar-chart-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-subdirectory-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sort-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/favicon-160x160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inductiveautomation/kindling/HEAD/src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/favicon-160x160.png -------------------------------------------------------------------------------- /src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inductiveautomation/kindling/HEAD/src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/favicon-32x32.png -------------------------------------------------------------------------------- /src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inductiveautomation/kindling/HEAD/src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/favicon-48x48.png -------------------------------------------------------------------------------- /src/main/resources/icons/bx-chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sort-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | ij_kotlin_allow_trailing_comma=true 3 | ij_kotlin_allow_trailing_comma_on_call_site=true 4 | ktlint_code_style=intellij_idea 5 | ktlint_standard_value-parameter-comment=disabled 6 | ktlint_standard_value-argument-comment=disabled 7 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/statistics/StatisticCalculator.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.statistics 2 | 3 | fun interface StatisticCalculator { 4 | suspend fun calculate(backup: GatewayBackup): T? 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/README.txt: -------------------------------------------------------------------------------- 1 | The source for these files is the cytoscape-server IA internal repository, in the 'cytoscape-server.zip' file. Within the zip file, navigate to 'server/static' to view the actual files. 2 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/Metric.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.idb.metrics 2 | 3 | import java.util.Date 4 | 5 | @JvmInline 6 | value class Metric(val name: String) 7 | 8 | data class MetricData(val value: Double, val timestamp: Date) 9 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven("https://maven.hq.hydraulic.software") 5 | } 6 | } 7 | 8 | rootProject.name = "kindling" 9 | 10 | plugins { 11 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-collapse-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-expand-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-chevrons-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-box.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-chevrons-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-column.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-detail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-link-external.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bxs-eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sitemap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-book.svg: -------------------------------------------------------------------------------- 1 | 2 | / 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-line-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-purchase-tag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-time-five.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "ignoreDeps": [ 7 | "com.inductiveautomation.ignition:gateway-api", 8 | "com.inductiveautomation.ignition:common", 9 | "org.apache.commons:commons-lang3", 10 | "com.google.guava:guava" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/cache/SchemaModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.cache 2 | 3 | data class SchemaRecord( 4 | val id: Int, 5 | val name: String, 6 | val errors: List, 7 | ) 8 | 9 | data class SchemaRow( 10 | val id: Int, 11 | val signature: String, 12 | val message: String?, 13 | ) 14 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-chip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/cache/CacheEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.cache 2 | 3 | import java.sql.Timestamp 4 | 5 | data class CacheEntry( 6 | val id: Int, 7 | val schemaId: Int, 8 | val schemaName: String, 9 | val timestamp: Timestamp, 10 | val attemptCount: Int, 11 | val dataCount: Int, 12 | ) 13 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-spreadsheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-check-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-book-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-highlight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-block.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sort-a-z.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-sort-z-a.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-folder-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/core/KindlingSerializable.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.core 2 | 3 | interface KindlingSerializable { 4 | /** 5 | * A globally unique identifier used to disambiguate the _object_ this interface is implemented on. 6 | * Because this value will be stored in Kindling's preferences.json, these values cannot be renamed lightly. 7 | */ 8 | val serialKey: String 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/pr-build.yml: -------------------------------------------------------------------------------- 1 | name: Build PRs 2 | on: pull_request 3 | jobs: 4 | gradle: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v6 8 | with: 9 | fetch-depth: 0 10 | 11 | - uses: actions/setup-java@v5 12 | with: 13 | distribution: 'corretto' 14 | java-version: 21 15 | cache: 'gradle' 16 | 17 | - name: Execute Gradle build 18 | run: ./gradlew build 19 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-hdd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/null.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | NULL 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-file-find.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-loader-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-show.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/alarm/model/PersistedAlarmInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.alarm.model 2 | 3 | import com.inductiveautomation.ignition.common.alarming.AlarmEvent 4 | import com.inductiveautomation.ignition.common.alarming.AlarmState 5 | 6 | class PersistedAlarmInfo( 7 | @JvmField 8 | val data: Map>, 9 | ) : java.io.Serializable { 10 | companion object { 11 | @JvmStatic 12 | private val serialVersionUID = -7560255562233237831L 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-data.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons/Questdb-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-vector.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/core/CustomIconView.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.core 2 | 3 | import io.github.inductiveautomation.kindling.utils.ACTION_ICON_SCALE_FACTOR 4 | import java.io.File 5 | import javax.swing.Icon 6 | import javax.swing.filechooser.FileView 7 | 8 | class CustomIconView : FileView() { 9 | override fun getIcon(file: File): Icon? = if (file.isFile) { 10 | Tool.find(file)?.icon?.derive(ACTION_ICON_SCALE_FACTOR) 11 | } else { 12 | null 13 | } 14 | 15 | override fun getTypeDescription(file: File) = if (file.isFile) { 16 | Tool.find(file)?.description 17 | } else { 18 | null 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/thread/comparison/BlockerButton.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.thread.comparison 2 | 3 | import com.formdev.flatlaf.extras.components.FlatButton 4 | import io.github.inductiveautomation.kindling.utils.FlatActionIcon 5 | 6 | internal class BlockerButton : FlatButton() { 7 | var blocker: Long? = null 8 | set(value) { 9 | isVisible = value != null 10 | text = value?.toString() 11 | field = value 12 | } 13 | 14 | init { 15 | icon = FlatActionIcon("icons/bx-block.svg") 16 | toolTipText = "Jump to blocking thread" 17 | isVisible = false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/utils/Xml.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.utils 2 | 3 | import org.w3c.dom.Document 4 | import java.io.InputStream 5 | import javax.xml.XMLConstants 6 | import javax.xml.parsers.DocumentBuilderFactory 7 | 8 | val XML_FACTORY: DocumentBuilderFactory = 9 | DocumentBuilderFactory.newDefaultInstance().apply { 10 | isXIncludeAware = false 11 | isExpandEntityReferences = false 12 | setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) 13 | setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "") 14 | } 15 | 16 | fun DocumentBuilderFactory.parse(inputStream: InputStream): Document = newDocumentBuilder().parse(inputStream).also(Document::normalizeDocument) 17 | -------------------------------------------------------------------------------- /ci.conveyor.conf: -------------------------------------------------------------------------------- 1 | include required("generated.conveyor.conf") 2 | include required("conveyor.conf") 3 | 4 | app { 5 | signing-key = ${env.SIGNING_KEY} 6 | 7 | windows { 8 | certificate = "win-cert.pfx" 9 | signing-key = "win-cert.pfx" 10 | } 11 | 12 | mac { 13 | certificate = "mac-cert.p12" 14 | signing-key = "mac-cert.p12" 15 | 16 | notarization { 17 | team-id = ${env.TEAM_ID} 18 | apple-id = ${env.DEVELOPER_ID_USR} 19 | app-specific-password = ${env.DEVELOPER_ID_PSW} 20 | } 21 | info-plist { 22 | CFBundleIdentifier = "com.inductiveautomation.kindling" 23 | } 24 | } 25 | 26 | site { 27 | github { 28 | pages-branch = "gh-pages" 29 | oauth-token = ${env.GITHUB_TOKEN} 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/gatewaynetwork/DiagramModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.gatewaynetwork 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Contains the minimal data fields to be considered a valid gateway network diagram. There are more fields present 7 | * in an actual diagram, but they are all optional. 8 | */ 9 | @Serializable 10 | data class DiagramModel( 11 | val localGatewayName: String, 12 | val redundantRole: String, 13 | val version: String, 14 | val edition: String, 15 | val connections: List = emptyList(), 16 | ) { 17 | @Serializable 18 | data class Connection( 19 | val systemName: String, 20 | val connectionId: String, 21 | val connectionStatus: String, 22 | val redundantRole: String, 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/cache/model/ScriptedSFData.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.cache.model 2 | 3 | import io.github.inductiveautomation.kindling.core.Detail 4 | import java.io.Serializable 5 | 6 | @Suppress("unused") 7 | class ScriptedSFData( 8 | val query: String, 9 | val datasource: String, 10 | val values: Array, 11 | ) : Serializable { 12 | fun toDetail() = Detail( 13 | title = "system.db.runSFUpdate query data", 14 | message = query, 15 | details = mapOf( 16 | "datasource" to datasource, 17 | ), 18 | body = values.mapIndexed { index, parameterValue -> 19 | "param${index + 1} (${parameterValue?.javaClass?.simpleName}) = $parameterValue" 20 | }, 21 | ) 22 | 23 | companion object { 24 | @JvmStatic 25 | private val serialVersionUID = 1L 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/core/db/Column.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.core.db 2 | 3 | import java.util.Collections 4 | import java.util.Enumeration 5 | import javax.swing.tree.TreeNode 6 | 7 | data class Column( 8 | val name: String, 9 | val type: String, 10 | val notNull: Boolean, 11 | val defaultValue: String?, 12 | val primaryKey: Boolean, 13 | val hidden: Boolean, 14 | val _parent: () -> TreeNode, 15 | ) : TreeNode { 16 | override fun getChildAt(childIndex: Int): TreeNode? = null 17 | override fun getChildCount(): Int = 0 18 | override fun getParent(): TreeNode = _parent() 19 | override fun getIndex(node: TreeNode?): Int = -1 20 | override fun getAllowsChildren(): Boolean = false 21 | override fun isLeaf(): Boolean = true 22 | override fun children(): Enumeration = Collections.emptyEnumeration() 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/utils/Column.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.utils 2 | 3 | import org.jdesktop.swingx.table.TableColumnExt 4 | import javax.swing.table.TableModel 5 | 6 | data class Column( 7 | val header: String, 8 | val getValue: (row: R) -> C, 9 | val columnCustomization: (TableColumnExt.(model: TableModel) -> Unit)?, 10 | val clazz: Class, 11 | ) { 12 | companion object { 13 | inline operator fun invoke( 14 | header: String, 15 | noinline columnCustomization: (TableColumnExt.(model: TableModel) -> Unit)? = null, 16 | noinline getValue: (row: R) -> C, 17 | ) = Column( 18 | header = header, 19 | columnCustomization = columnCustomization, 20 | getValue = getValue, 21 | clazz = C::class.java, 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/cache/CacheColumns.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.cache 2 | 3 | import io.github.inductiveautomation.kindling.utils.ColumnList 4 | import org.jdesktop.swingx.renderer.DefaultTableRenderer 5 | 6 | @Suppress("unused") 7 | object CacheColumns : ColumnList() { 8 | val Id by column( 9 | column = { 10 | cellRenderer = DefaultTableRenderer(Any?::toString) 11 | }, 12 | value = CacheEntry::id, 13 | ) 14 | val SchemaId by column { it.schemaId } 15 | val Timestamp by column { it.timestamp } 16 | val AttemptCount by column(name = "Attempt Count") { it.attemptCount } 17 | val DataCount by column(name = "Data Count") { it.dataCount } 18 | val SchemaName by column( 19 | column = { 20 | isVisible = false 21 | }, 22 | value = CacheEntry::schemaName, 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/core/db/Table.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.core.db 2 | 3 | import java.util.Collections 4 | import java.util.Enumeration 5 | import javax.swing.tree.TreeNode 6 | 7 | data class Table( 8 | val name: String, 9 | val columns: List, 10 | val _parent: () -> TreeNode, 11 | val size: Long, 12 | val rowCount: Long, 13 | ) : TreeNode { 14 | override fun getChildAt(childIndex: Int): TreeNode = columns[childIndex] 15 | override fun getChildCount(): Int = columns.size 16 | override fun getParent(): TreeNode = _parent() 17 | override fun getIndex(node: TreeNode): Int = columns.indexOf(node) 18 | override fun getAllowsChildren(): Boolean = true 19 | override fun isLeaf(): Boolean = false 20 | override fun children(): Enumeration = Collections.enumeration(columns) 21 | 22 | override fun toString(): String = this.name 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/thread/model/NoneAsNullStringSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.thread.model 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.builtins.serializer 5 | import kotlinx.serialization.descriptors.SerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | 9 | object NoneAsNullStringSerializer : KSerializer { 10 | override val descriptor: SerialDescriptor = String.serializer().descriptor 11 | 12 | override fun deserialize(decoder: Decoder): String? { 13 | return decoder.decodeString().takeIf { it != "None" } 14 | } 15 | 16 | override fun serialize(encoder: Encoder, value: String?) { 17 | when (value) { 18 | "None" -> Unit 19 | null -> Unit 20 | else -> encoder.encodeString(value) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/io/github/inductiveautomation/kindling/gatewaynetwork/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Gateway Network Diagram 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/dokka.yml: -------------------------------------------------------------------------------- 1 | name: Publish new Dokka docs upon new version tag 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'Version to publish as' 7 | required: true 8 | type: string 9 | push: 10 | tags: 11 | - '[0-9]+.[0-9]+.[0-9]+' 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | - uses: actions/setup-java@v5 18 | with: 19 | distribution: 'corretto' 20 | java-version: 21 21 | cache: 'gradle' 22 | 23 | - name: Create Dokka docs 24 | run: > 25 | ./gradlew 26 | -Pversion=${{ inputs.version || github.ref_name }} 27 | dokkaHtml 28 | 29 | - name: Deploy 30 | uses: peaceiris/actions-gh-pages@v4 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./build/dokka/html 34 | destination_dir: docs 35 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/tagconfig/model/ValueStoreEntry.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.tagconfig.model 2 | 3 | data class ValueStoreEntry( 4 | val provider: String, 5 | val path: String, 6 | val dataType: Int, 7 | val textValue: String?, 8 | val numericValue: Any?, 9 | val nullValue: Int, 10 | val quality: Int, 11 | val t_stamp: Long, 12 | val updatedAt: Long, 13 | ) 14 | 15 | // TODO: Add support for all tag data types. Figure out how complex tags are encoded. 16 | 17 | enum class ValueStoreDataType(val dbValue: Int) { 18 | BYTE(0), 19 | SHORT(1), 20 | INTEGER(2), 21 | LONG(3), 22 | FLOAT(4), 23 | DOUBLE(5), 24 | BOOLEAN(6), 25 | STRING(7), 26 | DATETIME(8), 27 | ; 28 | 29 | companion object { 30 | private val entriesById = ValueStoreDataType.entries.associateBy { it.dbValue } 31 | fun fromDbValue(dbValue: Int) = entriesById[dbValue] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/thread/PoolPanel.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.thread 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.inductiveautomation.kindling.thread.model.Thread 5 | import io.github.inductiveautomation.kindling.utils.FileFilterResponsive 6 | import io.github.inductiveautomation.kindling.utils.FilterListPanel 7 | import io.github.inductiveautomation.kindling.utils.FilterModel 8 | 9 | class PoolPanel : 10 | FilterListPanel( 11 | tabName = "Pool", 12 | toStringFn = { it?.toString() ?: "(No Pool)" }, 13 | ), 14 | FileFilterResponsive { 15 | override val icon = FlatSVGIcon("icons/bx-chip.svg") 16 | 17 | override fun setModelData(data: List) { 18 | filterList.model = FilterModel.fromRawData(data.filterNotNull(), filterList.comparator) { it.pool } 19 | } 20 | 21 | override fun filter(item: Thread?) = item?.pool in filterList.checkBoxListSelectedValues 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/thread/SystemPanel.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.thread 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.inductiveautomation.kindling.thread.model.Thread 5 | import io.github.inductiveautomation.kindling.utils.FileFilterResponsive 6 | import io.github.inductiveautomation.kindling.utils.FilterListPanel 7 | import io.github.inductiveautomation.kindling.utils.FilterModel 8 | 9 | class SystemPanel : 10 | FilterListPanel( 11 | tabName = "System", 12 | toStringFn = { it?.toString() ?: "Unassigned" }, 13 | ), 14 | FileFilterResponsive { 15 | override val icon = FlatSVGIcon("icons/bx-hdd.svg") 16 | 17 | override fun setModelData(data: List) { 18 | filterList.model = FilterModel.fromRawData(data.filterNotNull(), filterList.comparator) { it.system } 19 | } 20 | 21 | override fun filter(item: Thread?): Boolean = item?.system in filterList.checkBoxListSelectedValues 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '23 5 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v6 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v4 28 | with: 29 | languages: 'java' 30 | 31 | - name: Set up Java 32 | uses: actions/setup-java@v5 33 | with: 34 | distribution: 'corretto' 35 | java-version: 21 36 | cache: 'gradle' 37 | 38 | - name: Execute Gradle build 39 | run: ./gradlew build 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v4 43 | with: 44 | category: "/language:java" 45 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/core/Detail.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.core 2 | 3 | import io.github.inductiveautomation.kindling.core.Detail.BodyLine 4 | 5 | data class Detail( 6 | val title: String, 7 | val message: String? = null, 8 | val details: Map = emptyMap(), 9 | val body: List = emptyList(), 10 | ) { 11 | data class BodyLine(val text: String, val link: String? = null) 12 | 13 | companion object { 14 | val EMPTY_LINE = BodyLine("", null) 15 | 16 | operator fun invoke( 17 | title: String, 18 | message: String? = null, 19 | details: Map = emptyMap(), 20 | body: List = emptyList(), 21 | ) = Detail( 22 | title, 23 | message, 24 | details, 25 | body.map(::BodyLine), 26 | ) 27 | } 28 | } 29 | 30 | fun MutableList.add(line: String, link: String? = null) { 31 | add(BodyLine(line, link)) 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/utils/NumericEntryField.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.utils 2 | 3 | import java.text.NumberFormat 4 | import javax.swing.InputVerifier 5 | import javax.swing.JComponent 6 | import javax.swing.JFormattedTextField 7 | import javax.swing.SwingConstants 8 | import javax.swing.text.DefaultFormatterFactory 9 | import javax.swing.text.NumberFormatter 10 | 11 | class NumericEntryField(inputValue: Long?) : JFormattedTextField(inputValue) { 12 | private val format = NumberFormat.getIntegerInstance().apply { isGroupingUsed = false } 13 | 14 | init { 15 | formatterFactory = DefaultFormatterFactory(NumberFormatter(format)) 16 | horizontalAlignment = SwingConstants.CENTER 17 | inputVerifier = 18 | object : InputVerifier() { 19 | override fun verify(input: JComponent): Boolean = (input as JFormattedTextField).text.let { text -> 20 | text.all { it.isDigit() } && text.length < 19 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/xml/XmlViewer.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.xml 2 | 3 | import io.github.inductiveautomation.kindling.core.Kindling.Preferences.UI.Theme 4 | import io.github.inductiveautomation.kindling.core.Theme.Companion.theme 5 | import net.miginfocom.swing.MigLayout 6 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea 7 | import org.fife.ui.rtextarea.RTextScrollPane 8 | import javax.swing.JPanel 9 | 10 | internal class XmlViewer(file: List) : JPanel(MigLayout("ins 6, fill, hidemode 3")) { 11 | init { 12 | add( 13 | RTextScrollPane( 14 | RSyntaxTextArea(file.joinToString("\n")).apply { 15 | isEditable = false 16 | syntaxEditingStyle = "text/xml" 17 | theme = Theme.currentValue 18 | 19 | Theme.addChangeListener { newValue -> 20 | theme = newValue 21 | } 22 | }, 23 | ), 24 | "grow, push", 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/core/db/QueryResult.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.core.db 2 | 3 | import javax.swing.table.AbstractTableModel 4 | 5 | sealed interface QueryResult { 6 | class Success( 7 | val columnNames: List, 8 | private val columnTypes: List>, 9 | val data: List>, 10 | ) : AbstractTableModel(), 11 | QueryResult { 12 | constructor() : this(emptyList(), emptyList(), emptyList()) 13 | 14 | init { 15 | require(columnNames.size == columnTypes.size) 16 | } 17 | 18 | override fun getRowCount(): Int = data.size 19 | override fun getColumnCount(): Int = columnNames.size 20 | override fun getColumnName(columnIndex: Int): String = columnNames[columnIndex] 21 | override fun getColumnClass(columnIndex: Int): Class<*> = columnTypes[columnIndex] 22 | override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? = data[rowIndex][columnIndex] 23 | } 24 | 25 | class Error( 26 | val details: String, 27 | ) : QueryResult 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/utils/ReifiedTableModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.utils 2 | 3 | import javax.swing.table.AbstractTableModel 4 | 5 | open class ReifiedListTableModel( 6 | open val data: List, 7 | override val columns: ColumnList, 8 | ) : AbstractTableModel(), 9 | ReifiedTableModel { 10 | override fun getColumnCount(): Int = columns.size 11 | 12 | override fun getRowCount(): Int = data.size 13 | 14 | override fun getColumnClass(columnIndex: Int) = columns[columnIndex].clazz 15 | 16 | override fun getColumnName(columnIndex: Int) = columns[columnIndex].header 17 | 18 | operator fun get( 19 | row: Int, 20 | column: Column, 21 | ): R = data[row].let { datum -> 22 | column.getValue(datum) 23 | } 24 | 25 | operator fun get(row: Int): T = data[row] 26 | 27 | override fun getValueAt( 28 | rowIndex: Int, 29 | columnIndex: Int, 30 | ): Any? = columns[columnIndex].getValue(data[rowIndex]) 31 | } 32 | 33 | interface ReifiedTableModel { 34 | val columns: ColumnList 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Inductive Automation LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/utils/FileFilter.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.utils 2 | 3 | import java.io.File 4 | import java.nio.file.Path 5 | import kotlin.io.path.extension 6 | import kotlin.io.path.isDirectory 7 | import java.io.FileFilter as IoFileFilter 8 | import javax.swing.filechooser.FileFilter as SwingFileFilter 9 | 10 | /** 11 | * A unified abstraction over the [java.io.FileFilter][IoFileFilter] interface and the Swing 12 | * [filechooser.FileFilter][SwingFileFilter] abstract class. 13 | */ 14 | class FileFilter( 15 | private val description: String, 16 | private val predicate: (path: Path) -> Boolean, 17 | ) : SwingFileFilter(), IoFileFilter { 18 | constructor(description: String, vararg extensions: String) : this( 19 | description, 20 | { path -> path.extension.lowercase() in extensions }, 21 | ) 22 | 23 | fun accept(path: Path): Boolean { 24 | return path.isDirectory() || predicate(path) 25 | } 26 | 27 | override fun accept(file: File): Boolean = file.isDirectory() || accept(file.toPath()) 28 | 29 | override fun getDescription(): String = description 30 | } 31 | -------------------------------------------------------------------------------- /conveyor.conf: -------------------------------------------------------------------------------- 1 | include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf") 2 | 3 | app { 4 | display-name = "Kindling" 5 | rdns-name = "io.github.inductiveautomation.kindling" 6 | vcs-url = "github.com/inductiveautomation/kindling" 7 | vendor = "Inductive Automation" 8 | license = "MIT" 9 | 10 | jvm { 11 | modules += java.desktop 12 | modules += java.sql 13 | modules += java.logging 14 | modules += java.naming 15 | modules += java.xml 16 | modules += jdk.zipfs 17 | } 18 | 19 | file-associations = [ 20 | .data application/data, 21 | .gwbk application/gwbk, 22 | .idb application/idb, 23 | .json application/json, 24 | .log text/log, 25 | .modl application/modl, 26 | .script application/script, 27 | .txt text/plain, 28 | .zip application/zip, 29 | ] 30 | 31 | icons = "src/main/resources/logo.svg" 32 | site { 33 | extra-header-html = """ 34 | 35 | """ 36 | } 37 | mac { 38 | info-plist { 39 | LSMinimumSystemVersion = 14.0 40 | } 41 | } 42 | } 43 | 44 | conveyor.compatibility-level = 18 45 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/internal/DetailsModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.internal 2 | 3 | import io.github.inductiveautomation.kindling.utils.ColumnList 4 | import javax.swing.table.AbstractTableModel 5 | import kotlin.properties.Delegates 6 | 7 | class DetailsModel(details: List>) : AbstractTableModel() { 8 | var details: List> by Delegates.observable(details) { _, _, _ -> 9 | fireTableDataChanged() 10 | } 11 | 12 | override fun getColumnName(column: Int): String = DetailsColumns[column].header 13 | override fun getRowCount(): Int = details.size 14 | override fun getColumnCount(): Int = size 15 | 16 | override fun getValueAt(row: Int, column: Int): Any? { 17 | return details[row].let { entry -> 18 | DetailsColumns[column].getValue(entry) 19 | } 20 | } 21 | 22 | override fun getColumnClass(column: Int): Class<*> = DetailsColumns[column].clazz 23 | 24 | @Suppress("unused") 25 | companion object DetailsColumns : ColumnList>() { 26 | val Key by column { it.key } 27 | val Value by column { it.value } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/cache/AliasingObjectInputStream.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.cache 2 | 3 | import java.io.InputStream 4 | import java.io.ObjectInputStream 5 | import java.io.ObjectStreamClass 6 | 7 | class AliasingObjectInputStream private constructor( 8 | inputStream: InputStream, 9 | private val aliases: Map>, 10 | ) : ObjectInputStream(inputStream) { 11 | constructor(inputStream: InputStream, block: MutableMap>.() -> Unit) : this(inputStream, buildMap(block)) 12 | 13 | override fun readClassDescriptor(): ObjectStreamClass { 14 | val baseDescriptor = super.readClassDescriptor() 15 | 16 | return if (aliases.containsKey(baseDescriptor.name)) { 17 | val aliasClassDescriptor = ObjectStreamClass.lookup(aliases[baseDescriptor.name]) 18 | val aliasUid = aliasClassDescriptor.serialVersionUID 19 | val expectedUid = baseDescriptor.serialVersionUID 20 | 21 | require(aliasUid == expectedUid) { "serialVersionUID mismatch; expected $expectedUid but got $aliasUid" } 22 | 23 | aliasClassDescriptor 24 | } else { 25 | baseDescriptor 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/zip/views/PathView.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.zip.views 2 | 3 | import io.github.inductiveautomation.kindling.utils.FloatableComponent 4 | import io.github.inductiveautomation.kindling.utils.PopupMenuCustomizer 5 | import net.miginfocom.swing.MigLayout 6 | import java.nio.file.Path 7 | import java.nio.file.spi.FileSystemProvider 8 | import javax.swing.JPanel 9 | import javax.swing.JPopupMenu 10 | import kotlin.io.path.name 11 | 12 | sealed class PathView(constraints: String) : 13 | JPanel(MigLayout(constraints)), 14 | FloatableComponent, 15 | PopupMenuCustomizer { 16 | abstract val paths: List 17 | abstract val provider: FileSystemProvider 18 | open val closable: Boolean = true 19 | 20 | override fun customizePopupMenu(menu: JPopupMenu) = Unit 21 | } 22 | 23 | abstract class SinglePathView(constraints: String = "ins 6, fill") : PathView(constraints) { 24 | protected abstract val path: Path 25 | 26 | override val paths: List by lazy { listOf(path) } 27 | override val tabName by lazy { path.name } 28 | override val tabTooltip by lazy { path.toString().substring(1) } 29 | 30 | override fun toString(): String = "${this::class.simpleName}(path=$path)" 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/internal/FileTransferHandler.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.internal 2 | 3 | import java.awt.datatransfer.DataFlavor 4 | import java.awt.datatransfer.UnsupportedFlavorException 5 | import java.io.File 6 | import java.io.IOException 7 | import javax.swing.TransferHandler 8 | 9 | class FileTransferHandler( 10 | private val predicate: (File) -> Boolean = { true }, 11 | private val callback: (List) -> Unit, 12 | ) : TransferHandler() { 13 | override fun canImport(support: TransferSupport): Boolean = support.isDataFlavorSupported(DataFlavor.javaFileListFlavor) 14 | 15 | @Suppress("UNCHECKED_CAST") 16 | override fun importData(support: TransferSupport): Boolean { 17 | if (!canImport(support)) { 18 | return false 19 | } 20 | val t = support.transferable 21 | try { 22 | val files = t.getTransferData(DataFlavor.javaFileListFlavor) as List 23 | files.filter(predicate) 24 | .takeIf { it.isNotEmpty() } 25 | ?.let(callback) 26 | } catch (e: UnsupportedFlavorException) { 27 | return false 28 | } catch (e: IOException) { 29 | return false 30 | } 31 | return true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/core/Filtering.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.core 2 | 3 | import com.formdev.flatlaf.extras.FlatSVGIcon 4 | import io.github.inductiveautomation.kindling.utils.Column 5 | import io.github.inductiveautomation.kindling.utils.add 6 | import java.util.EventListener 7 | import javax.swing.JComponent 8 | import javax.swing.JPopupMenu 9 | import javax.swing.event.EventListenerList 10 | 11 | fun interface Filter { 12 | /** 13 | * Return true if this filter should display this item. 14 | */ 15 | fun filter(item: T): Boolean 16 | } 17 | 18 | fun interface FilterChangeListener : EventListener { 19 | fun filterChanged() 20 | } 21 | 22 | abstract class FilterPanel : Filter { 23 | abstract val tabName: String 24 | 25 | abstract fun isFilterApplied(): Boolean 26 | 27 | abstract val component: JComponent 28 | 29 | abstract val icon: FlatSVGIcon 30 | 31 | protected val listeners = EventListenerList() 32 | 33 | fun addFilterChangeListener(listener: FilterChangeListener) { 34 | listeners.add(listener) 35 | } 36 | 37 | abstract fun reset() 38 | 39 | abstract fun customizePopupMenu( 40 | menu: JPopupMenu, 41 | column: Column, 42 | event: T, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/alarm/model/AlarmEventColumnList.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.alarm.model 2 | 3 | import com.inductiveautomation.ignition.common.alarming.AlarmEvent 4 | import io.github.inductiveautomation.kindling.utils.ColumnList 5 | 6 | @Suppress("unused") 7 | object AlarmEventColumnList : ColumnList() { 8 | val Id by column( 9 | value = AlarmEvent::getId, 10 | ) 11 | 12 | val DisplayPath by column( 13 | column = { 14 | title = "Display Path" 15 | isVisible = false 16 | }, 17 | value = AlarmEvent::getDisplayPath, 18 | ) 19 | 20 | val Source by column( 21 | value = { it.source.toStringSimple() }, 22 | ) 23 | 24 | val Name by column( 25 | value = AlarmEvent::getName, 26 | ) 27 | 28 | val State by column( 29 | value = AlarmEvent::getState, 30 | ) 31 | 32 | val Priority by column( 33 | value = AlarmEvent::getPriority, 34 | ) 35 | 36 | val Shelved by column( 37 | value = AlarmEvent::isShelved, 38 | ) 39 | 40 | val Label by column( 41 | value = AlarmEvent::getLabel, 42 | ) 43 | 44 | val Notes by column( 45 | column = { 46 | isVisible = false 47 | }, 48 | value = AlarmEvent::getNotes, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/utils/FilterListPanel.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.utils 2 | 3 | import io.github.inductiveautomation.kindling.core.FilterChangeListener 4 | import io.github.inductiveautomation.kindling.core.FilterPanel 5 | import javax.swing.JPopupMenu 6 | 7 | abstract class FilterListPanel( 8 | override val tabName: String, 9 | toStringFn: Stringifier = { it?.toString() }, 10 | ) : FilterPanel() { 11 | val filterList = FilterList(toStringFn = toStringFn) 12 | 13 | private val sortButtons = filterList.createSortButtons() 14 | 15 | override val component = ButtonPanel(sortButtons).apply { 16 | add(FlatScrollPane(filterList), "newline, push, grow, align right") 17 | } 18 | 19 | init { 20 | filterList.checkBoxListSelectionModel.addListSelectionListener { e -> 21 | if (!e.valueIsAdjusting && !filterList.comparatorIsAdjusting) { 22 | listeners.getAll().forEach(FilterChangeListener::filterChanged) 23 | } 24 | } 25 | } 26 | 27 | override fun isFilterApplied() = filterList.checkBoxListSelectedValues.size != filterList.model.size - 1 28 | 29 | override fun reset() = filterList.selectAll() 30 | 31 | override fun customizePopupMenu( 32 | menu: JPopupMenu, 33 | column: Column, 34 | event: T, 35 | ) = Unit 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/icons/bx-cog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/cache/model/AuditProfileData.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.cache.model 2 | 3 | import com.inductiveautomation.ignition.gateway.audit.AuditRecord 4 | import io.github.inductiveautomation.kindling.core.Detail 5 | import java.io.Serializable 6 | 7 | @Suppress("unused") 8 | class AuditProfileData( 9 | private val auditRecord: AuditRecord, 10 | private val insertQuery: String, 11 | private val parentLog: String, 12 | ) : Serializable { 13 | fun toDetail() = Detail( 14 | title = "Audit Profile Data", 15 | message = insertQuery, 16 | body = mapOf( 17 | "actor" to auditRecord.actor, 18 | "action" to auditRecord.action, 19 | "actionValue" to auditRecord.actionValue, 20 | "actionTarget" to auditRecord.actionTarget, 21 | "actorHost" to auditRecord.actorHost, 22 | "originatingContext" to when (auditRecord.originatingContext) { 23 | 1 -> "Gateway" 24 | 2 -> "Designer" 25 | 4 -> "Client" 26 | else -> "Unknown" 27 | }, 28 | "originatingSystem" to auditRecord.originatingSystem, 29 | "timestamp" to auditRecord.timestamp.toString(), 30 | ).map { (key, value) -> 31 | "$key: $value" 32 | }, 33 | ) 34 | 35 | companion object { 36 | @JvmStatic 37 | private val serialVersionUID = 3037488986978918285L 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/io/github/inductiveautomation/kindling/log/DurationUnit.kt: -------------------------------------------------------------------------------- 1 | package io.github.inductiveautomation.kindling.log 2 | 3 | import java.time.Duration 4 | import java.time.temporal.Temporal 5 | import java.time.temporal.TemporalUnit 6 | 7 | // Credit to https://stackoverflow.com/a/66203968 8 | class DurationUnit(private val duration: Duration) : TemporalUnit { 9 | init { 10 | require(!(duration.isZero || duration.isNegative)) { "Duration may not be zero or negative" } 11 | } 12 | 13 | override fun getDuration(): Duration = duration 14 | override fun isDurationEstimated(): Boolean = duration.seconds >= SECONDS_PER_DAY 15 | override fun isDateBased(): Boolean = duration.nano == 0 && duration.seconds % SECONDS_PER_DAY == 0L 16 | override fun isTimeBased(): Boolean = duration.seconds < SECONDS_PER_DAY && NANOS_PER_DAY % duration.toNanos() == 0L 17 | 18 | @Suppress("UNCHECKED_CAST") 19 | override fun 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 | 4 | 5 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 22 | 24 | 25 | 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 | --------------------------------------------------------------------------------