Skip to content

Commit 0acc5a1

Browse files
General timezone preference (#321)
Co-authored-by: Paul Griffith <[email protected]>
1 parent 026d3c3 commit 0acc5a1

File tree

10 files changed

+214
-103
lines changed

10 files changed

+214
-103
lines changed

src/main/kotlin/io/github/inductiveautomation/kindling/core/Kindling.kt

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.github.inductiveautomation.kindling.utils.ReifiedLabelProvider.Compani
2020
import io.github.inductiveautomation.kindling.utils.ReifiedListTableModel
2121
import io.github.inductiveautomation.kindling.utils.ThemeSerializer
2222
import io.github.inductiveautomation.kindling.utils.ToolSerializer
23+
import io.github.inductiveautomation.kindling.utils.ZoneIdSerializer
2324
import io.github.inductiveautomation.kindling.utils.configureCellRenderer
2425
import io.github.inductiveautomation.kindling.utils.debounce
2526
import io.github.inductiveautomation.kindling.utils.render
@@ -40,6 +41,7 @@ import java.awt.Image
4041
import java.net.URI
4142
import java.nio.charset.Charset
4243
import java.nio.file.Path
44+
import java.time.ZoneId
4345
import java.util.Vector
4446
import javax.swing.DefaultCellEditor
4547
import javax.swing.DefaultComboBoxModel
@@ -184,6 +186,29 @@ data object Kindling {
184186
},
185187
)
186188

189+
val DefaultTimezone = preference(
190+
name = "Timezone",
191+
description = "Timezone to use when displaying timestamps",
192+
legacyValueProvider = { allPrefs ->
193+
allPrefs["logview"]?.get("timezone")?.let {
194+
Json.decodeFromJsonElement(ZoneIdSerializer, it)
195+
}
196+
},
197+
default = ZoneId.systemDefault(),
198+
serializer = ZoneIdSerializer,
199+
editor = {
200+
val zoneIds = ZoneId.getAvailableZoneIds().filter { id ->
201+
id !in ZoneId.SHORT_IDS.keys
202+
}.sorted()
203+
JComboBox(Vector(zoneIds)).apply {
204+
selectedItem = currentValue.id
205+
addActionListener {
206+
currentValue = ZoneId.of(selectedItem as String)
207+
}
208+
}
209+
},
210+
)
211+
187212
override val displayName: String = "General"
188213
override val serialKey: String = "general"
189214
override val preferences: List<Preference<*>> = listOf(
@@ -194,6 +219,7 @@ data object Kindling {
194219
ShowLogTree,
195220
UseHyperlinks,
196221
HighlightByDefault,
222+
DefaultTimezone,
197223
)
198224
}
199225

@@ -297,7 +323,7 @@ data object Kindling {
297323
val categoryData = internalState.getOrPut(category.serialKey) { mutableMapOf() }
298324
return categoryData[preference.serialKey]?.let { currentValue ->
299325
preferencesJson.decodeFromJsonElement(preference.serializer, currentValue)
300-
}
326+
} ?: preference.legacyValueProvider?.invoke(internalState)
301327
}
302328

