mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
refactor: turn infos store to global state
This commit is contained in:
parent
deae1324e1
commit
60b5134dcf
13 changed files with 227 additions and 264 deletions
|
@ -1,23 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
|
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import { useRequests } from '@/composables/useRequests'
|
import { useRequests } from '@/composables/useRequests'
|
||||||
import { useSettings } from '@/composables/useSettings'
|
import { useSettings } from '@/composables/useSettings'
|
||||||
import { useStoreGetters } from '@/store/utils'
|
|
||||||
import { HistoryConsole } from '@/views/_partials'
|
import { HistoryConsole } from '@/views/_partials'
|
||||||
|
|
||||||
const store = useStore()
|
const { ssoLink, connected, yunohost, logout, onAppCreated } = useInfos()
|
||||||
|
|
||||||
const { connected, yunohost, ssoLink } = useStoreGetters()
|
|
||||||
const { locked } = useRequests()
|
const { locked } = useRequests()
|
||||||
const { spinner, dark } = useSettings()
|
const { spinner, dark } = useSettings()
|
||||||
|
|
||||||
async function logout() {
|
onAppCreated()
|
||||||
store.dispatch('LOGOUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
store.dispatch('ON_APP_CREATED')
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const copypastaCode = ['ArrowDown', 'ArrowDown', 'ArrowUp', 'ArrowUp']
|
const copypastaCode = ['ArrowDown', 'ArrowDown', 'ArrowUp', 'ArrowUp']
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import {
|
import {
|
||||||
useRequests,
|
useRequests,
|
||||||
type APIRequestAction,
|
type APIRequestAction,
|
||||||
|
@ -242,10 +243,10 @@ export default {
|
||||||
delay = 2000,
|
delay = 2000,
|
||||||
initialDelay = 0,
|
initialDelay = 0,
|
||||||
}: ReconnectingArgs = {}) {
|
}: ReconnectingArgs = {}) {
|
||||||
|
const { getYunoHostVersion } = useInfos()
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
function reconnect(n: number) {
|
function reconnect(n: number) {
|
||||||
store
|
getYunoHostVersion()
|
||||||
.dispatch('GET_YUNOHOST_INFOS')
|
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch((err: APIError) => {
|
.catch((err: APIError) => {
|
||||||
if (err instanceof APIUnauthorizedError) {
|
if (err instanceof APIUnauthorizedError) {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import errors from '@/api/errors'
|
import errors from '@/api/errors'
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import {
|
import {
|
||||||
STATUS_VARIANT,
|
STATUS_VARIANT,
|
||||||
type APIRequest,
|
type APIRequest,
|
||||||
|
@ -39,10 +40,9 @@ export async function getResponseData(response: Response) {
|
||||||
* @returns Promise that resolve on websocket 'open' or 'error' event.
|
* @returns Promise that resolve on websocket 'open' or 'error' event.
|
||||||
*/
|
*/
|
||||||
export function openWebSocket(request: APIRequestAction): Promise<Event> {
|
export function openWebSocket(request: APIRequestAction): Promise<Event> {
|
||||||
|
const { host } = useInfos()
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const ws = new WebSocket(
|
const ws = new WebSocket(`wss://${host.value}/yunohost/api/messages`)
|
||||||
`wss://${store.getters.host}/yunohost/api/messages`,
|
|
||||||
)
|
|
||||||
ws.onmessage = ({ data }) => {
|
ws.onmessage = ({ data }) => {
|
||||||
const messages: Record<'info' | 'success' | 'warning' | 'error', string> =
|
const messages: Record<'info' | 'success' | 'warning' | 'error', string> =
|
||||||
JSON.parse(data)
|
JSON.parse(data)
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStoreGetters } from '@/store/utils'
|
import { useInfos } from '@/composables/useInfos'
|
||||||
|
|
||||||
const { breadcrumb } = useStoreGetters()
|
const { breadcrumb } = useInfos()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.breadcrumb {
|
|
||||||
border: none;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BBreadcrumb v-if="breadcrumb.length">
|
<BBreadcrumb v-if="breadcrumb.length">
|
||||||
<BBreadcrumbItem to="/">
|
<BBreadcrumbItem to="/">
|
||||||
|
@ -19,12 +12,19 @@ const { breadcrumb } = useStoreGetters()
|
||||||
</BBreadcrumbItem>
|
</BBreadcrumbItem>
|
||||||
|
|
||||||
<BBreadcrumbItem
|
<BBreadcrumbItem
|
||||||
v-for="({ name, text }, i) in breadcrumb"
|
v-for="({ to, text }, i) in breadcrumb"
|
||||||
:key="name"
|
:key="i"
|
||||||
:to="{ name }"
|
:to="to"
|
||||||
:active="i === breadcrumb.length - 1"
|
:active="i === breadcrumb.length - 1"
|
||||||
>
|
>
|
||||||
{{ text }}
|
{{ text }}
|
||||||
</BBreadcrumbItem>
|
</BBreadcrumbItem>
|
||||||
</BBreadcrumb>
|
</BBreadcrumb>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.breadcrumb {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -10,13 +10,13 @@ import {
|
||||||
ModalWaiting,
|
ModalWaiting,
|
||||||
ModalWarning,
|
ModalWarning,
|
||||||
} from '@/components/modals'
|
} from '@/components/modals'
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import { useRequests } from '@/composables/useRequests'
|
import { useRequests } from '@/composables/useRequests'
|
||||||
import { useSettings } from '@/composables/useSettings'
|
import { useSettings } from '@/composables/useSettings'
|
||||||
import { useStoreGetters } from '@/store/utils'
|
|
||||||
import type { VueClass } from '@/types/commons'
|
import type { VueClass } from '@/types/commons'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { routerKey } = useStoreGetters()
|
const { routerKey } = useInfos()
|
||||||
const { reconnecting, currentRequest, dismissModal } = useRequests()
|
const { reconnecting, currentRequest, dismissModal } = useRequests()
|
||||||
const { transitions, transitionName, dark } = useSettings()
|
const { transitions, transitionName, dark } = useSettings()
|
||||||
|
|
||||||
|
|
|
@ -1,212 +1,189 @@
|
||||||
|
import { createGlobalState, useLocalStorage } from '@vueuse/core'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import type {
|
||||||
|
RouteLocationNormalized,
|
||||||
|
RouteLocationNormalizedLoaded,
|
||||||
|
RouteRecordNormalized,
|
||||||
|
} from 'vue-router'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useRequests, type ReconnectingArgs } from '@/composables/useRequests'
|
|
||||||
import { isEmptyValue, timeout } from '@/helpers/commons'
|
import { isEmptyValue, timeout } from '@/helpers/commons'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import router from '@/router'
|
import { useStoreGetters } from '@/store/utils'
|
||||||
|
import type { CustomRoute, RouteFromTo } from '@/types/commons'
|
||||||
|
import { useRequests, type ReconnectingArgs } from './useRequests'
|
||||||
|
|
||||||
export default {
|
export const useInfos = createGlobalState(() => {
|
||||||
state: {
|
const router = useRouter()
|
||||||
host: window.location.host, // String
|
|
||||||
installed: null,
|
|
||||||
connected: localStorage.getItem('connected') === 'true', // Boolean
|
|
||||||
yunohost: null, // Object { version, repo }
|
|
||||||
routerKey: undefined, // String if current route has params
|
|
||||||
breadcrumb: [], // Array of routes
|
|
||||||
transitionName: null, // String of CSS class if transitions are enabled
|
|
||||||
},
|
|
||||||
|
|
||||||
mutations: {
|
const host = ref(window.location.host)
|
||||||
SET_INSTALLED(state, boolean) {
|
const installed = ref<boolean | undefined>()
|
||||||
state.installed = boolean
|
const connected = useLocalStorage('connected', false)
|
||||||
},
|
const yunohost = ref<{ version: string; repo: string } | undefined>()
|
||||||
|
const routerKey = ref<string | undefined>()
|
||||||
|
const breadcrumb = ref<CustomRoute[]>([])
|
||||||
|
|
||||||
SET_CONNECTED(state, boolean) {
|
const { mainDomain } = useStoreGetters()
|
||||||
localStorage.setItem('connected', boolean)
|
const ssoLink = computed(() => {
|
||||||
state.connected = boolean
|
return `//${mainDomain.value ?? host.value}/yunohost/sso`
|
||||||
},
|
})
|
||||||
|
|
||||||
SET_YUNOHOST_INFOS(state, yunohost) {
|
// INIT
|
||||||
state.yunohost = yunohost
|
|
||||||
},
|
|
||||||
|
|
||||||
SET_ROUTER_KEY(state, key) {
|
async function _checkInstall(retry = 2) {
|
||||||
state.routerKey = key
|
// 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
|
||||||
SET_BREADCRUMB(state, breadcrumb) {
|
try {
|
||||||
state.breadcrumb = breadcrumb
|
const data = await timeout(
|
||||||
},
|
api.get<{ installed: boolean }>('installed'),
|
||||||
|
5000,
|
||||||
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) {
|
|
||||||
// FIXME vue3 currentRoute is now a ref (currentRoute.value)
|
|
||||||
dispatch('RESET_CONNECTED')
|
|
||||||
if (router.currentRoute.value.name === 'login') return
|
|
||||||
const previousRoute = route ?? router.currentRoute.value
|
|
||||||
router.push({
|
|
||||||
name: 'login',
|
|
||||||
// Add a redirect query if next route is not unknown (like `logout`) or `login`
|
|
||||||
query:
|
|
||||||
previousRoute && !['login', null].includes(previousRoute.name)
|
|
||||||
? { redirect: previousRoute.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?: ReconnectingArgs) {
|
|
||||||
// FIXME This is very ugly arguments forwarding, will use proper component way of doing this when switching to Vue 3 (teleport)
|
|
||||||
useRequests().reconnecting.value = args
|
|
||||||
},
|
|
||||||
|
|
||||||
GET_YUNOHOST_INFOS({ commit }) {
|
|
||||||
return api.get('versions').then((versions) => {
|
|
||||||
commit('SET_YUNOHOST_INFOS', versions.yunohost)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
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',
|
|
||||||
)
|
)
|
||||||
},
|
installed.value = data.installed
|
||||||
},
|
} catch (err) {
|
||||||
|
if (retry > 0) {
|
||||||
|
return _checkInstall(--retry)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getters: {
|
async function onAppCreated() {
|
||||||
host: (state) => state.host,
|
await _checkInstall()
|
||||||
installed: (state) => state.installed,
|
|
||||||
connected: (state) => state.connected,
|
if (!installed.value) {
|
||||||
yunohost: (state) => state.yunohost,
|
router.push({ name: 'post-install' })
|
||||||
routerKey: (state) => state.routerKey,
|
} else {
|
||||||
breadcrumb: (state) => state.breadcrumb,
|
_onLogin()
|
||||||
transitionName: (state) => state.transitionName,
|
}
|
||||||
ssoLink: (state, getters) => {
|
}
|
||||||
return `//${getters.mainDomain ?? state.host}/yunohost/sso`
|
|
||||||
},
|
function getYunoHostVersion() {
|
||||||
},
|
return api.get('versions').then((versions) => {
|
||||||
}
|
yunohost.value = versions.yunohost
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONNECTION
|
||||||
|
|
||||||
|
async function _onLogin() {
|
||||||
|
// If the user is not connected, the first action will throw
|
||||||
|
// and login prompt will be shown automaticly
|
||||||
|
await getYunoHostVersion()
|
||||||
|
connected.value = true
|
||||||
|
await api.get({ uri: 'domains', cachePath: 'domainList' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogout(route?: RouteLocationNormalizedLoaded) {
|
||||||
|
connected.value = false
|
||||||
|
yunohost.value = undefined
|
||||||
|
const previousRoute = route ?? router.currentRoute.value
|
||||||
|
if (previousRoute.name === 'login') return
|
||||||
|
router.push({
|
||||||
|
name: 'login',
|
||||||
|
// Add a redirect query if next route is not unknown (like `logout`) or `login`
|
||||||
|
query:
|
||||||
|
previousRoute && !['login', null].includes(previousRoute.name as any)
|
||||||
|
? { redirect: previousRoute.path }
|
||||||
|
: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function login(credentials: string) {
|
||||||
|
return api
|
||||||
|
.post({ uri: 'login', data: { credentials }, websocket: false })
|
||||||
|
.then(() => _onLogin())
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
onLogout()
|
||||||
|
return api.get('logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryToReconnect(args?: ReconnectingArgs) {
|
||||||
|
useRequests().reconnecting.value = args
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRouterKey({ to }: RouteFromTo) {
|
||||||
|
if (isEmptyValue(to.params)) {
|
||||||
|
routerKey.value = 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)
|
||||||
|
|
||||||
|
routerKey.value = `${to.name?.toString()}-${params.join('-')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBreadcrumb({ to }: RouteFromTo) {
|
||||||
|
function getRouteNames(route: RouteLocationNormalized): string[] {
|
||||||
|
if (route.meta.breadcrumb) return route.meta.breadcrumb
|
||||||
|
const parentRoute = route.matched
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.find((route) => route.meta.breadcrumb)
|
||||||
|
return parentRoute?.meta.breadcrumb || []
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRoute(
|
||||||
|
route: RouteRecordNormalized | RouteLocationNormalized,
|
||||||
|
) {
|
||||||
|
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 if (param) {
|
||||||
|
text = to.params[param] as string
|
||||||
|
}
|
||||||
|
return { to: { name: route.name! }, text }
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeNames = getRouteNames(to)
|
||||||
|
const allRoutes = router.getRoutes()
|
||||||
|
breadcrumb.value = routeNames.map((name) => {
|
||||||
|
const route = allRoutes.find((route) => route.name === name)!
|
||||||
|
return formatRoute(route)
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTitle(breadcrumb: CustomRoute[]) {
|
||||||
|
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.value)} | ${i18n.global.t('yunohost_admin')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
host,
|
||||||
|
installed,
|
||||||
|
connected,
|
||||||
|
yunohost,
|
||||||
|
routerKey,
|
||||||
|
breadcrumb,
|
||||||
|
ssoLink,
|
||||||
|
onAppCreated,
|
||||||
|
getYunoHostVersion,
|
||||||
|
onLogout,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
tryToReconnect,
|
||||||
|
updateRouterKey,
|
||||||
|
updateBreadcrumb,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
@ -7,8 +7,8 @@ import type { APIQuery, RequestMethod } from '@/api/api'
|
||||||
import { APIErrorLog, type APIError } from '@/api/errors'
|
import { APIErrorLog, type APIError } from '@/api/errors'
|
||||||
import { isObjectLiteral } from '@/helpers/commons'
|
import { isObjectLiteral } from '@/helpers/commons'
|
||||||
import i18n from '@/i18n'
|
import i18n from '@/i18n'
|
||||||
import store from '@/store'
|
|
||||||
import type { StateVariant } from '@/types/commons'
|
import type { StateVariant } from '@/types/commons'
|
||||||
|
import { useInfos } from './useInfos'
|
||||||
|
|
||||||
export type RequestStatus = 'pending' | 'success' | 'warning' | 'error'
|
export type RequestStatus = 'pending' | 'success' | 'warning' | 'error'
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ export const useRequests = createGlobalState(() => {
|
||||||
err.log()
|
err.log()
|
||||||
if (err.code === 401) {
|
if (err.code === 401) {
|
||||||
// Unauthorized
|
// Unauthorized
|
||||||
store.dispatch('DISCONNECT')
|
useInfos().onLogout()
|
||||||
} else if (err instanceof APIErrorLog) {
|
} else if (err instanceof APIErrorLog) {
|
||||||
// Errors that have produced logs
|
// Errors that have produced logs
|
||||||
router.push({ name: 'tool-log', params: { name: err.logRef } })
|
router.push({ name: 'tool-log', params: { name: err.logRef } })
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import { useRequests } from '@/composables/useRequests'
|
import { useRequests } from '@/composables/useRequests'
|
||||||
import { useSettings } from '@/composables/useSettings'
|
import { useSettings } from '@/composables/useSettings'
|
||||||
import store from '@/store'
|
|
||||||
import routes from './routes'
|
import routes from './routes'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
@ -39,20 +39,22 @@ router.beforeEach((to, from, next) => {
|
||||||
dismissModal(currentRequest.value.id)
|
dismissModal(currentRequest.value.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to.name === 'post-install' && store.getters.installed) {
|
const { installed, connected, onLogout } = useInfos()
|
||||||
|
if (to.name === 'post-install' && installed.value) {
|
||||||
return next('/')
|
return next('/')
|
||||||
}
|
}
|
||||||
// Allow if connected or route is not protected
|
// Allow if connected or route is not protected
|
||||||
if (store.getters.connected || to.meta.noAuth) {
|
if (connected.value || to.meta.noAuth) {
|
||||||
next()
|
next()
|
||||||
} else {
|
} else {
|
||||||
store.dispatch('DISCONNECT', to)
|
onLogout(to)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
router.afterEach((to, from) => {
|
router.afterEach((to, from) => {
|
||||||
store.dispatch('UPDATE_ROUTER_KEY', { to, from })
|
const { updateRouterKey, updateBreadcrumb } = useInfos()
|
||||||
store.dispatch('UPDATE_BREADCRUMB', { to, from })
|
updateRouterKey({ to, from })
|
||||||
|
updateBreadcrumb({ to, from })
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
import { reactive, ref } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter, type LocationQueryValue } from 'vue-router'
|
import { useRouter, type LocationQueryValue } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
|
|
||||||
import { useForm } from '@/composables/form'
|
import { useForm } from '@/composables/form'
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import { alphalownumdot_, minLength, required } from '@/helpers/validators'
|
import { alphalownumdot_, minLength, required } from '@/helpers/validators'
|
||||||
import { useStoreGetters } from '@/store/utils'
|
|
||||||
import type { FieldProps, FormFieldDict } from '@/types/form'
|
import type { FieldProps, FormFieldDict } from '@/types/form'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
@ -19,10 +18,8 @@ const props = withDefaults(
|
||||||
)
|
)
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useStore()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { login, installed } = useInfos()
|
||||||
const { installed } = useStoreGetters()
|
|
||||||
|
|
||||||
type Form = typeof form.value
|
type Form = typeof form.value
|
||||||
const form = ref({
|
const form = ref({
|
||||||
|
@ -57,8 +54,7 @@ const { v, onSubmit } = useForm(form, fields)
|
||||||
const onLogin = onSubmit((onError) => {
|
const onLogin = onSubmit((onError) => {
|
||||||
const { username, password } = form.value
|
const { username, password } = form.value
|
||||||
const credentials = [username, password].join(':')
|
const credentials = [username, password].join(':')
|
||||||
store
|
login(credentials)
|
||||||
.dispatch('LOGIN', credentials)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (props.forceReload) {
|
if (props.forceReload) {
|
||||||
window.location.href = '/yunohost/admin/'
|
window.location.href = '/yunohost/admin/'
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { APIBadRequestError, type APIError } from '@/api/errors'
|
import { APIBadRequestError, type APIError } from '@/api/errors'
|
||||||
import { useAutoModal } from '@/composables/useAutoModal'
|
import { useAutoModal } from '@/composables/useAutoModal'
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||||
import { isEmptyValue } from '@/helpers/commons'
|
import { isEmptyValue } from '@/helpers/commons'
|
||||||
import { readableDate } from '@/helpers/filters/date'
|
import { readableDate } from '@/helpers/filters/date'
|
||||||
|
@ -19,7 +19,6 @@ const props = defineProps<{
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useStore()
|
|
||||||
const modalConfirm = useAutoModal()
|
const modalConfirm = useAutoModal()
|
||||||
const { loading } = useInitialQueries(
|
const { loading } = useInitialQueries(
|
||||||
[{ uri: `backups/${props.name}?with_details` }],
|
[{ uri: `backups/${props.name}?with_details` }],
|
||||||
|
@ -128,9 +127,9 @@ async function deleteBackup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadBackup() {
|
function downloadBackup() {
|
||||||
const host = store.getters.host
|
const { host } = useInfos()
|
||||||
window.open(
|
window.open(
|
||||||
`https://${host}/yunohost/api/backups/${props.name}/download`,
|
`https://${host.value}/yunohost/api/backups/${props.name}/download`,
|
||||||
'_blank',
|
'_blank',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { useAutoModal } from '@/composables/useAutoModal'
|
import { useAutoModal } from '@/composables/useAutoModal'
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useStore()
|
|
||||||
const modalConfirm = useAutoModal()
|
const modalConfirm = useAutoModal()
|
||||||
|
const { tryToReconnect } = useInfos()
|
||||||
|
|
||||||
async function triggerAction(action) {
|
async function triggerAction(action) {
|
||||||
const confirmed = await modalConfirm(t('confirm_reboot_action_' + action))
|
const confirmed = await modalConfirm(t('confirm_reboot_action_' + action))
|
||||||
|
@ -15,11 +15,7 @@ async function triggerAction(action) {
|
||||||
|
|
||||||
api.put({ uri: action + '?force', humanKey: action }).then(() => {
|
api.put({ uri: action + '?force', humanKey: action }).then(() => {
|
||||||
const delay = action === 'reboot' ? 4000 : 10000
|
const delay = action === 'reboot' ? 4000 : 10000
|
||||||
store.dispatch('TRY_TO_RECONNECT', {
|
tryToReconnect({ attemps: Infinity, origin: action, delay })
|
||||||
attemps: Infinity,
|
|
||||||
origin: action,
|
|
||||||
delay,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import CardCollapse from '@/components/CardCollapse.vue'
|
import CardCollapse from '@/components/CardCollapse.vue'
|
||||||
import { useAutoModal } from '@/composables/useAutoModal'
|
import { useAutoModal } from '@/composables/useAutoModal'
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const store = useStore()
|
const { tryToReconnect } = useInfos()
|
||||||
const modalConfirm = useAutoModal()
|
const modalConfirm = useAutoModal()
|
||||||
const { loading } = useInitialQueries(
|
const { loading } = useInitialQueries(
|
||||||
[{ method: 'PUT', uri: 'update/all', humanKey: 'update' }],
|
[{ method: 'PUT', uri: 'update/all', humanKey: 'update' }],
|
||||||
|
@ -105,7 +105,7 @@ async function performSystemUpgrade() {
|
||||||
|
|
||||||
api.put({ uri: 'upgrade/system', humanKey: 'upgrade.system' }).then(() => {
|
api.put({ uri: 'upgrade/system', humanKey: 'upgrade.system' }).then(() => {
|
||||||
if (system.value.some(({ name }) => name.includes('yunohost'))) {
|
if (system.value.some(({ name }) => name.includes('yunohost'))) {
|
||||||
store.dispatch('TRY_TO_RECONNECT', {
|
tryToReconnect({
|
||||||
attemps: 1,
|
attemps: 1,
|
||||||
origin: 'upgrade_system',
|
origin: 'upgrade_system',
|
||||||
initialDelay: 2000,
|
initialDelay: 2000,
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type ComputedRef } from 'vue'
|
import { type ComputedRef } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
|
||||||
|
|
||||||
|
import { useInfos } from '@/composables/useInfos'
|
||||||
import { useInitialQueries } from '@/composables/useInitialQueries'
|
import { useInitialQueries } from '@/composables/useInitialQueries'
|
||||||
import { useSearch } from '@/composables/useSearch'
|
import { useSearch } from '@/composables/useSearch'
|
||||||
import { useStoreGetters } from '@/store/utils'
|
import { useStoreGetters } from '@/store/utils'
|
||||||
import type { Obj } from '@/types/commons'
|
import type { Obj } from '@/types/commons'
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
const { loading } = useInitialQueries([
|
const { loading } = useInitialQueries([
|
||||||
{
|
{
|
||||||
uri: 'users?fields=username&fields=fullname&fields=mail&fields=mailbox-quota&fields=groups',
|
uri: 'users?fields=username&fields=fullname&fields=mail&fields=mailbox-quota&fields=groups',
|
||||||
|
@ -23,8 +22,8 @@ const [search, filteredUsers] = useSearch(
|
||||||
)
|
)
|
||||||
|
|
||||||
function downloadExport() {
|
function downloadExport() {
|
||||||
const host = store.getters.host
|
const { host } = useInfos()
|
||||||
window.open(`https://${host}/yunohost/api/users/export`, '_blank')
|
window.open(`https://${host.value}/yunohost/api/users/export`, '_blank')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue