feat: events date filter and preserve list page in URL (SAC 0.7.1)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
<RouterLink v-if="fromReports" :to="{ path: '/reports', query: backToReportsQuery }">← Отчёты</RouterLink>
|
||||
|
||||
<RouterLink v-else to="/events">← События</RouterLink>
|
||||
<RouterLink v-else :to="{ path: '/events', query: backToEventsQuery }">← События</RouterLink>
|
||||
|
||||
</p>
|
||||
|
||||
@@ -105,6 +105,7 @@ import { apiFetch, type EventDetail } from "../api";
|
||||
|
||||
import ReportBodyCard from "../components/ReportBodyCard.vue";
|
||||
|
||||
import { eventsBackQueryFromDetail } from "../utils/eventsListQuery";
|
||||
import { isDailyReportType } from "../utils/reportDisplay";
|
||||
|
||||
|
||||
@@ -134,6 +135,8 @@ const backToReportsQuery = computed(() => {
|
||||
return q;
|
||||
});
|
||||
|
||||
const backToEventsQuery = computed(() => eventsBackQueryFromDetail(route.query));
|
||||
|
||||
|
||||
|
||||
function formatDt(iso: string) {
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<template>
|
||||
<h1>События</h1>
|
||||
<div class="filters">
|
||||
<input v-model="q" placeholder="Поиск в summary/title" @keyup.enter="load(1)" />
|
||||
<select v-model="severity" @change="load(1)">
|
||||
<input v-model="q" placeholder="Поиск в summary/title" @keyup.enter="applyFilters" />
|
||||
<select v-model="severity" @change="applyFilters">
|
||||
<option value="">Все severity</option>
|
||||
<option>info</option>
|
||||
<option>warning</option>
|
||||
<option>high</option>
|
||||
<option>critical</option>
|
||||
</select>
|
||||
<input v-model="typeFilter" placeholder="type" @keyup.enter="load(1)" />
|
||||
<input v-model="hostnameFilter" placeholder="hostname / имя сервера" @keyup.enter="load(1)" />
|
||||
<button type="button" @click="load(1)">Применить</button>
|
||||
<input v-model="typeFilter" placeholder="type" @keyup.enter="applyFilters" />
|
||||
<input v-model="hostnameFilter" placeholder="hostname / имя сервера" @keyup.enter="applyFilters" />
|
||||
<label class="events-date-field">
|
||||
<span class="events-date-label">С</span>
|
||||
<input v-model="dateFrom" type="date" @change="applyFilters" />
|
||||
</label>
|
||||
<label class="events-date-field">
|
||||
<span class="events-date-label">По</span>
|
||||
<input v-model="dateTo" type="date" @change="applyFilters" />
|
||||
</label>
|
||||
<button type="button" @click="applyFilters">Применить</button>
|
||||
<button v-if="hasActiveFilters" type="button" class="secondary" @click="clearFilters">Сбросить</button>
|
||||
</div>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="loading">Загрузка…</p>
|
||||
@@ -32,7 +41,7 @@
|
||||
<tbody>
|
||||
<tr v-for="e in data?.items ?? []" :key="e.id">
|
||||
<td>
|
||||
<RouterLink :to="`/events/${e.id}`">{{ e.id }}</RouterLink>
|
||||
<RouterLink :to="{ path: `/events/${e.id}`, query: detailLinkQuery }">{{ e.id }}</RouterLink>
|
||||
</td>
|
||||
<td>{{ formatDt(e.occurred_at) }}</td>
|
||||
<td>{{ e.hostname }}</td>
|
||||
@@ -59,12 +68,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { apiFetch, type EventListResponse } from "../api";
|
||||
import {
|
||||
apiParamsFromState,
|
||||
buildEventsListQuery,
|
||||
eventsListQueryMatches,
|
||||
parseEventsListQuery,
|
||||
type EventsListState,
|
||||
} from "../utils/eventsListQuery";
|
||||
import { formatServerName } from "../utils/hostDisplay";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const data = ref<EventListResponse | null>(null);
|
||||
const loading = ref(false);
|
||||
@@ -75,25 +92,72 @@ const q = ref("");
|
||||
const severity = ref("");
|
||||
const typeFilter = ref("");
|
||||
const hostnameFilter = ref("");
|
||||
const dateFrom = ref("");
|
||||
const dateTo = ref("");
|
||||
|
||||
const syncingFromRoute = ref(false);
|
||||
|
||||
const listState = computed(
|
||||
(): EventsListState => ({
|
||||
page: page.value,
|
||||
q: q.value,
|
||||
severity: severity.value,
|
||||
type: typeFilter.value,
|
||||
hostname: hostnameFilter.value,
|
||||
dateFrom: dateFrom.value,
|
||||
dateTo: dateTo.value,
|
||||
}),
|
||||
);
|
||||
|
||||
const detailLinkQuery = computed(() => buildEventsListQuery(listState.value));
|
||||
|
||||
const hasActiveFilters = computed(
|
||||
() =>
|
||||
Boolean(
|
||||
q.value.trim() ||
|
||||
severity.value ||
|
||||
typeFilter.value.trim() ||
|
||||
hostnameFilter.value.trim() ||
|
||||
dateFrom.value ||
|
||||
dateTo.value,
|
||||
),
|
||||
);
|
||||
|
||||
function formatDt(iso: string) {
|
||||
return new Date(iso).toLocaleString("ru-RU");
|
||||
}
|
||||
|
||||
async function load(p: number) {
|
||||
page.value = p;
|
||||
function applyStateFromQuery() {
|
||||
const parsed = parseEventsListQuery(route.query);
|
||||
page.value = parsed.page;
|
||||
q.value = parsed.q;
|
||||
severity.value = parsed.severity;
|
||||
typeFilter.value = parsed.type;
|
||||
hostnameFilter.value = parsed.hostname;
|
||||
dateFrom.value = parsed.dateFrom;
|
||||
dateTo.value = parsed.dateTo;
|
||||
}
|
||||
|
||||
async function syncRouteQuery() {
|
||||
if (eventsListQueryMatches(listState.value, route.query)) return;
|
||||
syncingFromRoute.value = true;
|
||||
try {
|
||||
await router.replace({ path: "/events", query: buildEventsListQuery(listState.value) });
|
||||
} finally {
|
||||
syncingFromRoute.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function load(p?: number, options?: { syncUrl?: boolean }) {
|
||||
if (p !== undefined) page.value = p;
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: String(page.value),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
if (severity.value) params.set("severity", severity.value);
|
||||
if (typeFilter.value) params.set("type", typeFilter.value);
|
||||
if (hostnameFilter.value.trim()) params.set("hostname", hostnameFilter.value.trim());
|
||||
if (q.value.trim()) params.set("q", q.value.trim());
|
||||
const params = apiParamsFromState(listState.value, pageSize);
|
||||
data.value = await apiFetch<EventListResponse>(`/api/v1/events?${params}`);
|
||||
if (options?.syncUrl !== false && !syncingFromRoute.value) {
|
||||
await syncRouteQuery();
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : "Ошибка загрузки";
|
||||
} finally {
|
||||
@@ -101,13 +165,52 @@ async function load(p: number) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const t = route.query.type;
|
||||
if (typeof t === "string" && t) typeFilter.value = t;
|
||||
const sev = route.query.severity;
|
||||
if (typeof sev === "string" && sev) severity.value = sev;
|
||||
const h = route.query.hostname;
|
||||
if (typeof h === "string" && h) hostnameFilter.value = h;
|
||||
function applyFilters() {
|
||||
load(1);
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
q.value = "";
|
||||
severity.value = "";
|
||||
typeFilter.value = "";
|
||||
hostnameFilter.value = "";
|
||||
dateFrom.value = "";
|
||||
dateTo.value = "";
|
||||
load(1);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
async () => {
|
||||
if (route.path !== "/events") return;
|
||||
const matched = eventsListQueryMatches(listState.value, route.query);
|
||||
if (!matched) {
|
||||
syncingFromRoute.value = true;
|
||||
applyStateFromQuery();
|
||||
syncingFromRoute.value = false;
|
||||
}
|
||||
if (!matched || !data.value) {
|
||||
await load(undefined, { syncUrl: false });
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.events-date-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.events-date-label {
|
||||
color: #9aa4b2;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.events-date-field input[type="date"] {
|
||||
min-width: 9.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user