303329
operator fun <T : Any> set(

src/main/kotlin/io/github/inductiveautomation/kindling/core/Preferences.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.github.inductiveautomation.kindling.utils.StyledLabel
55
import io.github.inductiveautomation.kindling.utils.dismissOnEscape
66
import io.github.inductiveautomation.kindling.utils.jFrame
77
import kotlinx.serialization.KSerializer
8+
import kotlinx.serialization.json.JsonElement
89
import kotlinx.serialization.serializer
910
import net.miginfocom.swing.MigLayout
1011
import org.jdesktop.swingx.JXTaskPane
@@ -31,6 +32,7 @@ class Preference<T : Any>(
3132
val category: PreferenceCategory,
3233
val name: String,
3334
override val serialKey: String,
35+
val legacyValueProvider: ((Map<String, Map<String, JsonElement>>) -> T?)? = null,
3436
val description: String? = null,
3537
val requiresRestart: Boolean = false,
3638
val default: T,
@@ -92,13 +94,15 @@ class Preference<T : Any>(
9294
name: String,
9395
description: String? = null,
9496
serialKey: String = name.lowercase().filter(Char::isJavaIdentifierStart),
97+
noinline legacyValueProvider: ((Map<String, Map<String, JsonElement>>) -> T?)? = null,
9598
requiresRestart: Boolean = false,
9699
default: T,
97100
serializer: KSerializer<T> = serializer(),
98101
noinline editor: (Preference<T>.() -> JComponent)?,
99102
): Preference<T> = Preference(
100103
name = name,
101104
serialKey = serialKey,
105+
legacyValueProvider = legacyValueProvider,
102106
category = this,
103107
description = description,
104108
requiresRestart = requiresRestart,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
}

src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricCard.kt

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.inductiveautomation.kindling.idb.metrics
22

3+
import io.github.inductiveautomation.kindling.core.Timezone
34
import io.github.inductiveautomation.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Cpu
45
import io.github.inductiveautomation.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Default
56
import io.github.inductiveautomation.kindling.idb.metrics.MetricCard.Companion.MetricPresentation.Heap
@@ -17,7 +18,7 @@ import java.text.DecimalFormat
1718
import java.text.FieldPosition
1819
import java.text.NumberFormat
1920
import java.text.ParsePosition
20-
import java.text.SimpleDateFormat
21+
import java.util.Date
2122
import javax.swing.JLabel
2223
import javax.swing.JPanel
2324
import javax.swing.SwingConstants.CENTER
@@ -65,9 +66,18 @@ class MetricCard(val metric: Metric, data: List<MetricData>) : JPanel(MigLayout(
6566
)
6667

6768
val aggregateData = DoubleArray(data.size) { i -> data[i].value }
68-
add(JLabel("Min: ${presentation.formatter.format(aggregateData.min())}", CENTER), "pushx, growx")
69-
add(JLabel("Avg: ${presentation.formatter.format(aggregateData.average())}", CENTER), "pushx, growx")
70-
add(JLabel("Max: ${presentation.formatter.format(aggregateData.max())}", CENTER), "pushx, growx, wrap")
69+
add(
70+
JLabel("Min: ${presentation.formatter.format(aggregateData.min())}", CENTER),
71+
"pushx, growx",
72+
)
73+
add(
74+
JLabel("Avg: ${presentation.formatter.format(aggregateData.average())}", CENTER),
75+
"pushx, growx",
76+
)
77+
add(
78+
JLabel("Max: ${presentation.formatter.format(aggregateData.max())}", CENTER),
79+
"pushx, growx, wrap",
80+
)
7181

7282
val minTimestamp = data.first().timestamp
7383
val maxTimestamp = data.last().timestamp
@@ -90,23 +100,46 @@ class MetricCard(val metric: Metric, data: List<MetricData>) : JPanel(MigLayout(
90100
}
91101

92102
add(sparkLine, "span, w 300, h 170, pushx, growx")
93-
add(JLabel("${DATE_FORMAT.format(minTimestamp)} - ${DATE_FORMAT.format(maxTimestamp)}", CENTER), "pushx, growx, span")
103+
104+
val timeLabel = JLabel(
105+
buildTimeLabelString(minTimestamp, maxTimestamp),
106+
CENTER,
107+
)
108+
add(timeLabel, "pushx, growx, span")
109+
110+
Timezone.Default.addChangeListener {
111+
timeLabel.text =
112+
buildTimeLabelString(minTimestamp, maxTimestamp)
113+
}
94114

95115
border = LineBorder(UIManager.getColor("Component.borderColor"), 3, true)
96116
}
97117

118+
private fun buildTimeLabelString(minTimestamp: Date, maxTimestamp: Date): String = buildString {
119+
append(Timezone.Default.format(minTimestamp))
120+
append(" - ")
121+
append(Timezone.Default.format(maxTimestamp))
122+
}
123+
98124
companion object {
99-
val DATE_FORMAT = SimpleDateFormat("MM/dd/yy HH:mm:ss")
100125

101126
private val mbFormatter = DecimalFormat("0.0 'mB'")
102127
private val heapFormatter = object : NumberFormat() {
103-
override fun format(number: Double, toAppendTo: StringBuffer, pos: FieldPosition): StringBuffer = mbFormatter.format(number / 1_000_000, toAppendTo, pos)
128+
override fun format(
129+
number: Double,
130+
toAppendTo: StringBuffer,
131+
pos: FieldPosition,
132+
): StringBuffer = mbFormatter.format(number / 1_000_000, toAppendTo, pos)
133+
134+
override fun format(
135+
number: Long,
136+
toAppendTo: StringBuffer,
137+
pos: FieldPosition,
138+
): StringBuffer = mbFormatter.format(number, toAppendTo, pos)
104139

105-
override fun format(number: Long, toAppendTo: StringBuffer, pos: FieldPosition): StringBuffer = mbFormatter.format(number, toAppendTo, pos)
106140
override fun parse(source: String, parsePosition: ParsePosition): Number = mbFormatter.parse(source, parsePosition)
107141
}
108142

109-
@Suppress("ktlint:standard:trailing-comma-on-declaration-site")
110143
enum class MetricPresentation(val formatter: NumberFormat, val isShowTrend: Boolean) {
111144
Heap(heapFormatter, true),
112145
Queue(NumberFormat.getIntegerInstance(), false),
@@ -117,7 +150,7 @@ class MetricCard(val metric: Metric, data: List<MetricData>) : JPanel(MigLayout(
117150
},
118151
true,
119152
),
120-
Default(NumberFormat.getInstance(), false)
153+
Default(NumberFormat.getInstance(), false),
121154
}
122155

123156
private val Metric.presentation: MetricPresentation

src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/MetricsView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class MetricsView(connection: Connection) : ToolPanel("ins 0, fill, hidemode 3")
5454
}
5555
.executeQuery()
5656
.toList { rs ->
57-
MetricData(rs.getDouble(1), rs.getDate(2))
57+
MetricData(rs.getDouble(1), rs.getTimestamp(2))
5858
}
5959

6060
MetricCard(metric, metricData)

src/main/kotlin/io/github/inductiveautomation/kindling/idb/metrics/Sparkline.kt

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package io.github.inductiveautomation.kindling.idb.metrics
22

33
import io.github.inductiveautomation.kindling.core.Kindling.Preferences.UI.Theme
44
import io.github.inductiveautomation.kindling.core.Theme.Companion.theme
5-
import io.github.inductiveautomation.kindling.idb.metrics.MetricCard.Companion.DATE_FORMAT
5+
import io.github.inductiveautomation.kindling.core.Timezone
66
import org.jfree.chart.ChartFactory
77
import org.jfree.chart.JFreeChart
88
import org.jfree.chart.axis.NumberAxis
@@ -11,44 +11,59 @@ import org.jfree.data.time.FixedMillisecond
1111
import org.jfree.data.time.TimeSeries
1212
import org.jfree.data.time.TimeSeriesCollection
1313
import java.text.NumberFormat
14+
import java.time.Instant
1415

15-
fun sparkline(data: List<MetricData>, formatter: NumberFormat): JFreeChart {
16-
return ChartFactory.createTimeSeriesChart(
17-
/* title = */ null,
18-
/* timeAxisLabel = */ null,
19-
/* valueAxisLabel = */ null,
20-
/* dataset = */
21-
TimeSeriesCollection(
22-
TimeSeries("Series").apply {
23-
for ((value, timestamp) in data) {
24-
add(FixedMillisecond(timestamp), value, false)
25-
}
26-
},
27-
),
28-
/* legend = */ false,
29-
/* tooltips = */ true,
30-
/* urls = */ false,
31-
).apply {
32-
xyPlot.apply {
33-
domainAxis.isPositiveArrowVisible = true
34-
rangeAxis.apply {
35-
isPositiveArrowVisible = true
36-
(this as NumberAxis).numberFormatOverride = formatter
16+
fun sparkline(data: List<MetricData>, 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)
3728
}
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 = {
3845
renderer.setDefaultToolTipGenerator { dataset, series, item ->
39-
"${DATE_FORMAT.format(dataset.getXValue(series, item))} - ${formatter.format(dataset.getYValue(series, item))}"
46+
val time = Instant.ofEpochMilli(dataset.getXValue(series, item).toLong())
47+
"${Timezone.Default.format(time)} - ${formatter.format(dataset.getYValue(series, item))}"
4048
}
41-
isDomainGridlinesVisible = false
42-
isRangeGridlinesVisible = false
43-
isOutlineVisible = false
4449
}
4550

46-
padding = RectangleInsets(10.0, 10.0, 10.0, 10.0)
47-
isBorderVisible = false
51+
updateTooltipGenerator()
4852

49-
theme = Theme.currentValue
50-
Theme.addChangeListener { newTheme ->
51-
theme = newTheme
53+
Timezone.Default.addChangeListener {
54+
updateTooltipGenerator()
5255
}
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
5368
}
5469
}

0 commit comments

Comments
 (0)