Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions ui/src/__tests__/system-hook-view.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { mount, flushPromises } from '@vue/test-utils'
import { VibrantDataTable, VibrantConfirmDialog, useI18nStore } from '@ligoj/host'
import SystemHookView from '../views/SystemHookView.vue'
import enMessages from '../i18n/en.js'

// One server-paginated hook row (DataTables shape). `match` is the JSON
// string the view splits into the path / method columns.
const HOOK = {
id: 1, name: 'deploy', command: 'echo hi', workingDirectory: '/opt/app',
match: '{"path":"system/.*","method":"POST"}', inject: [], timeout: 10, delay: 0,
}

function jsonResponse(body) {
return {
ok: true, status: 200,
headers: { get: () => 'application/json' },
clone() { return jsonResponse(body) },
json: async () => body,
text: async () => JSON.stringify(body),
}
}

// Render-the-slot stubs so the header actions, table rows, dialogs and row
// menu are reachable from the test (Vuetify is externalized in the plugin
// build, hence the v-* stubs too).
const stubs = {
LjPageHeader: { template: '<div class="hdr"><slot name="subtitle" /><slot name="actions" /></div>' },
LjSearch: { template: '<input class="search" />' },
LjButton: { emits: ['click'], template: '<button class="ljbtn" @click="$emit(\'click\')"><slot /></button>' },
LjDialog: { props: ['modelValue'], template: '<div v-if="modelValue" class="ljdialog"><slot /><slot name="footer" /></div>' },
LjAvailabilityField: { template: '<div class="avail" />' },
VibrantConfirmDialog: { props: ['modelValue'], emits: ['confirm'], template: '<div v-if="modelValue" class="confirm"><slot /></div>' },
RowActionsCog: { template: '<div class="cog"><slot /></div>' },
VibrantDataTable: {
props: ['headers', 'items', 'itemsLength', 'loading', 'defaultSort'],
template: '<div class="vdt"><div v-for="it in items" :key="it.id" class="row"><slot name="actions" :item="it" /></div></div>',
},
'v-form': { template: '<form><slot /></form>' },
'v-select': true, 'v-text-field': true, 'v-combobox': true, 'v-icon': true,
}

function mountView() {
return mount(SystemHookView, { global: { stubs } })
}

