Feat log calendar (#819)
This commit is contained in:
commit
92b951d582
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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": "退出代码",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: 在哪里找到帮助
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue