feat: UI Problems list filters, detail card and event timeline (d2-1)
- Filters status/severity/hostname; /problems/:id with Ack/Resolve and timeline - Dashboard drill-down to open problems Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
<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>
|
||||
</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>
|
||||
Reference in New Issue
Block a user