diff --git a/ui/src/__tests__/system-task-view.test.js b/ui/src/__tests__/system-task-view.test.js new file mode 100644 index 0000000..93d2c86 --- /dev/null +++ b/ui/src/__tests__/system-task-view.test.js @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { nextTick } from 'vue' + +// Runner list returned by GET rest/system/task. +const RUNNERS = [ + { key: 'importCatalogResource', label: 'ImportCatalogStatus', type: 'node', stats: { total: 3, running: 1, succeeded: 1, failed: 1 } }, + { key: 'subRunner', label: 'SubStatus', type: 'subscription', stats: { total: 0, running: 0, succeeded: 0, failed: 0 } }, +] + +// Single shared fake data table so we can assert load() calls across the test. +const dtStub = { + items: { value: [] }, totalItems: { value: 0 }, loading: { value: false }, + search: { value: '' }, load: vi.fn(), loadAll: vi.fn(), +} +const useDataTableSpy = vi.fn(() => dtStub) + +vi.mock('@ligoj/host', () => ({ + useApi: () => ({ get: vi.fn().mockResolvedValue(RUNNERS) }), + useAppStore: () => ({ setBreadcrumbs: vi.fn() }), + useI18nStore: () => ({ t: (k) => k, locale: 'en' }), + useDataTable: (...args) => useDataTableSpy(...args), + NodeIcon: { name: 'NodeIcon', props: ['node'], template: '' }, + LjPageHeader: { template: '
' }, + LjDialog: { props: ['modelValue'], template: '
' }, + LjButton: { template: '' }, + LjSegmented: { name: 'LjSegmented', props: ['modelValue', 'options'], emits: ['update:modelValue'], template: '
' }, + VibrantDataTable: { name: 'VibrantDataTable', props: ['headers', 'items', 'itemsLength', 'loading', 'defaultSort', 'defaultOrder'], template: '
' }, +})) + +import SystemTaskView from '../views/SystemTaskView.vue' + +function mountView() { + return mount(SystemTaskView, { global: { stubs: { 'v-icon': true, 'v-progress-circular': true, 'v-tooltip': true } } }) +} + +describe('SystemTaskView', () => { + beforeEach(() => { + useDataTableSpy.mockClear() + dtStub.load.mockClear() + }) + + it('renders one card per runner, grouped, with stats', async () => { + const w = mountView() + await flushPromises() + const cards = w.findAll('.card') + expect(cards).toHaveLength(2) + // Two non-empty sections (node + subscription). + expect(w.findAll('.section')).toHaveLength(2) + // The node runner shows its total (3) and the succeeded segment is 1/3 wide. + expect(cards[0].text()).toContain('3') + const succeeded = cards[0].find('.seg.succeeded') + expect(succeeded.attributes('style')).toContain('33.3') + }) + + it('opens the dialog and wires useDataTable on the runner endpoint', async () => { + const w = mountView() + await flushPromises() + expect(w.find('.ljdialog').exists()).toBe(false) + + await w.findAll('.card')[0].trigger('click') + expect(w.find('.ljdialog').exists()).toBe(true) + expect(useDataTableSpy).toHaveBeenCalledTimes(1) + const [endpoint, opts] = useDataTableSpy.mock.calls[0] + expect(endpoint).toBe('system/task/importCatalogResource') + expect(opts.defaultSort).toBe('start') + expect(opts.defaultOrder).toBe('desc') + }) + + it('passes the status filter through extraParams and reloads', async () => { + const w = mountView() + await flushPromises() + await w.findAll('.card')[0].trigger('click') + + const opts = useDataTableSpy.mock.calls[0][1] + expect(opts.extraParams()).toEqual({}) + + // Change the status filter -> extraParams reflects it and the table reloads. + await w.findComponent({ name: 'LjSegmented' }).vm.$emit('update:modelValue', 'running') + await nextTick() + expect(opts.extraParams()).toEqual({ status: 'running' }) + expect(dtStub.load).toHaveBeenCalled() + }) +}) diff --git a/ui/src/i18n/en.js b/ui/src/i18n/en.js index 98cb783..1ad35e2 100644 --- a/ui/src/i18n/en.js +++ b/ui/src/i18n/en.js @@ -153,6 +153,32 @@ export default { 'system.config.sourcePrefix': 'Source: {source}', 'system.config.sourceOverridden': '{base} — overridden', + // System → Tasks + 'system.task.title': 'Tasks', + 'system.task.countLabel': 'task runners', + 'system.task.section.node': 'Node runners', + 'system.task.section.subscription': 'Subscription runners', + 'system.task.section.other': 'Other runners', + 'system.task.type.node': 'Node', + 'system.task.type.subscription': 'Subscription', + 'system.task.type.other': 'Other', + 'system.task.statTotal': 'total', + 'system.task.status.running': 'Running', + 'system.task.status.succeeded': 'Succeeded', + 'system.task.status.failed': 'Failed', + 'system.task.colStatus': 'Status', + 'system.task.colAuthor': 'Author', + 'system.task.colStart': 'Started', + 'system.task.colDuration': 'Duration', + 'system.task.colLocked': 'Locked entity', + 'system.task.filter.all': 'All', + 'system.task.noTask': 'No task', + 'system.task.stillRunning': 'Still running', + 'system.task.unit.d': 'd', + 'system.task.unit.h': 'h', + 'system.task.unit.m': 'min', + 'system.task.unit.s': 's', + // System → Cache 'system.cache.title': 'Caches', 'system.cache.headerName': 'Cache', diff --git a/ui/src/i18n/fr.js b/ui/src/i18n/fr.js index d21f0b2..6f77ce3 100644 --- a/ui/src/i18n/fr.js +++ b/ui/src/i18n/fr.js @@ -152,6 +152,32 @@ export default { 'system.config.sourcePrefix': 'Source : {source}', 'system.config.sourceOverridden': '{base} — surchargée', + // Système → Tâches + 'system.task.title': 'Tâches', + 'system.task.countLabel': 'exécuteurs de tâches', + 'system.task.section.node': 'Exécuteurs Nœud', + 'system.task.section.subscription': 'Exécuteurs Souscription', + 'system.task.section.other': 'Autres exécuteurs', + 'system.task.type.node': 'Nœud', + 'system.task.type.subscription': 'Souscription', + 'system.task.type.other': 'Autre', + 'system.task.statTotal': 'total', + 'system.task.status.running': 'En cours', + 'system.task.status.succeeded': 'Réussie', + 'system.task.status.failed': 'Échouée', + 'system.task.colStatus': 'Statut', + 'system.task.colAuthor': 'Auteur', + 'system.task.colStart': 'Début', + 'system.task.colDuration': 'Durée', + 'system.task.colLocked': 'Entité verrouillée', + 'system.task.filter.all': 'Toutes', + 'system.task.noTask': 'Aucune tâche', + 'system.task.stillRunning': 'Toujours en cours', + 'system.task.unit.d': 'j', + 'system.task.unit.h': 'h', + 'system.task.unit.m': 'min', + 'system.task.unit.s': 's', + // Système → Cache 'system.cache.title': 'Caches', 'system.cache.headerName': 'Cache', diff --git a/ui/src/index.js b/ui/src/index.js index 2b3f00a..02198f0 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -73,6 +73,7 @@ import SystemPluginView from './views/SystemPluginView.vue' import SystemNodeView from './views/SystemNodeView.vue' import SystemCacheView from './views/SystemCacheView.vue' import SystemBenchView from './views/SystemBenchView.vue' +import SystemTaskView from './views/SystemTaskView.vue' import ApiHomeView from './views/ApiHomeView.vue' import ApiTokenView from './views/ApiTokenView.vue' @@ -114,6 +115,7 @@ const routes = [ { path: '/system/node', name: 'ui-system-node', component: SystemNodeView }, { path: '/system/cache', name: 'ui-system-cache', component: SystemCacheView }, { path: '/system/bench', name: 'ui-system-bench', component: SystemBenchView }, + { path: '/system/task', name: 'ui-system-task', component: SystemTaskView }, { path: '/api', name: 'ui-api', component: ApiHomeView }, { path: '/api/token', name: 'ui-api-token', component: ApiTokenView }, diff --git a/ui/src/views/SystemTaskView.vue b/ui/src/views/SystemTaskView.vue new file mode 100644 index 0000000..5af3cab --- /dev/null +++ b/ui/src/views/SystemTaskView.vue @@ -0,0 +1,281 @@ + + + + + +