Page Menu
Home
DevCentral
Search
Configure Global Search
Log In
Files
F24589901
D3980.id10328.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
5 KB
Referenced Files
None
Subscribers
None
D3980.id10328.diff
View Options
diff --git a/frontend/src/components/__tests__/router.test.js b/frontend/src/components/__tests__/router.test.js
new file mode 100644
--- /dev/null
+++ b/frontend/src/components/__tests__/router.test.js
@@ -0,0 +1,59 @@
+import { describe, it, expect, vi } from 'vitest'
+import { createRouter, createWebHistory } from 'vue-router'
+
+vi.mock('@/plugins/api', () => ({
+ authApi: { verify: vi.fn() },
+}))
+
+const routes = [
+ {
+ path: '/',
+ name: 'status',
+ component: { template: '<div>Status</div>' },
+ },
+ {
+ path: '/incidents',
+ name: 'incident-history',
+ component: { template: '<div>Incident History</div>' },
+ },
+]
+
+function createTestRouter() {
+ return createRouter({
+ history: createWebHistory(),
+ routes,
+ })
+}
+
+describe('Router', () => {
+ it('/incidents resolves to incident-history', async () => {
+ const router = createTestRouter()
+ await router.push('/incidents')
+ await router.isReady()
+
+ expect(router.currentRoute.value.name).toBe('incident-history')
+ expect(router.currentRoute.value.path).toBe('/incidents')
+ })
+
+ it('navigates from / to /incidents', async () => {
+ const router = createTestRouter()
+ await router.push('/')
+ await router.isReady()
+
+ expect(router.currentRoute.value.name).toBe('status')
+
+ await router.push('/incidents')
+
+ expect(router.currentRoute.value.name).toBe('incident-history')
+ })
+
+ it('navigates from /incidents back to /', async () => {
+ const router = createTestRouter()
+ await router.push('/incidents')
+ await router.isReady()
+
+ await router.push('/')
+
+ expect(router.currentRoute.value.name).toBe('status')
+ })
+})
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -10,6 +10,11 @@
name: 'status',
component: StatusPage,
},
+ {
+ path: '/incidents',
+ name: 'incident-history',
+ component: () => import('@/views/IncidentHistory.vue'),
+ },
{
path: '/confirm/:token',
name: 'confirm-subscription',
diff --git a/frontend/src/views/IncidentHistory.vue b/frontend/src/views/IncidentHistory.vue
new file mode 100644
--- /dev/null
+++ b/frontend/src/views/IncidentHistory.vue
@@ -0,0 +1,112 @@
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useIncidents } from '@/composables/useIncidents'
+import { incidentsApi } from '@/plugins/api'
+import IncidentTimeline from '@/components/IncidentTimeline.vue'
+
+const FILTER_OPTIONS = [
+ { label: 'Last 7 Days', value: 7 },
+ { label: 'Last 30 Days', value: 30 },
+ { label: 'Last 90 Days', value: 90 },
+ { label: 'All', value: 'all' },
+]
+
+const { incidents, loading, fetchIncidents } = useIncidents()
+const activeFilter = ref(7)
+const detailedIncidents = ref([])
+const detailsLoading = ref(false)
+
+async function fetchDetailedIncidents() {
+ detailsLoading.value = true
+ try {
+ detailedIncidents.value = await Promise.all(
+ incidents.value.map(
+ (incident) => incidentsApi.getById(incident.id).catch(() => incident)
+ )
+ )
+ } catch (err) {
+ console.error('Failed to fetch incident details:', err)
+ } finally {
+ detailsLoading.value = false
+ }
+}
+
+function isWithinDays(dateString, days) {
+ const cutoffDate = new Date()
+ cutoffDate.setDate(cutoffDate.getDate() - days)
+
+ return new Date(dateString) >= cutoffDate
+}
+
+function filterButtonClass(value) {
+ if (activeFilter.value === value) {
+ return 'bg-brand-500 text-white'
+ }
+
+ return 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
+}
+
+const filteredIncidents = computed(() => {
+ if (activeFilter.value === 'all') {
+ return detailedIncidents.value
+ }
+
+ return detailedIncidents.value.filter((incident) => {
+ return isWithinDays(incident.created_at, activeFilter.value)
+ })
+})
+
+const isLoading = computed(() => loading.value || detailsLoading.value)
+
+onMounted(async () => {
+ await fetchIncidents()
+ await fetchDetailedIncidents()
+})
+</script>
+
+<template>
+ <div class="max-w-3xl mx-auto px-4 sm:px-6 py-8 space-y-6">
+ <div class="flex items-center justify-between mb-2">
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
+ Incident History
+ </h1>
+ <router-link
+ to="/"
+ class="text-sm text-brand-500 hover:text-brand-600"
+ >
+ ← Back to status
+ </router-link>
+ </div>
+
+ <div class="flex gap-2">
+ <button
+ v-for="filter in FILTER_OPTIONS"
+ :key="filter.value"
+ :class="filterButtonClass(filter.value)"
+ class="px-4 py-2 rounded-md text-sm font-medium transition-colors"
+ @click="activeFilter = filter.value"
+ >
+ {{ filter.label }}
+ </button>
+ </div>
+
+ <!-- Loading -->
+ <div v-if="isLoading" class="flex justify-center py-20">
+ <div class="h-8 w-8 animate-spin rounded-full border-4 border-gray-200 border-t-brand-500"></div>
+ </div>
+
+ <template v-else>
+ <div v-if="filteredIncidents.length === 0" class="card p-6 text-center text-gray-500">
+ No incidents reported in this timeframe.
+ </div>
+
+ <div v-else class="space-y-3">
+ <IncidentTimeline
+ v-for="incident in filteredIncidents"
+ :key="incident.id"
+ :incident="incident"
+ />
+ </div>
+ </template>
+ </div>
+</template>
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Mar 1, 17:36 (17 h, 49 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3483013
Default Alt Text
D3980.id10328.diff (5 KB)
Attached To
Mode
D3980: Add incident history page with date filter
Attached
Detach File
Event Timeline
Log In to Comment