demo version prepped
This commit is contained in:
272
frontend/src/pages/Incident.vue
Normal file
272
frontend/src/pages/Incident.vue
Normal file
@@ -0,0 +1,272 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { store } from '../store'
|
||||
import { ALARMS_ENDPOINT, CELLSITES_ENDPOINT, INCIDENTS_ENDPOINT, ROBOTS_ENDPOINT } from '../main'
|
||||
import { get_request } from '../ajax_requests'
|
||||
import { use_timeline } from '../composables/use_timeline'
|
||||
import { use_filtered_data } from '../composables/use_filtered_data'
|
||||
import Nav from '../components/Nav.vue'
|
||||
import Map from '../components/Map.vue'
|
||||
import TimelineRow from '../components/TimelineRow.vue'
|
||||
import FilterRow from '../components/FilterRow.vue'
|
||||
import Table from '../components/Table.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const id = Number(route.params.id)
|
||||
const incident = ref(null)
|
||||
|
||||
const associated_site = computed(() =>
|
||||
store.cellsites.find(s => s.id === incident.value?.site_id)
|
||||
)
|
||||
|
||||
const stats_fields = computed(() => [
|
||||
['ID', incident.value?.id],
|
||||
['STATUS', incident.value?.status],
|
||||
['SEVERITY', incident.value?.severity],
|
||||
['ASSIGNED TO', incident.value?.assigned_to],
|
||||
['SITE', incident.value?.site_id],
|
||||
['CREATED', incident.value?.created],
|
||||
['UPDATED', incident.value?.updated],
|
||||
['TEXT', incident.value?.text],
|
||||
])
|
||||
|
||||
|
||||
// TODO: Maybe instead of only fetching the specific site, we also fetch the other sites IF the user scrolls the map?
|
||||
const fetch_all = async (before?: number) => {
|
||||
const was_live = store.is_live
|
||||
const site_id = incident.value?.site_id
|
||||
if (!site_id) return
|
||||
const alarm_url = before
|
||||
? `${ALARMS_ENDPOINT}?id=${site_id}&before=${before}`
|
||||
: `${ALARMS_ENDPOINT}?id=${site_id}`
|
||||
const [alarms, robots] = await Promise.all([
|
||||
get_request(alarm_url),
|
||||
get_request(ROBOTS_ENDPOINT),
|
||||
])
|
||||
store.set_alarms(alarms)
|
||||
store.set_robots(robots)
|
||||
|
||||
if (before === undefined && !store.is_frozen && was_live) {
|
||||
// FIXME: repeated again
|
||||
const now = Date.now()
|
||||
store.timeline_max = now
|
||||
store.timeline_position = now
|
||||
} else {
|
||||
store.timeline_max = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
const { timeline_position_debounced, is_live_debounced, manual_refresh, freeze, snap_to_live } = use_timeline(fetch_all)
|
||||
const { visible_alarms, filtered_alarms } = use_filtered_data(is_live_debounced, timeline_position_debounced)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
incident.value = await get_request(`${INCIDENTS_ENDPOINT}/${id}`)
|
||||
await fetch_all()
|
||||
const all = store.alarms
|
||||
if (all.length > 0) {
|
||||
const oldest = Math.min(...all.map(r => new Date(r.created + ' UTC').getTime()))
|
||||
store.timeline_min = store.timeline_min === 0 ? oldest : Math.min(store.timeline_min, oldest)
|
||||
} else if (store.timeline_min === 0) {
|
||||
store.timeline_min = Date.now() - 86400000
|
||||
}
|
||||
|
||||
const site_exists = store.cellsites.some(s => s.id === incident.value?.site_id)
|
||||
if (!site_exists && incident.value?.site_id) {
|
||||
const data = await get_request(`${CELLSITES_ENDPOINT}/${incident.value.site_id}`)
|
||||
store.set_cellsites(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching incident:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div :class="{ mode_historical: store.is_historical && !store.is_frozen, mode_frozen: store.is_frozen }">
|
||||
<Nav />
|
||||
<div class="site_container">
|
||||
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!incident" class="not-found">
|
||||
<p>Not found</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="site_content">
|
||||
<div class="stats">
|
||||
<div class="site_id">INCIDENT {{ incident.id }}</div>
|
||||
<div class="stat_row" v-for="[key, val] in stats_fields" :key="key">
|
||||
<span class="stat_key">{{ key }}</span>
|
||||
<span class="stat_val" :class="key === 'STATUS' ? incident.status : ''">{{ val }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map_container">
|
||||
<Map
|
||||
v-if="associated_site?.lat != null && associated_site?.lon != null"
|
||||
:center="[associated_site.lat, associated_site.lon]"
|
||||
:zoom="12"
|
||||
:site_id="incident.site_id"
|
||||
:robots="store.robots"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="data">
|
||||
<div class="section_label">ASSOCIATED SITES</div>
|
||||
<div class="site_list">
|
||||
<div v-if="associated_site" class="site_link" @click="router.push(`/cellsites/${associated_site.id}`)">
|
||||
<span class="site_link_id">SITE {{ associated_site.id }}</span>
|
||||
<span class="site_link_meta">{{ associated_site.radio }} · {{ associated_site.lat }}, {{ associated_site.lon }}</span>
|
||||
<span class="site_link_arrow">→</span>
|
||||
</div>
|
||||
<p v-else>No associated site found.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<FilterRow :count="filtered_alarms.length" label="alarms" />
|
||||
<TimelineRow @refresh="manual_refresh" @freeze="freeze" @snap_to_live="snap_to_live" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.site_container {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.site_content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"stats map"
|
||||
"data data";
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.site_id {
|
||||
font-size: 1.4rem;
|
||||
color: var(--theme, rgba(255, 100, 0, 1));
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.stat_row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.3rem 0;
|
||||
border-bottom: 1px solid var(--theme-faint);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.stat_key {
|
||||
color: var(--theme-dim);
|
||||
}
|
||||
|
||||
.stat_val {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.stat_val.active { color: rgba(220, 30, 30, 0.9); }
|
||||
.stat_val.closed { color: rgba(100, 200, 100, 0.7); }
|
||||
|
||||
.stats { grid-area: stats; }
|
||||
.map_container { grid-area: map; min-height: 400px; }
|
||||
|
||||
.data {
|
||||
grid-area: data;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.data_scroll {
|
||||
height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading-overlay,
|
||||
.not-found {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
z-index: 10;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
color: rgba(255, 100, 0, 0.8);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.site_content {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"stats"
|
||||
"map"
|
||||
"data";
|
||||
}
|
||||
|
||||
.map_container { height: 300px; }
|
||||
}
|
||||
|
||||
.section_label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--theme-dim);
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.site_list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.site_link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.6rem 1rem;
|
||||
background: rgba(20, 20, 20, 0.9);
|
||||
border-left: 3px solid var(--theme-faint);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.site_link:hover {
|
||||
background: rgba(40, 40, 40, 0.9);
|
||||
border-left-color: var(--theme);
|
||||
}
|
||||
|
||||
.site_link_id {
|
||||
color: var(--theme);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.site_link_meta {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.site_link_arrow {
|
||||
color: var(--theme-dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user