|
16 | 16 | ) |
17 | 17 | template(#top-table) |
18 | 18 | sesame-core-pan-filters(:columns='columns' mode='simple' placeholder='Rechercher par nom, description, ...') |
19 | | - //template(v-slot:row-actions='{ row }') |
20 | | - //q-btn(:to='toPathWithQueries(`/settings/cron/${row.name}`)' color='primary' icon='mdi-eye' size='sm' flat round dense) |
| 19 | + template(v-slot:row-actions='{ row }') |
| 20 | + q-btn( |
| 21 | + :disable='!hasPermission("/core/cron", "read")' |
| 22 | + color='primary' |
| 23 | + icon='mdi-file-document-outline' |
| 24 | + size='sm' |
| 25 | + flat |
| 26 | + round |
| 27 | + dense |
| 28 | + @click='openLogsModal(row)' |
| 29 | + ) |
| 30 | + q-tooltip.text-body2.bg-negative.text-white( |
| 31 | + v-if="!hasPermission('/core/cron', 'read')" |
| 32 | + anchor="top middle" |
| 33 | + self="center middle" |
| 34 | + ) Vous n'avez pas les permissions nécessaires pour effectuer cette action |
21 | 35 | template(#body-cell-enabled="props") |
22 | 36 | q-td |
23 | 37 | q-checkbox(:model-value="props.row.enabled" :disable="true" size="xs") |
| 38 | + q-dialog(v-model='logsDialog' maximized) |
| 39 | + q-card.fit.column.no-wrap(style='overflow: hidden;') |
| 40 | + q-toolbar.bg-info.text-white(bordered dense style='height: 28px; line-height: 28px;') |
| 41 | + q-toolbar-title Logs de la tâche "{{ selectedCronName }}" |
| 42 | + q-space |
| 43 | + q-btn(flat round dense icon='mdi-refresh' :loading='logsLoading' @click='loadCronLogs') |
| 44 | + q-btn(flat round dense icon='mdi-close' v-close-popup) |
| 45 | + q-separator |
| 46 | + q-card-section.col.q-pa-none(ref='logsContainer' style='min-height: 0; overflow: auto;') |
| 47 | + q-inner-loading(:showing='logsLoading') |
| 48 | + div.text-center(v-if='!logsLoading && !logsExists').text-grey-7 Aucun fichier de log trouvé pour cette tâche. |
| 49 | + client-only(v-if='logsExists') |
| 50 | + MonacoEditor.fit( |
| 51 | + ref='logsMonacoEditor' |
| 52 | + :model-value="logsContent || 'Aucun contenu de log.'" |
| 53 | + :options='logsMonacoOptions' |
| 54 | + lang='shell' |
| 55 | + @load='onLogsEditorLoad' |
| 56 | + ) |
24 | 57 | </template> |
25 | 58 |
|
26 | 59 | <script lang="ts"> |
27 | 60 | import type { LocationQueryValue } from 'vue-router' |
| 61 | +import { computed, ref } from 'vue' |
28 | 62 | import { NewTargetId } from '~/constants/variables' |
29 | 63 |
|
30 | 64 | export default defineNuxtComponent({ |
@@ -80,8 +114,32 @@ export default defineNuxtComponent({ |
80 | 114 | async setup() { |
81 | 115 | const { useHttpPaginationOptions, useHttpPaginationReactive } = usePagination() |
82 | 116 | const { toPathWithQueries, navigateToTab } = useRouteQueries() |
| 117 | + const { hasPermission } = useAccessControl() |
| 118 | + const { monacoOptions } = useDebug() |
| 119 | +
|
| 120 | + const editorEl = useTemplateRef<HTMLDivElement>('logsMonacoEditor') |
83 | 121 |
|
84 | 122 | const paginationOptions = useHttpPaginationOptions() |
| 123 | + const logsDialog = ref(false) |
| 124 | + const selectedCronName = ref('') |
| 125 | + const logsLoading = ref(false) |
| 126 | + const logsTail = ref(250) |
| 127 | + const logsTailStep = 250 |
| 128 | + const logsTailMax = 5_000 |
| 129 | + const logsFullyLoaded = ref(false) |
| 130 | + const logsContent = ref('') |
| 131 | + const logsExists = ref(false) |
| 132 | + const logsMonacoEditor = ref<any>(null) |
| 133 | + const logsMonacoOptions = computed(() => ({ |
| 134 | + ...monacoOptions.value, |
| 135 | + minimap: { enabled: false }, |
| 136 | + readOnly: true, |
| 137 | + wordWrap: 'off', |
| 138 | + scrollBeyondLastLine: false, |
| 139 | + lineNumbers: 'off', |
| 140 | + folding: false, |
| 141 | + glyphMargin: false, |
| 142 | + })) |
85 | 143 |
|
86 | 144 | const { |
87 | 145 | data: cronTasks, |
@@ -109,6 +167,19 @@ export default defineNuxtComponent({ |
109 | 167 | refresh, |
110 | 168 | toPathWithQueries, |
111 | 169 | navigateToTab, |
| 170 | + hasPermission, |
| 171 | + editorEl, |
| 172 | + logsDialog, |
| 173 | + selectedCronName, |
| 174 | + logsLoading, |
| 175 | + logsTail, |
| 176 | + logsTailStep, |
| 177 | + logsTailMax, |
| 178 | + logsFullyLoaded, |
| 179 | + logsContent, |
| 180 | + logsExists, |
| 181 | + logsMonacoEditor, |
| 182 | + logsMonacoOptions, |
112 | 183 | } |
113 | 184 | }, |
114 | 185 | computed: { |
@@ -139,6 +210,74 @@ export default defineNuxtComponent({ |
139 | 210 | } |
140 | 211 | return 'N/A' |
141 | 212 | }, |
| 213 | + async openLogsModal(cronTask: any): Promise<void> { |
| 214 | + this.selectedCronName = cronTask?.name || '' |
| 215 | + this.logsTail = this.logsTailStep |
| 216 | + this.logsFullyLoaded = false |
| 217 | + this.logsDialog = true |
| 218 | + await this.loadCronLogs() |
| 219 | + }, |
| 220 | + async loadCronLogs(): Promise<void> { |
| 221 | + if (!this.selectedCronName) { |
| 222 | + return |
| 223 | + } |
| 224 | +
|
| 225 | + this.logsLoading = true |
| 226 | + try { |
| 227 | + const response = await this.$http.get(`/core/cron/${encodeURIComponent(this.selectedCronName)}/logs`, { |
| 228 | + query: { |
| 229 | + tail: this.logsTail, |
| 230 | + }, |
| 231 | + }) |
| 232 | + this.logsContent = response?._data?.data?.content || '' |
| 233 | + this.logsExists = !!response?._data?.data?.exists |
| 234 | + const lineCount = this.logsContent ? this.logsContent.split('\n').length : 0 |
| 235 | + this.logsFullyLoaded = this.logsTail >= this.logsTailMax || lineCount < this.logsTail |
| 236 | + } catch (error: any) { |
| 237 | + this.logsContent = '' |
| 238 | + this.logsExists = false |
| 239 | + this.logsFullyLoaded = true |
| 240 | + this.$q.notify({ |
| 241 | + message: error?.response?._data?.message || 'Impossible de charger les logs de la tâche cron (timeout ou erreur réseau).', |
| 242 | + color: 'negative', |
| 243 | + position: 'top-right', |
| 244 | + icon: 'mdi-alert-circle-outline', |
| 245 | + }) |
| 246 | + } finally { |
| 247 | + this.logsLoading = false |
| 248 | + } |
| 249 | + }, |
| 250 | + onLogsEditorLoad(editor: any): void { |
| 251 | + this.logsMonacoEditor = editor |
| 252 | + const model = editor.getModel() |
| 253 | + const lineCount = model?.getLineCount() || 1 |
| 254 | + editor.revealLineNearTop(lineCount) |
| 255 | + editor.onDidScrollChange(async () => { |
| 256 | + const isAtTop = editor.getScrollTop() <= 0 |
| 257 | + if (!isAtTop || this.logsLoading || this.logsFullyLoaded || !this.logsExists) { |
| 258 | + return |
| 259 | + } |
| 260 | +
|
| 261 | + const nextTail = Math.min(this.logsTail + this.logsTailStep, this.logsTailMax) |
| 262 | + if (nextTail <= this.logsTail) { |
| 263 | + this.logsFullyLoaded = true |
| 264 | + return |
| 265 | + } |
| 266 | +
|
| 267 | + this.logsTail = nextTail |
| 268 | + const previousModel = editor.getModel() |
| 269 | + const previousLineCount = previousModel?.getLineCount() || 1 |
| 270 | + const anchorLine = editor.getVisibleRanges()?.[0]?.startLineNumber || 1 |
| 271 | + await this.loadCronLogs() |
| 272 | + this.$nextTick(() => { |
| 273 | + const updatedModel = editor.getModel() |
| 274 | + const updatedLineCount = updatedModel?.getLineCount() || previousLineCount |
| 275 | + const addedLines = Math.max(updatedLineCount - previousLineCount, 0) |
| 276 | + const nextAnchorLine = Math.min(anchorLine + addedLines, updatedLineCount) |
| 277 | + editor.revealLineNearTop(nextAnchorLine) |
| 278 | + }) |
| 279 | + }) |
| 280 | + }, |
142 | 281 | }, |
143 | 282 | }) |
144 | 283 | </script> |
0 commit comments