feat : ajout de la lecture des logs symfony et docker (#3)
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Reviewed-on: #3 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #3.
This commit is contained in:
113
frontend/components/ui/LogModal.vue
Normal file
113
frontend/components/ui/LogModal.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: boolean
|
||||
title: string
|
||||
content: string
|
||||
loading?: boolean
|
||||
showLevelFilter?: boolean
|
||||
}>(), {
|
||||
loading: false,
|
||||
showLevelFilter: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'refresh', lines: number, level: string): void
|
||||
}>()
|
||||
|
||||
const selectedLines = ref(100)
|
||||
const selectedLevel = ref('')
|
||||
|
||||
const lineOptions = [50, 100, 500, 1000]
|
||||
|
||||
const copied = ref(false)
|
||||
|
||||
function refresh() {
|
||||
emit('refresh', selectedLines.value, selectedLevel.value)
|
||||
}
|
||||
|
||||
async function copyLogs() {
|
||||
if (!props.content) return
|
||||
await navigator.clipboard.writeText(props.content)
|
||||
copied.value = true
|
||||
setTimeout(() => { copied.value = false }, 2000)
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (open) {
|
||||
copied.value = false
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppModal
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
max-width="2xl"
|
||||
>
|
||||
<template #title>{{ title }}</template>
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-xs text-neutral-400">{{ t('logs.lines') }}</label>
|
||||
<select
|
||||
v-model="selectedLines"
|
||||
class="rounded-md border border-neutral-300 px-2 py-1 text-sm"
|
||||
@change="refresh"
|
||||
>
|
||||
<option v-for="n in lineOptions" :key="n" :value="n">{{ n }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="showLevelFilter" class="flex items-center gap-2">
|
||||
<label class="text-xs text-neutral-400">{{ t('logs.level') }}</label>
|
||||
<select
|
||||
v-model="selectedLevel"
|
||||
class="rounded-md border border-neutral-300 px-2 py-1 text-sm"
|
||||
@change="refresh"
|
||||
>
|
||||
<option value="">{{ t('logs.levelAll') }}</option>
|
||||
<option value="ERROR">ERROR</option>
|
||||
<option value="WARNING">WARNING</option>
|
||||
<option value="INFO">INFO</option>
|
||||
<option value="DEBUG">DEBUG</option>
|
||||
</select>
|
||||
</div>
|
||||
<MalioButtonIcon
|
||||
icon="mdi:refresh"
|
||||
:aria-label="t('logs.refresh')"
|
||||
icon-size="18"
|
||||
@click="refresh"
|
||||
/>
|
||||
</div>
|
||||
<MalioButtonIcon
|
||||
:icon="copied ? 'mdi:check' : 'mdi:content-copy'"
|
||||
:aria-label="t('logs.copy')"
|
||||
icon-size="18"
|
||||
:button-class="copied ? 'text-green-500' : ''"
|
||||
@click="copyLogs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
v-if="content"
|
||||
class="max-h-96 overflow-auto rounded-lg bg-neutral-900 p-4 text-xs text-green-400 font-mono whitespace-pre-wrap"
|
||||
>{{ content }}</pre>
|
||||
|
||||
<p v-else-if="!loading" class="text-center text-neutral-400 py-8">
|
||||
{{ t('logs.noContent') }}
|
||||
</p>
|
||||
|
||||
<template #footer>
|
||||
<MalioButton
|
||||
:label="t('applications.form.cancel')"
|
||||
variant="tertiary"
|
||||
@click="emit('update:modelValue', false)"
|
||||
/>
|
||||
</template>
|
||||
</AppModal>
|
||||
</template>
|
||||
@@ -2,9 +2,7 @@
|
||||
<NuxtLink
|
||||
:to="to"
|
||||
class="group/link relative flex items-center transition-colors hover:text-primary-500"
|
||||
:class="linkClasses"
|
||||
:active-class="exact ? '' : activeClass"
|
||||
:exact-active-class="exact ? activeClass : ''"
|
||||
:class="[linkClasses, isActive ? activeClass : '']"
|
||||
>
|
||||
<Icon :name="icon" :size="sub ? '20' : '24'" class="flex-shrink-0" />
|
||||
<span
|
||||
@@ -33,6 +31,15 @@ const props = defineProps<{
|
||||
exact?: boolean
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const isActive = computed(() => {
|
||||
if (props.exact) {
|
||||
return route.path === props.to
|
||||
}
|
||||
return route.path === props.to || route.path.startsWith(props.to + '/')
|
||||
})
|
||||
|
||||
const activeClass = computed(() => {
|
||||
if (props.collapsed) {
|
||||
return '!text-primary-500 bg-primary-500/10'
|
||||
|
||||
@@ -48,6 +48,17 @@
|
||||
"logout": "Déconnexion réussie."
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"docker": "Logs Docker",
|
||||
"symfony": "Logs Symfony",
|
||||
"lines": "Lignes",
|
||||
"level": "Niveau",
|
||||
"levelAll": "Tous",
|
||||
"refresh": "Actualiser",
|
||||
"noContent": "Aucun log disponible",
|
||||
"copy": "Copier les logs",
|
||||
"title": "Logs"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"description": "Vue d'ensemble du SI",
|
||||
@@ -56,7 +67,8 @@
|
||||
"running": "En ligne",
|
||||
"exited": "Arrete",
|
||||
"restarting": "Redemarrage",
|
||||
"not_found": "Introuvable"
|
||||
"not_found": "Introuvable",
|
||||
"unavailable": "Docker indisponible"
|
||||
}
|
||||
},
|
||||
"applications": {
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { DeployResult } from '~/services/dto/deploy'
|
||||
import { getAvailableTags, deploy } from '~/services/deploy'
|
||||
import type { EnvironmentHealth } from '~/services/dto/dashboard'
|
||||
import { getEnvironmentHealth } from '~/services/dashboard'
|
||||
import type { LogOutput } from '~/services/dto/logs'
|
||||
import { getDockerLogs, getSymfonyLog } from '~/services/logs'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
@@ -29,6 +31,15 @@ const deployResult = ref<DeployResult | null>(null)
|
||||
const healthByEnvId = ref<Record<number, EnvironmentHealth>>({})
|
||||
const loadingHealth = ref(false)
|
||||
|
||||
// Log modals
|
||||
const showLogModal = ref(false)
|
||||
const logContent = ref('')
|
||||
const logLoading = ref(false)
|
||||
const logTitle = ref('')
|
||||
const logEnvId = ref<number | null>(null)
|
||||
const logFileId = ref<number | null>(null)
|
||||
const logIsSymfony = ref(false)
|
||||
|
||||
// App edit modal
|
||||
const showAppModal = ref(false)
|
||||
const editForm = ref<ApplicationWrite>({ name: '', slug: '', registryImage: '', description: '', giteaUrl: '' })
|
||||
@@ -226,6 +237,41 @@ function statusClass(status: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Log functions
|
||||
async function openDockerLogs(env: Environment) {
|
||||
logEnvId.value = env.id!
|
||||
logFileId.value = null
|
||||
logIsSymfony.value = false
|
||||
logTitle.value = `${t('logs.docker')} — ${env.name}`
|
||||
logContent.value = ''
|
||||
showLogModal.value = true
|
||||
}
|
||||
|
||||
async function openSymfonyLog(env: Environment, lf: { id?: number, label: string }) {
|
||||
logEnvId.value = env.id!
|
||||
logFileId.value = lf.id!
|
||||
logIsSymfony.value = true
|
||||
logTitle.value = `${t('logs.symfony')} — ${lf.label}`
|
||||
logContent.value = ''
|
||||
showLogModal.value = true
|
||||
}
|
||||
|
||||
async function refreshLogs(lines: number, level: string) {
|
||||
if (!logEnvId.value) return
|
||||
logLoading.value = true
|
||||
try {
|
||||
let result: LogOutput
|
||||
if (logIsSymfony.value && logFileId.value) {
|
||||
result = await getSymfonyLog(logEnvId.value, logFileId.value, lines, level || undefined)
|
||||
} else {
|
||||
result = await getDockerLogs(logEnvId.value, lines)
|
||||
}
|
||||
logContent.value = result.content
|
||||
} finally {
|
||||
logLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const envModalTitle = computed(() =>
|
||||
editingEnvId.value ? t('environments.editButton') : t('environments.addButton')
|
||||
)
|
||||
@@ -334,6 +380,13 @@ onMounted(loadApplication)
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<MalioButton
|
||||
:label="t('logs.docker')"
|
||||
variant="secondary"
|
||||
icon-name="mdi:text-box-outline"
|
||||
icon-position="left"
|
||||
@click="openDockerLogs(env)"
|
||||
/>
|
||||
<MalioButton
|
||||
:label="t('environments.deploy.button')"
|
||||
icon-name="mdi:rocket-launch-outline"
|
||||
@@ -358,9 +411,16 @@ onMounted(loadApplication)
|
||||
<!-- Log files -->
|
||||
<div v-if="env.logFiles.length" class="mt-4 border-t border-neutral-200 pt-3">
|
||||
<p class="text-sm font-bold uppercase tracking-wider mb-2">{{ t('environments.logFiles.title') }}</p>
|
||||
<div v-for="lf in env.logFiles" :key="lf.id" class="text-sm text-neutral-700 flex gap-2">
|
||||
<span class="font-medium">{{ lf.label }} :</span>
|
||||
<div v-for="lf in env.logFiles" :key="lf.id" class="text-sm text-neutral-700 flex gap-2 items-center">
|
||||
<span class="font-medium">{{ lf.label }}</span>
|
||||
<span class="font-mono text-neutral-400">{{ lf.path }}</span>
|
||||
<MalioButtonIcon
|
||||
icon="mdi:console"
|
||||
:aria-label="lf.label"
|
||||
variant="ghost"
|
||||
icon-size="16"
|
||||
@click="openSymfonyLog(env, lf)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -613,5 +673,15 @@ onMounted(loadApplication)
|
||||
/>
|
||||
</template>
|
||||
</AppModal>
|
||||
|
||||
<!-- Log modal -->
|
||||
<LogModal
|
||||
v-model="showLogModal"
|
||||
:title="logTitle"
|
||||
:content="logContent"
|
||||
:loading="logLoading"
|
||||
:show-level-filter="logIsSymfony"
|
||||
@refresh="refreshLogs"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
5
frontend/services/dto/logs.ts
Normal file
5
frontend/services/dto/logs.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
type LogOutput = {
|
||||
content: string
|
||||
lines: number
|
||||
source: string
|
||||
}
|
||||
15
frontend/services/logs.ts
Normal file
15
frontend/services/logs.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { LogOutput } from './dto/logs'
|
||||
|
||||
export function getDockerLogs(envId: number, lines: number = 100): Promise<LogOutput> {
|
||||
return useApi().get<LogOutput>(`/environments/${envId}/logs/docker`, { lines }, {
|
||||
toast: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function getSymfonyLog(envId: number, logFileId: number, lines: number = 100, level?: string): Promise<LogOutput> {
|
||||
const query: Record<string, any> = { lines }
|
||||
if (level) query.level = level
|
||||
return useApi().get<LogOutput>(`/environments/${envId}/logs/symfony/${logFileId}`, query, {
|
||||
toast: false,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user