diff --git a/ui/src/__tests__/system-user-log-view.test.js b/ui/src/__tests__/system-user-log-view.test.js new file mode 100644 index 0000000..f84decc --- /dev/null +++ b/ui/src/__tests__/system-user-log-view.test.js @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { setActivePinia, createPinia } from 'pinia' +import { mount, flushPromises } from '@vue/test-utils' +import { VibrantDataTable } from '@ligoj/host' +import SystemUserLogView from '../views/SystemUserLogView.vue' + +// Heavy host chrome (page header, table) is stubbed — we only verify the +// view wires useDataTable('user-log') and hands the right headers to the +// table, and that a table option change drives a GET against the endpoint. +function mountView() { + return mount(SystemUserLogView, { + global: { stubs: { LjPageHeader: true, VibrantDataTable: true } }, + }) +} + +describe('SystemUserLogView', () => { + beforeEach(() => { + setActivePinia(createPinia()) + globalThis.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + recordsTotal: 1, + recordsFiltered: 1, + data: [{ id: 1, user: 'ligoj-admin', date: 1781632761013, message: 'Boom', url: '#/x' }], + }), + }) + }) + + it('mounts and renders the data table with the expected columns', () => { + const w = mountView() + const table = w.findComponent(VibrantDataTable) + expect(table.exists()).toBe(true) + expect(table.props('headers').map((h) => h.key)).toEqual(['date', 'user', 'message', 'url']) + expect(table.props('defaultSort')).toBe('date') + expect(table.props('defaultOrder')).toBe('desc') + }) + + it('queries rest/user-log (date desc) when the table requests options', async () => { + const w = mountView() + await w.findComponent(VibrantDataTable).vm.$emit('update:options', { page: 1, itemsPerPage: 25, sortBy: [] }) + await flushPromises() + + expect(globalThis.fetch).toHaveBeenCalledTimes(1) + const url = globalThis.fetch.mock.calls[0][0] + expect(url).toContain('rest/user-log') + expect(url).toContain('sidx=date') + expect(url).toContain('sord=desc') + }) +}) diff --git a/ui/src/i18n/en.js b/ui/src/i18n/en.js index a4c86c7..20ff1d5 100644 --- a/ui/src/i18n/en.js +++ b/ui/src/i18n/en.js @@ -55,6 +55,18 @@ export default { 'system.logs.error': 'Unable to load this log.', 'system.logs.lines': '{n} lines', 'system.logs.bottom': 'Scroll to bottom', + + // System → Information → User logs + 'system.userLog.title': 'User logs', + 'system.userLog.subtitle': 'Browser errors reported by users', + 'system.userLog.link': 'User logs', + 'system.userLog.headerDate': 'Date', + 'system.userLog.headerUser': 'User', + 'system.userLog.headerMessage': 'Message', + 'system.userLog.headerUrl': 'URL', + 'system.userLog.filterFrom': 'From', + 'system.userLog.filterTo': 'To', + 'system.info.system': 'System', 'system.info.memory': 'Memory', 'system.info.memoryUsed': 'Used', diff --git a/ui/src/i18n/fr.js b/ui/src/i18n/fr.js index b8a5b20..c55cb29 100644 --- a/ui/src/i18n/fr.js +++ b/ui/src/i18n/fr.js @@ -54,6 +54,18 @@ export default { 'system.logs.error': 'Impossible de charger ce journal.', 'system.logs.lines': '{n} lignes', 'system.logs.bottom': 'Aller en bas', + + // Système → Information → Journaux d'erreurs + 'system.userLog.title': "Journaux d'erreurs", + 'system.userLog.subtitle': 'Erreurs navigateur signalées par les utilisateurs', + 'system.userLog.link': "Journaux d'erreurs", + 'system.userLog.headerDate': 'Date', + 'system.userLog.headerUser': 'Utilisateur', + 'system.userLog.headerMessage': 'Message', + 'system.userLog.headerUrl': 'URL', + 'system.userLog.filterFrom': 'Du', + 'system.userLog.filterTo': 'Au', + 'system.info.system': 'Système', 'system.info.memory': 'Mémoire', 'system.info.memoryUsed': 'Utilisée', diff --git a/ui/src/index.js b/ui/src/index.js index 7fdbab7..91a3a32 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -58,6 +58,7 @@ import ManualView from './views/ManualView.vue' import SystemView from './views/SystemView.vue' import SystemInfoView from './views/SystemInfoView.vue' +import SystemUserLogView from './views/SystemUserLogView.vue' import ActuatorView from './views/ActuatorView.vue' import SystemConfigurationView from './views/SystemConfigurationView.vue' import SystemUserView from './views/SystemUserView.vue' @@ -91,6 +92,7 @@ const routes = [ // legacy SystemView reachable at `/system` so the path isn't a dead end. { path: '/system', name: 'ui-system', component: SystemView }, { path: '/system/information', name: 'ui-system-information', component: SystemInfoView }, + { path: '/system/information/user-logs', name: 'ui-system-user-logs', component: SystemUserLogView }, // Actuator browser, nested under Information; one route per endpoint, default `info`. { path: '/system/information/actuator', redirect: '/system/information/actuator/info' }, { path: '/system/information/actuator/:endpoint', name: 'ui-system-actuator', component: ActuatorView }, diff --git a/ui/src/views/SystemInfoView.vue b/ui/src/views/SystemInfoView.vue index b255726..7411001 100644 --- a/ui/src/views/SystemInfoView.vue +++ b/ui/src/views/SystemInfoView.vue @@ -14,6 +14,7 @@ diff --git a/ui/src/views/SystemUserLogView.vue b/ui/src/views/SystemUserLogView.vue new file mode 100644 index 0000000..4ba0f27 --- /dev/null +++ b/ui/src/views/SystemUserLogView.vue @@ -0,0 +1,129 @@ + + + + + +