Skip to content

Commit

Permalink
feat: ✨ add admin log page
Browse files Browse the repository at this point in the history
  • Loading branch information
ArnaudTA authored and clairenollet committed Jun 12, 2023
1 parent 7adccb9 commit 5125954
Show file tree
Hide file tree
Showing 25 changed files with 1,387 additions and 350 deletions.
26 changes: 26 additions & 0 deletions apps/client/cypress/e2e/specs/admin/logs.e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
describe('Administration logs', () => {
let logCount
let logs

beforeEach(() => {
cy.intercept('GET', 'api/v1/admin/logs/count').as('countLogs')
cy.intercept('GET', 'api/v1/admin/logs/0/5').as('getAllLogs')

cy.kcLogin('tcolin')
cy.visit('/admin/logs')
cy.url().should('contain', '/admin/logs')
cy.wait('@countLogs', { timeout: 10000 }).its('response').then(response => {
logCount = response?.body
})
cy.wait('@getAllLogs', { timeout: 10000 }).its('response').then(response => {
logs = response?.body
})
})

it('Should display logs list, loggedIn as admin', () => {
cy.getByDataTestid('logCountInfo').should('contain', `Total : ${logCount} logs`)
logs.forEach(log => {
cy.getByDataTestid(`${log.id}-json`)
})
})
})
4 changes: 3 additions & 1 deletion apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@
"shared": "workspace:^1.0.0",
"typescript": "^5.1.3",
"vue": "^3.3.4",
"vue-router": "^4.2.2"
"vue-router": "^4.2.2",
"vue3-json-viewer": "^2.2.2"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.1",
"@types/jsdom": "^21.1.1",
"@types/markdown-it-emoji": "^2.0.2",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/node": "^18.16.16",
"@unocss/transformer-directives": "^0.53.1",
"@vitejs/plugin-vue": "^4.2.3",
"@vitest/coverage-v8": "^0.32.0",
"@vue/eslint-config-typescript": "^11.0.3",
Expand Down
11 changes: 11 additions & 0 deletions apps/client/src/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,14 @@ export const getAllProjects = async () => {
const response = await apiClient.get('/admin/projects')
return response.data
}

// Admin - Logs
export const getAllLogs = async ({ offset, limit }) => {
const response = await apiClient.get(`/admin/logs/${offset}/${limit}`)
return response.data
}

