diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/core/Kindling.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/core/Kindling.kt index 17c31b2e..715f2e6e 100644 --- a/src/main/kotlin/io/github/inductiveautomation/kindling/core/Kindling.kt +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/core/Kindling.kt @@ -13,6 +13,7 @@ import io.github.inductiveautomation.kindling.utils.PathSerializer import io.github.inductiveautomation.kindling.utils.PathSerializer.serializedForm import io.github.inductiveautomation.kindling.utils.ThemeSerializer import io.github.inductiveautomation.kindling.utils.ToolSerializer +import io.github.inductiveautomation.kindling.utils.ZoneIdSerializer import io.github.inductiveautomation.kindling.utils.configureCellRenderer import io.github.inductiveautomation.kindling.utils.debounce import io.github.inductiveautomation.kindling.utils.render @@ -28,6 +29,8 @@ import java.awt.Image import java.net.URI import java.nio.charset.Charset import java.nio.file.Path +import java.time.ZoneId +import java.time.zone.ZoneRulesProvider import java.util.Vector import javax.swing.JComboBox import javax.swing.JSpinner @@ -153,6 +156,21 @@ data object Kindling { }, ) + val SelectedTimeZone = preference( + name = "Timezone", + description = "Timezone to use when displaying timestamps", + default = ZoneId.systemDefault(), + serializer = ZoneIdSerializer, + editor = { + JComboBox(Vector(ZoneRulesProvider.getAvailableZoneIds().sorted())).apply { + selectedItem = currentValue.id + addActionListener { + currentValue = ZoneId.of(selectedItem as String) + } + } + }, + ) + override val displayName: String = "General" override val serialKey: String = "general" override val preferences: List> = listOf( @@ -162,6 +180,7 @@ data object Kindling { ShowLogTree, UseHyperlinks, HighlightByDefault, + SelectedTimeZone, ) } diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/core/TimePreferences.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/core/TimePreferences.kt new file mode 100644 index 00000000..03602b2e --- /dev/null +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/core/TimePreferences.kt @@ -0,0 +1,49 @@ +package io.github.inductiveautomation.kindling.core + +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.temporal.TemporalAccessor +import java.util.Date + +object TimePreferences { + val SelectedTimeZone = Kindling.Preferences.General.SelectedTimeZone + + private var formatter = createFormatter(SelectedTimeZone.currentValue) + + init { + Kindling.Preferences.General.SelectedTimeZone.addChangeListener { newValue -> + formatter = createFormatter(newValue) + } + } + private val listeners = mutableListOf<() -> Unit>() + + fun addChangeListener(listener: () -> Unit) { + listeners += listener + } + + init { + Kindling.Preferences.General.SelectedTimeZone.addChangeListener { newValue -> + formatter = createFormatter(newValue) + listeners.forEach { it() } // notify listeners when timezone changes + } + } + + private fun createFormatter(id: ZoneId): DateTimeFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss:SSS") + .withZone(id) + + /** + * Format [time] using an internal [DateTimeFormatter] that is in [SelectedTimeZone] automatically. + * + * This function is overloaded to also accept [Date] types, including [java.sql.Date] and [java.sql.Timestamp]. + * - [java.sql.Date] is converted via [toLocalDate] at the start of the day in the selected timezone. + * - [java.sql.Timestamp] and [java.util.Date] preserve full time-of-day precision. + */ + + fun format(time: TemporalAccessor): String = formatter.format(time) + + fun format(date: Date): String = when (date) { + is java.sql.Date -> formatter.format(date.toLocalDate().atStartOfDay(SelectedTimeZone.currentValue)) + is java.sql.Timestamp -> formatter.format(date.toInstant()) + else -> formatter.format(date.toInstant()) + } +} diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricCard.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricCard.kt index 03e4323b..eb6806dd 100644 --- a/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricCard.kt +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricCard.kt @@ -1,5 +1,6 @@ package io.github.inductiveautomation.kindling.idb.metrics +import io.github.inductiveautomation.kindling.core.TimePreferences import io.github.inductiveautomation.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Cpu import io.github.inductiveautomation.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Default import io.github.inductiveautomation.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Heap @@ -17,7 +18,6 @@ import java.text.DecimalFormat import java.text.FieldPosition import java.text.NumberFormat import java.text.ParsePosition -import java.text.SimpleDateFormat import javax.swing.JLabel import javax.swing.JPanel import javax.swing.SwingConstants.CENTER @@ -90,13 +90,18 @@ class MetricCard(val metric: Metric, data: List) : JPanel(MigLayout( } add(sparkLine, "span, w 300, h 170, pushx, growx") - add(JLabel("${DATE_FORMAT.format(minTimestamp)} - ${DATE_FORMAT.format(maxTimestamp)}", CENTER), "pushx, growx, span") + + val timeLabel = JLabel("${TimePreferences.format(minTimestamp)} - ${TimePreferences.format(maxTimestamp)}", CENTER) + add(timeLabel, "pushx, growx, span") + + TimePreferences.addChangeListener { + timeLabel.text = "${TimePreferences.format(minTimestamp)} - ${TimePreferences.format(maxTimestamp)}" + } border = LineBorder(UIManager.getColor("Component.borderColor"), 3, true) } companion object { - val DATE_FORMAT = SimpleDateFormat("MM/dd/yy HH:mm:ss") private val mbFormatter = DecimalFormat("0.0 'mB'") private val heapFormatter = object : NumberFormat() { @@ -117,7 +122,7 @@ class MetricCard(val metric: Metric, data: List) : JPanel(MigLayout( }, true, ), - Default(NumberFormat.getInstance(), false) + Default(NumberFormat.getInstance(), false), } private val Metric.presentation: MetricPresentation diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricsView.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricsView.kt index bf6cfd38..bb0bc7b0 100644 --- a/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricsView.kt +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricsView.kt @@ -54,7 +54,7 @@ class MetricsView(connection: Connection) : ToolPanel("ins 0, fill, hidemode 3") } .executeQuery() .toList { rs -> - MetricData(rs.getDouble(1), rs.getDate(2)) + MetricData(rs.getDouble(1), rs.getTimestamp(2)) } MetricCard(metric, metricData) diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/Sparkline.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/Sparkline.kt index 1858cf5c..7dd968f5 100644 --- a/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/Sparkline.kt +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/Sparkline.kt @@ -2,7 +2,7 @@ package io.github.inductiveautomation.kindling.idb.metrics import io.github.inductiveautomation.kindling.core.Kindling.Preferences.UI.Theme import io.github.inductiveautomation.kindling.core.Theme.Companion.theme -import io.github.inductiveautomation.kindling.idb.metrics.MetricCard.Companion.DATE_FORMAT +import io.github.inductiveautomation.kindling.core.TimePreferences import org.jfree.chart.ChartFactory import org.jfree.chart.JFreeChart import org.jfree.chart.axis.NumberAxis @@ -11,44 +11,59 @@ import org.jfree.data.time.FixedMillisecond import org.jfree.data.time.TimeSeries import org.jfree.data.time.TimeSeriesCollection import java.text.NumberFormat +import java.time.Instant -fun sparkline(data: List, formatter: NumberFormat): JFreeChart { - return ChartFactory.createTimeSeriesChart( - /* title = */ null, - /* timeAxisLabel = */ null, - /* valueAxisLabel = */ null, - /* dataset = */ - TimeSeriesCollection( - TimeSeries("Series").apply { - for ((value, timestamp) in data) { - add(FixedMillisecond(timestamp), value, false) - } - }, - ), - /* legend = */ false, - /* tooltips = */ true, - /* urls = */ false, - ).apply { - xyPlot.apply { - domainAxis.isPositiveArrowVisible = true - rangeAxis.apply { - isPositiveArrowVisible = true - (this as NumberAxis).numberFormatOverride = formatter +fun sparkline(data: List, formatter: NumberFormat): JFreeChart = ChartFactory.createTimeSeriesChart( + /* title = */ + null, + /* timeAxisLabel = */ + null, + /* valueAxisLabel = */ + null, + /* dataset = */ + TimeSeriesCollection( + TimeSeries("Series").apply { + for ((value, timestamp) in data) { + add(FixedMillisecond(timestamp), value, false) } + }, + ), + /* legend = */ + false, + /* tooltips = */ + true, + /* urls = */ + false, +).apply { + xyPlot.apply { + domainAxis.isPositiveArrowVisible = true + rangeAxis.apply { + isPositiveArrowVisible = true + (this as NumberAxis).numberFormatOverride = formatter + } + val updateTooltipGenerator = { renderer.setDefaultToolTipGenerator { dataset, series, item -> - "${DATE_FORMAT.format(dataset.getXValue(series, item))} - ${formatter.format(dataset.getYValue(series, item))}" + val time = Instant.ofEpochMilli(dataset.getXValue(series, item).toLong()) + "${TimePreferences.format(time)} - ${formatter.format(dataset.getYValue(series, item))}" } - isDomainGridlinesVisible = false - isRangeGridlinesVisible = false - isOutlineVisible = false } - padding = RectangleInsets(10.0, 10.0, 10.0, 10.0) - isBorderVisible = false + updateTooltipGenerator() - theme = Theme.currentValue - Theme.addChangeListener { newTheme -> - theme = newTheme + TimePreferences.addChangeListener { + updateTooltipGenerator() } + + isDomainGridlinesVisible = false + isRangeGridlinesVisible = false + isOutlineVisible = false + } + + padding = RectangleInsets(10.0, 10.0, 10.0, 10.0) + isBorderVisible = false + + theme = Theme.currentValue + Theme.addChangeListener { newTheme -> + theme = newTheme } } diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/log/LogPanel.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/log/LogPanel.kt index 1dd2b60d..6b48cba6 100644 --- a/src/main/kotlin/io/github/inductiveautomation/kindling/log/LogPanel.kt +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/log/LogPanel.kt @@ -8,6 +8,8 @@ import io.github.inductiveautomation.kindling.core.Kindling.Preferences.Advanced import io.github.inductiveautomation.kindling.core.Kindling.Preferences.General.ShowFullLoggerNames import io.github.inductiveautomation.kindling.core.Kindling.Preferences.General.UseHyperlinks import io.github.inductiveautomation.kindling.core.LinkHandlingStrategy +import io.github.inductiveautomation.kindling.core.TimePreferences +import io.github.inductiveautomation.kindling.core.TimePreferences.SelectedTimeZone import io.github.inductiveautomation.kindling.core.ToolOpeningException import io.github.inductiveautomation.kindling.core.ToolPanel import io.github.inductiveautomation.kindling.utils.Action @@ -313,7 +315,7 @@ sealed class LogPanel( table.selectionModel.updateDetails() } - LogViewer.SelectedTimeZone.addChangeListener { + SelectedTimeZone.addChangeListener { table.model.fireTableDataChanged() } } @@ -332,8 +334,8 @@ sealed class LogPanel( .map { event -> DetailEvent( title = when (event) { - is SystemLogEvent -> "${LogViewer.format(event.timestamp)} ${event.thread}" - else -> LogViewer.format(event.timestamp) + is SystemLogEvent -> "${TimePreferences.format(event.timestamp)} ${event.thread}" + else -> TimePreferences.format(event.timestamp) }, message = event.message, body = event.stacktrace.map { element -> diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/log/TableModel.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/log/TableModel.kt index 54ce1cad..1003d400 100644 --- a/src/main/kotlin/io/github/inductiveautomation/kindling/log/TableModel.kt +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/log/TableModel.kt @@ -2,6 +2,7 @@ package io.github.inductiveautomation.kindling.log import com.jidesoft.comparator.AlphanumComparator import io.github.inductiveautomation.kindling.core.Kindling.Preferences.General.ShowFullLoggerNames +import io.github.inductiveautomation.kindling.core.TimePreferences import io.github.inductiveautomation.kindling.utils.Column import io.github.inductiveautomation.kindling.utils.ColumnList import io.github.inductiveautomation.kindling.utils.FlatActionIcon @@ -81,7 +82,7 @@ sealed class LogColumnList : ColumnList() { minWidth = 155 maxWidth = 155 cellRenderer = DefaultTableRenderer { - (it as? Instant)?.let(LogViewer::format) + (it as? Instant)?.let(TimePreferences::format) } }, getValue = LogEvent::timestamp, diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/log/TimePanel.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/log/TimePanel.kt index 206471b5..b210f556 100644 --- a/src/main/kotlin/io/github/inductiveautomation/kindling/log/TimePanel.kt +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/log/TimePanel.kt @@ -5,6 +5,8 @@ import com.formdev.flatlaf.extras.components.FlatButton import io.github.inductiveautomation.kindling.core.FilterChangeListener import io.github.inductiveautomation.kindling.core.FilterPanel import io.github.inductiveautomation.kindling.core.Kindling.Preferences.UI.Theme +import io.github.inductiveautomation.kindling.core.TimePreferences +import io.github.inductiveautomation.kindling.core.TimePreferences.SelectedTimeZone import io.github.inductiveautomation.kindling.utils.Action import io.github.inductiveautomation.kindling.utils.Column import io.github.inductiveautomation.kindling.utils.ColumnList @@ -264,12 +266,12 @@ internal class TimePanel( ) { if (column == WrapperLogColumns.Timestamp || column == SystemLogColumns.Timestamp) { menu.add( - Action("Show only events after ${LogViewer.format(event.timestamp)}") { + Action("Show only events after ${TimePreferences.format(event.timestamp)}") { startSelector.time = event.timestamp }, ) menu.add( - Action("Show only events before ${LogViewer.format(event.timestamp)}") { + Action("Show only events before ${TimePreferences.format(event.timestamp)}") { endSelector.time = event.timestamp }, ) @@ -284,11 +286,11 @@ internal class TimePanel( } private var JXDatePicker.localDate: LocalDate? - get() = date?.toInstant()?.let { LocalDate.ofInstant(it, LogViewer.SelectedTimeZone.currentValue) } + get() = date?.toInstant()?.let { LocalDate.ofInstant(it, SelectedTimeZone.currentValue) } set(value) { date = value?.atStartOfDay() - ?.atOffset(LogViewer.SelectedTimeZone.currentValue.rules.getOffset(value.atStartOfDay())) + ?.atOffset(SelectedTimeZone.currentValue.rules.getOffset(value.atStartOfDay())) ?.toInstant() .let(Date::from) } @@ -307,7 +309,7 @@ class DateTimeSelector( } private val initialZonedTime: ZonedDateTime - get() = defaultValue.atZone(LogViewer.SelectedTimeZone.currentValue) + get() = defaultValue.atZone(SelectedTimeZone.currentValue) private val datePicker = JXDatePicker().apply { @@ -347,12 +349,12 @@ class DateTimeSelector( ZonedDateTime.of( localDate, timeSelector.localTime, - LogViewer.SelectedTimeZone.currentValue, + SelectedTimeZone.currentValue, ).toInstant() ?: defaultValue } } set(value) { - val zonedDateTime = value.atZone(LogViewer.SelectedTimeZone.currentValue) + val zonedDateTime = value.atZone(SelectedTimeZone.currentValue) datePicker.localDate = zonedDateTime.toLocalDate() timeSelector.localTime = zonedDateTime.toLocalTime() } @@ -511,10 +513,10 @@ private object DensityColumns : ColumnList() { get() { if (!this::_formatter.isInitialized) { _formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm") - .withZone(LogViewer.SelectedTimeZone.currentValue) + .withZone(SelectedTimeZone.currentValue) } - if (_formatter.zone != LogViewer.SelectedTimeZone.currentValue) { - _formatter = _formatter.withZone(LogViewer.SelectedTimeZone.currentValue) + if (_formatter.zone != SelectedTimeZone.currentValue) { + _formatter = _formatter.withZone(SelectedTimeZone.currentValue) } return _formatter } diff --git a/src/main/kotlin/io/github/inductiveautomation/kindling/log/WrapperLogPanel.kt b/src/main/kotlin/io/github/inductiveautomation/kindling/log/WrapperLogPanel.kt index 60367560..b93b86aa 100644 --- a/src/main/kotlin/io/github/inductiveautomation/kindling/log/WrapperLogPanel.kt +++ b/src/main/kotlin/io/github/inductiveautomation/kindling/log/WrapperLogPanel.kt @@ -5,15 +5,11 @@ import com.jidesoft.comparator.AlphanumComparator import io.github.inductiveautomation.kindling.core.ClipboardTool import io.github.inductiveautomation.kindling.core.Kindling.Preferences.General.DefaultEncoding import io.github.inductiveautomation.kindling.core.MultiTool -import io.github.inductiveautomation.kindling.core.Preference.Companion.preference -import io.github.inductiveautomation.kindling.core.PreferenceCategory import io.github.inductiveautomation.kindling.core.ToolPanel -import io.github.inductiveautomation.kindling.log.LogViewer.SelectedTimeZone import io.github.inductiveautomation.kindling.log.WrapperLogEvent.Companion.STDOUT import io.github.inductiveautomation.kindling.utils.FileFilter import io.github.inductiveautomation.kindling.utils.FileFilterSidebar import io.github.inductiveautomation.kindling.utils.TabStrip -import io.github.inductiveautomation.kindling.utils.ZoneIdSerializer import io.github.inductiveautomation.kindling.utils.getValue import io.github.inductiveautomation.kindling.utils.transferTo import kotlinx.coroutines.Dispatchers @@ -26,10 +22,6 @@ import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException -import java.time.temporal.TemporalAccessor -import java.time.zone.ZoneRulesProvider -import java.util.Vector -import javax.swing.JComboBox import javax.swing.SwingUtilities import kotlin.io.path.absolutePathString import kotlin.io.path.name @@ -191,7 +183,7 @@ class WrapperLogPanel( } } -data object LogViewer : MultiTool, ClipboardTool, PreferenceCategory { +data object LogViewer : MultiTool, ClipboardTool { override val serialKey = "logview" override val title = "Wrapper Log" override val description = "Wrapper Log(s) (wrapper.log, wrapper.log.1, wrapper.log...)" @@ -228,39 +220,4 @@ data object LogViewer : MultiTool, ClipboardTool, PreferenceCategory { ) return WrapperLogPanel(listOf(tempFile), listOf(fileData)) } - - val SelectedTimeZone = preference( - name = "Timezone", - description = "Timezone to use when displaying logs", - default = ZoneId.systemDefault(), - serializer = ZoneIdSerializer, - editor = { - JComboBox(Vector(ZoneRulesProvider.getAvailableZoneIds().sorted())).apply { - selectedItem = currentValue.id - addActionListener { - currentValue = ZoneId.of(selectedItem as String) - } - } - }, - ) - - private var formatter = createFormatter(SelectedTimeZone.currentValue) - - init { - SelectedTimeZone.addChangeListener { newValue -> - formatter = createFormatter(newValue) - } - } - - private fun createFormatter(id: ZoneId): DateTimeFormatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss:SSS") - .withZone(id) - - /** - * Format [time] using an internal [DateTimeFormatter] that is in [SelectedTimeZone] automatically. - */ - fun format(time: TemporalAccessor): String = formatter.format(time) - - override val displayName = "Log View" - - override val preferences = listOf(SelectedTimeZone) }