describe('SystemHookView', () => {
beforeEach(() => {
setActivePinia(createPinia())
useI18nStore().merge(enMessages, 'en')
globalThis.fetch = vi.fn().mockResolvedValue(
jsonResponse({ recordsTotal: 1, recordsFiltered: 1, data: [HOOK] }),
)
})

it('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(['name', 'method', 'path', 'command', 'delay'])
expect(table.props('defaultSort')).toBe('name')
})

it('queries rest/system/hook (name asc) when the table requests options', async () => {
const w = mountView()
await w.findComponent(VibrantDataTable).vm.$emit('update:options', { page: 1, itemsPerPage: 25, sortBy: [] })
await flushPromises()

const url = globalThis.fetch.mock.calls[0][0]
expect(url).toContain('rest/system/hook')
expect(url).toContain('sidx=name')
})

it('opens the create dialog when clicking New', async () => {
const w = mountView()
expect(w.find('.ljdialog').exists()).toBe(false)
await w.find('.ljbtn').trigger('click')
expect(w.find('.ljdialog').exists()).toBe(true)
})

it('deletes a hook through the confirm dialog (DELETE on its id)', async () => {
const w = mountView()
// Populate the table, then open the row's delete confirm and confirm it.
await w.findComponent(VibrantDataTable).vm.$emit('update:options', { page: 1, itemsPerPage: 25, sortBy: [] })
await flushPromises()

await w.find('.cog button.danger').trigger('click')
await w.findComponent(VibrantConfirmDialog).vm.$emit('confirm')
await flushPromises()

const delCall = globalThis.fetch.mock.calls.find((c) => c[1]?.method === 'DELETE')
expect(delCall).toBeTruthy()
expect(delCall[0]).toBe('rest/system/hook/1')
})
})
33 changes: 33 additions & 0 deletions ui/src/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,39 @@ export default {
'system.config.sourcePrefix': 'Source: {source}',
'system.config.sourceOverridden': '{base} — overridden',

// System → Hooks
'system.hook.title': 'Hooks',
'system.hook.countLabel': 'hooks',
'system.hook.searchPlaceholder': 'Search hooks…',
'system.hook.new': 'New hook',
'system.hook.headerName': 'Name',
'system.hook.headerMethod': 'Method',
'system.hook.headerPath': 'Path',
'system.hook.headerCommand': 'Command',
'system.hook.headerDelay': 'Execution',
'system.hook.methodAll': 'ALL',
'system.hook.sync': 'Sync',
'system.hook.async': 'Async',
'system.hook.editTitle': 'Edit hook',
'system.hook.newTitle': 'New hook',
'system.hook.deleteTitle': 'Delete hook',
'system.hook.deleteConfirmBefore': 'Delete the hook ',
'system.hook.deleteConfirmAfter': '?',
'system.hook.fieldName': 'Name',
'system.hook.fieldMethod': 'Method',
'system.hook.fieldPath': 'Path (regular expression)',
'system.hook.fieldCommand': 'Command',
'system.hook.fieldWorkingDirectory': 'Working directory',
'system.hook.fieldInject': 'Injected values',
'system.hook.fieldTimeout': 'Timeout (s)',
'system.hook.fieldDelay': 'Delay (s)',
'system.hook.pathHint': 'Matched against the REST path, e.g. system/.*',
'system.hook.workingDirectoryHint': 'No whitespace allowed',
'system.hook.injectHint': 'Configuration / secret names to inject (press Enter to add)',
'system.hook.timeoutHint': 'Empty = default (ligoj.hook.timeout)',
'system.hook.delayHint': '0 = synchronous execution',
'system.hook.noSpace': 'Whitespace is not allowed',

// System → Cache
'system.cache.title': 'Caches',
'system.cache.headerName': 'Cache',
Expand Down
33 changes: 33 additions & 0 deletions ui/src/i18n/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,39 @@ export default {
'system.config.sourcePrefix': 'Source : {source}',
'system.config.sourceOverridden': '{base} — surchargée',

// Système → Hooks
'system.hook.title': 'Hooks',
'system.hook.countLabel': 'hooks',
'system.hook.searchPlaceholder': 'Rechercher des hooks…',
'system.hook.new': 'Nouveau hook',
'system.hook.headerName': 'Nom',
'system.hook.headerMethod': 'Méthode',
'system.hook.headerPath': 'Chemin',
'system.hook.headerCommand': 'Commande',
'system.hook.headerDelay': 'Exécution',
'system.hook.methodAll': 'TOUTES',
'system.hook.sync': 'Synchrone',
'system.hook.async': 'Asynchrone',
'system.hook.editTitle': 'Modifier le hook',
'system.hook.newTitle': 'Nouveau hook',
'system.hook.deleteTitle': 'Supprimer le hook',
'system.hook.deleteConfirmBefore': 'Supprimer le hook ',
'system.hook.deleteConfirmAfter': ' ?',
'system.hook.fieldName': 'Nom',
'system.hook.fieldMethod': 'Méthode',
'system.hook.fieldPath': 'Chemin (expression régulière)',
'system.hook.fieldCommand': 'Commande',
'system.hook.fieldWorkingDirectory': 'Répertoire de travail',
'system.hook.fieldInject': 'Valeurs injectées',
'system.hook.fieldTimeout': 'Délai d\'expiration (s)',
'system.hook.fieldDelay': 'Délai (s)',
'system.hook.pathHint': 'Comparé au chemin REST, ex. system/.*',
'system.hook.workingDirectoryHint': 'Aucun espace autorisé',
'system.hook.injectHint': 'Noms de configuration / secrets à injecter (Entrée pour ajouter)',
'system.hook.timeoutHint': 'Vide = valeur par défaut (ligoj.hook.timeout)',
'system.hook.delayHint': '0 = exécution synchrone',
'system.hook.noSpace': 'Les espaces ne sont pas autorisés',

// Système → Cache
'system.cache.title': 'Caches',
'system.cache.headerName': 'Cache',
Expand Down
2 changes: 2 additions & 0 deletions ui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ 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 SystemHookView from './views/SystemHookView.vue'
import SystemUserView from './views/SystemUserView.vue'
import SystemRoleView from './views/SystemRoleView.vue'
import SystemPluginView from './views/SystemPluginView.vue'
Expand Down Expand Up @@ -108,6 +109,7 @@ const routes = [
{ path: '/system/actuator', redirect: '/system/information/actuator/info' },
{ path: '/system/logs', redirect: '/system/information/actuator/logfile' },
{ path: '/system/configuration', name: 'ui-system-configuration', component: SystemConfigurationView },
{ path: '/system/hook', name: 'ui-system-hook', component: SystemHookView },
{ path: '/system/user', name: 'ui-system-user', component: SystemUserView },
{ path: '/system/role', name: 'ui-system-role', component: SystemRoleView },
{ path: '/system/plugin', name: 'ui-system-plugin', component: SystemPluginView },
Expand Down
Loading