mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
add sse messages handling
This commit is contained in:
parent
a3558f54e3
commit
0b1030a2fb
8 changed files with 85 additions and 20 deletions
|
@ -81,7 +81,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import { connectSSE } from '@/api/handlers'
|
||||||
import { HistoryConsole, ViewLockOverlay } from '@/views/_partials'
|
import { HistoryConsole, ViewLockOverlay } from '@/views/_partials'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -118,6 +118,7 @@ export default {
|
||||||
// state will be automaticly reseted and user will be prompt with the login view.
|
// state will be automaticly reseted and user will be prompt with the login view.
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
this.$store.dispatch('GET_YUNOHOST_INFOS')
|
this.$store.dispatch('GET_YUNOHOST_INFOS')
|
||||||
|
connectSSE()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -85,13 +85,10 @@ export default {
|
||||||
humanKey = null,
|
humanKey = null,
|
||||||
{ wait = true, websocket = true, initial = false, asFormData = false } = {}
|
{ wait = true, websocket = true, initial = false, asFormData = false } = {}
|
||||||
) {
|
) {
|
||||||
|
// FIXME remove websocket mentions
|
||||||
// `await` because Vuex actions returns promises by default.
|
// `await` because Vuex actions returns promises by default.
|
||||||
const request = await store.dispatch('INIT_REQUEST', { method, uri, humanKey, initial, wait, websocket })
|
const request = await store.dispatch('INIT_REQUEST', { method, uri, humanKey, initial, wait, websocket })
|
||||||
|
|
||||||
if (websocket) {
|
|
||||||
await openWebSocket(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
let options = this.options
|
let options = this.options
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
uri += `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`
|
uri += `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`
|
||||||
|
|
|
@ -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.
|
* Handler for API errors.
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
<!-- REQUEST DESCRIPTION -->
|
<!-- REQUEST DESCRIPTION -->
|
||||||
<strong class="request-desc">
|
<strong class="request-desc">
|
||||||
{{ request.humanRoute }}
|
{{ request.humanRoute || request.operationId }}
|
||||||
</strong>
|
</strong>
|
||||||
|
|
||||||
<div v-if="request.errors || request.warnings">
|
<div v-if="request.errors || request.warnings">
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"api": {
|
"api": {
|
||||||
"partial_logs": "[...] (check in history for full logs)",
|
"partial_logs": "[...] (check in history for full logs)",
|
||||||
"processing": "The server is processing the action...",
|
"processing": "The server is processing the action...",
|
||||||
|
"external_action": "This action has been ran from another tab or from the cli",
|
||||||
"query_status": {
|
"query_status": {
|
||||||
"error": "Unsuccessful",
|
"error": "Unsuccessful",
|
||||||
"pending": "In progress",
|
"pending": "In progress",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import { connectSSE } from '@/api/handlers'
|
||||||
import { timeout, isEmptyValue, isObjectLiteral } from '@/helpers/commons'
|
import { timeout, isEmptyValue, isObjectLiteral } from '@/helpers/commons'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -122,6 +123,7 @@ export default {
|
||||||
|
|
||||||
'CONNECT' ({ commit, dispatch }) {
|
'CONNECT' ({ commit, dispatch }) {
|
||||||
commit('SET_CONNECTED', true)
|
commit('SET_CONNECTED', true)
|
||||||
|
connectSSE()
|
||||||
dispatch('GET_YUNOHOST_INFOS')
|
dispatch('GET_YUNOHOST_INFOS')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -170,9 +172,21 @@ export default {
|
||||||
const { key, ...args } = isObjectLiteral(humanKey) ? humanKey : { key: humanKey }
|
const { key, ...args } = isObjectLiteral(humanKey) ? humanKey : { key: humanKey }
|
||||||
const humanRoute = key ? i18n.t('human_routes.' + key, args) : `[${method}] /${uri}`
|
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) {
|
if (websocket) {
|
||||||
request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 }
|
|
||||||
commit('ADD_HISTORY_ACTION', request)
|
commit('ADD_HISTORY_ACTION', request)
|
||||||
}
|
}
|
||||||
commit('ADD_REQUEST', request)
|
commit('ADD_REQUEST', request)
|
||||||
|
@ -197,6 +211,7 @@ export default {
|
||||||
if (success && (request.warnings || request.errors)) {
|
if (success && (request.warnings || request.errors)) {
|
||||||
const messages = request.messages
|
const messages = request.messages
|
||||||
if (messages.length && messages[messages.length - 1].color === 'warning') {
|
if (messages.length && messages[messages.length - 1].color === 'warning') {
|
||||||
|
// request ends up with a warning, display it as a modal
|
||||||
request.showWarningMessage = true
|
request.showWarningMessage = true
|
||||||
}
|
}
|
||||||
status = 'warning'
|
status = 'warning'
|
||||||
|
@ -211,10 +226,47 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'DISPATCH_MESSAGE' ({ state, commit, dispatch }, { request, messages }) {
|
'START_EXTERNAL_ACTION' ({ state, commit, dispatch }, { timestamp, operationId }) {
|
||||||
for (const type in messages) {
|
// 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 = {
|
const message = {
|
||||||
text: messages[type].replaceAll('\n', '<br>'),
|
text: data.msg.replaceAll('\n', '<br>'),
|
||||||
color: type === 'error' ? 'danger' : type
|
color: type === 'error' ? 'danger' : type
|
||||||
}
|
}
|
||||||
let progressBar = message.text.match(/^\[#*\+*\.*\] > /)
|
let progressBar = message.text.match(/^\[#*\+*\.*\] > /)
|
||||||
|
@ -225,17 +277,18 @@ export default {
|
||||||
for (const char of progressBar) {
|
for (const char of progressBar) {
|
||||||
if (char in progress) progress[char] += 1
|
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) {
|
if (message.text) {
|
||||||
// To avoid rendering lag issues, limit the flow of websocket messages to batches of 50ms.
|
// To avoid rendering lag issues, limit the flow of websocket messages to batches of 50ms.
|
||||||
if (state.historyTimer === null) {
|
if (state.historyTimer === null) {
|
||||||
state.historyTimer = setTimeout(() => {
|
state.historyTimer = setTimeout(() => {
|
||||||
commit('UPDATE_DISPLAYED_MESSAGES', { request })
|
commit('UPDATE_DISPLAYED_MESSAGES', { request: action })
|
||||||
}, 50)
|
}, 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,
|
history: state => state.history,
|
||||||
lastAction: state => state.history[state.history.length - 1],
|
lastAction: state => state.history[state.history.length - 1],
|
||||||
currentRequest: state => {
|
currentRequest: state => {
|
||||||
const request = state.requests.find(({ status }) => status === 'pending')
|
return state.history.findLast(({ status }) => status === 'pending')
|
||||||
return request || state.requests[state.requests.length - 1]
|
|
||||||
},
|
},
|
||||||
routerKey: state => state.routerKey,
|
routerKey: state => state.routerKey,
|
||||||
breadcrumb: state => state.breadcrumb,
|
breadcrumb: state => state.breadcrumb,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<b-overlay
|
<b-overlay
|
||||||
variant="white" opacity="0.75"
|
variant="white" opacity="0.75"
|
||||||
no-center
|
no-center
|
||||||
:show="waiting || reconnecting || error !== null"
|
:show="(currentRequest && waiting) || reconnecting || error !== null"
|
||||||
>
|
>
|
||||||
<slot name="default" />
|
<slot name="default" />
|
||||||
|
|
||||||
|
@ -42,11 +42,12 @@ export default {
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return { name: 'ErrorDisplay', request: error }
|
return { name: 'ErrorDisplay', request: error }
|
||||||
} else if (request.showWarningMessage) {
|
|
||||||
return { name: 'WarningDisplay', request }
|
|
||||||
} else if (reconnecting) {
|
} else if (reconnecting) {
|
||||||
return { name: 'ReconnectingDisplay' }
|
return { name: 'ReconnectingDisplay' }
|
||||||
} else {
|
} else if (request) {
|
||||||
|
if (request.showWarningMessage) {
|
||||||
|
return { name: 'WarningDisplay', request }
|
||||||
|
}
|
||||||
return { name: 'WaitingDisplay', request }
|
return { name: 'WaitingDisplay', request }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
<b-card-body>
|
<b-card-body>
|
||||||
<b-card-title class="text-center mt-4" v-t="hasMessages ? 'api.processing' : 'api_waiting'" />
|
<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 -->
|
<!-- PROGRESS BAR -->
|
||||||
<b-progress
|
<b-progress
|
||||||
v-if="progress" class="my-4"
|
v-if="progress" class="my-4"
|
||||||
|
|
Loading…
Reference in a new issue