From 9bc365f32a4c980919ee24a0b97b2d224c6ec41f Mon Sep 17 00:00:00 2001 From: axolotle Date: Fri, 19 Feb 2021 18:36:22 +0100 Subject: [PATCH] Update store and overlay components to new api --- app/src/components/MessageListGroup.vue | 4 +- app/src/components/QueryHeader.vue | 55 +++-- app/src/i18n/locales/en.json | 1 - app/src/router/index.js | 2 +- app/src/store/data.js | 42 +--- app/src/store/info.js | 231 ++++++++++++-------- app/src/views/_partials/ErrorDisplay.vue | 25 +-- app/src/views/_partials/HistoryConsole.vue | 6 +- app/src/views/_partials/ViewLockOverlay.vue | 18 +- app/src/views/_partials/WaitingDisplay.vue | 12 +- 10 files changed, 223 insertions(+), 173 deletions(-) diff --git a/app/src/components/MessageListGroup.vue b/app/src/components/MessageListGroup.vue index 22b8da0f..b1dbcf11 100644 --- a/app/src/components/MessageListGroup.vue +++ b/app/src/components/MessageListGroup.vue @@ -3,8 +3,8 @@ v-bind="$attrs" ref="self" flush :class="{ 'fixed-height': fixedHeight, 'bordered': bordered }" > - - + + diff --git a/app/src/components/QueryHeader.vue b/app/src/components/QueryHeader.vue index ce47c558..6d265e49 100644 --- a/app/src/components/QueryHeader.vue +++ b/app/src/components/QueryHeader.vue @@ -1,38 +1,39 @@ @@ -42,11 +43,10 @@ export default { name: 'QueryHeader', props: { - action: { type: Object, required: true }, + request: { type: Object, required: true }, statusSize: { type: String, default: '' }, showTime: { type: Boolean, default: false }, - showError: { type: Boolean, default: false }, - truncate: { type: Boolean, default: true } + showError: { type: Boolean, default: false } }, computed: { @@ -57,21 +57,27 @@ export default { warning: 'warning', error: 'danger' } - return statuses[this.action.status] + return statuses[this.request.status] }, errorsCount () { - return this.action.messages.filter(({ type }) => type === 'danger').length + return this.request.messages.filter(({ type }) => type === 'danger').length }, warningsCount () { - return this.action.messages.filter(({ type }) => type === 'warning').length + return this.request.messages.filter(({ type }) => type === 'warning').length + } + }, + + methods: { + reviewError () { + this.$store.dispatch('REVIEW_ERROR', this.request) } }, filters: { readableUri (uri) { - return uri.split('?')[0].replace('/', ' > ') + return uri.split('?')[0].split('/').join(' > ') // replace('/', ' > ') }, hour (date) { @@ -110,6 +116,11 @@ div { } } +time { + min-width: 3.5rem; + text-align: right; +} + .count { display: flex; align-items: center; @@ -117,7 +128,7 @@ div { } @include media-breakpoint-down(xs) { - .xs-hide .action-desc { + .xs-hide .request-desc { display: none; } } diff --git a/app/src/i18n/locales/en.json b/app/src/i18n/locales/en.json index 86649174..a9c81591 100644 --- a/app/src/i18n/locales/en.json +++ b/app/src/i18n/locales/en.json @@ -203,7 +203,6 @@ "history": { "title": "History", "last_action": "Last action:", - "current_action": "Current action:", "methods": { "DELETE": "delete", "GET": "read", diff --git a/app/src/router/index.js b/app/src/router/index.js index 4b16cd50..ade0eb37 100644 --- a/app/src/router/index.js +++ b/app/src/router/index.js @@ -29,7 +29,7 @@ const router = new VueRouter({ router.beforeEach((to, from, next) => { if (store.getters.error) { - store.dispatch('DELETE_ERROR') + store.dispatch('DISMISS_ERROR', true) } // Allow if connected or route is not protected if (store.getters.connected || to.meta.noAuth) { diff --git a/app/src/store/data.js b/app/src/store/data.js index 5cb2f1f1..73747784 100644 --- a/app/src/store/data.js +++ b/app/src/store/data.js @@ -90,40 +90,21 @@ export default { }, actions: { - 'FETCH' ({ state, commit, rootState }, { uri, param, storeKey = uri, cache = rootState.cache }) { + 'GET' ({ state, commit, rootState }, { uri, param, storeKey = uri, options = {} }) { + const noCache = !rootState.cache || options.noCache || false const currentState = param ? state[storeKey][param] : state[storeKey] // if data has already been queried, simply return - if (currentState !== undefined && cache) return currentState + if (currentState !== undefined && !noCache) return currentState - return api.get(param ? `${uri}/${param}` : uri).then(responseData => { + return api.fetch('GET', param ? `${uri}/${param}` : uri, null, options).then(responseData => { const data = responseData[storeKey] ? responseData[storeKey] : responseData commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data) return param ? state[storeKey][param] : state[storeKey] }) }, - 'FETCH_ALL' ({ state, commit, rootState }, queries) { - return Promise.all(queries.map(({ uri, param, storeKey = uri, cache = rootState.cache }) => { - const currentState = param ? state[storeKey][param] : state[storeKey] - // if data has already been queried, simply return the state as cached - if (currentState !== undefined && cache) { - return { cached: currentState } - } - return api.get(param ? `${uri}/${param}` : uri).then(responseData => { - return { storeKey, param, responseData } - }) - })).then(responsesData => { - return responsesData.map(({ storeKey, param, responseData, cached = undefined }) => { - if (cached !== undefined) return cached - const data = responseData[storeKey] ? responseData[storeKey] : responseData - commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data) - return param ? state[storeKey][param] : state[storeKey] - }) - }) - }, - - 'POST' ({ state, commit }, { uri, data, storeKey = uri }) { - return api.post(uri, data).then(responseData => { + 'POST' ({ state, commit }, { uri, storeKey = uri, data, options }) { + return api.fetch('POST', uri, data, options).then(responseData => { // FIXME api/domains returns null if (responseData === null) responseData = data responseData = responseData[storeKey] ? responseData[storeKey] : responseData @@ -132,16 +113,16 @@ export default { }) }, - 'PUT' ({ state, commit }, { uri, param, data, storeKey = uri }) { - return api.put(param ? `${uri}/${param}` : uri, data).then(responseData => { + 'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, options }) { + return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, options).then(responseData => { const data = responseData[storeKey] ? responseData[storeKey] : responseData commit('UPDATE_' + storeKey.toUpperCase(), param ? [param, data] : data) return param ? state[storeKey][param] : state[storeKey] }) }, - 'DELETE' ({ commit }, { uri, param, data = {}, storeKey = uri }) { - return api.delete(param ? `${uri}/${param}` : uri, data).then(() => { + 'DELETE' ({ commit }, { uri, param, storeKey = uri, data, options }) { + return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, options).then(() => { commit('DEL_' + storeKey.toUpperCase(), param) }) } @@ -164,8 +145,7 @@ export default { }) }, - // not cached - user: state => name => state.users_details[name], + user: state => name => state.users_details[name], // not cached domains: state => state.domains, diff --git a/app/src/store/info.js b/app/src/store/info.js index 5c19075c..92f1fd2c 100644 --- a/app/src/store/info.js +++ b/app/src/store/info.js @@ -5,95 +5,70 @@ import { timeout } from '@/helpers/commons' export default { state: { - host: window.location.host, - connected: localStorage.getItem('connected') === 'true', - yunohost: null, // yunohost app infos: Object {version, repo} - error: null, - waiting: false, - history: [] + 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, connected) { - localStorage.setItem('connected', connected) - state.connected = connected + 'SET_CONNECTED' (state, boolean) { + localStorage.setItem('connected', boolean) + state.connected = boolean }, 'SET_YUNOHOST_INFOS' (state, yunohost) { state.yunohost = yunohost }, - 'UPDATE_WAITING' (state, boolean) { + 'SET_WAITING' (state, boolean) { state.waiting = boolean }, - 'ADD_HISTORY_ENTRY' (state, [uri, method, date]) { - state.history.push({ uri, method, date, status: 'pending', messages: [] }) + '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_LAST_HISTORY_ENTRY' (state, [key, value]) { - Vue.set(state.history[state.history.length - 1], key, value) + 'UPDATE_REQUEST' (state, { request, key, value }) { + // This rely on data persistance and reactivity. + Vue.set(request, key, value) }, - 'ADD_MESSAGE' (state, message) { - state.history[state.history.length - 1].messages.push(message) + 'REMOVE_REQUEST' (state, request) { + const index = state.requests.lastIndexOf(request) + state.requests.splice(index, 1) }, - 'UPDATE_PROGRESS' (state, progress) { - Vue.set(state.history[state.history.length - 1], 'progress', progress) + 'ADD_HISTORY_ACTION' (state, request) { + state.history.push(request) }, - 'SET_ERROR' (state, error) { - state.error = error + '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: { - 'LOGIN' ({ dispatch }, password) { - // Entering a wrong password will trigger a 401 api response. - // action `DISCONNECT` will then be triggered by the response handler but will not - // redirect to `/login` so the view can display the catched error. - return api.post('login', { password }).then(() => { - dispatch('CONNECT') - }) - }, - - 'LOGOUT' ({ dispatch }) { - return api.get('logout').then(() => { - dispatch('DISCONNECT') - }) - }, - - 'RESET_CONNECTED' ({ commit }) { - commit('SET_CONNECTED', false) - commit('SET_YUNOHOST_INFOS', null) - }, - - 'DISCONNECT' ({ dispatch, commit }, route) { - dispatch('RESET_CONNECTED') - commit('UPDATE_WAITING', false) - 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 } - : {} - }) - }, - - 'CONNECT' ({ commit, dispatch }) { - commit('SET_CONNECTED', true) - dispatch('GET_YUNOHOST_INFOS') - router.push(router.currentRoute.query.redirect || { name: 'home' }) - }, - - 'GET_YUNOHOST_INFOS' ({ commit }) { - return api.get('versions').then(versions => { - commit('SET_YUNOHOST_INFOS', versions.yunohost) - }) - }, - '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. @@ -108,29 +83,85 @@ export default { }) }, - 'WAITING_FOR_RESPONSE' ({ commit }, [uri, method]) { - commit('UPDATE_WAITING', true) - commit('ADD_HISTORY_ENTRY', [uri, method, Date.now()]) + 'CONNECT' ({ commit, dispatch }) { + commit('SET_CONNECTED', true) + dispatch('GET_YUNOHOST_INFOS') + router.push(router.currentRoute.query.redirect || { name: 'home' }) }, - 'SERVER_RESPONDED' ({ state, commit }, success) { - const action = state.history.length ? state.history[state.history.length - 1] : null - if (action) { - let status = success ? 'success' : 'error' - if (status === 'success' && action.messages.some(msg => msg.type === 'danger' || msg.type === 'warning')) { - status = 'warning' - } - commit('UPDATE_LAST_HISTORY_ENTRY', ['status', status]) + '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 }, { 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, initial, wait, websocket }) { + let request = { method, uri, initial, status: 'pending' } + if (websocket) { + request = { ...request, messages: [], date: Date.now(), warnings: 0, errors: 0 } + commit('ADD_HISTORY_ACTION', request) } - commit('UPDATE_WAITING', false) + 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 }, - 'DISPATCH_MESSAGE' ({ commit }, messages) { - const typeToColor = { error: 'danger' } + 'END_REQUEST' ({ commit }, { request, success, wait }) { + let status = success ? 'success' : 'error' + if (success && (request.warnings || request.errors)) { + status = 'warning' + } + + commit('UPDATE_REQUEST', { request, key: 'status', value: status }) + if (wait) { + // 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], - type: type in typeToColor ? typeToColor[type] : type + color: type === 'error' ? 'danger' : type } let progressBar = message.text.match(/^\[#*\+*\.*\] > /) if (progressBar) { @@ -140,15 +171,15 @@ export default { for (const char of progressBar) { if (char in progress) progress[char] += 1 } - commit('UPDATE_PROGRESS', Object.values(progress)) + commit('UPDATE_REQUEST', { request, key: 'progress', value: Object.values(progress) }) } if (message.text) { - commit('ADD_MESSAGE', message) + commit('ADD_MESSAGE', { request, message, type }) } } }, - 'HANDLE_ERROR' ({ state, commit, dispatch }, error) { + 'HANDLE_ERROR' ({ commit, dispatch }, error) { if (error.code === 401) { // Unauthorized dispatch('DISCONNECT') @@ -156,23 +187,47 @@ export default { // 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', error) + commit('SET_ERROR', request) } }, - 'DELETE_ERROR' ({ commit }) { + '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) } }, getters: { host: state => state.host, - connected: state => (state.connected), - yunohost: state => (state.yunohost), + 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] + 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] + } } } diff --git a/app/src/views/_partials/ErrorDisplay.vue b/app/src/views/_partials/ErrorDisplay.vue index 1eef7c25..344cdc0f 100644 --- a/app/src/views/_partials/ErrorDisplay.vue +++ b/app/src/views/_partials/ErrorDisplay.vue @@ -30,11 +30,11 @@
{{ error.traceback }}
- @@ -53,8 +54,13 @@ export default { ::v-deep { .card-body { padding: 1.5rem; + padding-bottom: 0; max-height: 60vh; overflow-y: auto; + + & > :last-child { + margin-bottom: 1.5rem; + } } .card-footer { diff --git a/app/src/views/_partials/WaitingDisplay.vue b/app/src/views/_partials/WaitingDisplay.vue index 986718da..e98e0edd 100644 --- a/app/src/views/_partials/WaitingDisplay.vue +++ b/app/src/views/_partials/WaitingDisplay.vue @@ -16,7 +16,7 @@
@@ -35,28 +35,28 @@ export default { }, props: { - action: { type: Object, required: true } + request: { type: Object, required: true } }, computed: { ...mapGetters(['spinner']), hasMessages () { - return this.action && this.action.messages.length > 0 + return this.request.messages && this.request.messages.length > 0 }, progress () { - const progress = this.action.progress + const progress = this.request.progress if (!progress) return null return { - values: progress, max: progress.reduce((sum, value) => (sum + value), 0) + values: progress, + max: progress.reduce((sum, value) => (sum + value), 0) } } } } -