Update store and overlay components to new api

This commit is contained in:
axolotle 2021-02-19 18:36:22 +01:00
parent b130aeda29
commit 9bc365f32a
10 changed files with 223 additions and 173 deletions

View file

@ -3,8 +3,8 @@
v-bind="$attrs" ref="self" v-bind="$attrs" ref="self"
flush :class="{ 'fixed-height': fixedHeight, 'bordered': bordered }" flush :class="{ 'fixed-height': fixedHeight, 'bordered': bordered }"
> >
<b-list-group-item v-for="({ type, text }, i) in messages" :key="i"> <b-list-group-item v-for="({ color, text }, i) in messages" :key="i">
<span class="status" :class="'bg-' + type" /> <span class="status" :class="'bg-' + color" />
<span v-html="text" /> <span v-html="text" />
</b-list-group-item> </b-list-group-item>
</b-list-group> </b-list-group>

View file

@ -1,38 +1,39 @@
<template> <template>
<div class="query-header w-100" v-on="$listeners" v-bind="$attrs"> <div class="query-header w-100" v-on="$listeners" v-bind="$attrs">
<!-- STATUS --> <!-- STATUS -->
<span class="status" :class="['bg-' + color, statusSize]" :aria-label="$t('api.query_status.' + action.status)" /> <span class="status" :class="['bg-' + color, statusSize]" :aria-label="$t('api.query_status.' + request.status)" />
<!-- ACTION DESCRIPTION --> <!-- REQUEST DESCRIPTION -->
<strong class="action-desc"> <strong class="request-desc">
{{ action.uri | readableUri }} {{ request.uri | readableUri }}
<small>({{ $t('history.methods.' + action.method) }})</small> <small>({{ $t('history.methods.' + request.method) }})</small>
</strong> </strong>
<div> <div v-if="request.errors || request.warnings">
<!-- WEBSOCKET ERRORS COUNT --> <!-- WEBSOCKET ERRORS COUNT -->
<span class="count" v-if="errorsCount"> <span class="count" v-if="request.errors">
{{ errorsCount }}<icon iname="bug" class="text-danger ml-1" /> {{ request.errors }}<icon iname="bug" class="text-danger ml-1" />
</span> </span>
<!-- WEBSOCKET WARNINGS COUNT --> <!-- WEBSOCKET WARNINGS COUNT -->
<span class="count" v-if="warningsCount"> <span class="count" v-if="request.warnings">
{{ warningsCount }}<icon iname="warning" class="text-warning ml-1" /> {{ request.warnings }}<icon iname="warning" class="text-warning ml-1" />
</span> </span>
</div> </div>
<!-- VIEW ERRO BUTTON --> <!-- VIEW ERROR BUTTON -->
<b-button <b-button
v-if="showError && action.status === 'error'" v-if="showError && request.error"
size="sm" pill size="sm" pill
class="error-btn ml-auto py-0" class="error-btn ml-auto py-0"
variant="danger" variant="danger"
@click="reviewError"
> >
<small v-t="'api_error.view_error'" /> <small v-t="'api_error.view_error'" />
</b-button> </b-button>
<!-- TIME DISPLAY --> <!-- TIME DISPLAY -->
<time v-if="showTime" :datetime="action.date | hour" :class="!showError || action.status !== 'error' ? 'ml-auto' : 'ml-2'"> <time v-if="showTime" :datetime="request.date | hour" :class="request.error ? 'ml-2' : 'ml-auto'">
{{ action.date | hour }} {{ request.date | hour }}
</time> </time>
</div> </div>
</template> </template>
@ -42,11 +43,10 @@ export default {
name: 'QueryHeader', name: 'QueryHeader',
props: { props: {
action: { type: Object, required: true }, request: { type: Object, required: true },
statusSize: { type: String, default: '' }, statusSize: { type: String, default: '' },
showTime: { type: Boolean, default: false }, showTime: { type: Boolean, default: false },
showError: { type: Boolean, default: false }, showError: { type: Boolean, default: false }
truncate: { type: Boolean, default: true }
}, },
computed: { computed: {
@ -57,21 +57,27 @@ export default {
warning: 'warning', warning: 'warning',
error: 'danger' error: 'danger'
} }
return statuses[this.action.status] return statuses[this.request.status]
}, },
errorsCount () { errorsCount () {
return this.action.messages.filter(({ type }) => type === 'danger').length return this.request.messages.filter(({ type }) => type === 'danger').length
}, },
warningsCount () { 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: { filters: {
readableUri (uri) { readableUri (uri) {
return uri.split('?')[0].replace('/', ' > ') return uri.split('?')[0].split('/').join(' > ') // replace('/', ' > ')
}, },
hour (date) { hour (date) {
@ -110,6 +116,11 @@ div {
} }
} }
time {
min-width: 3.5rem;
text-align: right;
}
.count { .count {
display: flex; display: flex;
align-items: center; align-items: center;
@ -117,7 +128,7 @@ div {
} }
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
.xs-hide .action-desc { .xs-hide .request-desc {
display: none; display: none;
} }
} }

View file

@ -203,7 +203,6 @@
"history": { "history": {
"title": "History", "title": "History",
"last_action": "Last action:", "last_action": "Last action:",
"current_action": "Current action:",
"methods": { "methods": {
"DELETE": "delete", "DELETE": "delete",
"GET": "read", "GET": "read",

View file

@ -29,7 +29,7 @@ const router = new VueRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (store.getters.error) { if (store.getters.error) {
store.dispatch('DELETE_ERROR') store.dispatch('DISMISS_ERROR', true)
} }
// Allow if connected or route is not protected // Allow if connected or route is not protected
if (store.getters.connected || to.meta.noAuth) { if (store.getters.connected || to.meta.noAuth) {

View file

@ -90,40 +90,21 @@ export default {
}, },
actions: { 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] const currentState = param ? state[storeKey][param] : state[storeKey]
// if data has already been queried, simply return // 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 const data = responseData[storeKey] ? responseData[storeKey] : responseData
commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data) commit('SET_' + storeKey.toUpperCase(), param ? [param, data] : data)
return param ? state[storeKey][param] : state[storeKey] return param ? state[storeKey][param] : state[storeKey]
}) })
}, },
'FETCH_ALL' ({ state, commit, rootState }, queries) { 'POST' ({ state, commit }, { uri, storeKey = uri, data, options }) {
return Promise.all(queries.map(({ uri, param, storeKey = uri, cache = rootState.cache }) => { return api.fetch('POST', uri, data, options).then(responseData => {
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 => {
// FIXME api/domains returns null // FIXME api/domains returns null
if (responseData === null) responseData = data if (responseData === null) responseData = data
responseData = responseData[storeKey] ? responseData[storeKey] : responseData responseData = responseData[storeKey] ? responseData[storeKey] : responseData
@ -132,16 +113,16 @@ export default {
}) })
}, },
'PUT' ({ state, commit }, { uri, param, data, storeKey = uri }) { 'PUT' ({ state, commit }, { uri, param, storeKey = uri, data, options }) {
return api.put(param ? `${uri}/${param}` : uri, data).then(responseData => { return api.fetch('PUT', param ? `${uri}/${param}` : uri, data, options).then(responseData => {
const data = responseData[storeKey] ? responseData[storeKey] : responseData const data = responseData[storeKey] ? responseData[storeKey] : responseData
commit('UPDATE_' + storeKey.toUpperCase(), param ? [param, data] : data) commit('UPDATE_' + storeKey.toUpperCase(), param ? [param, data] : data)
return param ? state[storeKey][param] : state[storeKey] return param ? state[storeKey][param] : state[storeKey]
}) })
}, },
'DELETE' ({ commit }, { uri, param, data = {}, storeKey = uri }) { 'DELETE' ({ commit }, { uri, param, storeKey = uri, data, options }) {
return api.delete(param ? `${uri}/${param}` : uri, data).then(() => { return api.fetch('DELETE', param ? `${uri}/${param}` : uri, data, options).then(() => {
commit('DEL_' + storeKey.toUpperCase(), param) commit('DEL_' + storeKey.toUpperCase(), param)
}) })
} }
@ -164,8 +145,7 @@ export default {
}) })
}, },
// not cached user: state => name => state.users_details[name], // not cached
user: state => name => state.users_details[name],
domains: state => state.domains, domains: state => state.domains,

