mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
433 lines
13 KiB
JavaScript
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`
|
|
},
|
|
},
|
|
}
|