Feat log calendar (#819)

This commit is contained in:
James Read 2026-01-11 08:59:19 +00:00 committed by GitHub
commit 92b951d582
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 4770 additions and 12 deletions

View File

@ -34,6 +34,7 @@
"standard": "^17.1.2", "standard": "^17.1.2",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^30.0.0",
"vite": "^7.3.1", "vite": "^7.3.1",
"vue": "^3.5.26",
"vue-i18n": "^11.2.8", "vue-i18n": "^11.2.8",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
} }

View File

@ -488,6 +488,13 @@ export declare type GetLogsRequest = Message<"olivetin.api.v1.GetLogsRequest"> &
* @generated from field: int64 start_offset = 1; * @generated from field: int64 start_offset = 1;
*/ */
startOffset: bigint; startOffset: bigint;
/**
* Optional date filter in YYYY-MM-DD format
*
* @generated from field: string date_filter = 2;
*/
dateFilter: string;
}; };
/** /**

File diff suppressed because one or more lines are too long

View File

@ -35,6 +35,18 @@ const routes = [
icon: LeftToRightListDashIcon 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', path: '/entities',
name: 'Entities', name: 'Entities',

View File

@ -0,0 +1,168 @@
<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 {
// Currently fetches only the default page (startOffset: 0)
// Multi-page fetching: loop through pages until no more logs or limit reached
const allLogs = []
let startOffset = BigInt(0)
const maxLogs = 10000 // Reasonable limit to prevent excessive API calls
const pageSize = 100 // Typical page size, will be updated from API response
while (allLogs.length < maxLogs) {
const args = {
"startOffset": startOffset,
}
const response = await window.client.getLogs(args)
const pageLogs = response.logs || []
// If no logs returned, we've reached the end
if (pageLogs.length === 0) {
break
}
// Append logs from this page
allLogs.push(...pageLogs)
// Update offset for next page
const currentPageSize = Number(response.pageSize) || pageLogs.length
startOffset += BigInt(currentPageSize)
// If we got fewer logs than the page size, we've reached the end
if (pageLogs.length < currentPageSize) {
break
}
}
logs.value = allLogs
} 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 using local date components to avoid timezone issues
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({ path: '/logs', query: { 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;
}
@media (prefers-color-scheme: dark) {
:deep(div.calendar-header-nav) {
background-color: var(--bg, #111);
color: var(--text-color, #fff);
border-color: var(--border-color, #333);
}
:deep(div.calendar-header-nav h2.calendar-title) {
color: #fff !important;
}
}
</style>

View File

@ -1,6 +1,9 @@
<template> <template>
<Section :title="t('logs.title')" :padding="false"> <Section :title="t('logs.title')" :padding="false">
<template #toolbar> <template #toolbar>
<router-link to="/logs/calendar" class="button neutral">
{{ t('logs.calendar') }}
</router-link>
<label class="input-with-icons"> <label class="input-with-icons">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path fill="currentColor" <path fill="currentColor"
@ -21,7 +24,20 @@
<table class="logs-table"> <table class="logs-table">
<thead> <thead>
<tr> <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.action') }}</th>
<th>{{ t('logs.metadata') }}</th> <th>{{ t('logs.metadata') }}</th>
<th>{{ t('logs.status') }}</th> <th>{{ t('logs.status') }}</th>
@ -56,7 +72,14 @@
@page-size-change="handlePageSizeChange" itemTitle="execution logs" /> @page-size-change="handlePageSizeChange" itemTitle="execution logs" />
</div> </div>
<div v-show="logs.length === 0" class="empty-state"> <div v-show="selectedDate && filteredLogs.length === 0" class="empty-state">
<p>{{ t('logs.no-logs-to-display') }} {{ formatDateFilter(selectedDate) }}.</p>
<button @click="clearDateFilter" class="button neutral">
{{ t('logs.clear-date-filter') }}
</button>
</div>
<div v-show="logs.length === 0 && !selectedDate" class="empty-state">
<p>{{ t('logs.no-logs-to-display') }}</p> <p>{{ t('logs.no-logs-to-display') }}</p>
<router-link to="/">{{ t('return-to-index') }}</router-link> <router-link to="/">{{ t('return-to-index') }}</router-link>
</div> </div>
@ -64,27 +87,50 @@
</template> </template>
<script setup> <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 Pagination from 'picocrank/vue/components/Pagination.vue'
import Section from 'picocrank/vue/components/Section.vue' import Section from 'picocrank/vue/components/Section.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import ActionStatusDisplay from '../components/ActionStatusDisplay.vue' import ActionStatusDisplay from '../components/ActionStatusDisplay.vue'
const route = useRoute()
const router = useRouter()
const logs = ref([]) const logs = ref([])
const searchText = ref('') const searchText = ref('')
const pageSize = ref(10) const pageSize = ref(10)
const currentPage = ref(1) const currentPage = ref(1)
const loading = ref(false) const loading = ref(false)
const totalCount = ref(0) const totalCount = ref(0)
const selectedDate = ref(null)
const { t } = useI18n() 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
}
// Re-fetch logs when date changes
fetchLogs()
}
// Watch for route changes to update date filter
watch(() => route.query.date, () => {
updateDateFromRoute()
})
const filteredLogs = computed(() => { const filteredLogs = computed(() => {
let result = logs.value let result = logs.value
// Date filtering is now done server-side, so we only need to filter by search text
if (searchText.value) { if (searchText.value) {
const searchLower = searchText.value.toLowerCase() const searchLower = searchText.value.toLowerCase()
result = logs.value.filter(log => result = result.filter(log =>
log.actionTitle.toLowerCase().includes(searchLower) log.actionTitle.toLowerCase().includes(searchLower)
) )
} }
@ -106,6 +152,11 @@ async function fetchLogs() {
"startOffset": BigInt(startOffset), "startOffset": BigInt(startOffset),
} }
// Add date filter if selected
if (selectedDate.value) {
args.dateFilter = selectedDate.value
}
const response = await window.client.getLogs(args) const response = await window.client.getLogs(args)
logs.value = response.logs logs.value = response.logs
@ -123,6 +174,24 @@ function clearSearch() {
searchText.value = '' 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) { function formatTimestamp(timestamp) {
if (!timestamp) return 'Unknown' if (!timestamp) return 'Unknown'
try { try {
@ -141,9 +210,11 @@ function handlePageChange(page) {
function handlePageSizeChange(newPageSize) { function handlePageSizeChange(newPageSize) {
pageSize.value = newPageSize pageSize.value = newPageSize
currentPage.value = 1 // Reset to first page currentPage.value = 1 // Reset to first page
fetchLogs()
} }
onMounted(() => { onMounted(() => {
updateDateFromRoute()
fetchLogs() fetchLogs()
}) })
</script> </script>
@ -227,4 +298,47 @@ onMounted(() => {
text-decoration: underline; 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>

View File

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

View File

@ -21,6 +21,10 @@ translations:
logs.exit-code: Ausführungscode logs.exit-code: Ausführungscode
logs.completed: Abgeschlossen logs.completed: Abgeschlossen
logs.clear-filter: Suchfilter löschen logs.clear-filter: Suchfilter löschen
logs.clear-date-filter: Datumsfilter löschen
logs.calendar: Kalender
logs.calendar-title: Protokoll-Kalender
logs.back-to-list: Zurück zur Liste
diagnostics.get-support: Unterstützung erhalten 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.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 diagnostics.where-to-find-help: Wo Sie Hilfe finden

View File

@ -21,6 +21,10 @@ translations:
logs.exit-code: Exit code logs.exit-code: Exit code
logs.completed: Completed logs.completed: Completed
logs.clear-filter: Clear search filter logs.clear-filter: Clear search filter
logs.clear-date-filter: Clear date filter
logs.calendar: Calendar
logs.calendar-title: Logs Calendar
logs.back-to-list: Back to List
diagnostics.get-support: Get support 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.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 diagnostics.where-to-find-help: Where to find help

View File

@ -21,6 +21,10 @@ translations:
logs.exit-code: Código de salida logs.exit-code: Código de salida
logs.completed: Completado logs.completed: Completado
logs.clear-filter: Limpiar filtro de búsqueda logs.clear-filter: Limpiar filtro de búsqueda
logs.clear-date-filter: Limpiar filtro de fecha
logs.calendar: Calendario
logs.calendar-title: Calendario de Registros
logs.back-to-list: Volver a la Lista
diagnostics.get-support: Obtener soporte 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.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 diagnostics.where-to-find-help: Dónde encontrar ayuda

View File

@ -21,6 +21,10 @@ translations:
logs.exit-code: Codice di uscita logs.exit-code: Codice di uscita
logs.completed: Completato logs.completed: Completato
logs.clear-filter: Cancella filtro di ricerca logs.clear-filter: Cancella filtro di ricerca
logs.clear-date-filter: Cancella filtro data
logs.calendar: Calendario
logs.calendar-title: Calendario dei Registri
logs.back-to-list: Torna all'Elenco
diagnostics.get-support: Ottenere supporto 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.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 diagnostics.where-to-find-help: Dove trovare aiuto

View File

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

View File

@ -120,6 +120,7 @@ message StartActionByGetAndWaitResponse {
message GetLogsRequest{ message GetLogsRequest{
int64 start_offset = 1; int64 start_offset = 1;
string date_filter = 2; // Optional date filter in YYYY-MM-DD format
}; };
message LogEntry { message LogEntry {

View File

@ -1104,6 +1104,7 @@ func (x *StartActionByGetAndWaitResponse) GetLogEntry() *LogEntry {
type GetLogsRequest struct { type GetLogsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
StartOffset int64 `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"` StartOffset int64 `protobuf:"varint,1,opt,name=start_offset,json=startOffset,proto3" json:"start_offset,omitempty"`
DateFilter string `protobuf:"bytes,2,opt,name=date_filter,json=dateFilter,proto3" json:"date_filter,omitempty"` // Optional date filter in YYYY-MM-DD format
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -1145,6 +1146,13 @@ func (x *GetLogsRequest) GetStartOffset() int64 {
return 0 return 0
} }
func (x *GetLogsRequest) GetDateFilter() string {
if x != nil {
return x.DateFilter
}
return ""
}
type LogEntry struct { type LogEntry struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
DatetimeStarted string `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"` DatetimeStarted string `protobuf:"bytes,1,opt,name=datetime_started,json=datetimeStarted,proto3" json:"datetime_started,omitempty"`
@ -3948,9 +3956,11 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
"\x1eStartActionByGetAndWaitRequest\x12\x1b\n" + "\x1eStartActionByGetAndWaitRequest\x12\x1b\n" +
"\taction_id\x18\x01 \x01(\tR\bactionId\"Y\n" + "\taction_id\x18\x01 \x01(\tR\bactionId\"Y\n" +
"\x1fStartActionByGetAndWaitResponse\x126\n" + "\x1fStartActionByGetAndWaitResponse\x126\n" +
"\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"3\n" + "\tlog_entry\x18\x01 \x01(\v2\x19.olivetin.api.v1.LogEntryR\blogEntry\"T\n" +
"\x0eGetLogsRequest\x12!\n" + "\x0eGetLogsRequest\x12!\n" +
"\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\"\x89\x05\n" + "\fstart_offset\x18\x01 \x01(\x03R\vstartOffset\x12\x1f\n" +
"\vdate_filter\x18\x02 \x01(\tR\n" +
"dateFilter\"\x89\x05\n" +
"\bLogEntry\x12)\n" + "\bLogEntry\x12)\n" +
"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" + "\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" + "\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +

File diff suppressed because it is too large Load Diff

View File

@ -499,7 +499,11 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetL
} }
ret := &apiv1.GetLogsResponse{} ret := &apiv1.GetLogsResponse{}
logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, api.cfg.LogHistoryPageSize) dateFilter := ""
if req.Msg.DateFilter != "" {
dateFilter = req.Msg.DateFilter
}
logEntries, paging := api.executor.GetLogTrackingIdsACL(api.cfg, user, req.Msg.StartOffset, api.cfg.LogHistoryPageSize, dateFilter)
for _, le := range logEntries { for _, le := range logEntries {
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user)) ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
} }

View File

@ -234,12 +234,27 @@ func isLogEntryAllowedByACL(cfg *config.Config, user *authpublic.AuthenticatedUs
return acl.IsAllowedLogs(cfg, user, entry.Binding.Action) return acl.IsAllowedLogs(cfg, user, entry.Binding.Action)
} }
func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser) []*InternalLogEntry { func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.AuthenticatedUser, dateFilter string) []*InternalLogEntry {
e.logmutex.RLock() e.logmutex.RLock()
defer e.logmutex.RUnlock() defer e.logmutex.RUnlock()
filtered := make([]*InternalLogEntry, 0, len(e.logsTrackingIdsByDate)) filtered := make([]*InternalLogEntry, 0, len(e.logsTrackingIdsByDate))
var filterDate time.Time
var hasDateFilter bool
if dateFilter != "" {
parsedDate, err := time.Parse("2006-01-02", dateFilter)
if err != nil {
log.WithFields(log.Fields{
"dateFilter": dateFilter,
"error": err,
}).Errorf("Failed to parse date filter, expected format YYYY-MM-DD")
} else {
filterDate = parsedDate
hasDateFilter = true
}
}
for _, trackingId := range e.logsTrackingIdsByDate { for _, trackingId := range e.logsTrackingIdsByDate {
entry := e.logs[trackingId] entry := e.logs[trackingId]
@ -247,6 +262,13 @@ func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.Authenti
continue continue
} }
if isLogEntryAllowedByACL(cfg, user, entry) { if isLogEntryAllowedByACL(cfg, user, entry) {
if hasDateFilter {
entryDate := entry.DatetimeStarted.UTC().Truncate(24 * time.Hour)
filterDateUTC := filterDate.UTC().Truncate(24 * time.Hour)
if !entryDate.Equal(filterDateUTC) {
continue
}
}
filtered = append(filtered, entry) filtered = append(filtered, entry)
} }
} }
@ -280,8 +302,9 @@ func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageC
// GetLogTrackingIdsACL returns logs filtered by ACL visibility for the user and // GetLogTrackingIdsACL returns logs filtered by ACL visibility for the user and
// paginated correctly based on the filtered set. // paginated correctly based on the filtered set.
func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.AuthenticatedUser, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) { // dateFilter is optional and should be in YYYY-MM-DD format. If empty, no date filtering is applied.
filtered := e.filterLogsByACL(cfg, user) func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.AuthenticatedUser, startOffset int64, pageCount int64, dateFilter string) ([]*InternalLogEntry, *PagingResult) {
filtered := e.filterLogsByACL(cfg, user, dateFilter)
return paginateFilteredLogs(filtered, startOffset, pageCount) return paginateFilteredLogs(filtered, startOffset, pageCount)
} }