Files
security-alert-center/frontend/src/views/ProblemDetailView.vue
T
PTah 95c5b9f25a fix(ui): table actions column layout and resolved label
Move flex off td.actions to inner wrapper; show Закрыта for resolved problems. SAC 0.8.2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-05 14:10:56 +10:00

137 lines
3.9 KiB
Vue

<template>
<p><RouterLink to="/problems"> Проблемы</RouterLink></p>
<p v-if="error" class="error">{{ error }}</p>
<p v-else-if="loading">Загрузка</p>
<template v-else-if="problem">
<h1>{{ problem.title }}</h1>
<div class="card report-meta">
<p>
<strong>ID:</strong> {{ problem.id }}
· <strong>Status:</strong>
<span :class="'status-' + problem.status">{{ problem.status }}</span>
· <strong>Severity:</strong>
<span :class="'sev-' + problem.severity">{{ problem.severity }}</span>
</p>
<p>
<strong>Хост:</strong>
<RouterLink
v-if="problem.hostname"
:to="{ path: '/reports', query: { hostname: problem.hostname } }"
>
{{ problem.hostname }}
</RouterLink>
<span v-else></span>
· <strong>Rule:</strong> <code>{{ problem.rule_id ?? "—" }}</code>
</p>
<p><strong>Summary:</strong> {{ problem.summary }}</p>
<p>
<strong>Событий:</strong> {{ problem.event_count ?? "—" }}
· <strong>Last seen:</strong> {{ formatDt(problem.last_seen_at ?? problem.updated_at) }}
· <strong>Обновлено:</strong> {{ formatDt(problem.updated_at) }}
</p>
<p v-if="problem.fingerprint">
<strong>Fingerprint:</strong> <code>{{ problem.fingerprint }}</code>
</p>
<p class="actions">
<button
v-if="problem.status === 'open'"
type="button"
class="secondary"
:disabled="acting"
@click="doAck"
>
Ack
</button>
<button v-if="problem.status !== 'resolved'" type="button" :disabled="acting" @click="doResolve">
Resolve
</button>
<span v-else class="actions-muted">Закрыта</span>
</p>
</div>
<h2>Таймлайн событий</h2>
<p v-if="!problem.events.length">Нет связанных событий.</p>
<table v-else class="timeline-table">
<thead>
<tr>
<th>Время</th>
<th>Severity</th>
<th>Type</th>
<th>Title</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr v-for="e in problem.events" :key="e.id">
<td>{{ formatDt(e.occurred_at) }}</td>
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
<td>
<RouterLink :to="`/events/${e.id}`"><code>{{ e.type }}</code></RouterLink>
</td>
<td>{{ e.title }}</td>
<td>{{ e.summary }}</td>
</tr>
</tbody>
</table>
</template>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from "vue";
import { ackProblem, fetchProblem, resolveProblem, type ProblemDetail } from "../api";
const props = defineProps<{ id: string }>();
const problem = ref<ProblemDetail | null>(null);
const loading = ref(false);
const error = ref("");
const acting = ref(false);
function formatDt(iso: string) {
return new Date(iso).toLocaleString("ru-RU");
}
async function load() {
loading.value = true;
error.value = "";
try {
problem.value = await fetchProblem(Number(props.id));
} catch (e) {
error.value = e instanceof Error ? e.message : "Не найдено";
} finally {
loading.value = false;
}
}
async function doAck() {
if (!problem.value) return;
acting.value = true;
error.value = "";
try {
await ackProblem(problem.value.id);
await load();
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка ack";
} finally {
acting.value = false;
}
}
async function doResolve() {
if (!problem.value) return;
acting.value = true;
error.value = "";
try {
await resolveProblem(problem.value.id);
await load();
} catch (e) {
error.value = e instanceof Error ? e.message : "Ошибка resolve";
} finally {
acting.value = false;
}
}
onMounted(load);
watch(() => props.id, load);
</script>