fix: show RDG flap badge on 302 and 303 events in SAC UI (0.20.5)
Seaca surfaced flap via Problems; the web UI only flagged 303 in DB and had no visible label. Resolve flap pairs at read time and show badge plus qwinsta on both sides of 302→303.
This commit is contained in:
@@ -25,7 +25,7 @@ def fetch_recent_events(db: Session, *, limit: int = 8) -> list[EventSummary]:
|
|||||||
.order_by(Event.received_at.desc())
|
.order_by(Event.received_at.desc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
).all()
|
).all()
|
||||||
return [event_to_summary(e) for e in rows]
|
return [event_to_summary(e, db) for e in rows]
|
||||||
|
|
||||||
|
|
||||||
class TopHostItem(BaseModel):
|
class TopHostItem(BaseModel):
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ def list_events(
|
|||||||
.limit(page_size)
|
.limit(page_size)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
items = [event_to_summary(e) for e in rows]
|
items = [event_to_summary(e, db) for e in rows]
|
||||||
return EventListResponse(items=items, total=total, page=page, page_size=page_size)
|
return EventListResponse(items=items, total=total, page=page, page_size=page_size)
|
||||||
|
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ def get_event(
|
|||||||
)
|
)
|
||||||
if event is None:
|
if event is None:
|
||||||
raise HTTPException(status_code=404, detail="Event not found")
|
raise HTTPException(status_code=404, detail="Event not found")
|
||||||
base = event_to_summary(event)
|
base = event_to_summary(event, db)
|
||||||
return EventDetail(
|
return EventDetail(
|
||||||
**base.model_dump(),
|
**base.model_dump(),
|
||||||
details=event.details,
|
details=event.details,
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ class EventSummary(BaseModel):
|
|||||||
summary: str
|
summary: str
|
||||||
actor_user: str | None = None
|
actor_user: str | None = None
|
||||||
rdg_flap: bool = False
|
rdg_flap: bool = False
|
||||||
|
rdg_flap_pair_event_id: int | None = None
|
||||||
|
rdg_flap_qwinsta_event_id: int | None = None
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.models.event import Event
|
from app.models.event import Event
|
||||||
from app.schemas.list_models import EventSummary
|
from app.schemas.list_models import EventSummary
|
||||||
from app.services.event_actor_user import extract_event_actor_user
|
from app.services.event_actor_user import extract_event_actor_user
|
||||||
from app.services.rdg_session_flap import event_has_rdg_flap
|
from app.services.rdg_session_flap import resolve_rdg_flap_summary
|
||||||
|
|
||||||
|
|
||||||
def event_to_summary(event: Event) -> EventSummary:
|
def event_to_summary(event: Event, db: Session | None = None) -> EventSummary:
|
||||||
host = event.host
|
host = event.host
|
||||||
|
rdg_flap = False
|
||||||
|
rdg_flap_pair_event_id: int | None = None
|
||||||
|
rdg_flap_qwinsta_event_id: int | None = None
|
||||||
|
if db is not None:
|
||||||
|
rdg_flap, rdg_flap_pair_event_id, rdg_flap_qwinsta_event_id = resolve_rdg_flap_summary(
|
||||||
|
db, event
|
||||||
|
)
|
||||||
|
|
||||||
return EventSummary(
|
return EventSummary(
|
||||||
id=event.id,
|
id=event.id,
|
||||||
event_id=event.event_id,
|
event_id=event.event_id,
|
||||||
@@ -21,5 +31,7 @@ def event_to_summary(event: Event) -> EventSummary:
|
|||||||
title=event.title,
|
title=event.title,
|
||||||
summary=event.summary,
|
summary=event.summary,
|
||||||
actor_user=extract_event_actor_user(event.type, event.details),
|
actor_user=extract_event_actor_user(event.type, event.details),
|
||||||
rdg_flap=event_has_rdg_flap(event),
|
rdg_flap=rdg_flap,
|
||||||
|
rdg_flap_pair_event_id=rdg_flap_pair_event_id,
|
||||||
|
rdg_flap_qwinsta_event_id=rdg_flap_qwinsta_event_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -122,3 +122,75 @@ def evaluate_rdg_session_flap(db: Session, event: Event) -> RuleMatch | None:
|
|||||||
def event_has_rdg_flap(event: Event) -> bool:
|
def event_has_rdg_flap(event: Event) -> bool:
|
||||||
details = event.details if isinstance(event.details, dict) else {}
|
details = event.details if isinstance(event.details, dict) else {}
|
||||||
return details.get("rdg_flap") is True
|
return details.get("rdg_flap") is True
|
||||||
|
|
||||||
|
|
||||||
|
def _stored_flap_pair_id(event: Event) -> int | None:
|
||||||
|
details = event.details if isinstance(event.details, dict) else {}
|
||||||
|
raw = details.get("rdg_flap_pair_event_id")
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_rdg_end_after_success(db: Session, success_event: Event) -> Event | None:
|
||||||
|
"""303 с rdg_flap, у которого пара — этот 302 (или вычисляется по окну)."""
|
||||||
|
if success_event.type != RDG_SUCCESS_TYPE:
|
||||||
|
return None
|
||||||
|
settings = get_settings()
|
||||||
|
min_sec = settings.sac_rdg_flap_window_min_sec
|
||||||
|
max_sec = settings.sac_rdg_flap_window_max_sec
|
||||||
|
start_at = _as_utc(success_event.occurred_at)
|
||||||
|
window_start = start_at + timedelta(seconds=min_sec)
|
||||||
|
window_end = start_at + timedelta(seconds=max_sec)
|
||||||
|
|
||||||
|
candidates = db.scalars(
|
||||||
|
select(Event)
|
||||||
|
.where(
|
||||||
|
Event.host_id == success_event.host_id,
|
||||||
|
Event.type.in_(RDG_END_TYPES),
|
||||||
|
Event.occurred_at >= window_start,
|
||||||
|
Event.occurred_at <= window_end,
|
||||||
|
Event.id != success_event.id,
|
||||||
|
)
|
||||||
|
.order_by(Event.occurred_at.asc())
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for end in candidates:
|
||||||
|
if not _users_match(end, success_event):
|
||||||
|
continue
|
||||||
|
if not _internal_ips_compatible(end, success_event):
|
||||||
|
continue
|
||||||
|
pair_id = _stored_flap_pair_id(end)
|
||||||
|
if pair_id == success_event.id:
|
||||||
|
return end
|
||||||
|
prior = find_rdg_success_before_end(db, end)
|
||||||
|
if prior is not None and prior.id == success_event.id:
|
||||||
|
return end
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_rdg_flap_summary(
|
||||||
|
db: Session, event: Event
|
||||||
|
) -> tuple[bool, int | None, int | None]:
|
||||||
|
"""
|
||||||
|
(rdg_flap, pair_event_id, qwinsta_event_id).
|
||||||
|
qwinsta_event_id — всегда 303; для 302 указывает на связанный end-event.
|
||||||
|
"""
|
||||||
|
if event_has_rdg_flap(event):
|
||||||
|
pair_id = _stored_flap_pair_id(event)
|
||||||
|
return True, pair_id, event.id
|
||||||
|
|
||||||
|
if event.type in RDG_END_TYPES:
|
||||||
|
prior = find_rdg_success_before_end(db, event)
|
||||||
|
if prior is not None:
|
||||||
|
return True, prior.id, event.id
|
||||||
|
|
||||||
|
if event.type == RDG_SUCCESS_TYPE:
|
||||||
|
end = find_rdg_end_after_success(db, event)
|
||||||
|
if end is not None:
|
||||||
|
return True, end.id, end.id
|
||||||
|
|
||||||
|
return False, None, None
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||||
|
|
||||||
APP_NAME = "Security Alert Center"
|
APP_NAME = "Security Alert Center"
|
||||||
APP_VERSION = "0.20.4"
|
APP_VERSION = "0.20.5"
|
||||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ from app.services.rdg_session_flap import (
|
|||||||
evaluate_rdg_session_flap,
|
evaluate_rdg_session_flap,
|
||||||
event_has_rdg_flap,
|
event_has_rdg_flap,
|
||||||
find_rdg_success_before_end,
|
find_rdg_success_before_end,
|
||||||
|
resolve_rdg_flap_summary,
|
||||||
)
|
)
|
||||||
|
from app.services.event_summary import event_to_summary
|
||||||
from tests.test_ingest import VALID_EVENT
|
from tests.test_ingest import VALID_EVENT
|
||||||
|
|
||||||
|
|
||||||
@@ -156,3 +158,49 @@ def test_rdg_flap_dedup_within_30_sec(db_session, rdg_settings):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert open_count == 1
|
assert open_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_rdg_flap_summary_for_302_and_303(db_session, rdg_settings):
|
||||||
|
t0 = datetime.now(timezone.utc)
|
||||||
|
user = "B26\\pair.user"
|
||||||
|
details = {"user": user, "internal_ip": "192.168.163.48"}
|
||||||
|
|
||||||
|
start = _ingest(
|
||||||
|
db_session,
|
||||||
|
t0,
|
||||||
|
type="rdg.connection.success",
|
||||||
|
category="auth",
|
||||||
|
severity="warning",
|
||||||
|
title="RD Gateway event 302",
|
||||||
|
summary="302",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
end = _ingest(
|
||||||
|
db_session,
|
||||||
|
t0 + timedelta(seconds=5),
|
||||||
|
type="rdg.connection.disconnected",
|
||||||
|
category="auth",
|
||||||
|
severity="info",
|
||||||
|
title="RD Gateway event 303",
|
||||||
|
summary="303",
|
||||||
|
details=details,
|
||||||
|
)
|
||||||
|
evaluate_rdg_session_flap(db_session, end)
|
||||||
|
db_session.flush()
|
||||||
|
|
||||||
|
end_flap, end_pair, end_qwinsta = resolve_rdg_flap_summary(db_session, end)
|
||||||
|
start_flap, start_pair, start_qwinsta = resolve_rdg_flap_summary(db_session, start)
|
||||||
|
|
||||||
|
assert end_flap is True
|
||||||
|
assert end_pair == start.id
|
||||||
|
assert end_qwinsta == end.id
|
||||||
|
|
||||||
|
assert start_flap is True
|
||||||
|
assert start_pair == end.id
|
||||||
|
assert start_qwinsta == end.id
|
||||||
|
|
||||||
|
end_summary = event_to_summary(end, db_session)
|
||||||
|
start_summary = event_to_summary(start, db_session)
|
||||||
|
assert end_summary.rdg_flap is True
|
||||||
|
assert start_summary.rdg_flap is True
|
||||||
|
assert start_summary.rdg_flap_qwinsta_event_id == end.id
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ export interface EventSummary {
|
|||||||
summary: string;
|
summary: string;
|
||||||
actor_user?: string | null;
|
actor_user?: string | null;
|
||||||
rdg_flap?: boolean;
|
rdg_flap?: boolean;
|
||||||
|
rdg_flap_pair_event_id?: number | null;
|
||||||
|
rdg_flap_qwinsta_event_id?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentCommandResponse {
|
export interface AgentCommandResponse {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<span class="rdg-flap-badge" :title="tooltip">
|
||||||
|
RDG flap
|
||||||
|
<template v-if="event.rdg_flap_pair_event_id">
|
||||||
|
{{ pairArrow }}
|
||||||
|
<RouterLink :to="pairLink">{{ event.rdg_flap_pair_event_id }}</RouterLink>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { EventSummary } from "../api";
|
||||||
|
import { rdgFlapPairLabel } from "../utils/rdgFlap";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
event: EventSummary;
|
||||||
|
detailLinkQuery?: Record<string, string>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const pairArrow = computed(() => rdgFlapPairLabel(props.event));
|
||||||
|
const tooltip = computed(
|
||||||
|
() =>
|
||||||
|
"302→303 за 1–10 с. Возможна зависшая сессия на ПК пользователя — qwinsta/logoff.",
|
||||||
|
);
|
||||||
|
const pairLink = computed(() => ({
|
||||||
|
path: `/events/${props.event.rdg_flap_pair_event_id}`,
|
||||||
|
query: props.detailLinkQuery,
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rdg-flap-badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.45rem;
|
||||||
|
padding: 0.1rem 0.45rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #f0c674;
|
||||||
|
background: rgba(240, 198, 116, 0.12);
|
||||||
|
border: 1px solid rgba(240, 198, 116, 0.45);
|
||||||
|
border-radius: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rdg-flap-badge a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
margin-left: 0.15rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -114,10 +114,11 @@ export function useRdgQwinsta() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runQwinsta(event: EventSummary) {
|
async function runQwinsta(event: EventSummary) {
|
||||||
|
const targetId = event.rdg_flap_qwinsta_event_id ?? event.id;
|
||||||
qwinstaLoadingId.value = event.id;
|
qwinstaLoadingId.value = event.id;
|
||||||
qwinstaModal.value = {
|
qwinstaModal.value = {
|
||||||
open: true,
|
open: true,
|
||||||
eventId: event.id,
|
eventId: targetId,
|
||||||
actorUser: event.actor_user || "",
|
actorUser: event.actor_user || "",
|
||||||
commandUuid: "",
|
commandUuid: "",
|
||||||
target: "",
|
target: "",
|
||||||
@@ -130,13 +131,13 @@ export function useRdgQwinsta() {
|
|||||||
logoffId: null,
|
logoffId: null,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const cmd = await postEventQwinsta(event.id);
|
const cmd = await postEventQwinsta(targetId);
|
||||||
applyCommandMeta(qwinstaModal.value, cmd);
|
applyCommandMeta(qwinstaModal.value, cmd);
|
||||||
qwinstaModal.value.commandUuid = cmd.command_uuid;
|
qwinstaModal.value.commandUuid = cmd.command_uuid;
|
||||||
if (applyCommandResult(event.id, event.actor_user || "", cmd)) {
|
if (applyCommandResult(targetId, event.actor_user || "", cmd)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pollQwinstaCommand(event.id, cmd.command_uuid, event.actor_user || "");
|
pollQwinstaCommand(targetId, cmd.command_uuid, event.actor_user || "");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
qwinstaModal.value.loading = false;
|
qwinstaModal.value.loading = false;
|
||||||
qwinstaModal.value.error = e instanceof Error ? e.message : "Не удалось отправить qwinsta";
|
qwinstaModal.value.error = e instanceof Error ? e.message : "Не удалось отправить qwinsta";
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { EventSummary } from "../api";
|
||||||
|
|
||||||
|
export function hasRdgFlapUi(event: EventSummary): boolean {
|
||||||
|
return Boolean(event.rdg_flap || event.rdg_flap_qwinsta_event_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rdgFlapQwinstaEventId(event: EventSummary): number | null {
|
||||||
|
if (event.rdg_flap_qwinsta_event_id) return event.rdg_flap_qwinsta_event_id;
|
||||||
|
return event.rdg_flap ? event.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rdgFlapPairLabel(event: EventSummary): string {
|
||||||
|
if (!event.rdg_flap_pair_event_id) return "";
|
||||||
|
return event.type === "rdg.connection.success" ? "→" : "←";
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||||
export const APP_NAME = "Security Alert Center";
|
export const APP_NAME = "Security Alert Center";
|
||||||
export const APP_VERSION = "0.20.4";
|
export const APP_VERSION = "0.20.5";
|
||||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||||
|
|||||||
@@ -281,12 +281,15 @@
|
|||||||
|
|
||||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||||
|
|
||||||
<td>{{ e.title }}</td>
|
<td>
|
||||||
|
<RdgFlapBadge v-if="hasRdgFlapUi(e)" :event="e" />
|
||||||
|
{{ e.title }}
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="dash-actions">
|
<td class="dash-actions">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="e.rdg_flap"
|
v-if="rdgFlapQwinstaEventId(e)"
|
||||||
type="button"
|
type="button"
|
||||||
class="secondary dash-qwinsta-btn"
|
class="secondary dash-qwinsta-btn"
|
||||||
:disabled="qwinstaLoadingId === e.id"
|
:disabled="qwinstaLoadingId === e.id"
|
||||||
@@ -333,10 +336,12 @@ import {
|
|||||||
type DashboardSummary,
|
type DashboardSummary,
|
||||||
type EventSummary,
|
type EventSummary,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
|
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
|
||||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||||
import { useSacVersion } from "../composables/useSacVersion";
|
import { useSacVersion } from "../composables/useSacVersion";
|
||||||
import { formatServerName } from "../utils/hostDisplay";
|
import { formatServerName } from "../utils/hostDisplay";
|
||||||
|
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
|
||||||
|
|
||||||
const { appName, appVersion } = useSacVersion();
|
const { appName, appVersion } = useSacVersion();
|
||||||
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
|
const { qwinstaLoadingId, qwinstaModal, closeQwinstaModal, runQwinsta, runLogoff } = useRdgQwinsta();
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
<p v-else-if="loading">Загрузка…</p>
|
<p v-else-if="loading">Загрузка…</p>
|
||||||
<template v-else-if="event">
|
<template v-else-if="event">
|
||||||
<h1>{{ event.title }}</h1>
|
<h1>{{ event.title }}</h1>
|
||||||
<p v-if="event.rdg_flap" class="event-rdg-actions">
|
<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="rdgFlapQwinstaEventId(event)" class="event-rdg-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
@@ -89,9 +93,11 @@
|
|||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { apiFetch, type EventDetail } from "../api";
|
import { apiFetch, type EventDetail } from "../api";
|
||||||
|
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
|
||||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||||
import ReportBodyCard from "../components/ReportBodyCard.vue";
|
import ReportBodyCard from "../components/ReportBodyCard.vue";
|
||||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||||
|
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
|
||||||
import { eventsBackQueryFromDetail } from "../utils/eventsListQuery";
|
import { eventsBackQueryFromDetail } from "../utils/eventsListQuery";
|
||||||
import { dailyReportTypeLabel, isDailyReportType } from "../utils/reportDisplay";
|
import { dailyReportTypeLabel, isDailyReportType } from "../utils/reportDisplay";
|
||||||
|
|
||||||
@@ -138,3 +144,13 @@ async function load() {
|
|||||||
onMounted(load);
|
onMounted(load);
|
||||||
watch(() => props.id, load);
|
watch(() => props.id, load);
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
@@ -53,10 +53,13 @@
|
|||||||
<td>{{ e.product_version || "—" }}</td>
|
<td>{{ e.product_version || "—" }}</td>
|
||||||
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
<td :class="'sev-' + e.severity">{{ e.severity }}</td>
|
||||||
<td><code>{{ e.type }}</code></td>
|
<td><code>{{ e.type }}</code></td>
|
||||||
<td>{{ e.title }}</td>
|
<td>
|
||||||
|
<RdgFlapBadge v-if="hasRdgFlapUi(e)" :event="e" :detail-link-query="detailLinkQuery" />
|
||||||
|
{{ e.title }}
|
||||||
|
</td>
|
||||||
<td class="events-actions">
|
<td class="events-actions">
|
||||||
<button
|
<button
|
||||||
v-if="e.rdg_flap"
|
v-if="rdgFlapQwinstaEventId(e)"
|
||||||
type="button"
|
type="button"
|
||||||
class="secondary events-qwinsta-btn"
|
class="secondary events-qwinsta-btn"
|
||||||
:disabled="qwinstaLoadingId === e.id"
|
:disabled="qwinstaLoadingId === e.id"
|
||||||
@@ -92,8 +95,10 @@
|
|||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { apiFetch, type EventListResponse } from "../api";
|
import { apiFetch, type EventListResponse } from "../api";
|
||||||
|
import RdgFlapBadge from "../components/RdgFlapBadge.vue";
|
||||||
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
import RdgQwinstaModal from "../components/RdgQwinstaModal.vue";
|
||||||
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
import { useRdgQwinsta } from "../composables/useRdgQwinsta";
|
||||||
|
import { hasRdgFlapUi, rdgFlapQwinstaEventId } from "../utils/rdgFlap";
|
||||||
import {
|
import {
|
||||||
apiParamsFromState,
|
apiParamsFromState,
|
||||||
buildEventsListQuery,
|
buildEventsListQuery,
|
||||||
|
|||||||
Reference in New Issue
Block a user