add ReconnectingDisplay and reconnection mecanism

This commit is contained in:
axolotle 2021-11-10 19:14:50 +01:00
parent 246c001f84
commit c259a97a74
5 changed files with 133 additions and 2 deletions

View file

@ -189,5 +189,34 @@ export default {
delete (uri, data = {}, humanKey = null, options = {}) {
if (typeof uri === 'string') return this.fetch('DELETE', uri, data, humanKey, options)
return store.dispatch('DELETE', { ...uri, data, humanKey, options })
},
/**
* Api reconnection helper. Resolve when server is reachable or fail after n attemps
*
* @param {Number} attemps - number of attemps before rejecting
* @param {Number} delay - delay between calls to the API in ms.
* @param {Number} initialDelay - delay before calling the API for the first time in ms.
* @return {Promise<undefined|Error>}
*/
tryToReconnect ({ attemps = 1, delay = 2000, initialDelay = 0 } = {}) {
return new Promise((resolve, reject) => {
const api = this
function reconnect (n) {
api.get('logout', {}, { key: 'reconnecting' }).then(resolve).catch(err => {
if (err.name === 'APIUnauthorizedError') {
resolve()
} else if (n < 1) {
reject(err)
} else {
setTimeout(() => reconnect(n - 1), delay)
}
})
}
if (initialDelay > 0) setTimeout(() => reconnect(attemps), initialDelay)
else reconnect(attemps)
})
}
}

View file

@ -21,6 +21,15 @@
"pending": "In progress",
"success": "Successfully completed",
"warning": "Successfully completed with errors or alerts"
},
"reconnecting": {
"title": "Trying to communicate with the server...",
"failed": "Looks like the server is not responding. You can try to reconnect again or try to run `systemctl restart yunohost-api` thru ssh.",
"reason": {
"unknown": "Connection with the server has been closed for unknown reasons.",
"upgrade_system": "Connection with the server has been closed due to yunohost upgrade. Waiting for the server to be reachable again…"
},
"success": "The server is now reachable! You can try to login"
}
},
"api_error": {
@ -363,6 +372,7 @@
"rerun_diagnosis": "Rerun diagnosis",
"restore": "Restore",
"restart": "Restart",
"retry": "Retry",
"human_routes": {
"adminpw": "Change admin password",
"apps": {
@ -422,6 +432,7 @@
},
"postinstall": "Run the post-install",
"reboot": "Reboot the server",
"reconnecting": "Reconnecting",
"services": {
"restart": "Restart the service '{name}'",
"start": "Start the service '{name}'",

View file

@ -10,6 +10,7 @@ export default {
connected: localStorage.getItem('connected') === 'true', // Boolean
yunohost: null, // Object { version, repo }
waiting: false, // Boolean
reconnecting: false, // Boolean
history: [], // Array of `request`
requests: [], // Array of `request`
error: null, // null || request
@ -31,6 +32,10 @@ export default {
state.waiting = boolean
},
'SET_RECONNECTING' (state, boolean) {
state.reconnecting = boolean
},
'ADD_REQUEST' (state, request) {
if (state.requests.length > 10) {
// We do not remove requests right after it resolves since an error might bring
@ -133,6 +138,11 @@ export default {
return api.get('logout')
},
'TRY_TO_RECONNECT' ({ commit, dispatch }) {
commit('SET_RECONNECTING', true)
dispatch('RESET_CONNECTED')
},
'GET_YUNOHOST_INFOS' ({ commit }) {
return api.get('versions').then(versions => {
commit('SET_YUNOHOST_INFOS', versions.yunohost)
@ -144,7 +154,7 @@ 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, humanRoute, initial, status: 'pending' }
let request = { method, uri, humanRouteKey: key, humanRoute, initial, status: 'pending' }
if (websocket) {
request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 }
commit('ADD_HISTORY_ACTION', request)
@ -252,7 +262,7 @@ export default {
'DISMISS_WARNING' ({ commit, state }, request) {
commit('SET_WAITING', false)
delete request.showWarningMessage
Vue.delete(request, 'showWarningMessage')
}
},
@ -262,6 +272,7 @@ export default {
yunohost: state => state.yunohost,
error: state => state.error,
waiting: state => state.waiting,
reconnecting: state => state.reconnecting,
history: state => state.history,
lastAction: state => state.history[state.history.length - 1],
currentRequest: state => {

View file

@ -0,0 +1,79 @@
<template>
<!-- This card receives style from `ViewLockOverlay` if used inside it -->
<b-card-body>
<b-card-title class="text-center my-4" v-t="'api.reconnecting.title'" />
<template v-if="status === 'reconnecting'">
<spinner class="mb-4" />
<b-alert
v-if="origin"
v-t="'api.reconnecting.reason.' + origin"
:variant="origin === 'unknow' ? 'warning' : 'info'"
/>
</template>
<template v-if="status === 'failed'">
<b-alert variant="danger">
<markdown-item :label="$t('api.reconnecting.failed')" />
</b-alert>
<div class="d-flex justify-content-end">
<b-button
variant="success" v-t="'retry'" class="ml-auto"
@click="tryToReconnect()"
/>
</div>
</template>
<template v-if="status === 'success'">
<b-alert variant="success" v-t="'api.reconnecting.success'" />
<login-view skip-install-check force-reload />
</template>
</b-card-body>
</template>
<script>
import { mapGetters } from 'vuex'
import api from '@/api'
import LoginView from '@/views/Login'
export default {
name: 'ReconnectingDisplay',
components: {
LoginView
},
data () {
return {
status: 'reconnecting',
origin: undefined
}
},
computed: {
...mapGetters(['currentRequest'])
},
methods: {
tryToReconnect ({ initialDelay = 0 } = {}) {
this.status = 'reconnecting'
api.tryToReconnect({ initialDelay }).then(() => {
this.status = 'success'
}).catch(() => {
this.status = 'failed'
})
}
},
created () {
const origin = this.currentRequest.humanRouteKey
this.origin = ['upgrade.system'].includes(origin) ? origin.replace('.', '_') : 'unknown'
this.tryToReconnect({ initialDelay: 2000 })
}
}
</script>

View file

@ -1,6 +1,7 @@
export { default as ErrorDisplay } from './ErrorDisplay'
export { default as WarningDisplay } from './WarningDisplay'
export { default as WaitingDisplay } from './WaitingDisplay'
export { default as ReconnectingDisplay } from './ReconnectingDisplay'
export { default as HistoryConsole } from './HistoryConsole'
export { default as ViewLockOverlay } from './ViewLockOverlay'