feat: RDG session break aligned with SAC rdg_qwinsta_enabled (0.5.14)
Co-authored-by: Cursor <cursoragent@cursor.com>
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 = 27
|
versionCode = 28
|
||||||
versionName = "0.5.13"
|
versionName = "0.5.14"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ data class EventSummary(
|
|||||||
@SerialName("rdg_flap") val rdgFlap: Boolean = false,
|
@SerialName("rdg_flap") val rdgFlap: Boolean = false,
|
||||||
@SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null,
|
@SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null,
|
||||||
@SerialName("rdg_flap_qwinsta_event_id") val rdgFlapQwinstaEventId: Long? = null,
|
@SerialName("rdg_flap_qwinsta_event_id") val rdgFlapQwinstaEventId: Long? = null,
|
||||||
|
@SerialName("rdg_access_path") val rdgAccessPath: String? = null,
|
||||||
|
@SerialName("rdg_qwinsta_enabled") val rdgQwinstaEnabled: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -87,6 +89,8 @@ data class EventDetail(
|
|||||||
@SerialName("rdg_flap") val rdgFlap: Boolean = false,
|
@SerialName("rdg_flap") val rdgFlap: Boolean = false,
|
||||||
@SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null,
|
@SerialName("rdg_flap_pair_event_id") val rdgFlapPairEventId: Long? = null,
|
||||||
@SerialName("rdg_flap_qwinsta_event_id") val rdgFlapQwinstaEventId: Long? = null,
|
@SerialName("rdg_flap_qwinsta_event_id") val rdgFlapQwinstaEventId: Long? = null,
|
||||||
|
@SerialName("rdg_access_path") val rdgAccessPath: String? = null,
|
||||||
|
@SerialName("rdg_qwinsta_enabled") val rdgQwinstaEnabled: Boolean = false,
|
||||||
val details: JsonElement? = null,
|
val details: JsonElement? = null,
|
||||||
val raw: JsonElement? = null,
|
val raw: JsonElement? = null,
|
||||||
val payload: JsonElement? = null,
|
val payload: JsonElement? = null,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package ru.kalinamall.seaca.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
private val RdgAccessText = Color(0xFF7EC8E3)
|
||||||
|
private val RdgAccessBackground = Color(0x1A7EC8E3)
|
||||||
|
private val RdgAccessBorder = Color(0x667EC8E3)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RdgAccessBadge(
|
||||||
|
path: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier
|
||||||
|
.border(1.dp, RdgAccessBorder, RoundedCornerShape(4.dp)),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = RdgAccessBackground,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = path,
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
|
color = RdgAccessText,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
letterSpacing = 0.3.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ import ru.kalinamall.seaca.data.SacRepository
|
|||||||
import ru.kalinamall.seaca.ui.util.QwinstaSessionRow
|
import ru.kalinamall.seaca.ui.util.QwinstaSessionRow
|
||||||
import ru.kalinamall.seaca.ui.util.agentCommandError
|
import ru.kalinamall.seaca.ui.util.agentCommandError
|
||||||
import ru.kalinamall.seaca.ui.util.parseQwinstaSessions
|
import ru.kalinamall.seaca.ui.util.parseQwinstaSessions
|
||||||
import ru.kalinamall.seaca.ui.util.rdgFlapQwinstaEventId
|
import ru.kalinamall.seaca.ui.util.rdgQwinstaEventId
|
||||||
|
|
||||||
class RdgQwinstaController(private val repo: SacRepository) {
|
class RdgQwinstaController(private val repo: SacRepository) {
|
||||||
var open by mutableStateOf(false)
|
var open by mutableStateOf(false)
|
||||||
@@ -61,7 +61,7 @@ class RdgQwinstaController(private val repo: SacRepository) {
|
|||||||
private var actorUser: String? = null
|
private var actorUser: String? = null
|
||||||
|
|
||||||
fun openFor(event: EventSummary, scope: kotlinx.coroutines.CoroutineScope) {
|
fun openFor(event: EventSummary, scope: kotlinx.coroutines.CoroutineScope) {
|
||||||
val targetId = rdgFlapQwinstaEventId(event) ?: return
|
val targetId = rdgQwinstaEventId(event) ?: return
|
||||||
targetEventId = targetId
|
targetEventId = targetId
|
||||||
actorUser = event.actorUser
|
actorUser = event.actorUser
|
||||||
open = true
|
open = true
|
||||||
@@ -69,7 +69,7 @@ class RdgQwinstaController(private val repo: SacRepository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun openFor(event: EventDetail, scope: kotlinx.coroutines.CoroutineScope) {
|
fun openFor(event: EventDetail, scope: kotlinx.coroutines.CoroutineScope) {
|
||||||
val targetId = rdgFlapQwinstaEventId(event) ?: return
|
val targetId = rdgQwinstaEventId(event) ?: return
|
||||||
targetEventId = targetId
|
targetEventId = targetId
|
||||||
actorUser = event.actorUser
|
actorUser = event.actorUser
|
||||||
open = true
|
open = true
|
||||||
@@ -191,7 +191,7 @@ fun RdgQwinstaDialog(
|
|||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
Text("qwinsta / logoff", style = MaterialTheme.typography.titleLarge)
|
Text("Оборвать сессию", style = MaterialTheme.typography.titleLarge)
|
||||||
controller.meta?.let {
|
controller.meta?.let {
|
||||||
Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
@@ -329,6 +329,6 @@ fun RdgQwinstaButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(if (controller.loading) "qwinsta…" else "qwinsta")
|
Text(if (controller.loading) "…" else "Оборвать сессию")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ import ru.kalinamall.seaca.ui.components.rememberRdgQwinstaController
|
|||||||
import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton
|
import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton
|
||||||
import ru.kalinamall.seaca.ui.components.HostSessionsSection
|
import ru.kalinamall.seaca.ui.components.HostSessionsSection
|
||||||
import ru.kalinamall.seaca.ui.util.hasRdgFlapUi
|
import ru.kalinamall.seaca.ui.util.hasRdgFlapUi
|
||||||
import ru.kalinamall.seaca.ui.util.rdgFlapQwinstaEventId
|
import ru.kalinamall.seaca.ui.util.rdgQwinstaEventId
|
||||||
import ru.kalinamall.seaca.ui.util.eventSupportsSessionTerminate
|
import ru.kalinamall.seaca.ui.util.eventSupportsSessionTerminate
|
||||||
import ru.kalinamall.seaca.ui.util.sessionTerminateLabel
|
import ru.kalinamall.seaca.ui.util.sessionTerminateLabel
|
||||||
import ru.kalinamall.seaca.push.NotificationHelper
|
import ru.kalinamall.seaca.push.NotificationHelper
|
||||||
@@ -115,16 +115,19 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) {
|
|||||||
|
|
||||||
DetailFrame(title = data?.title ?: "Событие", loading, error, onBack) {
|
DetailFrame(title = data?.title ?: "Событие", loading, error, onBack) {
|
||||||
val d = data ?: return@DetailFrame
|
val d = data ?: return@DetailFrame
|
||||||
|
if (!d.rdgAccessPath.isNullOrBlank()) {
|
||||||
|
Field("Путь RDS", d.rdgAccessPath)
|
||||||
|
}
|
||||||
if (hasRdgFlapUi(d)) {
|
if (hasRdgFlapUi(d)) {
|
||||||
Field("RDG flap", "302→303 — возможна зависшая сессия на ПК пользователя")
|
Field("RDG flap", "302→303 — возможна зависшая сессия на ПК пользователя")
|
||||||
d.rdgFlapPairEventId?.let { Field("Пара", "#$it") }
|
d.rdgFlapPairEventId?.let { Field("Пара", "#$it") }
|
||||||
rdgFlapQwinstaEventId(d)?.let {
|
}
|
||||||
RdgQwinstaButton(
|
rdgQwinstaEventId(d)?.let {
|
||||||
controller = qwinsta,
|
RdgQwinstaButton(
|
||||||
onClick = { qwinsta.openFor(d, scope) },
|
controller = qwinsta,
|
||||||
modifier = Modifier.padding(vertical = 4.dp),
|
onClick = { qwinsta.openFor(d, scope) },
|
||||||
)
|
modifier = Modifier.padding(vertical = 4.dp),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
if (eventSupportsSessionTerminate(d)) {
|
if (eventSupportsSessionTerminate(d)) {
|
||||||
EventSessionTerminateButton(
|
EventSessionTerminateButton(
|
||||||
|
|||||||
@@ -53,6 +53,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.EventSessionTerminateButton
|
import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton
|
||||||
|
import ru.kalinamall.seaca.ui.components.RdgAccessBadge
|
||||||
import ru.kalinamall.seaca.ui.components.RdgFlapBadge
|
import ru.kalinamall.seaca.ui.components.RdgFlapBadge
|
||||||
import ru.kalinamall.seaca.ui.components.RdgQwinstaButton
|
import ru.kalinamall.seaca.ui.components.RdgQwinstaButton
|
||||||
import ru.kalinamall.seaca.ui.components.RdgQwinstaDialog
|
import ru.kalinamall.seaca.ui.components.RdgQwinstaDialog
|
||||||
@@ -60,7 +61,7 @@ import ru.kalinamall.seaca.ui.components.rememberRdgQwinstaController
|
|||||||
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.SacDateTimes
|
||||||
import ru.kalinamall.seaca.ui.util.hasRdgFlapUi
|
import ru.kalinamall.seaca.ui.util.hasRdgFlapUi
|
||||||
import ru.kalinamall.seaca.ui.util.rdgFlapQwinstaEventId
|
import ru.kalinamall.seaca.ui.util.rdgQwinstaEventId
|
||||||
import ru.kalinamall.seaca.ui.util.eventSupportsSessionTerminate
|
import ru.kalinamall.seaca.ui.util.eventSupportsSessionTerminate
|
||||||
import ru.kalinamall.seaca.ui.util.sessionTerminateLabel
|
import ru.kalinamall.seaca.ui.util.sessionTerminateLabel
|
||||||
|
|
||||||
@@ -138,6 +139,9 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
|
|||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
) {
|
) {
|
||||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
if (!e.rdgAccessPath.isNullOrBlank()) {
|
||||||
|
RdgAccessBadge(path = e.rdgAccessPath)
|
||||||
|
}
|
||||||
if (hasRdgFlapUi(e)) {
|
if (hasRdgFlapUi(e)) {
|
||||||
RdgFlapBadge(pairEventId = e.rdgFlapPairEventId)
|
RdgFlapBadge(pairEventId = e.rdgFlapPairEventId)
|
||||||
}
|
}
|
||||||
@@ -146,7 +150,7 @@ fun DashboardScreen(repo: SacRepository, refreshKey: Int = 0) {
|
|||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
if (rdgFlapQwinstaEventId(e) != null) {
|
rdgQwinstaEventId(e)?.let {
|
||||||
RdgQwinstaButton(
|
RdgQwinstaButton(
|
||||||
controller = qwinsta,
|
controller = qwinsta,
|
||||||
onClick = { qwinsta.openFor(e, scope) },
|
onClick = { qwinsta.openFor(e, scope) },
|
||||||
@@ -258,9 +262,10 @@ 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} · ${SacDateTimes.format(it.occurredAt, dateMode)}" },
|
itemSub = { "${it.hostname} · ${SacDateTimes.format(it.occurredAt, dateMode)}" },
|
||||||
|
itemRdgAccessPath = { it.rdgAccessPath },
|
||||||
itemRdgFlap = { hasRdgFlapUi(it) },
|
itemRdgFlap = { hasRdgFlapUi(it) },
|
||||||
itemRdgFlapPairId = { it.rdgFlapPairEventId },
|
itemRdgFlapPairId = { it.rdgFlapPairEventId },
|
||||||
itemQwinsta = { rdgFlapQwinstaEventId(it) != null },
|
itemQwinsta = { rdgQwinstaEventId(it) != null },
|
||||||
onQwinsta = { qwinsta.openFor(it, scope) },
|
onQwinsta = { qwinsta.openFor(it, scope) },
|
||||||
qwinstaLoading = qwinsta.loading,
|
qwinstaLoading = qwinsta.loading,
|
||||||
itemTerminate = { eventSupportsSessionTerminate(it) },
|
itemTerminate = { eventSupportsSessionTerminate(it) },
|
||||||
@@ -406,6 +411,7 @@ private fun <T> PagedListScreen(
|
|||||||
itemSub: (T) -> String,
|
itemSub: (T) -> String,
|
||||||
onOpen: (Long) -> Unit,
|
onOpen: (Long) -> Unit,
|
||||||
severities: List<String?>,
|
severities: List<String?>,
|
||||||
|
itemRdgAccessPath: ((T) -> String?)? = null,
|
||||||
itemRdgFlap: ((T) -> Boolean)? = null,
|
itemRdgFlap: ((T) -> Boolean)? = null,
|
||||||
itemRdgFlapPairId: ((T) -> Long?)? = null,
|
itemRdgFlapPairId: ((T) -> Long?)? = null,
|
||||||
itemQwinsta: ((T) -> Boolean)? = null,
|
itemQwinsta: ((T) -> Boolean)? = null,
|
||||||
@@ -476,6 +482,7 @@ private fun <T> PagedListScreen(
|
|||||||
title = itemLabel(item),
|
title = itemLabel(item),
|
||||||
subtitle = itemSub(item),
|
subtitle = itemSub(item),
|
||||||
onClick = { onOpen(itemKey(item)) },
|
onClick = { onOpen(itemKey(item)) },
|
||||||
|
rdgAccessPath = itemRdgAccessPath?.invoke(item),
|
||||||
rdgFlap = itemRdgFlap?.invoke(item) == true,
|
rdgFlap = itemRdgFlap?.invoke(item) == true,
|
||||||
rdgFlapPairId = itemRdgFlapPairId?.invoke(item),
|
rdgFlapPairId = itemRdgFlapPairId?.invoke(item),
|
||||||
showQwinsta = itemQwinsta?.invoke(item) == true,
|
showQwinsta = itemQwinsta?.invoke(item) == true,
|
||||||
@@ -497,6 +504,7 @@ private fun ListRow(
|
|||||||
title: String,
|
title: String,
|
||||||
subtitle: String,
|
subtitle: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
rdgAccessPath: String? = null,
|
||||||
rdgFlap: Boolean = false,
|
rdgFlap: Boolean = false,
|
||||||
rdgFlapPairId: Long? = null,
|
rdgFlapPairId: Long? = null,
|
||||||
showQwinsta: Boolean = false,
|
showQwinsta: Boolean = false,
|
||||||
@@ -522,6 +530,9 @@ private fun ListRow(
|
|||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
|
if (!rdgAccessPath.isNullOrBlank()) {
|
||||||
|
RdgAccessBadge(path = rdgAccessPath)
|
||||||
|
}
|
||||||
if (rdgFlap) {
|
if (rdgFlap) {
|
||||||
RdgFlapBadge(pairEventId = rdgFlapPairId)
|
RdgFlapBadge(pairEventId = rdgFlapPairId)
|
||||||
}
|
}
|
||||||
@@ -532,7 +543,7 @@ private fun ListRow(
|
|||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
enabled = !qwinstaLoading,
|
enabled = !qwinstaLoading,
|
||||||
onClick = onQwinstaClick,
|
onClick = onQwinstaClick,
|
||||||
) { Text(if (qwinstaLoading) "qwinsta…" else "qwinsta / logoff") }
|
) { Text(if (qwinstaLoading) "…" else "Оборвать сессию") }
|
||||||
}
|
}
|
||||||
if (showTerminate && onTerminateClick != null) {
|
if (showTerminate && onTerminateClick != null) {
|
||||||
EventSessionTerminateButton(
|
EventSessionTerminateButton(
|
||||||
|
|||||||
@@ -13,14 +13,31 @@ data class QwinstaSessionRow(
|
|||||||
fun hasRdgFlapUi(event: EventSummary): Boolean =
|
fun hasRdgFlapUi(event: EventSummary): Boolean =
|
||||||
event.rdgFlap || event.rdgFlapQwinstaEventId != null
|
event.rdgFlap || event.rdgFlapQwinstaEventId != null
|
||||||
|
|
||||||
fun rdgFlapQwinstaEventId(event: EventSummary): Long? =
|
|
||||||
event.rdgFlapQwinstaEventId ?: if (event.rdgFlap) event.id else null
|
|
||||||
|
|
||||||
fun hasRdgFlapUi(event: EventDetail): Boolean =
|
fun hasRdgFlapUi(event: EventDetail): Boolean =
|
||||||
event.rdgFlap || event.rdgFlapQwinstaEventId != null
|
event.rdgFlap || event.rdgFlapQwinstaEventId != null
|
||||||
|
|
||||||
fun rdgFlapQwinstaEventId(event: EventDetail): Long? =
|
fun hasRdgAccessPath(event: EventSummary): Boolean =
|
||||||
event.rdgFlapQwinstaEventId ?: if (event.rdgFlap) event.id else null
|
!event.rdgAccessPath.isNullOrBlank()
|
||||||
|
|
||||||
|
fun hasRdgAccessPath(event: EventDetail): Boolean =
|
||||||
|
!event.rdgAccessPath.isNullOrBlank()
|
||||||
|
|
||||||
|
/** Как SAC rdgQwinstaEventId: кнопка только при rdg_qwinsta_enabled от API. */
|
||||||
|
fun rdgQwinstaEventId(event: EventSummary): Long? {
|
||||||
|
if (!event.rdgQwinstaEnabled) return null
|
||||||
|
return event.rdgFlapQwinstaEventId ?: event.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rdgQwinstaEventId(event: EventDetail): Long? {
|
||||||
|
if (!event.rdgQwinstaEnabled) return null
|
||||||
|
return event.rdgFlapQwinstaEventId ?: event.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use rdgQwinstaEventId */
|
||||||
|
fun rdgFlapQwinstaEventId(event: EventSummary): Long? = rdgQwinstaEventId(event)
|
||||||
|
|
||||||
|
/** @deprecated use rdgQwinstaEventId */
|
||||||
|
fun rdgFlapQwinstaEventId(event: EventDetail): Long? = rdgQwinstaEventId(event)
|
||||||
|
|
||||||
fun parseQwinstaSessions(stdout: String, actorUser: String?): List<QwinstaSessionRow> {
|
fun parseQwinstaSessions(stdout: String, actorUser: String?): List<QwinstaSessionRow> {
|
||||||
val lines = stdout.lines().map { it.trim() }.filter { it.isNotEmpty() }
|
val lines = stdout.lines().map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
|
|||||||
Reference in New Issue
Block a user