update console and logs stream to use fetchEventStream

This commit is contained in:
arabcoders
2025-05-14 23:24:16 +03:00
parent 10901e52d4
commit b0683d5dbc
2 changed files with 89 additions and 67 deletions

View File

@@ -128,7 +128,8 @@ import {FitAddon} from "@xterm/addon-fit"
import {useStorage} from '@vueuse/core'
import {disableOpacity, enableOpacity, notification} from '~/utils/index'
import Message from '~/components/Message'
import request from "~/utils/request.js";
import request from "~/utils/request.js"
import {fetchEventSource} from '@microsoft/fetch-event-source'
useHead({title: `Console`})
@@ -150,11 +151,10 @@ const hasPrefix = computed(() => command.value.startsWith('console') || command.
const hasPlaceholder = computed(() => command.value && command.value.match(/\[.*]/))
const show_page_tips = useStorage('show_page_tips', true)
const allEnabled = ref(false)
const ctrl = new AbortController();
const RunCommand = async () => {
const api_path = useStorage('api_path', '/v1/api')
const api_url = useStorage('api_url', '')
const api_token = useStorage('api_token', '')
const token = useStorage('token', '')
/** @type {string} */
let userCommand = command.value
@@ -200,7 +200,7 @@ const RunCommand = async () => {
}
isLoading.value = true
let token;
let commandToken;
try {
const response = await request('/system/command', {
@@ -215,29 +215,54 @@ const RunCommand = async () => {
return;
}
token = json.token
commandToken = json.token
} catch (e) {
await finished()
notification('error', 'Error', e.message, 5000)
return;
}
sse = new EventSource(`${api_url.value}${api_path.value}/system/command/${token}?apikey=${api_token.value}`)
sse = fetchEventSource(`/v1/api/system/command/${commandToken}`, {
signal: ctrl.signal,
headers: {'Authorization': `Token ${token.value}`},
async onmessage(evt) {
switch (evt.event) {
case 'data':
terminal.value.write(JSON.parse(evt.data).data)
break
case 'close':
await finished()
break
case 'exit_code':
exitCode.value = parseInt(evt.data)
break
default:
break
}
},
async onopen(response) {
if (response.ok) {
return
}
const json = await parse_api_response(response)
const message = `${json.error.code}: ${json.error.message}`
notification('error', 'Error', message, 3000)
await finished()
},
async onerror() {
await finished()
},
})
if ('' !== command.value) {
terminal.value.writeln(`(${exitCode.value}) ~ ${userCommand}`)
}
sse.addEventListener('data', async e => terminal.value.write(JSON.parse(e.data).data))
sse.addEventListener('close', async () => finished())
sse.addEventListener('exit_code', async e => exitCode.value = e.data)
sse.onclose = async () => finished()
sse.onerror = async () => finished()
}
const finished = async () => {
if (sse) {
sse.close()
ctrl.abort();
}
isLoading.value = false
@@ -287,7 +312,7 @@ const clearOutput = async () => {
onUnmounted(() => {
window.removeEventListener("resize", reSizeTerminal)
if (sse) {
sse.close()
ctrl.abort();
}
enableOpacity()
})

View File

@@ -3,7 +3,7 @@
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span class="title is-4">
<span class="icon"><i class="fas fa-globe" :class="{ 'fa-spin': isLoading }" />&nbsp;</span>
<span class="icon"><i class="fas fa-globe" :class="{ 'fa-spin': isLoading }"/>&nbsp;</span>
<NuxtLink to="/logs">Logs</NuxtLink>
: {{ filename }}
</span>
@@ -12,46 +12,46 @@
<div class="field is-grouped">
<div class="control">
<button v-if="!autoScroll" @click="scrollToBottom" class="button is-primary"
v-tooltip.bottom="'Go to bottom'">
v-tooltip.bottom="'Go to bottom'">
<span class="icon"><i class="fas fa-arrow-down"></i></span>
</button>
</div>
<div class="control has-icons-left" v-if="toggleFilter">
<input type="search" v-model.lazy="query" class="input" id="filter" placeholder="Filter">
<span class="icon is-left"><i class="fas fa-filter" /></span>
<span class="icon is-left"><i class="fas fa-filter"/></span>
</div>
<div class="control">
<button class="button is-danger is-light" v-tooltip.bottom="'Filter log lines.'"
@click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter" /></span>
@click="toggleFilter = !toggleFilter">
<span class="icon"><i class="fas fa-filter"/></span>
</button>
</div>
<p class="control">
<button class="button is-danger" v-tooltip.bottom="'Delete Logfile.'" @click="deleteFile">
<span class="icon"><i class="fas fa-trash" /></span>
<span class="icon"><i class="fas fa-trash"/></span>
</button>
</p>
<p class="control">
<button class="button is-purple is-light" v-tooltip.bottom="'Download the entire logfile.'"
@click="downloadFile" :class="{ 'is-loading': isDownloading }">
<span class="icon"><i class="fas fa-download" /></span>
@click="downloadFile" :class="{ 'is-loading': isDownloading }">
<span class="icon"><i class="fas fa-download"/></span>
</button>
</p>
<p class="control">
<button class="button is-warning" @click="wrapLines = !wrapLines" v-tooltip.bottom="'Toggle wrap line'">
<span class="icon"><i class="fas fa-text-width" /></span>
<span class="icon"><i class="fas fa-text-width"/></span>
</button>
</p>
<p class="control">
<button class="button" v-tooltip.bottom="'Copy showing logs'"
@click="() => copyText(filterItems.map(i => i.text).join('\n'))">
<span class="icon"><i class="fas fa-copy" /></span>
@click="() => copyText(filterItems.map(i => i.text).join('\n'))">
<span class="icon"><i class="fas fa-copy"/></span>
</button>
</p>
</div>
@@ -67,35 +67,36 @@
<div class="column is-12">
<div class="logbox is-grid" ref="logContainer" v-if="!error" @scroll.passive="handleScroll">
<code id="logView" class="p-1 logline is-block" :class="{ 'is-pre-wrap': wrapLines, 'is-pre': !wrapLines }">
<span class="is-block m-0 notification is-info is-dark has-text-centered" v-if="reachedEnd && !query">
<span class="notification-title">
<span class="icon"><i class="fas fa-exclamation-triangle" /></span>
No more logs available for this file.
</span>
</span>
<span v-for="item in filterItems" :key="item.id" class="is-block">
<span v-if="item.date">[<span class="has-tooltip" :title="item.date">{{ formatDate(item.date)
}}</span>]:&nbsp;</span>
<span v-if="item?.item_id"><span class="is-clickable has-tooltip" @click="goto_history_item(item)"><span
class="icon"><i class="fas fa-history" /></span><span>View</span></span>&nbsp;</span>
<span>{{ item.text }}</span>
</span>
<span class="is-block" v-if="filterItems.length < 1">
<span class="is-block m-0 notification is-warning is-dark has-text-centered" v-if="query">
<span class="notification-title is-danger">
<span class="icon"><i class="fas fa-filter" /></span>
No logs match this query: <u>{{ query }}</u>
</span>
</span>
<span v-else>
<span class="has-text-danger">No logs available</span></span>
</span>
</code>
<span class="is-block m-0 notification is-info is-dark has-text-centered" v-if="reachedEnd && !query">
<span class="notification-title">
<span class="icon"><i class="fas fa-exclamation-triangle"/></span>
No more logs available for this file.
</span>
</span>
<span v-for="item in filterItems" :key="item.id" class="is-block">
<span v-if="item.date">[<span class="has-tooltip" :title="item.date">{{
formatDate(item.date)
}}</span>]:&nbsp;</span>
<span v-if="item?.item_id"><span class="is-clickable has-tooltip" @click="goto_history_item(item)"><span
class="icon"><i class="fas fa-history"/></span><span>View</span></span>&nbsp;</span>
<span>{{ item.text }}</span>
</span>
<span class="is-block" v-if="filterItems.length < 1">
<span class="is-block m-0 notification is-warning is-dark has-text-centered" v-if="query">
<span class="notification-title is-danger">
<span class="icon"><i class="fas fa-filter"/></span>
No logs match this query: <u>{{ query }}</u>
</span>
</span>
<span v-else>
<span class="has-text-danger">No logs available</span></span>
</span>
</code>
<div ref="bottomMarker"></div>
</div>
<Message v-if="error" title="API Error" message_class="has-background-warning-90 has-text-dark" :message="error"
:use-close="true" @close="router.push('/logs')" />
:use-close="true" @close="router.push('/logs')"/>
</div>
</div>
</div>
@@ -108,11 +109,11 @@
max-width: 100%;
}
#logView>span:nth-child(even) {
#logView > span:nth-child(even) {
color: #ffc9d4;
}
#logView>span:nth-child(odd) {
#logView > span:nth-child(odd) {
color: #e3c981;
}
@@ -144,15 +145,15 @@ div.logbox pre {
<script setup>
import Message from '~/components/Message'
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import { disableOpacity, enableOpacity, goto_history_item, notification, parse_api_response } from '~/utils/index'
import {useStorage} from '@vueuse/core'
import {disableOpacity, enableOpacity, goto_history_item, notification, parse_api_response} from '~/utils/index'
import request from '~/utils/request'
import { fetchEventSource } from '@microsoft/fetch-event-source';
import {fetchEventSource} from '@microsoft/fetch-event-source'
const router = useRouter()
const filename = useRoute().params.filename
useHead({ title: `Logs : ${filename}` })
useHead({title: `Logs : ${filename}`})
const query = ref()
const data = ref([])
@@ -233,7 +234,7 @@ const loadContent = async () => {
// Auto-scroll only if the user was already at the bottom
await nextTick(() => {
if (autoScroll.value && bottomMarker.value) {
bottomMarker.value.scrollIntoView({ behavior: 'auto' })
bottomMarker.value.scrollIntoView({behavior: 'auto'})
}
})
@@ -273,7 +274,7 @@ const scrollToBottom = () => {
autoScroll.value = true
nextTick(() => {
if (bottomMarker.value) {
bottomMarker.value.scrollIntoView({ behavior: 'smooth' })
bottomMarker.value.scrollIntoView({behavior: 'smooth'})
}
})
}
@@ -315,7 +316,7 @@ const watchLog = () => {
await nextTick(() => {
if (autoScroll.value && bottomMarker.value) {
bottomMarker.value.scrollIntoView({ behavior: 'smooth' })
bottomMarker.value.scrollIntoView({behavior: 'smooth'})
}
})
} catch (error) {
@@ -332,11 +333,7 @@ const watchLog = () => {
const closeStream = () => {
if (stream.value) {
try {
stream.value.close()
ctrl.abort()
} catch (e) {
}
ctrl.abort()
stream.value = null
}
}
@@ -375,7 +372,7 @@ const deleteFile = async () => {
try {
closeStream();
const response = await request(`/log/${filename}`, { method: 'DELETE' })
const response = await request(`/log/${filename}`, {method: 'DELETE'})
if (response.ok) {
notification('success', 'Information', `Logfile '${filename}' has been deleted.`)
@@ -390,7 +387,7 @@ const deleteFile = async () => {
json = await response.json()
} catch (e) {
json = {
error: { code: response.status, message: response.statusText }
error: {code: response.status, message: response.statusText}
}
}