24ca87a47a
Show RDS access via RDG-Comp or Haproxy-RDG-Comp, enable qwinsta/logoff on any RDG event with internal_ip. Co-authored-by: Cursor <cursoragent@cursor.com>
163 lines
5.5 KiB
Vue
163 lines
5.5 KiB
Vue
<template>
|
|
<p>
|
|
<RouterLink v-if="fromReports" :to="{ path: '/reports', query: backToReportsQuery }">← Отчёты</RouterLink>
|
|
<RouterLink v-else :to="{ path: '/events', query: backToEventsQuery }">← События</RouterLink>
|
|
</p>
|
|
<p v-if="error" class="error">{{ error }}</p>
|
|
<p v-else-if="loading">Загрузка…</p>
|
|
<template v-else-if="event">
|
|
<h1>{{ event.title }}</h1>
|
|
<p v-if="event.rdg_access_path" class="event-rdg-access-banner">
|
|
<RdgAccessBadge :event="event" />
|
|
<span class="muted">Вход на рабочий ПК через RD Gateway.</span>
|
|
</p>
|
|
<p v-if="hasRdgFlapUi(event)" class="event-rdg-flap-banner">
|
|
<RdgFlapBadge :event="event" :detail-link-query="backToEventsQuery" />
|
|
<span class="muted">302→303 за несколько секунд — возможна зависшая сессия на ПК пользователя.</span>
|
|
</p>
|
|
<p v-if="rdgQwinstaEventId(event)" class="event-rdg-actions">
|
|
<button
|
|
type="button"
|
|
class="secondary"
|
|
:disabled="qwinstaLoadingId === event.id"
|
|
title="qwinsta и завершение сеанса на рабочем ПК"
|
|
@click="runQwinsta(event)"
|
|
>
|
|
Обрыв RDS
|
|
</button>
|
|
</p>
|
|
<RdgQwinstaModal
|
|
:modal="qwinstaModal"
|
|
@close="closeQwinstaModal"
|
|
@logoff="runLogoff"
|
|
/>
|
|
<div class="card report-meta">
|
|
<dl class="detail-fields">
|
|
<div class="detail-field">
|
|
<dt>ID</dt>
|
|
<dd>{{ event.id }}</dd>
|
|
</div>
|
|
<div class="detail-field detail-field-wide">
|
|
<dt>event_id</dt>
|
|
<dd><code>{{ event.event_id }}</code></dd>
|
|
</div>
|
|
<div class="detail-field">
|
|
<dt>Хост</dt>
|
|
<dd>
|
|
<RouterLink :to="{ path: '/reports', query: { hostname: event.hostname } }">
|
|
{{ event.hostname }}
|
|
</RouterLink>
|
|
</dd>
|
|
</div>
|
|
<div class="detail-field">
|
|
<dt>Severity</dt>
|
|
<dd><span :class="'sev-' + event.severity">{{ event.severity }}</span></dd>
|
|
</div>
|
|
<div class="detail-field">
|
|
<dt>Тип</dt>
|
|
<dd>
|
|
<template v-if="isReport">{{ reportTypeLabel(event.type) }}</template>
|
|
<code v-else>{{ event.type }}</code>
|
|
</dd>
|
|
</div>
|
|
<div class="detail-field">
|
|
<dt>Occurred</dt>
|
|
<dd>{{ formatDt(event.occurred_at) }}</dd>
|
|
</div>
|
|
<div v-if="!isReport" class="detail-field detail-field-wide">
|
|
<dt>Summary</dt>
|
|
<dd>{{ event.summary }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<template v-if="isReport">
|
|
<h2>Отчёт</h2>
|
|
<p class="report-summary-line">{{ event.summary }}</p>
|
|
<ReportBodyCard :type="event.type" :summary="event.summary" :details="event.details" />
|
|
</template>
|
|
|
|
<details v-if="event.details && !isReport" class="raw-details">
|
|
<summary>details (JSON)</summary>
|
|
<pre>{{ JSON.stringify(event.details, null, 2) }}</pre>
|
|
</details>
|
|
|
|
<details v-if="isReport && event.details" class="raw-details">
|
|
<summary>details (JSON)</summary>
|
|
<pre>{{ JSON.stringify(event.details, null, 2) }}</pre>
|
|
</details>
|
|
|
|
<details class="raw-details">
|
|
<summary>payload (JSON)</summary>
|
|
<pre>{{ JSON.stringify(event.payload, null, 2) }}</pre>
|
|
</details>
|
|
</template>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref, watch } from "vue";
|
|
import { useRoute } from "vue-router";
|
|
import { apiFetch, type EventDetail } from "../api";
|
|
import RdgAccessBadge from "../components/RdgAccessBadge.vue";
|
|
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
|
|
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
|
import ReportBodyCard from "../components/ReportBodyCard.vue";
|
|
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
|
import { hasRdgFlapUi, rdgQwinstaEventId } from "../utils/rdgFlap";
|
|
import { eventsBackQueryFromDetail } from "../utils/eventsListQuery";
|
|
import { dailyReportTypeLabel, isDailyReportType } from "../utils/reportDisplay";
|
|
|
|
const props = defineProps<{ id: string }>();
|
|
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));
|
|
|
|
const backToReportsQuery = computed(() => {
|
|
const q: Record<string, string> = {};
|
|
if (typeof route.query.type === "string" && route.query.type) q.type = route.query.type;
|
|
if (typeof route.query.hostname === "string" && route.query.hostname) q.hostname = route.query.hostname;
|
|
return q;
|
|
});
|
|
|
|
const backToEventsQuery = computed(() => eventsBackQueryFromDetail(route.query));
|
|
|
|
function reportTypeLabel(type: string) {
|
|
return dailyReportTypeLabel(type);
|
|
}
|
|
|
|
function formatDt(iso: string) {
|
|
return new Date(iso).toLocaleString("ru-RU");
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true;
|
|
error.value = "";
|
|
try {
|
|
event.value = await apiFetch<EventDetail>(`/api/v1/events/${props.id}`);
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : "Не найдено";
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(load);
|
|
watch(() => props.id, load);
|
|
</script>
|
|
|
|
<style scoped>
|
|
.event-rdg-flap-banner {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.5rem 0.75rem;
|
|
margin: 0.5rem 0 1rem;
|
|
}
|
|
</style>
|