import Vue from 'vue' import api from '@/api' import router from '@/router' import i18n from '@/i18n' import { timeout, isObjectLiteral } from '@/helpers/commons' export default { state: { host: window.location.host, // String connected: localStorage.getItem('connected') === 'true', // Boolean yunohost: null, // Object { version, repo } waiting: false, // Boolean history: [], // Array of `request` requests: [], // Array of `request` error: null // null || request }, mutations: { 'SET_CONNECTED' (state, boolean) { localStorage.setItem('connected', boolean) state.connected = boolean }, 'SET_YUNOHOST_INFOS' (state, yunohost) { state.yunohost = yunohost }, 'SET_WAITING' (state, boolean) { state.waiting = boolean }, 'ADD_REQUEST' (state, request) { if (state.requests.length > 10) { // We do not remove requests right after it resolves since an error might bring // one back to life but we can safely remove some here. state.requests.shift() } state.requests.push(request) }, 'UPDATE_REQUEST' (state, { request, key, value }) { // This rely on data persistance and reactivity. Vue.set(request, key, value) }, 'REMOVE_REQUEST' (state, request) { const index = state.requests.lastIndexOf(request) state.requests.splice(index, 1) }, 'ADD_HISTORY_ACTION' (state, request) { state.history.push(request) }, 'ADD_MESSAGE' (state, { message, type }) { const request = state.history[state.history.length - 1] request.messages.push(message) if (['error', 'warning'].includes(type)) { request[type + 's']++ } }, 'SET_ERROR' (state, request) { if (request) { state.error = request } else { state.error = null } } }, actions: { 'CHECK_INSTALL' ({ dispatch }, retry = 2) { // this action will try to query the `/installed` route 3 times every 5 s with // a timeout of the same delay. // FIXME need testing with api not responding return timeout(api.get('installed'), 5000).then(({ installed }) => { return installed }).catch(err => { if (retry > 0) { return dispatch('CHECK_INSTALL', --retry) } throw err }) }, 'CONNECT' ({ commit, dispatch }) { commit('SET_CONNECTED', true) dispatch('GET_YUNOHOST_INFOS') router.push(router.currentRoute.query.redirect || { name: 'home' }) }, 'RESET_CONNECTED' ({ commit }) { commit('SET_CONNECTED', false) commit('SET_YUNOHOST_INFOS', null) }, 'DISCONNECT' ({ dispatch }, route = router.currentRoute) { dispatch('RESET_CONNECTED') if (router.currentRoute.name === 'login') return router.push({ name: 'login', // Add a redirect query if next route is not unknown (like `logout`) or `login` query: route && !['login', null].includes(route.name) ? { redirect: route.path } : {} }) }, 'LOGIN' ({ dispatch }, password) { return api.post('login', { password }, null, { websocket: false }).then(() => { dispatch('CONNECT') }) }, 'LOGOUT' ({ dispatch }) { dispatch('DISCONNECT') return api.get('logout') }, 'GET_YUNOHOST_INFOS' ({ commit }) { return api.get('versions').then(versions => { commit('SET_YUNOHOST_INFOS', versions.yunohost) }) }, 'INIT_REQUEST' ({ commit }, { method, uri, humanKey, initial, wait, websocket }) { // Try to find a description for an API route to display in history and modals 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' } if (websocket) { request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 } commit('ADD_HISTORY_ACTION', request) } commit('ADD_REQUEST', request) if (wait) { setTimeout(() => { // Display the waiting modal only if the request takes some time. if (request.status === 'pending') { commit('SET_WAITING', true) } }, 400) } return request }, 'END_REQUEST' ({ commit }, { request, success, wait }) { let status = success ? 'success' : 'error' if (success && (request.warnings || request.errors)) { const messages = request.messages if (messages.length && messages[messages.length - 1].color === 'warning') { request.showWarningMessage = true } status = 'warning' } commit('UPDATE_REQUEST', { request, key: 'status', value: status }) if (wait && !request.showWarningMessage) { // Remove the overlay after a short delay to allow an error to display withtout flickering. setTimeout(() => { commit('SET_WAITING', false) }, 100) } }, 'DISPATCH_MESSAGE' ({ commit }, { request, messages }) { for (const type in messages) { const message = { text: messages[type].replace('\n', '
'), color: type === 'error' ? 'danger' : type } let progressBar = message.text.match(/^\[#*\+*\.*\] > /) if (progressBar) { progressBar = progressBar[0] message.text = message.text.replace(progressBar, '') const progress = { '#': 0, '+': 0, '.': 0 } for (const char of progressBar) { if (char in progress) progress[char] += 1 } commit('UPDATE_REQUEST', { request, key: 'progress', value: Object.values(progress) }) } if (message.text) { commit('ADD_MESSAGE', { request, message, type }) } } }, 'HANDLE_ERROR' ({ commit, dispatch }, error) { if (error.code === 401) { // Unauthorized dispatch('DISCONNECT') } else if (error.logRef) { // Errors that have produced logs router.push({ name: 'tool-log', params: { name: error.logRef } }) } else { // The request is temporarely stored in the error for reference, but we reverse // the ownership to stay generic. const request = error.request delete error.request Vue.set(request, 'error', error) // Display the error in a modal on the current view. commit('SET_ERROR', request) } }, 'REVIEW_ERROR' ({ commit }, request) { request.review = true commit('SET_ERROR', request) }, 'DISMISS_ERROR' ({ commit, state }, { initial, review = false }) { if (initial && !review) { // In case of an initial request (data that is needed by a view to render itself), // try to go back so the user doesn't get stuck at a never ending skeleton view. if (history.length > 2) { history.back() } else { // if the url was opened in a new tab, return to home router.push({ name: 'home' }) } } commit('SET_ERROR', null) }, 'DISMISS_WARNING' ({ commit, state }, request) { commit('SET_WAITING', false) delete request.showWarningMessage } }, getters: { host: state => state.host, connected: state => state.connected, yunohost: state => state.yunohost, error: state => state.error, waiting: state => state.waiting, 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] } } }