feat: формат даты/времени и pull-to-refresh на Обзоре (0.5.1)
Локальный вывод дат (по умолчанию ДД/ММ/ГГГГ ЧЧ:мм:сс), выбор формата в Устройство, дата напротив событий на Обзоре, исправление compile и жеста обновления.
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "ru.kalinamall.seaca"
|
applicationId = "ru.kalinamall.seaca"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 14
|
versionCode = 15
|
||||||
versionName = "0.5.0"
|
versionName = "0.5.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf as composeMutableStateOf
|
import androidx.compose.runtime.mutableStateOf as composeMutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ru.kalinamall.seaca.data
|
||||||
|
|
||||||
|
enum class DateTimeDisplayMode(
|
||||||
|
val storageKey: String,
|
||||||
|
val label: String,
|
||||||
|
val pattern: String?,
|
||||||
|
) {
|
||||||
|
SLASH("slash", "ДД/ММ/ГГГГ ЧЧ:мм:сс", "dd/MM/yyyy HH:mm:ss"),
|
||||||
|
DASH("dash", "ДД-ММ-ГГГГ ЧЧ:мм:сс", "dd-MM-yyyy HH:mm:ss"),
|
||||||
|
DOT("dot", "ДД.ММ.ГГГГ ЧЧ:мм:сс", "dd.MM.yyyy HH:mm:ss"),
|
||||||
|
LOCALE("locale", "Как в системе телефона", null),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromStorage(value: String?): DateTimeDisplayMode =
|
||||||
|
entries.firstOrNull { it.storageKey == value } ?: SLASH
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,13 +36,24 @@ class SessionStore(private val context: Context) {
|
|||||||
NotificationMode.fromStorage(prefs[KEY_NOTIFICATION_MODE])
|
NotificationMode.fromStorage(prefs[KEY_NOTIFICATION_MODE])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dateTimeDisplayMode: Flow<DateTimeDisplayMode> = context.dataStore.data.map { prefs ->
|
||||||
|
DateTimeDisplayMode.fromStorage(prefs[KEY_DATE_TIME_FORMAT])
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getNotificationMode(): NotificationMode =
|
suspend fun getNotificationMode(): NotificationMode =
|
||||||
NotificationMode.fromStorage(context.dataStore.data.first()[KEY_NOTIFICATION_MODE])
|
NotificationMode.fromStorage(context.dataStore.data.first()[KEY_NOTIFICATION_MODE])
|
||||||
|
|
||||||
|
suspend fun getDateTimeDisplayMode(): DateTimeDisplayMode =
|
||||||
|
DateTimeDisplayMode.fromStorage(context.dataStore.data.first()[KEY_DATE_TIME_FORMAT])
|
||||||
|
|
||||||
suspend fun setNotificationMode(mode: NotificationMode) {
|
suspend fun setNotificationMode(mode: NotificationMode) {
|
||||||
context.dataStore.edit { it[KEY_NOTIFICATION_MODE] = mode.storageKey }
|
context.dataStore.edit { it[KEY_NOTIFICATION_MODE] = mode.storageKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun setDateTimeDisplayMode(mode: DateTimeDisplayMode) {
|
||||||
|
context.dataStore.edit { it[KEY_DATE_TIME_FORMAT] = mode.storageKey }
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getBaseUrl(): String? = context.dataStore.data.first()[KEY_BASE_URL]
|
suspend fun getBaseUrl(): String? = context.dataStore.data.first()[KEY_BASE_URL]
|
||||||
|
|
||||||
suspend fun trustAllCerts(): Boolean = context.dataStore.data.first()[KEY_TRUST_ALL] ?: false
|
suspend fun trustAllCerts(): Boolean = context.dataStore.data.first()[KEY_TRUST_ALL] ?: false
|
||||||
@@ -102,6 +113,7 @@ class SessionStore(private val context: Context) {
|
|||||||
private val KEY_DEVICE_UUID = stringPreferencesKey("device_uuid")
|
private val KEY_DEVICE_UUID = stringPreferencesKey("device_uuid")
|
||||||
private val KEY_TRUST_ALL = booleanPreferencesKey("trust_all_certs")
|
private val KEY_TRUST_ALL = booleanPreferencesKey("trust_all_certs")
|
||||||
private val KEY_NOTIFICATION_MODE = stringPreferencesKey("notification_mode")
|
private val KEY_NOTIFICATION_MODE = stringPreferencesKey("notification_mode")
|
||||||
|
private val KEY_DATE_TIME_FORMAT = stringPreferencesKey("date_time_format")
|
||||||
private const val KEY_ACCESS = "access_token"
|
private const val KEY_ACCESS = "access_token"
|
||||||
private const val KEY_REFRESH = "refresh_token"
|
private const val KEY_REFRESH = "refresh_token"
|
||||||
private const val KEY_DEVICE_ID = "device_id"
|
private const val KEY_DEVICE_ID = "device_id"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.material3.Button
|
|||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -42,11 +43,14 @@ import ru.kalinamall.seaca.R
|
|||||||
import ru.kalinamall.seaca.data.EventSummary
|
import ru.kalinamall.seaca.data.EventSummary
|
||||||
import ru.kalinamall.seaca.data.EventDetail
|
import ru.kalinamall.seaca.data.EventDetail
|
||||||
import ru.kalinamall.seaca.data.HostDetail
|
import ru.kalinamall.seaca.data.HostDetail
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import ru.kalinamall.seaca.data.DateTimeDisplayMode
|
||||||
import ru.kalinamall.seaca.data.NotificationMode
|
import ru.kalinamall.seaca.data.NotificationMode
|
||||||
import ru.kalinamall.seaca.data.ProblemDetail
|
import ru.kalinamall.seaca.data.ProblemDetail
|
||||||
import ru.kalinamall.seaca.data.SacRepository
|
import ru.kalinamall.seaca.data.SacRepository
|
||||||
import ru.kalinamall.seaca.ui.components.BackArrowButton
|
import ru.kalinamall.seaca.ui.components.BackArrowButton
|
||||||
import ru.kalinamall.seaca.ui.components.ForwardArrowButton
|
import ru.kalinamall.seaca.ui.components.ForwardArrowButton
|
||||||
|
import ru.kalinamall.seaca.ui.util.SacDateTimes
|
||||||
import ru.kalinamall.seaca.ui.util.dailyReportTypeLabel
|
import ru.kalinamall.seaca.ui.util.dailyReportTypeLabel
|
||||||
import ru.kalinamall.seaca.ui.util.formatEventDetails
|
import ru.kalinamall.seaca.ui.util.formatEventDetails
|
||||||
import ru.kalinamall.seaca.ui.util.inventoryFieldLines
|
import ru.kalinamall.seaca.ui.util.inventoryFieldLines
|
||||||
@@ -63,6 +67,9 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) {
|
|||||||
var data by remember { mutableStateOf<EventDetail?>(null) }
|
var data by remember { mutableStateOf<EventDetail?>(null) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
var loading by remember { mutableStateOf(true) }
|
var loading by remember { mutableStateOf(true) }
|
||||||
|
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||||
|
initialValue = DateTimeDisplayMode.SLASH,
|
||||||
|
)
|
||||||
|
|
||||||
LaunchedEffect(id) {
|
LaunchedEffect(id) {
|
||||||
loading = true
|
loading = true
|
||||||
@@ -83,7 +90,7 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) {
|
|||||||
if (isDailyReportType(d.type)) dailyReportTypeLabel(d.type) else d.type,
|
if (isDailyReportType(d.type)) dailyReportTypeLabel(d.type) else d.type,
|
||||||
)
|
)
|
||||||
Field("Хост", d.hostname)
|
Field("Хост", d.hostname)
|
||||||
Field("Время", d.occurredAt)
|
Field("Время", SacDateTimes.format(d.occurredAt, dateMode))
|
||||||
Field("Кратко", d.summary)
|
Field("Кратко", d.summary)
|
||||||
if (isDailyReportType(d.type)) {
|
if (isDailyReportType(d.type)) {
|
||||||
reportTextFromDetails(d.details)?.let { MultilineField("Отчёт", it) }
|
reportTextFromDetails(d.details)?.let { MultilineField("Отчёт", it) }
|
||||||
@@ -126,6 +133,9 @@ fun ProblemDetailScreen(
|
|||||||
var loading by remember { mutableStateOf(true) }
|
var loading by remember { mutableStateOf(true) }
|
||||||
var acting by remember { mutableStateOf(false) }
|
var acting by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||||
|
initialValue = DateTimeDisplayMode.SLASH,
|
||||||
|
)
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -192,7 +202,11 @@ fun ProblemDetailScreen(
|
|||||||
if (d.events.isNotEmpty()) {
|
if (d.events.isNotEmpty()) {
|
||||||
Text("Связанные события", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
|
Text("Связанные события", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
|
||||||
d.events.forEach { e ->
|
d.events.forEach { e ->
|
||||||
Text("${e.severity} · ${e.title}\n${e.occurredAt}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(
|
||||||
|
"${e.severity} · ${e.title}\n${SacDateTimes.format(e.occurredAt, dateMode)}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,6 +223,9 @@ fun HostDetailScreen(
|
|||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
var loading by remember { mutableStateOf(true) }
|
var loading by remember { mutableStateOf(true) }
|
||||||
var selectedTab by remember { mutableIntStateOf(HostDetailTab.Info.ordinal) }
|
var selectedTab by remember { mutableIntStateOf(HostDetailTab.Info.ordinal) }
|
||||||
|
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||||
|
initialValue = DateTimeDisplayMode.SLASH,
|
||||||
|
)
|
||||||
|
|
||||||
LaunchedEffect(id) {
|
LaunchedEffect(id) {
|
||||||
loading = true
|
loading = true
|
||||||
@@ -264,7 +281,7 @@ fun HostDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (HostDetailTab.entries[selectedTab]) {
|
when (HostDetailTab.entries[selectedTab]) {
|
||||||
HostDetailTab.Info -> HostInfoContent(host)
|
HostDetailTab.Info -> HostInfoContent(host, dateMode)
|
||||||
HostDetailTab.Events -> HostEventsContent(
|
HostDetailTab.Events -> HostEventsContent(
|
||||||
repo = repo,
|
repo = repo,
|
||||||
hostname = host.hostname,
|
hostname = host.hostname,
|
||||||
@@ -276,7 +293,7 @@ fun HostDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HostInfoContent(host: HostDetail) {
|
private fun HostInfoContent(host: HostDetail, dateMode: DateTimeDisplayMode) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -288,7 +305,7 @@ private fun HostInfoContent(host: HostDetail) {
|
|||||||
Field("Версия", host.productVersion ?: "—")
|
Field("Версия", host.productVersion ?: "—")
|
||||||
Field("ОС", "${host.osFamily} ${host.osVersion.orEmpty()}".trim())
|
Field("ОС", "${host.osFamily} ${host.osVersion.orEmpty()}".trim())
|
||||||
Field("Агент", host.agentStatus)
|
Field("Агент", host.agentStatus)
|
||||||
Field("Последний контакт", host.lastSeenAt)
|
Field("Последний контакт", SacDateTimes.format(host.lastSeenAt, dateMode))
|
||||||
host.inventory?.let { inv ->
|
host.inventory?.let { inv ->
|
||||||
val lines = inventoryFieldLines(inv)
|
val lines = inventoryFieldLines(inv)
|
||||||
if (lines.isEmpty()) {
|
if (lines.isEmpty()) {
|
||||||
@@ -301,6 +318,7 @@ private fun HostInfoContent(host: HostDetail) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun HostEventsContent(
|
private fun HostEventsContent(
|
||||||
repo: SacRepository,
|
repo: SacRepository,
|
||||||
@@ -314,6 +332,9 @@ private fun HostEventsContent(
|
|||||||
var refreshing by remember { mutableStateOf(false) }
|
var refreshing by remember { mutableStateOf(false) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||||
|
initialValue = DateTimeDisplayMode.SLASH,
|
||||||
|
)
|
||||||
|
|
||||||
fun load(showRefreshIndicator: Boolean = false) {
|
fun load(showRefreshIndicator: Boolean = false) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -369,7 +390,7 @@ private fun HostEventsContent(
|
|||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
event.occurredAt,
|
SacDateTimes.format(event.occurredAt, dateMode),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -398,6 +419,9 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
|
|||||||
var role by remember { mutableStateOf<String?>(null) }
|
var role by remember { mutableStateOf<String?>(null) }
|
||||||
var deviceId by remember { mutableStateOf(0) }
|
var deviceId by remember { mutableStateOf(0) }
|
||||||
val notificationMode by repo.session.notificationMode.collectAsState(initial = NotificationMode.PUSH_SOUND)
|
val notificationMode by repo.session.notificationMode.collectAsState(initial = NotificationMode.PUSH_SOUND)
|
||||||
|
val dateTimeMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||||
|
initialValue = DateTimeDisplayMode.SLASH,
|
||||||
|
)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -458,6 +482,34 @@ fun DeviceScreen(repo: SacRepository, onLogout: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.date_time_format_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
)
|
||||||
|
DateTimeDisplayMode.entries.forEach { mode ->
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = dateTimeMode == mode,
|
||||||
|
onClick = {
|
||||||
|
scope.launch { repo.session.setDateTimeDisplayMode(mode) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
mode.label,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package ru.kalinamall.seaca.ui.screens
|
package ru.kalinamall.seaca.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -33,8 +35,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.kalinamall.seaca.data.DateTimeDisplayMode
|
||||||
import ru.kalinamall.seaca.data.DashboardSummary
|
import ru.kalinamall.seaca.data.DashboardSummary
|
||||||
import ru.kalinamall.seaca.data.EventListResponse
|
import ru.kalinamall.seaca.data.EventListResponse
|
||||||
import ru.kalinamall.seaca.data.EventSummary
|
import ru.kalinamall.seaca.data.EventSummary
|
||||||
@@ -43,6 +47,7 @@ import ru.kalinamall.seaca.data.ProblemSummary
|
|||||||
import ru.kalinamall.seaca.data.SacRepository
|
import ru.kalinamall.seaca.data.SacRepository
|
||||||
import ru.kalinamall.seaca.ui.components.BackArrowButton
|
import ru.kalinamall.seaca.ui.components.BackArrowButton
|
||||||
import ru.kalinamall.seaca.ui.components.ForwardArrowButton
|
import ru.kalinamall.seaca.ui.components.ForwardArrowButton
|
||||||
|
import ru.kalinamall.seaca.ui.util.SacDateTimes
|
||||||
|
|
||||||
private val REPORT_TYPE_OPTIONS = listOf(
|
private val REPORT_TYPE_OPTIONS = listOf(
|
||||||
"report.daily.ssh" to "Отчёты ssh клиентов",
|
"report.daily.ssh" to "Отчёты ssh клиентов",
|
||||||
@@ -57,6 +62,10 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
|
|||||||
var loading by remember { mutableStateOf(false) }
|
var loading by remember { mutableStateOf(false) }
|
||||||
var refreshing by remember { mutableStateOf(false) }
|
var refreshing by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||||
|
initialValue = DateTimeDisplayMode.SLASH,
|
||||||
|
)
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
fun load(showRefreshIndicator: Boolean = false) {
|
fun load(showRefreshIndicator: Boolean = false) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -89,7 +98,13 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
|
|||||||
onRefresh = { load(showRefreshIndicator = true) },
|
onRefresh = { load(showRefreshIndicator = true) },
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
||||||
data?.let { d ->
|
data?.let { d ->
|
||||||
StatCard("События 24ч", d.eventsLast24h.toString())
|
StatCard("События 24ч", d.eventsLast24h.toString())
|
||||||
@@ -101,11 +116,23 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
|
|||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
d.recentEvents.take(8).forEach { e ->
|
d.recentEvents.take(8).forEach { e ->
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
dashboardRecentEventLine(e),
|
dashboardRecentEventLine(e),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
Text(
|
||||||
|
SacDateTimes.format(e.occurredAt, dateMode),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data == null && loading) {
|
if (data == null && loading) {
|
||||||
@@ -144,6 +171,9 @@ private fun StatCard(label: String, value: String) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
|
fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
|
||||||
var hostname by remember { mutableStateOf("") }
|
var hostname by remember { mutableStateOf("") }
|
||||||
|
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||||
|
initialValue = DateTimeDisplayMode.SLASH,
|
||||||
|
)
|
||||||
|
|
||||||
PagedListScreen(
|
PagedListScreen(
|
||||||
title = "События",
|
title = "События",
|
||||||
@@ -160,7 +190,7 @@ fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
|
|||||||
},
|
},
|
||||||
itemKey = EventSummary::id,
|
itemKey = EventSummary::id,
|
||||||
itemLabel = { "${it.severity} · ${it.title}" },
|
itemLabel = { "${it.severity} · ${it.title}" },
|
||||||
itemSub = { "${it.hostname} · ${it.occurredAt}" },
|
itemSub = { "${it.hostname} · ${SacDateTimes.format(it.occurredAt, dateMode)}" },
|
||||||
onOpen = onOpen,
|
onOpen = onOpen,
|
||||||
severities = listOf(null, "info", "warning", "high", "critical"),
|
severities = listOf(null, "info", "warning", "high", "critical"),
|
||||||
hostnameFilter = hostname,
|
hostnameFilter = hostname,
|
||||||
@@ -224,6 +254,9 @@ fun ReportsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
|
|||||||
var refreshing by remember { mutableStateOf(false) }
|
var refreshing by remember { mutableStateOf(false) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||||
|
initialValue = DateTimeDisplayMode.SLASH,
|
||||||
|
)
|
||||||
|
|
||||||
fun load(showRefreshIndicator: Boolean = false) {
|
fun load(showRefreshIndicator: Boolean = false) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -274,7 +307,7 @@ fun ReportsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
|
|||||||
items(data?.items.orEmpty(), key = { it.id }) { item ->
|
items(data?.items.orEmpty(), key = { it.id }) { item ->
|
||||||
ListRow(
|
ListRow(
|
||||||
title = item.summary,
|
title = item.summary,
|
||||||
subtitle = "${item.hostname} · ${item.occurredAt}",
|
subtitle = "${item.hostname} · ${SacDateTimes.format(item.occurredAt, dateMode)}",
|
||||||
onClick = { onOpen(item.id) },
|
onClick = { onOpen(item.id) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package ru.kalinamall.seaca.ui.util
|
||||||
|
|
||||||
|
import ru.kalinamall.seaca.data.DateTimeDisplayMode
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object SacDateTimes {
|
||||||
|
fun format(raw: String?, mode: DateTimeDisplayMode): String {
|
||||||
|
if (raw.isNullOrBlank()) return "—"
|
||||||
|
val zoned = parse(raw) ?: return raw.trim()
|
||||||
|
val local = zoned.withZoneSameInstant(ZoneId.systemDefault())
|
||||||
|
return when (mode) {
|
||||||
|
DateTimeDisplayMode.LOCALE -> {
|
||||||
|
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM)
|
||||||
|
.withLocale(Locale.getDefault())
|
||||||
|
.format(local)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val pattern = mode.pattern ?: DateTimeDisplayMode.SLASH.pattern!!
|
||||||
|
DateTimeFormatter.ofPattern(pattern, Locale.getDefault()).format(local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parse(raw: String): ZonedDateTime? {
|
||||||
|
val trimmed = raw.trim()
|
||||||
|
val normalised = trimmed.replace(Regex(" ([+-]\\d{2}:\\d{2})$"), "$1")
|
||||||
|
val candidates = listOf(normalised, trimmed)
|
||||||
|
for (candidate in candidates) {
|
||||||
|
runCatching { return OffsetDateTime.parse(candidate).toZonedDateTime() }
|
||||||
|
runCatching { return ZonedDateTime.parse(candidate) }
|
||||||
|
runCatching { return Instant.parse(candidate).atZone(ZoneId.systemDefault()) }
|
||||||
|
runCatching {
|
||||||
|
return LocalDateTime.parse(candidate, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@
|
|||||||
<string name="channel_high_silent">SAC: важные (без звука)</string>
|
<string name="channel_high_silent">SAC: важные (без звука)</string>
|
||||||
<string name="channel_critical_silent">SAC: критические (без звука)</string>
|
<string name="channel_critical_silent">SAC: критические (без звука)</string>
|
||||||
<string name="notification_mode_title">Push-уведомления</string>
|
<string name="notification_mode_title">Push-уведомления</string>
|
||||||
|
<string name="date_time_format_title">Формат даты и времени</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user