add sse messages handling

This commit is contained in:
axolotle 2023-06-09 15:27:48 +02:00
parent a3558f54e3
commit 0b1030a2fb
8 changed files with 85 additions and 20 deletions

View file

@ -81,7 +81,7 @@
<script>
import { mapGetters } from 'vuex'
import { connectSSE } from '@/api/handlers'
import { HistoryConsole, ViewLockOverlay } from '@/views/_partials'
export default {
@ -118,6 +118,7 @@ export default {
// state will be automaticly reseted and user will be prompt with the login view.
if (this.connected) {
this.$store.dispatch('GET_YUNOHOST_INFOS')
connectSSE()
}
},

View file

@ -85,13 +85,10 @@ export default {
humanKey = null,
{ wait = true, websocket = true, initial = false, asFormData = false } = {}
) {
// FIXME remove websocket mentions
// `await` because Vuex actions returns promises by default.
const request = await store.dispatch('INIT_REQUEST', { method, uri, humanKey, initial, wait, websocket })
if (websocket) {
await openWebSocket(request)
}
let options = this.options
if (method === 'GET') {
uri += `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`

View file

@ -46,6 +46,17 @@ export function openWebSocket (request) {
})
}
export function connectSSE () {
const host = store.getters.host.split(':')[0]
const evtSource = new EventSource(`https://${host}/yunohost/api/sse`)
evtSource.onmessage = (event) => {
store.dispatch('ON_SSE_MESSAGE', JSON.parse(atob(event.data)))
}
// FIXME handle 'onerror' hook
}
/**
* Handler for API errors.

View file

@ -5,7 +5,7 @@
<!-- REQUEST DESCRIPTION -->
<strong class="request-desc">
{{ request.humanRoute }}
{{ request.humanRoute || request.operationId }}
</strong>
<div v-if="request.errors || request.warnings">

View file

@ -17,6 +17,7 @@
"api": {
"partial_logs": "[...] (check in history for full logs)",
"processing": "The server is processing the action...",
"external_action": "This action has been ran from another tab or from the cli",
"query_status": {
"error": "Unsuccessful",
"pending": "In progress",

View file

@ -2,6 +2,7 @@ import Vue from 'vue'
import router from '@/router'
import i18n from '@/i18n'
import api from '@/api'
import { connectSSE } from '@/api/handlers'
import { timeout, isEmptyValue, isObjectLiteral } from '@/helpers/commons'
export default {
@ -122,6 +123,7 @@ export default {
'CONNECT' ({ commit, dispatch }) {
commit('SET_CONNECTED', true)
connectSSE()
dispatch('GET_YUNOHOST_INFOS')
},
@ -170,9 +172,21 @@ export default {
const { key, ...args } = isObjectLiteral(humanKey) ? humanKey : { key: humanKey }
const humanRoute = key ? i18n.t('human_routes.' + key, args) : `[${method}] /${uri}`
let request = { method, uri, humanRouteKey: key, humanRoute, initial, status: 'pending' }
let request = {
method,
uri,
humanRouteKey: key,
humanRoute,
initial,
status: 'pending',
messages: [],
date: Date.now(),
operation_id: null, // will be given on sse start if request is an action
warnings: 0,
errors: 0,
external: false
}
if (websocket) {
request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 }
commit('ADD_HISTORY_ACTION', request)
}
commit('ADD_REQUEST', request)
@ -197,6 +211,7 @@ export default {
if (success && (request.warnings || request.errors)) {
const messages = request.messages
if (messages.length && messages[messages.length - 1].color === 'warning') {
// request ends up with a warning, display it as a modal
request.showWarningMessage = true
}
status = 'warning'
@ -211,10 +226,47 @@ export default {
}
},
'DISPATCH_MESSAGE' ({ state, commit, dispatch }, { request, messages }) {
for (const type in messages) {
'START_EXTERNAL_ACTION' ({ state, commit, dispatch }, { timestamp, operationId }) {
// Action triggered by another client/cli
const action = {
status: 'pending',
messages: [],
date: timestamp,
operationId,
warnings: 0,
errors: 0,
external: true
}
commit('ADD_HISTORY_ACTION', action)
setTimeout(() => {
// Display the waiting modal only if the request takes some time.
if (action.status === 'pending') {
commit('SET_WAITING', true)
}
}, 400)
return action
},
async 'ON_SSE_MESSAGE' ({ state, commit, dispatch }, data) {
let action = state.history.findLast((action) => action.operationId === data.operation_id)
if (!action) {
action = await dispatch('START_EXTERNAL_ACTION', { operationId: data.operation_id, timestamp: data.timestamp })
}
if (data.type === 'start') {
if (!action.external) {
commit('UPDATE_REQUEST', { request: action, key: 'operationId', value: data.operation_id })
}
} else if (data.type === 'end') {
// End request on this last message if the action was external (else default http response will end it)
if (action.external) {
dispatch('END_REQUEST', { request: action, success: data.success, wait: true })
}
} else {
const type = data.level.toLowerCase()
const message = {
text: messages[type].replaceAll('\n', '<br>'),
text: data.msg.replaceAll('\n', '<br>'),
color: type === 'error' ? 'danger' : type
}
let progressBar = message.text.match(/^\[#*\+*\.*\] > /)
@ -225,17 +277,18 @@ export default {
for (const char of progressBar) {
if (char in progress) progress[char] += 1
}
commit('UPDATE_REQUEST', { request, key: 'progress', value: Object.values(progress) })
Vue.set(action, 'progress', Object.values(progress))
}
if (message.text) {
// To avoid rendering lag issues, limit the flow of websocket messages to batches of 50ms.
if (state.historyTimer === null) {
state.historyTimer = setTimeout(() => {
commit('UPDATE_DISPLAYED_MESSAGES', { request })
commit('UPDATE_DISPLAYED_MESSAGES', { request: action })
}, 50)
}
commit('ADD_TEMP_MESSAGE', { request, message, type })
commit('ADD_TEMP_MESSAGE', { request: action, message, type })
}
action.messages.push(message)
}
},
@ -358,8 +411,7 @@ export default {
history: state => state.history,
lastAction: state => state.history[state.history.length - 1],
currentRequest: state => {
const request = state.requests.find(({ status }) => status === 'pending')
return request || state.requests[state.requests.length - 1]
return state.history.findLast(({ status }) => status === 'pending')
},
routerKey: state => state.routerKey,
breadcrumb: state => state.breadcrumb,

View file

@ -2,7 +2,7 @@
<b-overlay
variant="white" opacity="0.75"
no-center
:show="waiting || reconnecting || error !== null"
:show="(currentRequest && waiting) || reconnecting || error !== null"
>
<slot name="default" />
@ -42,11 +42,12 @@ export default {
if (error) {
return { name: 'ErrorDisplay', request: error }
} else if (request.showWarningMessage) {
return { name: 'WarningDisplay', request }
} else if (reconnecting) {
return { name: 'ReconnectingDisplay' }
} else {
} else if (request) {
if (request.showWarningMessage) {
return { name: 'WarningDisplay', request }
}
return { name: 'WaitingDisplay', request }
}
}

View file

@ -3,6 +3,8 @@
<b-card-body>
<b-card-title class="text-center mt-4" v-t="hasMessages ? 'api.processing' : 'api_waiting'" />
<p v-if="request.external" v-t="'api.external_action'" />
<!-- PROGRESS BAR -->
<b-progress
v-if="progress" class="my-4"