mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
add experimental error handling
This commit is contained in:
parent
da1442d375
commit
14bdfbce2f
8 changed files with 251 additions and 39 deletions
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import { handleResponse, handleError } from './handlers'
|
import { handleResponse } from './handlers'
|
||||||
import { objectToParams } from '@/helpers/commons'
|
import { objectToParams } from '@/helpers/commons'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,6 +63,8 @@ export default {
|
||||||
const localeQs = `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`
|
const localeQs = `${uri.includes('?') ? '&' : '?'}locale=${store.getters.locale}`
|
||||||
return fetch('/yunohost/api/' + uri + localeQs, this.options)
|
return fetch('/yunohost/api/' + uri + localeQs, this.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.dispatch('WAITING_FOR_RESPONSE', [uri, method])
|
||||||
return fetch('/yunohost/api/' + uri, {
|
return fetch('/yunohost/api/' + uri, {
|
||||||
...this.options,
|
...this.options,
|
||||||
method,
|
method,
|
||||||
|
@ -77,7 +79,7 @@ export default {
|
||||||
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api response as an object, a string or as an error.
|
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api response as an object, a string or as an error.
|
||||||
*/
|
*/
|
||||||
get (uri) {
|
get (uri) {
|
||||||
return this.fetch('GET', uri).then(handleResponse)
|
return this.fetch('GET', uri).then(response => handleResponse(response, 'GET'))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,8 +100,7 @@ export default {
|
||||||
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api responses as an array.
|
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api responses as an array.
|
||||||
*/
|
*/
|
||||||
post (uri, data = {}) {
|
post (uri, data = {}) {
|
||||||
store.dispatch('WAITING_FOR_RESPONSE', [uri, 'POST'])
|
return this.fetch('POST', uri, data).then(response => handleResponse(response, 'POST'))
|
||||||
return this.fetch('POST', uri, data).then(handleResponse)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,8 +111,7 @@ export default {
|
||||||
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api responses as an array.
|
* @return {Promise<module:api~DigestedResponse>} Promise that resolve the api responses as an array.
|
||||||
*/
|
*/
|
||||||
put (uri, data = {}) {
|
put (uri, data = {}) {
|
||||||
store.dispatch('WAITING_FOR_RESPONSE', [uri, 'PUT'])
|
return this.fetch('PUT', uri, data).then(response => handleResponse(response, 'PUT'))
|
||||||
return this.fetch('PUT', uri, data).then(handleResponse)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,10 +122,6 @@ export default {
|
||||||
* @return {Promise<('ok'|Error)>} Promise that resolve the api responses as an array.
|
* @return {Promise<('ok'|Error)>} Promise that resolve the api responses as an array.
|
||||||
*/
|
*/
|
||||||
delete (uri, data = {}) {
|
delete (uri, data = {}) {
|
||||||
store.dispatch('WAITING_FOR_RESPONSE', [uri, 'DELETE'])
|
return this.fetch('DELETE', uri, data).then(response => handleResponse(response, 'DELETE'))
|
||||||
return this.fetch('DELETE', uri, data).then(response => {
|
|
||||||
store.dispatch('SERVER_RESPONDED')
|
|
||||||
return response.ok ? 'ok' : handleError(response)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
86
app/src/api/errors.js
Normal file
86
app/src/api/errors.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/**
|
||||||
|
* API errors definitionss.
|
||||||
|
* @module api/errors
|
||||||
|
*/
|
||||||
|
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
|
||||||
|
class APIError extends Error {
|
||||||
|
constructor (method, { url, status, statusText }, message) {
|
||||||
|
super(message || i18n.t('error_server_unexpected'))
|
||||||
|
this.uri = new URL(url).pathname.replace('/yunohost', '')
|
||||||
|
this.method = method
|
||||||
|
this.code = status
|
||||||
|
this.status = statusText
|
||||||
|
this.name = 'APIError'
|
||||||
|
}
|
||||||
|
|
||||||
|
print () {
|
||||||
|
console.error(`${this.name} (${this.code}): ${this.uri}\n${this.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 401 — Unauthorized
|
||||||
|
class APIUnauthorizedError extends APIError {
|
||||||
|
constructor (method, response, message) {
|
||||||
|
super(method, response, i18n.t('unauthorized'))
|
||||||
|
this.name = 'APIUnauthorizedError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 400 — Bad Request
|
||||||
|
class APIBadRequestError extends APIError {
|
||||||
|
constructor (method, response, message) {
|
||||||
|
super(method, response, message)
|
||||||
|
this.name = 'APIBadRequestError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 500 — Server Internal Error
|
||||||
|
class APIInternalError extends APIError {
|
||||||
|
constructor (method, response, data) {
|
||||||
|
// not tested (message should be json but in )
|
||||||
|
const traceback = typeof data === 'object' ? data.traceback : null
|
||||||
|
super(method, response, 'none')
|
||||||
|
if (traceback) {
|
||||||
|
this.traceback = traceback
|
||||||
|
}
|
||||||
|
this.name = 'APIInternalError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 502 — Bad gateway (means API is down)
|
||||||
|
class APINotRespondingError extends APIError {
|
||||||
|
constructor (method, response) {
|
||||||
|
super(method, response, i18n.t('api_not_responding'))
|
||||||
|
this.name = 'APINotRespondingError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 — (means "the connexion has been closed" apparently)
|
||||||
|
class APIConnexionError extends APIError {
|
||||||
|
constructor (method, response) {
|
||||||
|
super(method, response, i18n.t('error_connection_interrupted'))
|
||||||
|
this.name = 'APIConnexionError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temp factory
|
||||||
|
const errors = {
|
||||||
|
[undefined]: APIError,
|
||||||
|
0: APIConnexionError,
|
||||||
|
400: APIBadRequestError,
|
||||||
|
401: APIUnauthorizedError,
|
||||||
|
500: APIInternalError,
|
||||||
|
502: APINotRespondingError
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
errors as default,
|
||||||
|
APIError,
|
||||||
|
APIUnauthorizedError,
|
||||||
|
APIBadRequestError,
|
||||||
|
APIInternalError,
|
||||||
|
APINotRespondingError,
|
||||||
|
APIConnexionError
|
||||||
|
}
|
|
@ -1,15 +1,20 @@
|
||||||
|
/**
|
||||||
|
* API handlers.
|
||||||
|
* @module api/handlers
|
||||||
|
*/
|
||||||
|
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
|
import errors from './errors'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for API responses.
|
* Try to get response content as json and if it's not as text.
|
||||||
*
|
*
|
||||||
* @param {Response} response - A fetch `Response` object.
|
* @param {Response} response - A fetch `Response` object.
|
||||||
* @return {(Object|String)} Parsed response's json or response's text.
|
* @return {(Object|String)} Parsed response's json or response's text.
|
||||||
*/
|
*/
|
||||||
async function handleResponse (response) {
|
|
||||||
store.dispatch('SERVER_RESPONDED')
|
async function _getResponseContent (response) {
|
||||||
if (!response.ok) return handleError(response)
|
// FIXME the api should always return json as response
|
||||||
// FIXME the api should always return json objects
|
|
||||||
const responseText = await response.text()
|
const responseText = await response.text()
|
||||||
try {
|
try {
|
||||||
return JSON.parse(responseText)
|
return JSON.parse(responseText)
|
||||||
|
@ -18,23 +23,42 @@ async function handleResponse (response) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for API responses.
|
||||||
|
*
|
||||||
|
* @param {Response} response - A fetch `Response` object.
|
||||||
|
* @return {(Object|String)} Parsed response's json, response's text or an error.
|
||||||
|
*/
|
||||||
|
export function handleResponse (response, method) {
|
||||||
|
store.dispatch('SERVER_RESPONDED', response.ok)
|
||||||
|
if (!response.ok) return handleError(response, method)
|
||||||
|
// FIXME the api should always return json objects
|
||||||
|
return _getResponseContent(response)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for API errors.
|
* Handler for API errors.
|
||||||
*
|
*
|
||||||
* @param {Response} response - A fetch `Response` object.
|
* @param {Response} response - A fetch `Response` object.
|
||||||
* @throws Will throw an error with the API response text or custom message.
|
* @throws Will throw a custom error with response data.
|
||||||
*/
|
*/
|
||||||
async function handleError (response) {
|
export async function handleError (response, method) {
|
||||||
if (response.status === 401) {
|
console.log(response.url)
|
||||||
store.dispatch('DISCONNECT')
|
const message = await _getResponseContent(response)
|
||||||
throw new Error('Unauthorized')
|
const errorCode = response.status in errors ? response.status : undefined
|
||||||
} else if (response.status === 400) {
|
const error = new errors[errorCode](method, response, message)
|
||||||
const message = await response.text()
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
if (error.code === 401) {
|
||||||
handleResponse,
|
store.dispatch('DISCONNECT')
|
||||||
handleError
|
} else if (error.code === 400) {
|
||||||
|
// FIXME for now while in form, the error is catched by the caller and displayed in the form
|
||||||
|
// Hide the waiting screen
|
||||||
|
store.dispatch('SERVER_RESPONDED', true)
|
||||||
|
} else {
|
||||||
|
store.dispatch('DISPATCH_ERROR', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// error.print()
|
||||||
|
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,16 @@
|
||||||
|
|
||||||
<template v-slot:overlay>
|
<template v-slot:overlay>
|
||||||
<b-card no-body>
|
<b-card no-body>
|
||||||
<div class="d-flex justify-content-center">
|
<div v-if="!error" class="d-flex justify-content-center mt-3">
|
||||||
<!-- <b-spinner /> -->
|
<!-- <b-spinner /> -->
|
||||||
<img class="pacman" src="@/assets/ajax-loader.gif">
|
<img class="pacman" src="@/assets/ajax-loader.gif">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-card-body class="pb-4">
|
<b-card-body v-if="error">
|
||||||
|
<error-page />
|
||||||
|
</b-card-body>
|
||||||
|
|
||||||
|
<b-card-body v-else class="pb-4">
|
||||||
<b-card-title class="text-center m-0" v-t="'api_waiting'" />
|
<b-card-title class="text-center m-0" v-t="'api_waiting'" />
|
||||||
|
|
||||||
<!-- PROGRESS BAR -->
|
<!-- PROGRESS BAR -->
|
||||||
|
@ -36,6 +40,10 @@
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</b-list-group-item>
|
</b-list-group-item>
|
||||||
</b-list-group>
|
</b-list-group>
|
||||||
|
|
||||||
|
<b-card-footer v-if="error">
|
||||||
|
<b-button variant="primary" v-t="'ok'" @click="$store.dispatch('SERVER_RESPONDED', true)" />
|
||||||
|
</b-card-footer>
|
||||||
</b-card>
|
</b-card>
|
||||||
</template>
|
</template>
|
||||||
</b-overlay>
|
</b-overlay>
|
||||||
|
@ -43,12 +51,13 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import ErrorPage from '@/views/ErrorPage'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ApiWaitOverlay',
|
name: 'ApiWaitOverlay',
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['waiting', 'lastAction']),
|
...mapGetters(['waiting', 'lastAction', 'error']),
|
||||||
|
|
||||||
progress () {
|
progress () {
|
||||||
const progress = this.lastAction.progress
|
const progress = this.lastAction.progress
|
||||||
|
@ -62,6 +71,10 @@ export default {
|
||||||
const messages = this.lastAction.messages
|
const messages = this.lastAction.messages
|
||||||
return messages.length > 0 ? this.lastAction.messages : null
|
return messages.length > 0 ? this.lastAction.messages : null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
ErrorPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -71,7 +84,6 @@ export default {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 5vh;
|
top: 5vh;
|
||||||
margin: 0 5%;
|
margin: 0 5%;
|
||||||
padding: 3rem 0;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
margin: 0 10%;
|
margin: 0 10%;
|
||||||
|
@ -83,6 +95,8 @@ export default {
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
|
|
|
@ -5,6 +5,18 @@
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
"administration_password": "Administration password",
|
"administration_password": "Administration password",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
|
"api_error": {
|
||||||
|
"help": "You should look for help on <a href=\"https://forum.yunohost.org/\">the forum</a> or <a href=\"https://chat.yunohost.org/\">the chat</a> to fix the situation, or report the bug on <a href=\"https://github.com/YunoHost/issues\">the bugtracker</a>.",
|
||||||
|
"info": "The following information might be useful for the person helping you:",
|
||||||
|
"sorry": "Really sorry about that."
|
||||||
|
},
|
||||||
|
"api_errors_titles": {
|
||||||
|
"APIError": "Yunohost encountered an unexpected error",
|
||||||
|
"APIBadRequestError": "Yunohost encountered an error",
|
||||||
|
"APIInternalError": "Yunohost encountered an internal error",
|
||||||
|
"APINotRespondingError": "Yunohost API is not responding",
|
||||||
|
"APIConnexionError": "Yunohost encountered an connexion error"
|
||||||
|
},
|
||||||
"all_apps": "All apps",
|
"all_apps": "All apps",
|
||||||
"api_not_responding": "The YunoHost API is not responding. Maybe 'yunohost-api' is down or got restarted?",
|
"api_not_responding": "The YunoHost API is not responding. Maybe 'yunohost-api' is down or got restarted?",
|
||||||
"api_waiting": "Waiting for the server's response...",
|
"api_waiting": "Waiting for the server's response...",
|
||||||
|
@ -122,6 +134,7 @@
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
|
"error": "Error",
|
||||||
"error_modify_something": "You should modify something",
|
"error_modify_something": "You should modify something",
|
||||||
"error_retrieve_feed": "Could not retrieve feed: %s. You might have a plugin prevent your browser from performing this request (or the website is down).",
|
"error_retrieve_feed": "Could not retrieve feed: %s. You might have a plugin prevent your browser from performing this request (or the website is down).",
|
||||||
"error_select_domain": "You should indicate a domain",
|
"error_select_domain": "You should indicate a domain",
|
||||||
|
@ -371,6 +384,7 @@
|
||||||
"transitions": "Page transition animations"
|
"transitions": "Page transition animations"
|
||||||
},
|
},
|
||||||
"tools_webadmin_settings": "Web-admin settings",
|
"tools_webadmin_settings": "Web-admin settings",
|
||||||
|
"traceback": "Traceback",
|
||||||
"udp": "UDP",
|
"udp": "UDP",
|
||||||
"unauthorized": "Unauthorized",
|
"unauthorized": "Unauthorized",
|
||||||
"unignore": "Unignore",
|
"unignore": "Unignore",
|
||||||
|
|
|
@ -3,8 +3,14 @@
|
||||||
* @module router/routes
|
* @module router/routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Simple views are normally imported and will be included into the main webpack entry.
|
||||||
|
// Others will be chunked by webpack so they can be lazy loaded.
|
||||||
|
// Webpack chunk syntax is:
|
||||||
|
// `() => import(/* webpackChunkName: "views/:nameOfWantedFile" */ '@/views/:ViewComponent')`
|
||||||
|
|
||||||
import Home from '@/views/Home'
|
import Home from '@/views/Home'
|
||||||
import Login from '@/views/Login'
|
import Login from '@/views/Login'
|
||||||
|
import ErrorPage from '@/views/ErrorPage'
|
||||||
import ToolList from '@/views/tool/ToolList'
|
import ToolList from '@/views/tool/ToolList'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
@ -23,6 +29,18 @@ const routes = [
|
||||||
meta: { noAuth: true, breadcrumb: [] }
|
meta: { noAuth: true, breadcrumb: [] }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* ────────╮
|
||||||
|
│ ERROR │
|
||||||
|
╰──────── */
|
||||||
|
{
|
||||||
|
name: 'error',
|
||||||
|
path: '/error/:type',
|
||||||
|
component: ErrorPage,
|
||||||
|
props: true,
|
||||||
|
// Leave the breadcrumb
|
||||||
|
meta: { noAuth: true, breadcrumb: [] }
|
||||||
|
},
|
||||||
|
|
||||||
/* ───────────────╮
|
/* ───────────────╮
|
||||||
│ POST INSTALL │
|
│ POST INSTALL │
|
||||||
╰─────────────── */
|
╰─────────────── */
|
||||||
|
|
|
@ -8,6 +8,7 @@ export default {
|
||||||
host: window.location.host,
|
host: window.location.host,
|
||||||
connected: localStorage.getItem('connected') === 'true',
|
connected: localStorage.getItem('connected') === 'true',
|
||||||
yunohost: null, // yunohost app infos: Object {version, repo}
|
yunohost: null, // yunohost app infos: Object {version, repo}
|
||||||
|
error: null,
|
||||||
waiting: false,
|
waiting: false,
|
||||||
history: []
|
history: []
|
||||||
},
|
},
|
||||||
|
@ -36,6 +37,10 @@ export default {
|
||||||
|
|
||||||
'UPDATE_PROGRESS' (state, progress) {
|
'UPDATE_PROGRESS' (state, progress) {
|
||||||
Vue.set(state.history[state.history.length - 1], 'progress', progress)
|
Vue.set(state.history[state.history.length - 1], 'progress', progress)
|
||||||
|
},
|
||||||
|
|
||||||
|
'SET_ERROR' (state, error) {
|
||||||
|
state.error = error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -55,11 +60,14 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
'DISCONNECT' ({ commit }, route) {
|
'RESET_CONNECTED' ({ commit }) {
|
||||||
commit('SET_CONNECTED', false)
|
commit('SET_CONNECTED', false)
|
||||||
commit('SET_YUNOHOST_INFOS', null)
|
commit('SET_YUNOHOST_INFOS', null)
|
||||||
// Do not redirect if the current route needs to display an error.
|
},
|
||||||
if (['login', 'tool-adminpw'].includes(router.currentRoute.name)) return
|
|
||||||
|
'DISCONNECT' ({ dispatch }, route) {
|
||||||
|
dispatch('RESET_CONNECTED')
|
||||||
|
if (router.currentRoute.name === 'login') return
|
||||||
router.push({
|
router.push({
|
||||||
name: 'login',
|
name: 'login',
|
||||||
// Add a redirect query if next route is not unknown (like `logout`) or `login`
|
// Add a redirect query if next route is not unknown (like `logout`) or `login`
|
||||||
|
@ -100,9 +108,10 @@ export default {
|
||||||
commit('ADD_HISTORY_ENTRY', [uri, method, Date.now()])
|
commit('ADD_HISTORY_ENTRY', [uri, method, Date.now()])
|
||||||
},
|
},
|
||||||
|
|
||||||
'SERVER_RESPONDED' ({ state, dispatch, commit }) {
|
'SERVER_RESPONDED' ({ state, dispatch, commit }, responseIsOk) {
|
||||||
if (!state.waiting) return
|
if (responseIsOk) {
|
||||||
commit('UPDATE_WAITING', false)
|
commit('UPDATE_WAITING', false)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
'DISPATCH_MESSAGE' ({ commit }, messages) {
|
'DISPATCH_MESSAGE' ({ commit }, messages) {
|
||||||
|
@ -126,6 +135,14 @@ export default {
|
||||||
commit('ADD_MESSAGE', message)
|
commit('ADD_MESSAGE', message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'DISPATCH_ERROR' ({ state, commit }, error) {
|
||||||
|
commit('SET_ERROR', error)
|
||||||
|
if (error.method === 'GET') {
|
||||||
|
router.push({ name: 'error', params: { type: error.code } })
|
||||||
|
}
|
||||||
|
// else the waiting screen will display the error
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -133,6 +150,7 @@ export default {
|
||||||
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,
|
||||||
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]
|
||||||
|
|
42
app/src/views/ErrorPage.vue
Normal file
42
app/src/views/ErrorPage.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="error mt-4" v-if="error">
|
||||||
|
<h2>{{ $t('api_errors_titles' + error.name) }} :/</h2>
|
||||||
|
|
||||||
|
<em v-t="'api_error.sorry'" />
|
||||||
|
|
||||||
|
<b-alert variant="info" class="mt-4" show>
|
||||||
|
<span v-html="$t('api_error.help')" />
|
||||||
|
<br>{{ $t('api_error.info') }}
|
||||||
|
</b-alert>
|
||||||
|
|
||||||
|
<h5 v-t="'error'" />
|
||||||
|
<pre><code>"{{ error.code }}" {{ error.status }}</code></pre>
|
||||||
|
|
||||||
|
<h5 v-t="'action'" />
|
||||||
|
<pre><code>"{{ error.method }}" {{ error.uri }}</code></pre>
|
||||||
|
|
||||||
|
<h5>Message</h5>
|
||||||
|
<p>{{ error.message }}</p>
|
||||||
|
|
||||||
|
<template v-if="error.traceback">
|
||||||
|
<h5 v-t="'traceback'" />
|
||||||
|
<pre><code class="text-dark">{{ error.traceback }}</code></pre>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ErrorPage',
|
||||||
|
|
||||||
|
computed: mapGetters(['error'])
|
||||||
|
|
||||||
|
// FIXME add redirect if they're no error (if reload or route entered by hand)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
</style>
|
Loading…
Add table
Reference in a new issue