95c5b9f25a
Move flex off td.actions to inner wrapper; show Закрыта for resolved problems. SAC 0.8.2. Co-authored-by: Cursor <cursoragent@cursor.com>
137 lines
3.9 KiB
Vue
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>
|