export const countAllLogs = async () => {
const response = await apiClient.get('/admin/logs/count')
return response.data
}
1 change: 1 addition & 0 deletions apps/client/src/components/CIForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const prepareForDownload = async () => {
const copyContent = async (key) => {
try {
await navigator.clipboard.writeText(generatedCI.value[key])
snackbarStore.setMessage('Fichier copié', 'success')
} catch (error) {
snackbarStore.setMessage(error?.message, 'error')
}
Expand Down
11 changes: 10 additions & 1 deletion apps/client/src/components/SideMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function toggleExpand (key) {
}
watch(routePath, (routePath) => {
if (/projects*/.test(routePath)) {
if (/^\/projects*/.test(routePath)) {
isExpanded.value.projects = true
isExpanded.value.administration = false
return
Expand Down Expand Up @@ -232,6 +232,15 @@ onMounted(() => {
Liste des projets
</DsfrSideMenuLink>
</DsfrSideMenuListItem>
<DsfrSideMenuListItem>
<DsfrSideMenuLink
data-testid="menuAdministrationLogs"
:active="routeName === 'ListLogs'"
to="/admin/logs"
>
Liste des logs
</DsfrSideMenuLink>
</DsfrSideMenuListItem>
</DsfrSideMenuList>
</DsfrSideMenuListItem>

Expand Down
4 changes: 4 additions & 0 deletions apps/client/src/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ export {
RiArchiveFill,
RiHeartPulseLine,
RiLoader4Line,
RiArrowDropRightLine,
RiArrowDropLeftLine,
RiArrowDropLeftFill,
RiArrowDropRightFill,
} from 'oh-vue-icons/icons/ri/index.js'
3 changes: 3 additions & 0 deletions apps/client/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import '@gouvfr/dsfr/dist/dsfr.min.css'
import '@gouvfr/dsfr/dist/utility/icons/icons.min.css'
import '@gouvfr/dsfr/dist/utility/utility.main.min.css'
import '@gouvminint/vue-dsfr/styles'
import 'vue3-json-viewer/dist/index.css'

import { createApp } from 'vue'
import { createPinia } from 'pinia'
Expand All @@ -12,6 +13,7 @@ import VueDsfr from '@gouvminint/vue-dsfr'
import App from './App.vue'
import router from './router/index'
import * as icons from './icons'
import JsonViewer from 'vue3-json-viewer'

import 'virtual:uno.css'
import 'uno.css'
Expand All @@ -29,5 +31,6 @@ import './main.css'
.use(createPinia())
.use(router)
.use(VueDsfr, { icons: Object.values(icons) })
.use(JsonViewer)
.mount('#app')
})()
8 changes: 7 additions & 1 deletion apps/client/src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useUserStore } from '@/stores/user.js'
import { useProjectStore } from '@/stores/project.js'
import { useSnackbarStore } from '@/stores/snackbar.js'

import DsoHome from '@/views/DsoHome.vue'
const DsoHome = () => import('@/views/DsoHome.vue')
const ServicesHealth = () => import('@/views/ServicesHealth.vue')
const CreateProject = () => import('@/views/CreateProject.vue')
const ManageEnvironments = () => import('@/views/projects/ManageEnvironments.vue')
Expand All @@ -16,6 +16,7 @@ const DsoDoc = () => import('@/views/DsoDoc.vue')
const ListUser = () => import('@/views/admin/ListUser.vue')
const ListOrganizations = () => import('@/views/admin/ListOrganizations.vue')
const ListProjects = () => import('@/views/admin/ListProjects.vue')
const ListLogs = () => import('@/views/admin/ListLogs.vue')

const MAIN_TITLE = 'Console Cloud π Native'

Expand Down Expand Up @@ -101,6 +102,11 @@ const routes = [
name: 'ListProjects',
component: ListProjects,
},
{
path: '/admin/logs',
name: 'ListLogs',
component: ListLogs,
},
{
path: '/doc',
name: 'Doc',
Expand Down
17 changes: 17 additions & 0 deletions apps/client/src/stores/admin/log.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineStore } from 'pinia'
import api from '@/api/index.js'

export const useAdminLogStore = defineStore('admin-log', () => {
const getAllLogs = async ({ offset, limit } = { offset: 0, limit: 100 }) => {
return api.getAllLogs({ offset, limit })
}

const countAllLogs = async () => {
return api.countAllLogs()
}

return {
getAllLogs,
countAllLogs,
}
})
45 changes: 45 additions & 0 deletions apps/client/src/stores/admin/log.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { vi, describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { apiClient } from '../../api/xhr-client.js'
import { useAdminLogStore } from './log.js'

vi.spyOn(apiClient, 'get')
vi.spyOn(apiClient, 'post')
vi.spyOn(apiClient, 'put')
vi.spyOn(apiClient, 'patch')
vi.spyOn(apiClient, 'delete')

describe('Counter Store', () => {
beforeEach(() => {
vi.resetAllMocks()
// creates a fresh pinia and make it active so it's automatically picked
// up by any useStore() call without having to pass it to it: `useStore(pinia)`
setActivePinia(createPinia())
})

it('Should get organization list by api call', async () => {
const data = [
{ id: 'thisIsAnId', data: {}, action: 'Create Project', userId: 'cb8e5b4b-7b7b-40f5-935f-594f48ae6565' },
]
apiClient.get.mockReturnValueOnce(Promise.resolve({ data }))
const adminLogStore = useAdminLogStore()

const res = await adminLogStore.getAllLogs({ offset: 5, limit: 10 })

expect(res).toBe(data)
expect(apiClient.get).toHaveBeenCalledTimes(1)
expect(apiClient.get.mock.calls[0][0]).toBe('/admin/logs/5/10')
})

it('Should count logs by api call', async () => {
const data = 12
apiClient.get.mockReturnValueOnce(Promise.resolve({ data }))
const adminLogStore = useAdminLogStore()

const res = await adminLogStore.countAllLogs()

expect(res).toBe(data)
expect(apiClient.get).toHaveBeenCalledTimes(1)
expect(apiClient.get.mock.calls[0][0]).toBe('/admin/logs/count')
})
})
6 changes: 3 additions & 3 deletions apps/client/src/views/ServicesHealth.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ const services = computed(() => serviceStore.services)
const servicesHealth = computed(() => serviceStore.servicesHealth)
const checkServicesHealth = async () => {
isUpdating.value = ref(true)
isUpdating.value = true
await serviceStore.checkServicesHealth()
isUpdating.value = ref(false)
isUpdating.value = false
}
onBeforeMount(async () => {
Expand All @@ -37,7 +37,7 @@ onBeforeMount(async () => {
secondary
icon-only
icon="ri-refresh-fill"
:disabled="isUpdating.value === true"
:disabled="isUpdating === true"
@click="checkServicesHealth()"
/>
</div>
Expand Down
158 changes: 158 additions & 0 deletions apps/client/src/views/admin/ListLogs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useAdminLogStore } from '@/stores/admin/log.js'
import { useSnackbarStore } from '@/stores/snackbar.js'
import { JsonViewer } from 'vue3-json-viewer'
const adminLogStore = useAdminLogStore()
const snackbarStore = useSnackbarStore()
const step = 5
const isUpdating = ref(true)
const logs = ref(undefined)
const logsLength = ref(0)
const logsPagination = ref({
offset: 0,
limit: step,
})
const showLogs = async (key) => {
if (key === 'first' ||
(key === 'previous' &&
(logsPagination.value.offset < 0 ||
logsPagination.value.limit < step))
) {
logsPagination.value.offset = 0
logsPagination.value.limit = step
} else if (key === 'previous') {
logsPagination.value.offset -= step
logsPagination.value.limit -= step
} else if (key === 'last' ||
(key === 'next' &&
logsPagination.value.offset >= logsLength.value - step)) {
logsPagination.value.offset = logsLength.value - step
logsPagination.value.limit = logsLength.value
} else {
logsPagination.value.offset += step
logsPagination.value.limit += step
}
await getAllLogs({ offset: logsPagination.value.offset, limit: logsPagination.value.limit })
}
const getAllLogs = async ({ offset, limit }, isDisplayingSuccess = true) => {
isUpdating.value = true
try {
logs.value = await adminLogStore.getAllLogs({ offset, limit })
if (isDisplayingSuccess) {
snackbarStore.setMessage('Logs récupérés avec succès', 'success')
}
} catch (error) {
snackbarStore.setMessage(error?.message, 'error')
}
isUpdating.value = false
}
const refreshLogs = async ({ offset, limit }) => {
logsLength.value = await adminLogStore.countAllLogs()
await getAllLogs({ offset, limit })
}
onMounted(async () => {
await getAllLogs({ offset: logsPagination.value.offset, limit: logsPagination.value.limit }, false)
logsLength.value = await adminLogStore.countAllLogs()
})
</script>
<template>
<h1
class="fr-h3"
>
Logs des services associés à la chaîne DSO
</h1>
<div
class="flex justify-between"
>
<DsfrAlert
v-if="!isUpdating"
:description="!logsLength ? 'Aucun logs en base de donnée.' : `Total : ${logsLength} logs`"
data-testid="logCountInfo"
type="info"
small
/>
<DsfrButton
data-testid="refresh-btn"
title="Renouveler l'appel"
secondary
icon-only
icon="ri-refresh-fill"
:disabled="isUpdating === true"
@click="refreshLogs({ offset: logsPagination.offset, limit: logsPagination.limit })"
/>
</div>
<JsonViewer
v-for="log in logs"
:key="log.id"
:data-testid="`${log.id}-json`"
:value="log"
class="log-box"
copyable
boxed
/>
<div
class="flex justify-between"
>
<div
class="flex gap-2"
>
<DsfrButton
title="voir les premiers logs"
secondary
icon-only
:disabled="isUpdating === true || logsPagination.offset <= 0"
icon="ri-arrow-drop-left-fill"
@click="showLogs('first')"
/>
<DsfrButton
title="voir les logs précédents"
secondary
icon-only
:disabled="isUpdating === true || logsPagination.offset <= 0"
icon="ri-arrow-drop-left-line"
@click="showLogs('previous')"
/>
</div>
<div
class="flex gap-2"
>
<DsfrButton
title="voir les logs suivants"
secondary
icon-only
:disabled="isUpdating === true || logsPagination.offset >= logsLength - step"
icon="ri-arrow-drop-right-line"
@click="showLogs('next')"
/>
<DsfrButton
title="voir les derniers logs"
secondary
icon-only
:disabled="isUpdating === true || logsPagination.offset >= logsLength - step"
icon="ri-arrow-drop-right-fill"
@click="showLogs('last')"
/>
</div>
</div>
</template>
<style>
.log-box.jv-container span.jv-item.jv-object,
.log-box.jv-container span.jv-key {
color: var(--text-default-grey);
}
.log-box.jv-container {
@apply my-6;
background-color: var(--background-default-grey);
}
</style>
Loading

0 comments on commit 5125954

Please sign in to comment.