feat: host sessions and event terminate actions (0.5.13)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -45,6 +45,21 @@ interface SacApi {
|
||||
@Body body: LogoffActionRequest,
|
||||
): AgentCommandResponse
|
||||
|
||||
@POST("api/v1/events/{id}/actions/terminate-session")
|
||||
suspend fun postEventTerminateSession(
|
||||
@Path("id") id: Long,
|
||||
@Body body: EventSessionTerminateRequest = EventSessionTerminateRequest(),
|
||||
): SessionActionResponse
|
||||
|
||||
@POST("api/v1/hosts/{id}/actions/sessions/list")
|
||||
suspend fun postHostSessionsList(@Path("id") id: Long): HostSessionsResponse
|
||||
|
||||
@POST("api/v1/hosts/{id}/actions/sessions/terminate")
|
||||
suspend fun postHostSessionTerminate(
|
||||
@Path("id") id: Long,
|
||||
@Body body: HostSessionTerminateRequest,
|
||||
): SessionActionResponse
|
||||
|
||||
@GET("api/v1/problems")
|
||||
suspend fun listProblems(
|
||||
@Query("page") page: Int = 1,
|
||||
|
||||
@@ -204,6 +204,43 @@ data class LogoffActionRequest(
|
||||
@SerialName("session_id") val sessionId: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HostSessionItem(
|
||||
@SerialName("session_id") val sessionId: String,
|
||||
val user: String,
|
||||
val tty: String? = null,
|
||||
val state: String? = null,
|
||||
@SerialName("source_ip") val sourceIp: String? = null,
|
||||
@SerialName("session_name") val sessionName: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HostSessionsResponse(
|
||||
val ok: Boolean,
|
||||
val message: String,
|
||||
val target: String? = null,
|
||||
val sessions: List<HostSessionItem> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HostSessionTerminateRequest(
|
||||
@SerialName("session_id") val sessionId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EventSessionTerminateRequest(
|
||||
@SerialName("session_id") val sessionId: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionActionResponse(
|
||||
val ok: Boolean,
|
||||
val message: String,
|
||||
val target: String? = null,
|
||||
val stdout: String? = null,
|
||||
val stderr: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AgentCommandResponse(
|
||||
@SerialName("command_uuid") val commandUuid: String,
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
package ru.kalinamall.seaca.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import ru.kalinamall.seaca.data.HostSessionItem
|
||||
import ru.kalinamall.seaca.data.HostSessionTerminateRequest
|
||||
import ru.kalinamall.seaca.data.SacRepository
|
||||
|
||||
@Composable
|
||||
fun HostSessionsSection(
|
||||
repo: SacRepository,
|
||||
hostId: Long,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var loading by remember(hostId) { mutableStateOf(false) }
|
||||
var loaded by remember(hostId) { mutableStateOf(false) }
|
||||
var sessions by remember(hostId) { mutableStateOf<List<HostSessionItem>>(emptyList()) }
|
||||
var error by remember(hostId) { mutableStateOf<String?>(null) }
|
||||
var message by remember(hostId) { mutableStateOf<String?>(null) }
|
||||
var messageOk by remember(hostId) { mutableStateOf(true) }
|
||||
var terminatingId by remember(hostId) { mutableStateOf<String?>(null) }
|
||||
var confirmSession by remember(hostId) { mutableStateOf<HostSessionItem?>(null) }
|
||||
|
||||
fun load() {
|
||||
scope.launch {
|
||||
loading = true
|
||||
error = null
|
||||
message = null
|
||||
try {
|
||||
val res = repo.withAuth { it.postHostSessionsList(hostId) }
|
||||
if (!res.ok) {
|
||||
error = res.message
|
||||
sessions = emptyList()
|
||||
} else {
|
||||
sessions = res.sessions
|
||||
loaded = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
error = if (e is HttpException) SacRepository.parseError(e) else e.message
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun terminate(sessionId: String) {
|
||||
scope.launch {
|
||||
terminatingId = sessionId
|
||||
message = null
|
||||
try {
|
||||
val res = repo.withAuth {
|
||||
it.postHostSessionTerminate(hostId, HostSessionTerminateRequest(sessionId))
|
||||
}
|
||||
messageOk = res.ok
|
||||
message = res.message
|
||||
if (res.ok) {
|
||||
sessions = sessions.filter { it.sessionId != sessionId }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
messageOk = false
|
||||
message = if (e is HttpException) SacRepository.parseError(e) else e.message
|
||||
} finally {
|
||||
terminatingId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
confirmSession?.let { session ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirmSession = null },
|
||||
title = { Text("Завершить сессию?") },
|
||||
text = { Text("Завершить сессию ${session.sessionId} (${session.user})?") },
|
||||
confirmButton = {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
onClick = {
|
||||
val sid = session.sessionId
|
||||
confirmSession = null
|
||||
terminate(sid)
|
||||
},
|
||||
) { Text("Завершить") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { confirmSession = null }) { Text("Отмена") }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
"Залогиненные пользователи",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
"Linux: loginctl, Windows: qwinsta. Нужны учётные данные admin в настройках SAC.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
error?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
if (loaded) {
|
||||
if (sessions.isEmpty()) {
|
||||
Text(
|
||||
"Активных сессий не найдено.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
sessions.forEach { session ->
|
||||
HostSessionRow(
|
||||
session = session,
|
||||
terminating = terminatingId == session.sessionId,
|
||||
enabled = terminatingId == null && !loading,
|
||||
onTerminate = { confirmSession = session },
|
||||
)
|
||||
}
|
||||
}
|
||||
message?.let {
|
||||
Text(
|
||||
it,
|
||||
color = if (messageOk) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
enabled = !loading,
|
||||
onClick = { load() },
|
||||
) {
|
||||
if (loading) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
Text("Загрузка…")
|
||||
}
|
||||
} else {
|
||||
Text(if (loaded) "Обновить" else "Показать пользователей")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HostSessionRow(
|
||||
session: HostSessionItem,
|
||||
terminating: Boolean,
|
||||
enabled: Boolean,
|
||||
onTerminate: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(session.user, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
buildString {
|
||||
append("ID ${session.sessionId}")
|
||||
session.tty?.takeIf { it.isNotBlank() }?.let { append(" · $it") }
|
||||
session.sessionName?.takeIf { it.isNotBlank() }?.let { append(" · $it") }
|
||||
session.state?.takeIf { it.isNotBlank() }?.let { append(" · $it") }
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (terminating) {
|
||||
CircularProgressIndicator(modifier = Modifier.padding(start = 8.dp))
|
||||
} else {
|
||||
Button(
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
onClick = onTerminate,
|
||||
) { Text("Завершить") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EventSessionTerminateButton(
|
||||
enabled: Boolean,
|
||||
loading: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OutlinedButton(
|
||||
enabled = enabled && !loading,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error,
|
||||
),
|
||||
) {
|
||||
Text(if (loading) "…" else "Завершить")
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.Card
|
||||
@@ -48,6 +49,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import retrofit2.HttpException
|
||||
import ru.kalinamall.seaca.R
|
||||
import ru.kalinamall.seaca.data.EventSummary
|
||||
import ru.kalinamall.seaca.data.EventDetail
|
||||
@@ -63,8 +65,12 @@ import ru.kalinamall.seaca.data.SacRepository
|
||||
import ru.kalinamall.seaca.ui.components.RdgQwinstaButton
|
||||
import ru.kalinamall.seaca.ui.components.RdgQwinstaDialog
|
||||
import ru.kalinamall.seaca.ui.components.rememberRdgQwinstaController
|
||||
import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton
|
||||
import ru.kalinamall.seaca.ui.components.HostSessionsSection
|
||||
import ru.kalinamall.seaca.ui.util.hasRdgFlapUi
|
||||
import ru.kalinamall.seaca.ui.util.rdgFlapQwinstaEventId
|
||||
import ru.kalinamall.seaca.ui.util.eventSupportsSessionTerminate
|
||||
import ru.kalinamall.seaca.ui.util.sessionTerminateLabel
|
||||
import ru.kalinamall.seaca.push.NotificationHelper
|
||||
import ru.kalinamall.seaca.push.NotificationSoundResolver
|
||||
import ru.kalinamall.seaca.ui.RingtonePicker
|
||||
@@ -89,6 +95,9 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) {
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val qwinsta = rememberRdgQwinstaController(repo)
|
||||
var terminateLoading by remember { mutableStateOf(false) }
|
||||
var terminateConfirm by remember { mutableStateOf(false) }
|
||||
var terminateMessage by remember { mutableStateOf<String?>(null) }
|
||||
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||
initialValue = DateTimeDisplayMode.SLASH,
|
||||
)
|
||||
@@ -117,6 +126,17 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) {
|
||||
)
|
||||
}
|
||||
}
|
||||
if (eventSupportsSessionTerminate(d)) {
|
||||
EventSessionTerminateButton(
|
||||
enabled = !terminateLoading,
|
||||
loading = terminateLoading,
|
||||
onClick = { terminateConfirm = true },
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
)
|
||||
terminateMessage?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
Field("Severity", d.severity)
|
||||
Field(
|
||||
"Тип",
|
||||
@@ -133,6 +153,49 @@ fun EventDetailScreen(repo: SacRepository, id: Long, onBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
if (terminateConfirm) {
|
||||
val d = data
|
||||
AlertDialog(
|
||||
onDismissRequest = { terminateConfirm = false },
|
||||
title = { Text("Завершить сессию?") },
|
||||
text = {
|
||||
Text(
|
||||
if (d != null) {
|
||||
"Завершить сессию пользователя ${sessionTerminateLabel(d)}?"
|
||||
} else {
|
||||
"Завершить сессию пользователя?"
|
||||
},
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
onClick = {
|
||||
terminateConfirm = false
|
||||
scope.launch {
|
||||
terminateLoading = true
|
||||
terminateMessage = null
|
||||
try {
|
||||
val res = repo.withAuth { it.postEventTerminateSession(id) }
|
||||
if (!res.ok) terminateMessage = res.message
|
||||
} catch (e: Exception) {
|
||||
terminateMessage = if (e is HttpException) SacRepository.parseError(e) else e.message
|
||||
} finally {
|
||||
terminateLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
) { Text("Завершить") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { terminateConfirm = false }) { Text("Отмена") }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
RdgQwinstaDialog(qwinsta)
|
||||
}
|
||||
|
||||
@@ -316,7 +379,7 @@ fun HostDetailScreen(
|
||||
}
|
||||
}
|
||||
when (HostDetailTab.entries[selectedTab]) {
|
||||
HostDetailTab.Info -> HostInfoContent(host, dateMode)
|
||||
HostDetailTab.Info -> HostInfoContent(host, repo, dateMode)
|
||||
HostDetailTab.Events -> HostEventsContent(
|
||||
repo = repo,
|
||||
hostname = host.hostname,
|
||||
@@ -328,7 +391,7 @@ fun HostDetailScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HostInfoContent(host: HostDetail, dateMode: DateTimeDisplayMode) {
|
||||
private fun HostInfoContent(host: HostDetail, repo: SacRepository, dateMode: DateTimeDisplayMode) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -341,6 +404,7 @@ private fun HostInfoContent(host: HostDetail, dateMode: DateTimeDisplayMode) {
|
||||
Field("ОС", "${host.osFamily} ${host.osVersion.orEmpty()}".trim())
|
||||
Field("Агент", host.agentStatus)
|
||||
Field("Последний контакт", SacDateTimes.format(host.lastSeenAt, dateMode))
|
||||
HostSessionsSection(repo = repo, hostId = host.id)
|
||||
host.inventory?.let { inv ->
|
||||
val lines = inventoryFieldLines(inv)
|
||||
if (lines.isEmpty()) {
|
||||
|
||||
@@ -21,6 +21,10 @@ import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
@@ -39,6 +43,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import ru.kalinamall.seaca.data.DateTimeDisplayMode
|
||||
import ru.kalinamall.seaca.data.DashboardSummary
|
||||
import ru.kalinamall.seaca.data.EventListResponse
|
||||
@@ -47,6 +52,7 @@ import ru.kalinamall.seaca.data.HostSummary
|
||||
import ru.kalinamall.seaca.data.ProblemSummary
|
||||
import ru.kalinamall.seaca.data.SacRepository
|
||||
import ru.kalinamall.seaca.ui.components.BackArrowButton
|
||||
import ru.kalinamall.seaca.ui.components.EventSessionTerminateButton
|
||||
import ru.kalinamall.seaca.ui.components.RdgFlapBadge
|
||||
import ru.kalinamall.seaca.ui.components.RdgQwinstaButton
|
||||
import ru.kalinamall.seaca.ui.components.RdgQwinstaDialog
|
||||
@@ -55,6 +61,8 @@ import ru.kalinamall.seaca.ui.components.ForwardArrowButton
|
||||
import ru.kalinamall.seaca.ui.util.SacDateTimes
|
||||
import ru.kalinamall.seaca.ui.util.hasRdgFlapUi
|
||||
import ru.kalinamall.seaca.ui.util.rdgFlapQwinstaEventId
|
||||
import ru.kalinamall.seaca.ui.util.eventSupportsSessionTerminate
|
||||
import ru.kalinamall.seaca.ui.util.sessionTerminateLabel
|
||||
|
||||
private val REPORT_TYPE_OPTIONS = listOf(
|
||||
"report.daily.ssh" to "Отчёты ssh клиентов",
|
||||
@@ -192,10 +200,48 @@ fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
|
||||
var hostname by remember { mutableStateOf("") }
|
||||
val scope = rememberCoroutineScope()
|
||||
val qwinsta = rememberRdgQwinstaController(repo)
|
||||
var terminateLoadingId by remember { mutableStateOf<Long?>(null) }
|
||||
var terminateConfirm by remember { mutableStateOf<EventSummary?>(null) }
|
||||
var terminateError by remember { mutableStateOf<String?>(null) }
|
||||
val dateMode by repo.session.dateTimeDisplayMode.collectAsStateWithLifecycle(
|
||||
initialValue = DateTimeDisplayMode.SLASH,
|
||||
)
|
||||
|
||||
terminateConfirm?.let { event ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { terminateConfirm = null },
|
||||
title = { Text("Завершить сессию?") },
|
||||
text = { Text("Завершить сессию пользователя ${sessionTerminateLabel(event)}?") },
|
||||
confirmButton = {
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
onClick = {
|
||||
val target = event
|
||||
terminateConfirm = null
|
||||
scope.launch {
|
||||
terminateLoadingId = target.id
|
||||
terminateError = null
|
||||
try {
|
||||
val res = repo.withAuth { it.postEventTerminateSession(target.id) }
|
||||
if (!res.ok) terminateError = res.message
|
||||
} catch (e: Exception) {
|
||||
terminateError = if (e is HttpException) SacRepository.parseError(e) else e.message
|
||||
} finally {
|
||||
terminateLoadingId = null
|
||||
}
|
||||
}
|
||||
},
|
||||
) { Text("Завершить") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { terminateConfirm = null }) { Text("Отмена") }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
PagedListScreen(
|
||||
title = "События",
|
||||
loadPage = { page, severity, host ->
|
||||
@@ -217,10 +263,16 @@ fun EventsScreen(repo: SacRepository, onOpen: (Long) -> Unit) {
|
||||
itemQwinsta = { rdgFlapQwinstaEventId(it) != null },
|
||||
onQwinsta = { qwinsta.openFor(it, scope) },
|
||||
qwinstaLoading = qwinsta.loading,
|
||||
itemTerminate = { eventSupportsSessionTerminate(it) },
|
||||
onTerminate = { terminateConfirm = it },
|
||||
terminateLoadingId = terminateLoadingId,
|
||||
onOpen = onOpen,
|
||||
severities = listOf(null, "info", "warning", "high", "critical"),
|
||||
hostnameFilter = hostname,
|
||||
headerContent = {
|
||||
terminateError?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = hostname,
|
||||
onValueChange = { hostname = it },
|
||||
@@ -359,6 +411,9 @@ private fun <T> PagedListScreen(
|
||||
itemQwinsta: ((T) -> Boolean)? = null,
|
||||
onQwinsta: ((T) -> Unit)? = null,
|
||||
qwinstaLoading: Boolean = false,
|
||||
itemTerminate: ((T) -> Boolean)? = null,
|
||||
onTerminate: ((T) -> Unit)? = null,
|
||||
terminateLoadingId: Long? = null,
|
||||
filterLabel: String = "Severity",
|
||||
hostnameFilter: String = "",
|
||||
headerContent: @Composable () -> Unit = {},
|
||||
@@ -426,6 +481,9 @@ private fun <T> PagedListScreen(
|
||||
showQwinsta = itemQwinsta?.invoke(item) == true,
|
||||
onQwinstaClick = onQwinsta?.let { handler -> { handler(item) } },
|
||||
qwinstaLoading = qwinstaLoading,
|
||||
showTerminate = itemTerminate?.invoke(item) == true,
|
||||
onTerminateClick = onTerminate?.let { handler -> { handler(item) } },
|
||||
terminateLoading = terminateLoadingId == itemKey(item),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -444,6 +502,9 @@ private fun ListRow(
|
||||
showQwinsta: Boolean = false,
|
||||
onQwinstaClick: (() -> Unit)? = null,
|
||||
qwinstaLoading: Boolean = false,
|
||||
showTerminate: Boolean = false,
|
||||
onTerminateClick: (() -> Unit)? = null,
|
||||
terminateLoading: Boolean = false,
|
||||
) {
|
||||
Card(
|
||||
Modifier
|
||||
@@ -473,6 +534,13 @@ private fun ListRow(
|
||||
onClick = onQwinstaClick,
|
||||
) { Text(if (qwinstaLoading) "qwinsta…" else "qwinsta / logoff") }
|
||||
}
|
||||
if (showTerminate && onTerminateClick != null) {
|
||||
EventSessionTerminateButton(
|
||||
enabled = !terminateLoading,
|
||||
loading = terminateLoading,
|
||||
onClick = onTerminateClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.kalinamall.seaca.ui.util
|
||||
|
||||
import ru.kalinamall.seaca.data.EventDetail
|
||||
import ru.kalinamall.seaca.data.EventSummary
|
||||
|
||||
private val LINUX_SESSION_EVENT_TYPES = setOf(
|
||||
"session.logind.new",
|
||||
"ssh.login.success",
|
||||
"privilege.sudo.command",
|
||||
)
|
||||
|
||||
private val WINDOWS_SESSION_EVENT_TYPES = setOf("rdp.login.success")
|
||||
|
||||
fun eventSupportsSessionTerminate(event: EventSummary): Boolean =
|
||||
event.type in LINUX_SESSION_EVENT_TYPES || event.type in WINDOWS_SESSION_EVENT_TYPES
|
||||
|
||||
fun eventSupportsSessionTerminate(event: EventDetail): Boolean =
|
||||
event.type in LINUX_SESSION_EVENT_TYPES || event.type in WINDOWS_SESSION_EVENT_TYPES
|
||||
|
||||
fun sessionTerminateLabel(event: EventSummary): String =
|
||||
event.actorUser?.takeIf { it.isNotBlank() } ?: event.title
|
||||
|
||||
fun sessionTerminateLabel(event: EventDetail): String =
|
||||
event.actorUser?.takeIf { it.isNotBlank() } ?: event.title
|
||||
Reference in New Issue
Block a user