diff --git a/service/src/main/java/org/kiwiproject/champagne/App.java b/service/src/main/java/org/kiwiproject/champagne/App.java index b6d4ab83..2c0d871a 100644 --- a/service/src/main/java/org/kiwiproject/champagne/App.java +++ b/service/src/main/java/org/kiwiproject/champagne/App.java @@ -32,6 +32,7 @@ import org.kiwiproject.champagne.resource.AuditRecordResource; import org.kiwiproject.champagne.resource.AuthResource; import org.kiwiproject.champagne.resource.BuildResource; +import org.kiwiproject.champagne.resource.DeployableSystemResource; import org.kiwiproject.champagne.resource.DeploymentEnvironmentResource; import org.kiwiproject.champagne.resource.HostConfigurationResource; import org.kiwiproject.champagne.resource.TaskResource; @@ -116,6 +117,7 @@ public void run(AppConfig configuration, Environment environment) { environment.jersey().register(new TaskResource(releaseDao, releaseStatusDao, taskDao, taskStatusDao, deploymentEnvironmentDao, auditRecordDao, errorDao)); environment.jersey().register(new UserResource(userDao, auditRecordDao, errorDao)); environment.jersey().register(new ApplicationErrorResource(errorDao)); + environment.jersey().register(new DeployableSystemResource(deployableSystemDao, userDao, auditRecordDao, errorDao)); configureCors(environment); diff --git a/ui/src/layouts/MainLayout.vue b/ui/src/layouts/MainLayout.vue index b029e829..8513be0b 100644 --- a/ui/src/layouts/MainLayout.vue +++ b/ui/src/layouts/MainLayout.vue @@ -44,8 +44,8 @@ show-if-above :mini="miniState" - @mouseover="miniState.value = false" - @mouseout="miniState.value = true" + @mouseover="miniState = false" + @mouseout="miniState = true" bordered :width="200" @@ -117,6 +117,8 @@ + + @@ -125,6 +127,15 @@ Audit + + + + + + + Systems + + diff --git a/ui/src/pages/AdminSystemPage.vue b/ui/src/pages/AdminSystemPage.vue new file mode 100644 index 00000000..d03f4557 --- /dev/null +++ b/ui/src/pages/AdminSystemPage.vue @@ -0,0 +1,116 @@ + + + + + + + {{ fromNow(props.row.createdAt) }} + + {{ formatDate(props.row.createdAt) }} + + + + + + + {{ fromNow(props.row.updatedAt) }} + + {{ formatDate(props.row.updatedAt) }} + + + + + + + + Delete System + + + + + + + + + + + + + + Add new deployable system + + + + + + + + Cancel + Save + + + + + + + diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js index 449d8344..dce630c0 100644 --- a/ui/src/router/routes.js +++ b/ui/src/router/routes.js @@ -12,7 +12,8 @@ const routes = [ { path: '/tasks', name: 'ManualTaskPage', component: () => import('pages/ManualTaskPage.vue') }, { path: '/builds', name: 'BuildsPage', component: () => import('pages/BuildsPage.vue') }, { path: '/hostConfig', name: 'HostConfigurationPage', component: () => import('pages/HostConfigurationPage.vue') }, - { path: '/errors', name: 'ErrorsPage', component: () => import('pages/ErrorsPage.vue') } + { path: '/errors', name: 'ErrorsPage', component: () => import('pages/ErrorsPage.vue') }, + { path: '/adminSystems', name: 'AdminSystemPage', component: () => import('pages/AdminSystemPage.vue') } ] }, diff --git a/ui/src/stores/adminSystemStore.js b/ui/src/stores/adminSystemStore.js new file mode 100644 index 00000000..07c1ff0a --- /dev/null +++ b/ui/src/stores/adminSystemStore.js @@ -0,0 +1,30 @@ +import { defineStore } from 'pinia' +import { api } from 'src/boot/axios' +import { ref } from 'vue' +import { doPagedRequest } from 'src/utils/data' + +export const useAdminSystemStore = defineStore('adminSystem', () => { + const systems = ref([]) + const loading = ref(false) + const pagination = ref({ + page: 1, + rowsPerPage: 25, + rowsNumber: 0 + }) + + async function load (props) { + doPagedRequest(loading, props, pagination, '/systems/admin', systems) + } + + function create (systemData) { + return api.post('/systems', systemData) + .then(() => load()) + } + + function deleteSystem (id) { + api.delete(`/systems/${id}`) + .then(() => load()) + } + + return { systems, loading, load, create, deleteSystem } +}) diff --git a/ui/src/stores/systemStore.js b/ui/src/stores/systemStore.js new file mode 100644 index 00000000..8f705c45 --- /dev/null +++ b/ui/src/stores/systemStore.js @@ -0,0 +1,19 @@ +import { defineStore } from 'pinia' +import { api } from 'src/boot/axios' +import { ref } from 'vue' + +export const useSystemStore = defineStore('system', () => { + const systems = ref([]) + const loading = ref(false) + const currentSystem = ref(null) + + async function load () { + loading.value = true + + const response = await api.get('/systems') + systems.value = response.data + loading.value = false + } + + return { systems, loading, currentSystem, load } +}) diff --git a/ui/test/vitest/__tests__/pages/AdminSystemPage.spec.js b/ui/test/vitest/__tests__/pages/AdminSystemPage.spec.js new file mode 100644 index 00000000..eed28efe --- /dev/null +++ b/ui/test/vitest/__tests__/pages/AdminSystemPage.spec.js @@ -0,0 +1,61 @@ +import { installQuasar } from '@quasar/quasar-app-extension-testing-unit-vitest' +import { shallowMount } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import AdminSystemPage from 'pages/AdminSystemPage.vue' +import { createTestingPinia } from '@pinia/testing' +import { useAdminSystemStore } from 'src/stores/adminSystemStore' +import { confirmAction } from 'src/utils/alerts' + +installQuasar() + +vi.mock('src/utils/alerts', () => { + const confirmAction = vi.fn() + + return { confirmAction } +}) + +const wrapper = shallowMount(AdminSystemPage, { + global: { + plugins: [createTestingPinia()] + } +}) + +describe('AdminSystemPage', () => { + it('should mount the admin system page without errors', () => { + expect(wrapper).toBeTruthy() + }) +}) + +describe('createSystem', () => { + it('should call the admin system store to create a new system', () => { + const adminSystemStore = useAdminSystemStore() + adminSystemStore.create.mockImplementation(() => Promise.resolve(1)) + + wrapper.vm.activeSystem.name = 'kiwi' + + wrapper.vm.createSystem() + + expect(adminSystemStore.create).toHaveBeenLastCalledWith({ name: 'kiwi' }) + expect(wrapper.vm.showSystemAdd).toEqual(false) + }) +}) + +describe('deleteSystem', () => { + it('should call deleteSystem on the admin system store', () => { + const adminSystemStore = useAdminSystemStore() + + wrapper.vm.deleteSystem(1) + + expect(adminSystemStore.deleteSystem).toHaveBeenCalledTimes(1) + expect(adminSystemStore.deleteSystem).toHaveBeenLastCalledWith(1) + }) +}) + +describe('confirmDelete', () => { + it('should call confirm utility', () => { + wrapper.vm.confirmDelete({ name: 'kiwi', id: 1 }) + + expect(confirmAction).toHaveBeenCalled() + expect(confirmAction).toHaveBeenCalledWith('Are you sure you want to delete system kiwi? This will cause all related components to be deleted as well!', expect.any(Function)) + }) +}) diff --git a/ui/test/vitest/__tests__/stores/adminSystemStore.spec.js b/ui/test/vitest/__tests__/stores/adminSystemStore.spec.js new file mode 100644 index 00000000..372b513d --- /dev/null +++ b/ui/test/vitest/__tests__/stores/adminSystemStore.spec.js @@ -0,0 +1,53 @@ +import { createPinia, setActivePinia } from 'pinia' +import { useAdminSystemStore } from 'src/stores/adminSystemStore' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { api } from 'src/boot/axios' +import { doPagedRequest } from 'src/utils/data' + +vi.mock('src/utils/data', () => { + const doPagedRequest = vi.fn() + + return { doPagedRequest } +}) + +beforeEach(() => { + setActivePinia(createPinia()) + + vi.mock('src/boot/axios', () => { + const api = { + post: vi.fn().mockImplementation(() => Promise.resolve(true)), + delete: vi.fn().mockImplementation(() => Promise.resolve(true)) + } + + return { api } + }) +}) + +describe('load', () => { + it('should load systems', async () => { + const adminSystemStore = useAdminSystemStore() + await adminSystemStore.load() + + expect(doPagedRequest).toHaveBeenCalled() + }) +}) + +describe('create', () => { + it('should make call to create system', async () => { + const adminSystemStore = useAdminSystemStore() + await adminSystemStore.create({ name: 'kiwi' }) + + expect(api.post).toHaveBeenCalled() + expect(api.post).toHaveBeenCalledWith('/systems', { name: 'kiwi' }) + }) +}) + +describe('deleteSystem', () => { + it('should make call to delete system', async () => { + const adminSystemStore = useAdminSystemStore() + await adminSystemStore.deleteSystem(1) + + expect(api.delete).toHaveBeenCalled() + expect(api.delete).toHaveBeenCalledWith('/systems/1') + }) +}) diff --git a/ui/test/vitest/__tests__/stores/systemStore.spec.js b/ui/test/vitest/__tests__/stores/systemStore.spec.js new file mode 100644 index 00000000..7c495e82 --- /dev/null +++ b/ui/test/vitest/__tests__/stores/systemStore.spec.js @@ -0,0 +1,28 @@ +import { createPinia, setActivePinia } from 'pinia' +import { useSystemStore } from 'src/stores/systemStore' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { api } from 'src/boot/axios' + +beforeEach(() => { + setActivePinia(createPinia()) + + vi.mock('src/boot/axios', () => { + const api = { + get: vi.fn().mockImplementation(() => Promise.resolve({ data: [{ name: 'kiwi' }] })) + } + + return { api } + }) +}) + +describe('load', () => { + it('should load systems for user', async () => { + const systemStore = useSystemStore() + + await systemStore.load() + + expect(api.get).toHaveBeenCalled() + expect(api.get).toHaveBeenCalledWith('/systems') + expect(systemStore.systems).toEqual([{ name: 'kiwi' }]) + }) +})