Przeglądaj źródła

feat: Logs now have a calendar view

jamesread 5 miesięcy temu
rodzic
commit
2ffb8b0d81

+ 14 - 14
frontend/package-lock.json

@@ -17,10 +17,10 @@
 				"@xterm/addon-fit": "^0.11.0",
 				"@xterm/xterm": "^6.0.0",
 				"iconify-icon": "^3.0.2",
-				"picocrank": "^1.12.1",
+				"picocrank": "^1.12.5",
 				"standard": "^17.1.2",
 				"unplugin-vue-components": "^30.0.0",
-				"vite": "^7.3.0",
+				"vite": "^7.3.1",
 				"vue-i18n": "^11.2.8",
 				"vue-router": "^4.6.4"
 			},
@@ -2961,9 +2961,9 @@
 			}
 		},
 		"node_modules/femtocrank": {
-			"version": "2.4.12",
-			"resolved": "https://registry.npmjs.org/femtocrank/-/femtocrank-2.4.12.tgz",
-			"integrity": "sha512-X2a4WVG1ADGjQcULUyH2FJ4njJNZobfP+iPO1MpAEtWRyVEDxps6dmktbOb3igoyCxNObF0T0uKwUC7zV21C0A==",
+			"version": "2.5.0",
+			"resolved": "https://registry.npmjs.org/femtocrank/-/femtocrank-2.5.0.tgz",
+			"integrity": "sha512-plV1HNS/fUzohWJ349kuCBZ3TCfXz7V4F/sY2lVbVWtGXUV+aHxLG6IddAMEf64k2LJ8j0KVrj+nIIKepFaKvg==",
 			"license": "AGPL-3.0"
 		},
 		"node_modules/file-entry-cache": {
@@ -4607,17 +4607,17 @@
 			"license": "ISC"
 		},
 		"node_modules/picocrank": {
-			"version": "1.12.1",
-			"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.12.1.tgz",
-			"integrity": "sha512-2qcIcveWQkkA2Wyo+KQdZANTbjb/9ydzinbpNN/1U/4x0BBUjyHhWoK5lNAx/KDVNl6ZM3xGo3eMb5/n6xWoVA==",
+			"version": "1.12.5",
+			"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.12.5.tgz",
+			"integrity": "sha512-z0EP/I56cFGzvXV4EAEpkczYDYkdHGtRfHQA+k7rbrBEHMO1fi7qW8VbDj7/2eqeG6IbNqWRIG1IexRQWZj7bQ==",
 			"license": "ISC",
 			"dependencies": {
-				"@hugeicons/core-free-icons": "^3.1.0",
+				"@hugeicons/core-free-icons": "^3.1.1",
 				"@hugeicons/vue": "^1.0.4",
 				"@vitejs/plugin-vue": "^6.0.3",
-				"femtocrank": "^2.4.12",
+				"femtocrank": "^2.5.0",
 				"unplugin-vue-components": "^30.0.0",
-				"vite": "^7.3.0",
+				"vite": "^7.3.1",
 				"vue": "^3.5.26",
 				"vue-router": "^4.6.4"
 			}
@@ -6122,9 +6122,9 @@
 			}
 		},
 		"node_modules/vite": {
-			"version": "7.3.0",
-			"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
-			"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
+			"version": "7.3.1",
+			"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+			"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
 			"license": "MIT",
 			"dependencies": {
 				"esbuild": "^0.27.0",

+ 2 - 2
frontend/package.json

@@ -30,10 +30,10 @@
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
-		"picocrank": "^1.12.1",
+		"picocrank": "^1.12.5",
 		"standard": "^17.1.2",
 		"unplugin-vue-components": "^30.0.0",
-		"vite": "^7.3.0",
+		"vite": "^7.3.1",
 		"vue-i18n": "^11.2.8",
 		"vue-router": "^4.6.4"
 	}

+ 12 - 0
frontend/resources/vue/router.js

@@ -35,6 +35,18 @@ const routes = [
       icon: LeftToRightListDashIcon
     }
   },