View file

@ -5,95 +5,70 @@ import { timeout } from '@/helpers/commons'
export default { export default {
state: { state: {
host: window.location.host, host: window.location.host, // String
connected: localStorage.getItem('connected') === 'true', connected: localStorage.getItem('connected') === 'true', // Boolean
yunohost: null, // yunohost app infos: Object {version, repo} yunohost: null, // Object { version, repo }
error: null, waiting: false, // Boolean
waiting: false, history: [], // Array of `request`
history: [] requests: [], // Array of `request`
error: null // null || request
}, },
mutations: { mutations: {
'SET_CONNECTED' (state, connected) { 'SET_CONNECTED' (state, boolean) {
localStorage.setItem('connected', connected) localStorage.setItem('connected', boolean)
state.connected = connected state.connected = boolean
}, },
'SET_YUNOHOST_INFOS' (state, yunohost) { 'SET_YUNOHOST_INFOS' (state, yunohost) {
state.yunohost = yunohost state.yunohost = yunohost
}, },
'UPDATE_WAITING' (state, boolean) { 'SET_WAITING' (state, boolean) {
state.waiting = boolean state.waiting = boolean
}, },
'ADD_HISTORY_ENTRY' (state, [uri, method, date]) { 'ADD_REQUEST' (state, request) {
state.history.push({ uri, method, date, status: 'pending', messages: [] }) 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]) { 'UPDATE_REQUEST' (state, { request, key, value }) {
Vue.set(state.history[state.history.length - 1], key, value) // This rely on data persistance and reactivity.
Vue.set(request, key, value)
}, },
'ADD_MESSAGE' (state, message) { 'REMOVE_REQUEST' (state, request) {
state.history[state.history.length - 1].messages.push(message) const index = state.requests.lastIndexOf(request)
state.requests.splice(index, 1)
}, },
'UPDATE_PROGRESS' (state, progress) { 'ADD_HISTORY_ACTION' (state, request) {
Vue.set(state.history[state.history.length - 1], 'progress', progress) state.history.push(request)
}, },
'SET_ERROR' (state, error) { 'ADD_MESSAGE' (state, { message, type }) {
state.error = error 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: { 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) { 'CHECK_INSTALL' ({ dispatch }, retry = 2) {
// this action will try to query the `/installed` route 3 times every 5 s with // this action will try to query the `/installed` route 3 times every 5 s with
// a timeout of the same delay. // a timeout of the same delay.
@ -108,29 +83,85 @@ export default {
}) })
}, },
'WAITING_FOR_RESPONSE' ({ commit }, [uri, method]) { 'CONNECT' ({ commit, dispatch }) {
commit('UPDATE_WAITING', true) commit('SET_CONNECTED', true)
commit('ADD_HISTORY_ENTRY', [uri, method, Date.now()]) dispatch('GET_YUNOHOST_INFOS')
router.push(router.currentRoute.query.redirect || { name: 'home' })
}, },
'SERVER_RESPONDED' ({ state, commit }, success) { 'RESET_CONNECTED' ({ commit }) {
const action = state.history.length ? state.history[state.history.length - 1] : null commit('SET_CONNECTED', false)
if (action) { 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('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' let status = success ? 'success' : 'error'
if (status === 'success' && action.messages.some(msg => msg.type === 'danger' || msg.type === 'warning')) { if (success && (request.warnings || request.errors)) {
status = 'warning' status = 'warning'
} }
commit('UPDATE_LAST_HISTORY_ENTRY', ['status', status])
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)
} }
commit('UPDATE_WAITING', false)
}, },
'DISPATCH_MESSAGE' ({ commit }, messages) { 'DISPATCH_MESSAGE' ({ commit }, { request, messages }) {
const typeToColor = { error: 'danger' }
for (const type in messages) { for (const type in messages) {
const message = { const message = {
text: messages[type], text: messages[type],
type: type in typeToColor ? typeToColor[type] : type color: type === 'error' ? 'danger' : type
} }
let progressBar = message.text.match(/^\[#*\+*\.*\] > /) let progressBar = message.text.match(/^\[#*\+*\.*\] > /)
if (progressBar) { if (progressBar) {
@ -140,15 +171,15 @@ 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_PROGRESS', Object.values(progress)) commit('UPDATE_REQUEST', { request, key: 'progress', value: Object.values(progress) })
} }
if (message.text) { 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) { if (error.code === 401) {
// Unauthorized // Unauthorized
dispatch('DISCONNECT') dispatch('DISCONNECT')
@ -156,23 +187,47 @@ export default {
// Errors that have produced logs // Errors that have produced logs
router.push({ name: 'tool-log', params: { name: error.logRef } }) router.push({ name: 'tool-log', params: { name: error.logRef } })
} else { } 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. // 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) commit('SET_ERROR', null)
} }
}, },
getters: { getters: {
host: state => state.host, host: state => state.host,
connected: state => (state.connected), connected: state => state.connected,
yunohost: state => (state.yunohost), yunohost: state => state.yunohost,
error: state => state.error, error: state => state.error,
waiting: state => state.waiting, waiting: state => state.waiting,
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 => {
const request = state.requests.find(({ status }) => status === 'pending')
return request || state.requests[state.requests.length - 1]
}
} }
} }

View file

@ -30,11 +30,11 @@
<pre><code>{{ error.traceback }}</code></pre> <pre><code>{{ error.traceback }}</code></pre>
</template> </template>
<template v-if="hasMessages"> <template v-if="messages">
<p class="my-2"> <p class="my-2">
<strong v-t="'api_error.server_said'" /> <strong v-t="'api_error.server_said'" />
</p> </p>
<message-list-group :messages="action.messages" bordered /> <message-list-group :messages="messages" bordered />
</template> </template>
</b-card-body> </b-card-body>
@ -49,35 +49,34 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'
import MessageListGroup from '@/components/MessageListGroup' import MessageListGroup from '@/components/MessageListGroup'
export default { export default {
name: 'ErrorPage', name: 'ErrorDisplay',
components: { components: {
MessageListGroup MessageListGroup
}, },
props: { props: {
action: { type: Object, required: true } request: { type: [Object, null], default: null }
}, },
computed: { computed: {
...mapGetters(['error']), error () {
return this.request.error
},
hasMessages () { messages () {
return this.action && this.action.messages.length > 0 const messages = this.request.messages
if (messages && messages.length > 0) return messages
return null
} }
}, },
methods: { methods: {
dismiss () { dismiss () {
if (this.error && this.error.method === 'GET') { this.$store.dispatch('DISMISS_ERROR', this.request)
history.back()
}
this.$store.dispatch('DELETE_ERROR')
} }
} }
} }

View file

@ -23,9 +23,9 @@
@click.prevent="onLastActionClick" @click.prevent="onLastActionClick"
@keyup.enter.space.prevent="onLastActionClick" @keyup.enter.space.prevent="onLastActionClick"
> >
<small>{{ $t('history.' + (lastAction.status === 'pending' ? 'current_action' : 'last_action')) }}</small> <small>{{ $t('history.last_action') }}</small>
</b-button> </b-button>
<query-header v-if="lastAction" :action="lastAction" class="w-auto ml-2 xs-hide" /> <query-header v-if="lastAction" :request="lastAction" class="w-auto ml-2 xs-hide" />
</b-card-header> </b-card-header>
<b-collapse id="console-collapse" v-model="open"> <b-collapse id="console-collapse" v-model="open">
@ -43,7 +43,7 @@
<!-- ACTION DESC --> <!-- ACTION DESC -->
<query-header <query-header
role="tab" v-b-toggle="action.messages.length ? 'messages-collapse-' + i : false" role="tab" v-b-toggle="action.messages.length ? 'messages-collapse-' + i : false"
:action="action" show-time show-error :request="action" show-time show-error
/> />
</b-card-header> </b-card-header>

View file

@ -7,12 +7,13 @@
<slot name="default" /> <slot name="default" />
<template v-slot:overlay> <template v-slot:overlay>
<b-card no-body class="card-overlay" v-if="lastAction"> <b-card no-body class="card-overlay">
<b-card-header header-bg-variant="white"> <b-card-header header-bg-variant="white">
<query-header :action="lastAction" status-size="lg" /> <query-header :request="error || currentRequest" status-size="lg" />
</b-card-header> </b-card-header>
<component :is="error ? 'ErrorDisplay' : 'WaitingDisplay'" :action="lastAction" /> <component v-if="error" :is="'ErrorDisplay'" :request="error" />
<component v-else :is="'WaitingDisplay'" :request="currentRequest" />
</b-card> </b-card>
</template> </template>
</b-overlay> </b-overlay>
@ -26,13 +27,13 @@ import QueryHeader from '@/components/QueryHeader'
export default { export default {
name: 'ViewLockOverlay', name: 'ViewLockOverlay',
computed: mapGetters(['waiting', 'error', 'lastAction']),
components: { components: {
ErrorDisplay, ErrorDisplay,
WaitingDisplay, WaitingDisplay,
QueryHeader QueryHeader
} },
computed: mapGetters(['waiting', 'error', 'currentRequest'])
} }
</script> </script>
@ -53,8 +54,13 @@ export default {
::v-deep { ::v-deep {
.card-body { .card-body {
padding: 1.5rem; padding: 1.5rem;
padding-bottom: 0;
max-height: 60vh; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
& > :last-child {
margin-bottom: 1.5rem;
}
} }
.card-footer { .card-footer {

View file

@ -16,7 +16,7 @@
<div v-else class="custom-spinner my-4" :class="spinner" /> <div v-else class="custom-spinner my-4" :class="spinner" />
<message-list-group <message-list-group
v-if="hasMessages" :messages="action.messages" v-if="hasMessages" :messages="request.messages"
bordered fixed-height auto-scroll bordered fixed-height auto-scroll
/> />
</b-card-body> </b-card-body>
@ -35,28 +35,28 @@ export default {
}, },
props: { props: {
action: { type: Object, required: true } request: { type: Object, required: true }
}, },
computed: { computed: {
...mapGetters(['spinner']), ...mapGetters(['spinner']),
hasMessages () { hasMessages () {
return this.action && this.action.messages.length > 0 return this.request.messages && this.request.messages.length > 0
}, },
progress () { progress () {
const progress = this.action.progress const progress = this.request.progress
if (!progress) return null if (!progress) return null
return { return {
values: progress, max: progress.reduce((sum, value) => (sum + value), 0) values: progress,
max: progress.reduce((sum, value) => (sum + value), 0)
} }
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.custom-spinner { .custom-spinner {
animation: 4s linear infinite; animation: 4s linear infinite;