Feat log calendar (#819)
This commit is contained in:
commit
92b951d582
|
|
@ -34,6 +34,7 @@
|
|||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.26",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -488,6 +488,13 @@ export declare type GetLogsRequest = Message<"olivetin.api.v1.GetLogsRequest"> &
|
|||
* @generated from field: int64 start_offset = 1;
|
||||
*/
|
||||
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
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<Section :title="t('logs.title')" :padding="false">
|
||||
<template #toolbar>
|
||||
<router-link to="/logs/calendar" class="button neutral">
|
||||
{{ t('logs.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>{{ 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>
|
||||
<router-link to="/">{{ t('return-to-index') }}</router-link>
|
||||
</div>
|
||||
|
|
@ -64,27 +87,50 @@
|
|||
</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
|
||||
}
|
||||
// Re-fetch logs when date changes
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
// Watch for route changes to update date filter
|
||||
watch(() => route.query.date, () => {
|
||||
updateDateFromRoute()
|
||||
})
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logs.value
|
||||
|
||||
// Date filtering is now done server-side, so we only need to filter by search text
|
||||
if (searchText.value) {
|
||||
const searchLower = searchText.value.toLowerCase()
|
||||
result = logs.value.filter(log =>
|
||||
result = result.filter(log =>
|
||||
log.actionTitle.toLowerCase().includes(searchLower)
|
||||
)
|
||||
}
|
||||
|
|
@ -106,6 +152,11 @@ async function fetchLogs() {
|
|||
"startOffset": BigInt(startOffset),
|
||||
}
|
||||
|
||||
// Add date filter if selected
|
||||
if (selectedDate.value) {
|
||||
args.dateFilter = selectedDate.value
|
||||
}
|
||||
|
||||
const response = await window.client.getLogs(args)
|
||||
|
||||
logs.value = response.logs
|
||||
|
|
@ -123,6 +174,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 {
|
||||
|
|
@ -141,9 +210,11 @@ function handlePageChange(page) {
|
|||
function handlePageSizeChange(newPageSize) {
|
||||
pageSize.value = newPageSize
|
||||
currentPage.value = 1 // Reset to first page
|
||||
fetchLogs()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateDateFromRoute()
|
||||
fetchLogs()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -227,4 +298,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>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,11 @@
|
|||
"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": "Kalender",
|
||||
"logs.calendar-title": "Protokoll-Kalender",
|
||||
"logs.clear-date-filter": "Datumsfilter löschen",
|
||||
"logs.clear-filter": "Suchfilter löschen",
|
||||
"logs.completed": "Abgeschlossen",
|
||||
"logs.exit-code": "Ausführungscode",
|
||||
|
|
@ -73,7 +77,11 @@
|
|||
"language-dialog.title": "Select Language",
|
||||
"login-button": "Login",
|
||||
"logs.action": "Action",
|
||||
"logs.back-to-list": "Back to List",
|
||||
"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.completed": "Completed",
|
||||
"logs.exit-code": "Exit code",
|
||||
|
|
@ -119,7 +127,11 @@
|
|||
"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": "Calendario",
|
||||
"logs.calendar-title": "Calendario de Registros",
|
||||
"logs.clear-date-filter": "Limpiar filtro de fecha",
|
||||
"logs.clear-filter": "Limpiar filtro de búsqueda",
|
||||
"logs.completed": "Completado",
|
||||
"logs.exit-code": "Código de salida",
|
||||
|
|
@ -165,7 +177,11 @@
|
|||
"language-dialog.title": "Seleziona lingua",
|
||||
"login-button": "Login",
|
||||
"logs.action": "Azione",
|
||||
"logs.back-to-list": "Torna all'Elenco",
|
||||
"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.completed": "Completato",
|
||||
"logs.exit-code": "Codice di uscita",
|
||||
|
|
@ -211,7 +227,11 @@
|
|||
"language-dialog.title": "选择语言",
|
||||
"login-button": "登录",
|
||||
"logs.action": "动作",
|
||||
"logs.back-to-list": "返回列表",
|
||||
"logs.blocked": "阻塞",
|
||||
"logs.calendar": "日历",
|
||||
"logs.calendar-title": "日志日历",
|
||||
"logs.clear-date-filter": "清除日期筛选器",
|
||||
"logs.clear-filter": "清除搜索筛选器",
|
||||
"logs.completed": "完成",
|
||||
"logs.exit-code": "退出代码",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ translations:
|
|||
logs.exit-code: Ausführungscode
|
||||
logs.completed: Abgeschlossen
|
||||
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-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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ translations:
|
|||
logs.exit-code: Exit code
|
||||
logs.completed: Completed
|
||||
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-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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ translations:
|
|||
logs.exit-code: Código de salida
|
||||
logs.completed: Completado
|
||||
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-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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ translations:
|
|||
logs.exit-code: Codice di uscita
|
||||
logs.completed: Completato
|
||||
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-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
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ translations:
|
|||
logs.exit-code: 退出代码
|
||||
logs.completed: 完成
|
||||
logs.clear-filter: 清除搜索筛选器
|
||||
logs.clear-date-filter: 清除日期筛选器
|
||||
logs.calendar: 日历
|
||||
logs.calendar-title: 日志日历
|
||||
logs.back-to-list: 返回列表
|
||||
diagnostics.get-support: 获取支持
|
||||
diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含 sosreport 将非常有帮助。
|
||||
diagnostics.where-to-find-help: 在哪里找到帮助
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ message StartActionByGetAndWaitResponse {
|
|||
|
||||
message GetLogsRequest{
|
||||
int64 start_offset = 1;
|
||||
string date_filter = 2; // Optional date filter in YYYY-MM-DD format
|
||||
};
|
||||
|
||||
message LogEntry {
|
||||
|
|
|
|||
|
|
@ -1104,6 +1104,7 @@ func (x *StartActionByGetAndWaitResponse) GetLogEntry() *LogEntry {
|
|||
type GetLogsRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
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
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -1145,6 +1146,13 @@ func (x *GetLogsRequest) GetStartOffset() int64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
func (x *GetLogsRequest) GetDateFilter() string {
|
||||
if x != nil {
|
||||
return x.DateFilter
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
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" +
|
||||
"\taction_id\x18\x01 \x01(\tR\bactionId\"Y\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" +
|
||||
"\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" +
|
||||
"\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" +
|
||||
"\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" +
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -499,7 +499,11 @@ func (api *oliveTinAPI) GetLogs(ctx ctx.Context, req *connect.Request[apiv1.GetL
|
|||
}
|
||||
|
||||
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 {
|
||||
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,12 +234,27 @@ func isLogEntryAllowedByACL(cfg *config.Config, user *authpublic.AuthenticatedUs
|
|||
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()
|
||||
defer e.logmutex.RUnlock()
|
||||
|
||||
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 {
|
||||
entry := e.logs[trackingId]
|
||||
|
||||
|
|
@ -247,6 +262,13 @@ func (e *Executor) filterLogsByACL(cfg *config.Config, user *authpublic.Authenti
|
|||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -280,8 +302,9 @@ func paginateFilteredLogs(filtered []*InternalLogEntry, startOffset int64, pageC
|
|||
|
||||
// GetLogTrackingIdsACL returns logs filtered by ACL visibility for the user and
|
||||
// paginated correctly based on the filtered set.
|
||||
func (e *Executor) GetLogTrackingIdsACL(cfg *config.Config, user *authpublic.AuthenticatedUser, startOffset int64, pageCount int64) ([]*InternalLogEntry, *PagingResult) {
|
||||
filtered := e.filterLogsByACL(cfg, user)
|
||||
// dateFilter is optional and should be in YYYY-MM-DD format. If empty, no date filtering is applied.
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue