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:
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div v-if="modal.open" class="modal-backdrop" @click.self="emit('close')">
|
||||
<div class="modal-card qwinsta-modal" role="dialog" aria-modal="true">
|
||||
<h3>qwinsta — {{ modal.clientHostname || modal.target || `#${modal.eventId}` }}</h3>
|
||||
<p v-if="modal.internalIp" class="muted qwinsta-meta">Клиентский ПК: {{ modal.internalIp }}</p>
|
||||
<p v-if="modal.error" class="error">{{ modal.error }}</p>
|
||||
<p v-else-if="modal.loading">Ожидание ответа агента…</p>
|
||||
<template v-else>
|
||||
<pre v-if="modal.stdout" class="qwinsta-raw">{{ modal.stdout }}</pre>
|
||||
<p v-else class="muted">Нет вывода</p>
|
||||
<table v-if="modal.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 modal.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="modal.logoffId === s.id"
|
||||
@click="emit('logoff', s.id)"
|
||||
>
|
||||
logoff
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="secondary" @click="emit('close')">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { QwinstaSessionRow } from "../composables/useRdgQwinsta";
|
||||
|
||||
defineProps<{
|
||||
modal: {
|
||||
open: boolean;
|
||||
eventId: number;
|
||||
clientHostname: string;
|
||||
target: string;
|
||||
internalIp: string;
|
||||
loading: boolean;
|
||||
error: string;
|
||||
stdout: string;
|
||||
sessions: QwinstaSessionRow[];
|
||||
logoffId: number | null;
|
||||
};
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
logoff: [sessionId: number];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
white-space: pre-wrap;
|
||||
background: #0d1117;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import {
|
||||
fetchEventAction,
|
||||
postEventLogoff,
|
||||
postEventQwinsta,
|
||||
type AgentCommandResponse,
|
||||
type EventSummary,
|
||||
} from "../api";
|
||||
|
||||
export type QwinstaSessionRow = {
|
||||
sessionName: string;
|
||||
userName: string;
|
||||
id: number;
|
||||
state: string;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function useRdgQwinsta() {
|
||||
let qwinstaPollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const qwinstaLoadingId = ref<number | null>(null);
|
||||
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 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 applyCommandMeta(modal: typeof qwinstaModal.value, cmd: AgentCommandResponse) {
|
||||
modal.target = cmd.target || "";
|
||||
modal.clientHostname = cmd.client_hostname || "";
|
||||
modal.internalIp = cmd.internal_ip || "";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => stopQwinstaPoll());
|
||||
|
||||
return {
|
||||
qwinstaLoadingId,
|
||||
qwinstaModal,
|
||||
closeQwinstaModal,
|
||||
runQwinsta,
|
||||
runLogoff,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||
export const APP_NAME = "Security Alert Center";
|
||||
export const APP_VERSION = "0.20.1";
|
||||
export const APP_VERSION = "0.20.4";
|
||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,21 @@
|
||||
<p v-else-if="loading">Загрузка…</p>
|
||||
<template v-else-if="event">
|
||||
<h1>{{ event.title }}</h1>
|
||||
<p v-if="event.rdg_flap" class="event-rdg-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
:disabled="qwinstaLoadingId === event.id"
|
||||
@click="runQwinsta(event)"
|
||||
>
|
||||
qwinsta (RDG flap)
|
||||
</button>
|
||||
</p>
|
||||
<RdgQwinstaModal
|
||||
:modal="qwinstaModal"
|
||||
@close="closeQwinstaModal"
|
||||
@logoff="runLogoff"
|
||||
/>
|
||||
<div class="card report-meta">
|
||||
<dl class="detail-fields">
|
||||
<div class="detail-field">
|
||||
@@ -74,7 +89,9 @@
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { apiFetch, type EventDetail } from "../api";
|
||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||
import ReportBodyCard from "../components/ReportBodyCard.vue";
|
||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||
import { eventsBackQueryFromDetail } from "../utils/eventsListQuery";
|
||||
import { dailyReportTypeLabel, isDailyReportType } from "../utils/reportDisplay";
|
||||
|
||||
@@ -84,6 +101,7 @@ const route = useRoute();
|
||||
const event = ref<EventDetail | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
|
||||
|
||||
const fromReports = computed(() => route.query.from === "reports");
|
||||
const isReport = computed(() => (event.value ? isDailyReportType(event.value.type) : false));
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<th>Severity</th>
|
||||
<th>Type</th>
|
||||
<th>Title</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -53,9 +54,25 @@
|
||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||
<td><code>{{ e.type }}</code></td>
|
||||
<td>{{ e.title }}</td>
|
||||
<td class="events-actions">
|
||||
<button
|
||||
v-if="e.rdg_flap"
|
||||
type="button"
|
||||
class="secondary events-qwinsta-btn"
|
||||
:disabled="qwinstaLoadingId === e.id"
|
||||
@click="runQwinsta(e)"
|
||||
>
|
||||
qwinsta
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<RdgQwinstaModal
|
||||
:modal="qwinstaModal"
|
||||
@close="closeQwinstaModal"
|
||||
@logoff="runLogoff"
|
||||
/>
|
||||
<div class="filters" style="margin-top: 1rem">
|
||||
<button type="button" class="secondary" :disabled="page <= 1" @click="load(page - 1)">Назад</button>
|
||||
<span>Стр. {{ page }}</span>
|
||||
@@ -75,6 +92,8 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { apiFetch, type EventListResponse } from "../api";
|
||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||
import {
|
||||
apiParamsFromState,
|
||||
buildEventsListQuery,
|
||||
@@ -86,6 +105,7 @@ import { formatServerName } from "../utils/hostDisplay";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
|
||||
|
||||
const data = ref<EventListResponse | null>(null);
|
||||
const loading = ref(false);
|
||||
@@ -217,4 +237,13 @@ watch(
|
||||
.events-date-field input[type="date"] {
|
||||
min-width: 9.5rem;
|
||||
}
|
||||
|
||||
.events-actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.events-qwinsta-btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user