yunohost-admin/app/src/store/info.js

433 lines
13 KiB
JavaScript

import router from '@/router'
import i18n from '@/i18n'
import api from '@/api'
import { timeout, isEmptyValue, isObjectLiteral } from '@/helpers/commons'
export default {
state: {
host: window.location.host, // String
installed: null,
connected: localStorage.getItem('connected') === 'true', // Boolean
yunohost: null, // Object { version, repo }
waiting: false, // Boolean
reconnecting: null, // null|Object { attemps, delay, initialDelay }
history: [], // Array of `request`
requests: [], // Array of `request`
error: null, // null || request
historyTimer: null, // null || setTimeout id
tempMessages: [], // Array of messages
routerKey: undefined, // String if current route has params
breadcrumb: [], // Array of routes
transitionName: null, // String of CSS class if transitions are enabled
},
mutations: {
SET_INSTALLED(state, boolean) {
state.installed = boolean
},
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
},
SET_RECONNECTING(state, args) {
state.reconnecting = args
},
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.
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_TEMP_MESSAGE(state, { request, message, type }) {
state.tempMessages.push([message, type])
},
UPDATE_DISPLAYED_MESSAGES(state, { request }) {
if (!state.tempMessages.length) {
state.historyTimer = null
return
}
const { messages, warnings, errors } = state.tempMessages.reduce(
(acc, [message, type]) => {
acc.messages.push(message)
if (['error', 'warning'].includes(type)) acc[type + 's']++
return acc
},
{ messages: [], warnings: 0, errors: 0 },
)
state.tempMessages = []
state.historyTimer = null
request.messages = request.messages.concat(messages)
request.warnings += warnings
request.errors += errors
},
SET_ERROR(state, request) {
if (request) {
state.error = request
} else {
state.error = null
}
},
SET_ROUTER_KEY(state, key) {
state.routerKey = key
},
SET_BREADCRUMB(state, breadcrumb) {
state.breadcrumb = breadcrumb
},
SET_TRANSITION_NAME(state, transitionName) {
state.transitionName = transitionName
},
},
actions: {
async ON_APP_CREATED({ dispatch, state }) {
await dispatch('CHECK_INSTALL')
if (!state.installed) {
router.push({ name: 'post-install' })
} else {
dispatch('CONNECT')
}
},
async CHECK_INSTALL({ dispatch, commit }, 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
try {
const { installed } = await timeout(api.get('installed'), 5000)
commit('SET_INSTALLED', installed)
return installed
} catch (err) {
if (retry > 0) {
return dispatch('CHECK_INSTALL', --retry)
}
throw err
}
},
async CONNECT({ commit, dispatch }) {
// If the user is not connected, the first action will throw
// and login prompt will be shown automaticly
await dispatch('GET_YUNOHOST_INFOS')
commit('SET_CONNECTED', true)
await api.get({ uri: 'domains', storeKey: 'domains' })
},
RESET_CONNECTED({ commit }) {
commit('SET_CONNECTED', false)
commit('SET_YUNOHOST_INFOS', null)
},
DISCONNECT({ dispatch }, route = router.currentRoute) {
// FIXME vue3 currentRoute is now a ref (currentRoute.value)
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 }, credentials) {
return api
.post('login', { credentials }, null, { websocket: false })
.then(() => {
return dispatch('CONNECT')
})
},
LOGOUT({ dispatch }) {
dispatch('DISCONNECT')
return api.get('logout')
},
TRY_TO_RECONNECT({ commit }, args = {}) {
// FIXME This is very ugly arguments forwarding, will use proper component way of doing this when switching to Vue 3 (teleport)
commit('SET_RECONNECTING', args)
},
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.global.t('human_routes.' + key, args)
: `[${method}] /${uri}`
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)
}
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({ state, commit }, { request, success, wait }) {
// Update last messages before finishing this request
clearTimeout(state.historyTimer)
commit('UPDATE_DISPLAYED_MESSAGES', { request })
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({ state, commit, dispatch }, { request, messages }) {
for (const type in messages) {
const message = {
text: messages[type].replaceAll('\n', '<br>'),
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) {
// To avoid rendering lag issues, limit the flow of websocket messages to batches of 50ms.
if (state.historyTimer === null) {
state.historyTimer = setTimeout(() => {
commit('UPDATE_DISPLAYED_MESSAGES', { request })
}, 50)
}
commit('ADD_TEMP_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
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
},
UPDATE_ROUTER_KEY({ commit }, { to, from }) {
if (isEmptyValue(to.params)) {
commit('SET_ROUTER_KEY', undefined)
return
}
// If the next route uses the same component as the previous one, Vue will not
// recreate an instance of that component, so hooks like `created()` will not be
// triggered and data will not be fetched.
// For routes with params, we create a unique key to force the recreation of a view.
// Params can be declared in route `meta` to stricly define which params should be
// taken into account.
const params = to.meta.routerParams
? to.meta.routerParams.map((key) => to.params[key])
: Object.values(to.params)
commit('SET_ROUTER_KEY', `${to.name}-${params.join('-')}`)
},
UPDATE_BREADCRUMB({ commit }, { to, from }) {
function getRouteNames(route) {
if (route.meta.breadcrumb) return route.meta.breadcrumb
const parentRoute = route.matched
.slice()
.reverse()
.find((route) => route.meta.breadcrumb)
if (parentRoute) return parentRoute.meta.breadcrumb
return []
}
function formatRoute(route) {
const { trad, param } = route.meta.args || {}
let text = ''
// if a traduction key string has been given and we also need to pass
// the route param as a variable.
if (trad && param) {
text = i18n.global.t(trad, { [param]: to.params[param] })
} else if (trad) {
text = i18n.global.t(trad)
} else {
text = to.params[param]
}
return { name: route.name, text }
}
const routeNames = getRouteNames(to)
const allRoutes = router.getRoutes()
const breadcrumb = routeNames.map((name) => {
const route = allRoutes.find((route) => route.name === name)
return formatRoute(route)
})
commit('SET_BREADCRUMB', breadcrumb)
function getTitle(breadcrumb) {
if (breadcrumb.length === 0) return formatRoute(to).text
return (breadcrumb.length > 2 ? breadcrumb.slice(-2) : breadcrumb)
.map((route) => route.text)
.reverse()
.join(' / ')
}
// Display a simplified breadcrumb as the document title.
document.title = `${getTitle(breadcrumb)} | ${i18n.global.t('yunohost_admin')}`
},
UPDATE_TRANSITION_NAME({ state, commit }, { to, from }) {
// Use the breadcrumb array length as a direction indicator
const toDepth = (to.meta.breadcrumb || []).length
const fromDepth = (from.meta.breadcrumb || []).length
commit(
'SET_TRANSITION_NAME',
toDepth < fromDepth ? 'slide-right' : 'slide-left',
)
},
},
getters: {
host: (state) => state.host,
installed: (state) => state.installed,
connected: (state) => state.connected,
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) => {
const request = state.requests.find(({ status }) => status === 'pending')
return request || state.requests[state.requests.length - 1]
},
routerKey: (state) => state.routerKey,
breadcrumb: (state) => state.breadcrumb,
transitionName: (state) => state.transitionName,
ssoLink: (state, getters) => {
return `//${getters.mainDomain ?? state.host}/yunohost/sso`
},
},
}