+  {
+    path: '/logs/calendar',
+    name: 'LogsCalendar',
+    component: () => import('./views/LogsCalendarView.vue'),
+    meta: { 
+      title: 'Logs Calendar',
+      breadcrumb: [
+        { name: "Logs", href: "/logs" },
+        { name: "Calendar" },
+      ]
+    }
+  },
   {
     path: '/entities',
     name: 'Entities',

+ 130 - 0
frontend/resources/vue/views/LogsCalendarView.vue

@@ -0,0 +1,130 @@
+<template>
+  <Section :title="t('logs.calendar-title')" :padding="false">
+    <template #toolbar>
+      <router-link to="/logs" class="button neutral">
+        <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
+          <path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8l8 8l1.41-1.41L7.83 13H20z"/>
+        </svg>
+        {{ t('logs.back-to-list') }}
+      </router-link>
+    </template>
+
+    <div class="padding">
+      <Calendar
+        :events="calendarEvents"
+        :loading="loading"
+        :error="error"
+        :current-month="currentMonthIndex"
+        :current-year="currentYear"
+        @event-click="handleEventClick"
+        @date-click="handleDayClick"
+        @month-change="handleMonthChange"
+      />
+    </div>
+  </Section>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import Calendar from 'picocrank/vue/components/Calendar.vue'
+import Section from 'picocrank/vue/components/Section.vue'
+
+const router = useRouter()
+const { t } = useI18n()
+
+const logs = ref([])
+const loading = ref(false)
+const error = ref(null)
+const currentMonthIndex = ref(new Date().getMonth())
+const currentYear = ref(new Date().getFullYear())
+
+// Convert logs to calendar events format
+const calendarEvents = computed(() => {
+  return logs.value
+    .filter(log => {
+      // Only include logs with valid start dates
+      if (!log.datetimeStarted) return false
+      const startDate = new Date(log.datetimeStarted)
+      return !isNaN(startDate.getTime())
+    })
+    .map(log => {
+      const startDate = new Date(log.datetimeStarted)
+      let endDate = log.datetimeFinished ? new Date(log.datetimeFinished) : null
+      
+      // Validate end date
+      if (endDate && isNaN(endDate.getTime())) {
+        endDate = null
+      }
+
+      return {
+        id: log.executionTrackingId,
+        title: log.actionTitle || 'Untitled Action',
+        date: startDate,
+        startDate: startDate,
+        endDate: endDate,
+        actionIcon: log.actionIcon,
+        user: log.user,
+        tags: log.tags,
+        logEntry: log
+      }
+    })
+})
+
+async function fetchLogs() {
+  loading.value = true
+  error.value = null
+  
+  try {
+    // Fetch a large number of logs to populate the calendar
+    // We'll fetch more than a single page to get better calendar coverage
+    const args = {
+      "startOffset": BigInt(0),
+    }
+
+    const response = await window.client.getLogs(args)
+    logs.value = response.logs || []
+  } catch (err) {
+    console.error('Failed to fetch logs:', err)
+    error.value = 'Failed to load logs'
+    window.showBigError('fetch-logs-calendar', 'getting logs for calendar', err, false)
+  } finally {
+    loading.value = false
+  }
+}
+
+function handleEventClick(event) {
+  // Navigate to the execution view when clicking on a calendar event
+  if (event.id) {
+    router.push(`/logs/${event.id}`)
+  }
+}
+
+function handleDayClick(date) {
+  // Navigate to logs list filtered by the selected date
+  // Format date as YYYY-MM-DD for the query parameter
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  const dateString = `${year}-${month}-${day}`
+  router.push(`/logs?date=${dateString}`)
+}
+
+function handleMonthChange(month, year) {
+  currentMonthIndex.value = month
+  currentYear.value = year
+  // Optionally fetch logs for the new month if needed
+  // For now, we'll keep all logs loaded
+}
+
+onMounted(() => {
+  fetchLogs()
+})
+</script>
+
+<style scoped>
+.padding {
+  padding: 1rem;
+}
+</style>

+ 120 - 5
frontend/resources/vue/views/LogsListView.vue

@@ -1,6 +1,9 @@
 <template>
   <Section :title="t('logs.title')" :padding="false">
       <template #toolbar>
+        <router-link to="/logs/calendar" class="button neutral">
+          Calendar
+        </router-link>
         <label class="input-with-icons">
           <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
             <path fill="currentColor"
@@ -21,7 +24,20 @@
         <table class="logs-table">
           <thead>
             <tr>
-              <th>{{ t('logs.timestamp') }}</th>
+              <th>
+                <div class="timestamp-header">
+                  <span>{{ t('logs.timestamp') }}</span>
+                  <span v-if="selectedDate" class="date-filter-indicator">
+                    <span class="date-filter-text">{{ formatDateFilter(selectedDate) }}</span>
+                    <button :title="t('logs.clear-date-filter')" @click="clearDateFilter" class="clear-date-button">
+                      <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
+                        <path fill="currentColor"
+                          d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z" />
+                      </svg>
+                    </button>
+                  </span>
+                </div>
+              </th>
               <th>{{ t('logs.action') }}</th>
               <th>{{ t('logs.metadata') }}</th>
               <th>{{ t('logs.status') }}</th>
@@ -56,7 +72,14 @@
           @page-size-change="handlePageSizeChange" itemTitle="execution logs" />
       </div>
 
-      <div v-show="logs.length === 0" class="empty-state">
+      <div v-show="selectedDate && filteredLogs.length === 0" class="empty-state">
+        <p>No logs found for {{ formatDateFilter(selectedDate) }}.</p>
+        <button @click="clearDateFilter" class="button neutral">
+          Clear date filter
+        </button>
+      </div>
+
+      <div v-show="logs.length === 0 && !selectedDate" class="empty-state">
         <p>{{ t('logs.no-logs-to-display') }}</p>
         <router-link to="/">{{ t('return-to-index') }}</router-link>
       </div>
@@ -64,27 +87,57 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
 import Pagination from 'picocrank/vue/components/Pagination.vue'
 import Section from 'picocrank/vue/components/Section.vue'
 import { useI18n } from 'vue-i18n'
 import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
 
+const route = useRoute()
+const router = useRouter()
+
 const logs = ref([])
 const searchText = ref('')
 const pageSize = ref(10)
 const currentPage = ref(1)
 const loading = ref(false)
 const totalCount = ref(0)
+const selectedDate = ref(null)
 
 const { t } = useI18n()
 
+// Read date query parameter from route
+function updateDateFromRoute() {
+  const dateParam = route.query.date
+  if (dateParam) {
+    selectedDate.value = dateParam
+  } else {
+    selectedDate.value = null
+  }
+}
+
+// Watch for route changes to update date filter
+watch(() => route.query.date, () => {
+  updateDateFromRoute()
+})
+
 const filteredLogs = computed(() => {
   let result = logs.value
   
+  // Filter by selected date if present
+  if (selectedDate.value) {
+    result = result.filter(log => {
+      if (!log.datetimeStarted) return false
+      const logDate = new Date(log.datetimeStarted)
+      const logDateString = `${logDate.getFullYear()}-${String(logDate.getMonth() + 1).padStart(2, '0')}-${String(logDate.getDate()).padStart(2, '0')}`
+      return logDateString === selectedDate.value
+    })
+  }
+  
   if (searchText.value) {
     const searchLower = searchText.value.toLowerCase()
-    result = logs.value.filter(log =>
+    result = result.filter(log =>
       log.actionTitle.toLowerCase().includes(searchLower)
     )
   }
@@ -123,6 +176,24 @@ function clearSearch() {
   searchText.value = ''
 }
 
+function clearDateFilter() {
+  selectedDate.value = null
+  // Remove date query parameter from URL
+  const query = { ...route.query }
+  delete query.date
+  router.push({ path: route.path, query })
+}
+
+function formatDateFilter(dateString) {
+  // Format YYYY-MM-DD to a short format (e.g., "Jan 15, 2024")
+  try {
+    const date = new Date(dateString + 'T00:00:00')
+    return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
+  } catch (err) {
+    return dateString
+  }
+}
+
 function formatTimestamp(timestamp) {
   if (!timestamp) return 'Unknown'
   try {
@@ -144,6 +215,7 @@ function handlePageSizeChange(newPageSize) {
 }
 
 onMounted(() => {
+  updateDateFromRoute()
   fetchLogs()
 })
 </script>
@@ -227,4 +299,47 @@ onMounted(() => {
   text-decoration: underline;
 }
 
-</style>
+.timestamp-header {
+  display: flex;
+  flex-direction: column;
+  gap: 0.25rem;
+}
+
+.date-filter-indicator {
+  display: flex;
+  align-items: center;
+  gap: 0.25rem;
+  font-size: 0.75rem;
+  font-weight: normal;
+  color: var(--text-secondary, #666);
+  white-space: nowrap;
+}
+
+.date-filter-text {
+  font-style: italic;
+}
+
+.timestamp-header .clear-date-button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 0.125rem;
+  border-radius: 3px;
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  opacity: 0.7;
+  transition: opacity 0.2s;
+}
+
+.timestamp-header .clear-date-button:hover {
+  opacity: 1;
+  background: var(--hover-background, rgba(0, 0, 0, 0.05));
+}
+
+.timestamp-header .clear-date-button svg {
+  width: 0.75rem;
+  height: 0.75rem;
+}
+
+</style>

+ 10 - 0
lang/combined_output.json

@@ -27,7 +27,9 @@
             "language-dialog.title": "Sprache auswählen",
             "login-button": "Login",
             "logs.action": "Aktion",
+            "logs.back-to-list": "Zurück zur Liste",
             "logs.blocked": "Blockiert",
+            "logs.calendar-title": "Protokoll-Kalender",
             "logs.clear-filter": "Suchfilter löschen",
             "logs.completed": "Abgeschlossen",
             "logs.exit-code": "Ausführungscode",
@@ -73,7 +75,9 @@
             "language-dialog.title": "Select Language",
             "login-button": "Login",
             "logs.action": "Action",
+            "logs.back-to-list": "Back to List",
             "logs.blocked": "Blocked",
+            "logs.calendar-title": "Logs Calendar",
             "logs.clear-filter": "Clear search filter",
             "logs.completed": "Completed",
             "logs.exit-code": "Exit code",
@@ -119,7 +123,9 @@
             "language-dialog.title": "Seleccionar idioma",
             "login-button": "Iniciar sesión",
             "logs.action": "Acción",
+            "logs.back-to-list": "Volver a la Lista",
             "logs.blocked": "Bloqueado",
+            "logs.calendar-title": "Calendario de Registros",
             "logs.clear-filter": "Limpiar filtro de búsqueda",
             "logs.completed": "Completado",
             "logs.exit-code": "Código de salida",
@@ -165,7 +171,9 @@
             "language-dialog.title": "Seleziona lingua",
             "login-button": "Login",
             "logs.action": "Azione",
+            "logs.back-to-list": "Torna all'Elenco",
             "logs.blocked": "Bloccato",
+            "logs.calendar-title": "Calendario dei Registri",
             "logs.clear-filter": "Cancella filtro di ricerca",
             "logs.completed": "Completato",
             "logs.exit-code": "Codice di uscita",
@@ -211,7 +219,9 @@
             "language-dialog.title": "选择语言",
             "login-button": "登录",
             "logs.action": "动作",
+            "logs.back-to-list": "返回列表",
             "logs.blocked": "阻塞",
+            "logs.calendar-title": "日志日历",
             "logs.clear-filter": "清除搜索筛选器",
             "logs.completed": "完成",
             "logs.exit-code": "退出代码",

+ 2 - 0
lang/de-DE.yaml

@@ -21,6 +21,8 @@ translations:
   logs.exit-code: Ausführungscode
   logs.completed: Abgeschlossen
   logs.clear-filter: Suchfilter löschen
+  logs.calendar-title: Protokoll-Kalender
+  logs.back-to-list: Zurück zur Liste
   diagnostics.get-support: Unterstützung erhalten
   diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, einen sosreport von dieser Seite einzufügen.
   diagnostics.where-to-find-help: Wo Sie Hilfe finden

+ 2 - 0
lang/en.yaml

@@ -21,6 +21,8 @@ translations:
   logs.exit-code: Exit code
   logs.completed: Completed
   logs.clear-filter: Clear search filter
+  logs.calendar-title: Logs Calendar
+  logs.back-to-list: Back to List
   diagnostics.get-support: Get support
   diagnostics.get-support-description: If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include a sosreport from this page.
   diagnostics.where-to-find-help: Where to find help

+ 2 - 0
lang/es-ES.yaml

@@ -21,6 +21,8 @@ translations:
   logs.exit-code: Código de salida
   logs.completed: Completado
   logs.clear-filter: Limpiar filtro de búsqueda
+  logs.calendar-title: Calendario de Registros
+  logs.back-to-list: Volver a la Lista
   diagnostics.get-support: Obtener soporte
   diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir un sosreport de esta página.
   diagnostics.where-to-find-help: Dónde encontrar ayuda

+ 2 - 0
lang/it-IT.yaml

@@ -21,6 +21,8 @@ translations:
   logs.exit-code: Codice di uscita
   logs.completed: Completato
   logs.clear-filter: Cancella filtro di ricerca
+  logs.calendar-title: Calendario dei Registri
+  logs.back-to-list: Torna all'Elenco
   diagnostics.get-support: Ottenere supporto
   diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere un sosreport da questa pagina.
   diagnostics.where-to-find-help: Dove trovare aiuto

+ 2 - 0
lang/zh-Hans-CN.yaml

@@ -27,6 +27,8 @@ translations:
   logs.exit-code: 退出代码
   logs.completed: 完成
   logs.clear-filter: 清除搜索筛选器
+  logs.calendar-title: 日志日历
+  logs.back-to-list: 返回列表
   diagnostics.get-support: 获取支持
   diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含 sosreport 将非常有帮助。
   diagnostics.where-to-find-help: 在哪里找到帮助