feat: qwinsta/logoff buttons on events list and detail (0.20.4)
RDG flap actions were only on the dashboard; add shared composable and modal so operators can reset stuck sessions from События and event detail.
This commit is contained in:
@@ -303,50 +303,11 @@
|
||||
|
||||
</table>
|
||||
|
||||
<div v-if="qwinstaModal.open" class="modal-backdrop" @click.self="closeQwinstaModal">
|
||||
<div class="modal-card qwinsta-modal" role="dialog" aria-modal="true">
|
||||
<h3>qwinsta — {{ qwinstaModal.clientHostname || qwinstaModal.target || `#${qwinstaModal.eventId}` }}</h3>
|
||||
<p v-if="qwinstaModal.internalIp" class="muted qwinsta-meta">Клиентский ПК: {{ qwinstaModal.internalIp }}</p>
|
||||
<p v-if="qwinstaModal.error" class="error">{{ qwinstaModal.error }}</p>
|
||||
<p v-else-if="qwinstaModal.loading">Ожидание ответа агента…</p>
|
||||
<template v-else>
|
||||
<pre v-if="qwinstaModal.stdout" class="qwinsta-raw">{{ qwinstaModal.stdout }}</pre>
|
||||
<p v-else class="muted">Нет вывода</p>
|
||||
<table v-if="qwinstaModal.sessions.length" class="dash-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>SESSION</th>
|
||||
<th>USER</th>
|
||||
<th>ID</th>
|
||||
<th>STATE</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="s in qwinstaModal.sessions" :key="s.id">
|
||||
<td>{{ s.sessionName }}</td>
|
||||
<td>{{ s.userName }}</td>
|
||||
<td>{{ s.id }}</td>
|
||||
<td>{{ s.state }}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="danger"
|
||||
:disabled="qwinstaModal.logoffId === s.id"
|
||||
@click="runLogoff(s.id)"
|
||||
>
|
||||
logoff
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="secondary" @click="closeQwinstaModal">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RdgQwinstaModal
|
||||
:modal="qwinstaModal"
|
||||
@close="closeQwinstaModal"
|
||||
@logoff="runLogoff"
|
||||
/>
|
||||
|
||||
<div class="filters" style="margin-top: 1rem">
|
||||
|
||||
@@ -368,20 +329,17 @@ import { onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
import {
|
||||
apiFetch,
|
||||
fetchEventAction,
|
||||
getToken,
|
||||
postEventLogoff,
|
||||
postEventQwinsta,
|
||||
type AgentCommandResponse,
|
||||
type DashboardSummary,
|
||||
type EventSummary,
|
||||
} from "../api";
|
||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||
import { useSacVersion } from "../composables/useSacVersion";
|
||||
import { formatServerName } from "../utils/hostDisplay";
|
||||
|
||||
const { appName, appVersion } = useSacVersion();
|
||||
|
||||
|
||||
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
|
||||
|
||||
const data = ref<DashboardSummary | null>(null);
|
||||
|
||||
@@ -397,174 +355,6 @@ let eventSource: EventSource | null = null;
|
||||
|
||||
let refreshingRecent = false;
|
||||
|
||||
let qwinstaPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const qwinstaLoadingId = ref<number | null>(null);
|
||||
|
||||
type QwinstaSessionRow = {
|
||||
sessionName: string;
|
||||
userName: string;
|
||||
id: number;
|
||||
state: string;
|
||||
};
|
||||
|
||||
const qwinstaModal = ref({
|
||||
open: false,
|
||||
eventId: 0,
|
||||
actorUser: "",
|
||||
commandUuid: "",
|
||||
target: "",
|
||||
clientHostname: "",
|
||||
internalIp: "",
|
||||
loading: false,
|
||||
error: "",
|
||||
stdout: "",
|
||||
sessions: [] as QwinstaSessionRow[],
|
||||
logoffId: null as number | null,
|
||||
});
|
||||
|
||||
function parseQwinstaSessions(stdout: string, actorUser: string): QwinstaSessionRow[] {
|
||||
const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
||||
const rows: QwinstaSessionRow[] = [];
|
||||
for (const line of lines) {
|
||||
if (/^SESSION/i.test(line) || /^---/.test(line)) continue;
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 4) continue;
|
||||
const sessionName = parts[0];
|
||||
const userName = parts[1];
|
||||
const id = Number.parseInt(parts[2], 10);
|
||||
const state = parts.slice(3).join(" ");
|
||||
if (!Number.isFinite(id)) continue;
|
||||
const norm = (s: string) => s.replace(/^B26\\/i, "").toLowerCase();
|
||||
const matchUser = actorUser && norm(userName).includes(norm(actorUser));
|
||||
if (actorUser && !matchUser) continue;
|
||||
rows.push({ sessionName, userName, id, state });
|
||||
}
|
||||
if (rows.length === 0 && stdout.trim()) {
|
||||
for (const line of lines) {
|
||||
if (/^SESSION/i.test(line) || /^---/.test(line)) continue;
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 4) continue;
|
||||
const id = Number.parseInt(parts[2], 10);
|
||||
if (!Number.isFinite(id)) continue;
|
||||
rows.push({
|
||||
sessionName: parts[0],
|
||||
userName: parts[1],
|
||||
id,
|
||||
state: parts.slice(3).join(" "),
|
||||
});
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function stopQwinstaPoll() {
|
||||
if (qwinstaPollTimer) {
|
||||
clearInterval(qwinstaPollTimer);
|
||||
qwinstaPollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function closeQwinstaModal() {
|
||||
stopQwinstaPoll();
|
||||
qwinstaModal.value.open = false;
|
||||
}
|
||||
|
||||
function applyCommandResult(eventId: number, actorUser: string, cmd: AgentCommandResponse) {
|
||||
if (cmd.status === "pending") return false;
|
||||
qwinstaModal.value.loading = false;
|
||||
if (cmd.status === "failed") {
|
||||
qwinstaModal.value.error = cmd.result_stderr || cmd.result_stdout || "Команда завершилась с ошибкой";
|
||||
return true;
|
||||
}
|
||||
qwinstaModal.value.stdout = cmd.result_stdout || "";
|
||||
qwinstaModal.value.sessions = parseQwinstaSessions(qwinstaModal.value.stdout, actorUser);
|
||||
return true;
|
||||
}
|
||||
|
||||
function pollQwinstaCommand(eventId: number, commandUuid: string, actorUser: string) {
|
||||
stopQwinstaPoll();
|
||||
qwinstaPollTimer = setInterval(async () => {
|
||||
try {
|
||||
const cmd = await fetchEventAction(eventId, commandUuid);
|
||||
if (applyCommandResult(eventId, actorUser, cmd)) {
|
||||
stopQwinstaPoll();
|
||||
}
|
||||
} catch (e) {
|
||||
qwinstaModal.value.loading = false;
|
||||
qwinstaModal.value.error = e instanceof Error ? e.message : "Ошибка опроса команды";
|
||||
stopQwinstaPoll();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function applyCommandMeta(modal: typeof qwinstaModal.value, cmd: AgentCommandResponse) {
|
||||
modal.target = cmd.target || "";
|
||||
modal.clientHostname = cmd.client_hostname || "";
|
||||
modal.internalIp = cmd.internal_ip || "";
|
||||
}
|
||||
|
||||
async function runQwinsta(event: EventSummary) {
|
||||
qwinstaLoadingId.value = event.id;
|
||||
qwinstaModal.value = {
|
||||
open: true,
|
||||
eventId: event.id,
|
||||
actorUser: event.actor_user || "",
|
||||
commandUuid: "",
|
||||
target: "",
|
||||
clientHostname: "",
|
||||
internalIp: "",
|
||||
loading: true,
|
||||
error: "",
|
||||
stdout: "",
|
||||
sessions: [],
|
||||
logoffId: null,
|
||||
};
|
||||
try {
|
||||
const cmd = await postEventQwinsta(event.id);
|
||||
applyCommandMeta(qwinstaModal.value, cmd);
|
||||
qwinstaModal.value.commandUuid = cmd.command_uuid;
|
||||
if (applyCommandResult(event.id, event.actor_user || "", cmd)) {
|
||||
return;
|
||||
}
|
||||
pollQwinstaCommand(event.id, cmd.command_uuid, event.actor_user || "");
|
||||
} catch (e) {
|
||||
qwinstaModal.value.loading = false;
|
||||
qwinstaModal.value.error = e instanceof Error ? e.message : "Не удалось отправить qwinsta";
|
||||
} finally {
|
||||
qwinstaLoadingId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function runLogoff(sessionId: number) {
|
||||
const modal = qwinstaModal.value;
|
||||
if (!modal.eventId) return;
|
||||
if (!window.confirm(`Завершить сеанс ID ${sessionId}?`)) return;
|
||||
modal.logoffId = sessionId;
|
||||
modal.error = "";
|
||||
try {
|
||||
const cmd = await postEventLogoff(modal.eventId, sessionId);
|
||||
applyCommandMeta(modal, cmd);
|
||||
if (cmd.status === "failed") {
|
||||
modal.error = cmd.result_stderr || cmd.result_stdout || "logoff failed";
|
||||
return;
|
||||
}
|
||||
modal.loading = true;
|
||||
modal.stdout = "";
|
||||
modal.sessions = [];
|
||||
const again = await postEventQwinsta(modal.eventId);
|
||||
applyCommandMeta(modal, again);
|
||||
if (applyCommandResult(modal.eventId, modal.actorUser, again)) {
|
||||
return;
|
||||
}
|
||||
pollQwinstaCommand(modal.eventId, again.command_uuid, modal.actorUser);
|
||||
} catch (e) {
|
||||
modal.error = e instanceof Error ? e.message : "logoff error";
|
||||
} finally {
|
||||
modal.logoffId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDt(iso: string) {
|
||||
|
||||
return new Date(iso).toLocaleString("ru-RU");
|
||||
@@ -790,13 +580,8 @@ onMounted(() => {
|
||||
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
stopQwinstaPoll();
|
||||
|
||||
eventSource?.close();
|
||||
|
||||
eventSource = null;
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -836,49 +621,6 @@ onUnmounted(() => {
|
||||
padding: 0.2rem 0.55rem;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
background: var(--card-bg, #1e2430);
|
||||
border: 1px solid var(--border, #3a4556);
|
||||
border-radius: 8px;
|
||||
max-width: 720px;
|
||||
width: 100%;
|
||||
max-height: 85vh;
|
||||
overflow: auto;
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.qwinsta-meta {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.qwinsta-raw {
|
||||
font-size: 0.8rem;
|
||||
max-height: 12rem;
|
||||
overflow: auto;
|
||||
background: #0d1117;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user