diff --git a/app/components.d.ts b/app/components.d.ts
index 02e15a179..76504360d 100644
--- a/app/components.d.ts
+++ b/app/components.d.ts
@@ -23,6 +23,7 @@ declare module 'vue' {
BCardHeader: typeof import('bootstrap-vue-next/components/BCard')['BCardHeader']
BCardText: typeof import('bootstrap-vue-next/components/BCard')['BCardText']
BCardTitle: typeof import('bootstrap-vue-next/components/BCard')['BCardTitle']
+ BCloseButton: typeof import('bootstrap-vue-next/components/BButton')['BCloseButton']
BCol: typeof import('bootstrap-vue-next/components/BContainer')['BCol']
BCollapse: typeof import('bootstrap-vue-next/components/BCollapse')['BCollapse']
BDropdown: typeof import('bootstrap-vue-next/components/BDropdown')['BDropdown']
@@ -70,6 +71,8 @@ declare module 'vue' {
BTab: typeof import('bootstrap-vue-next/components/BTabs')['BTab']
BTable: typeof import('bootstrap-vue-next/components/BTable')['BTable']
BTabs: typeof import('bootstrap-vue-next/components/BTabs')['BTabs']
+ BToast: typeof import('bootstrap-vue-next/components/BToast')['BToast']
+ BToastOrchestrator: typeof import('bootstrap-vue-next/components/BToast')['BToastOrchestrator']
ButtonItem: typeof import('./src/components/globals/formItems/ButtonItem.vue')['default']
CardCollapse: typeof import('./src/components/CardCollapse.vue')['default']
CardDeckFeed: typeof import('./src/components/CardDeckFeed.vue')['default']
@@ -116,6 +119,7 @@ declare module 'vue' {
YListGroupItem: typeof import('./src/components/globals/YListGroupItem.vue')['default']
YListItem: typeof import('./src/components/globals/YListItem.vue')['default']
YSpinner: typeof import('./src/components/globals/YSpinner.vue')['default']
+ YToast: typeof import('./src/components/YToast.vue')['default']
}
export interface ComponentCustomProperties {
vBModal: typeof import('bootstrap-vue-next/directives/BModal')['vBModal']
diff --git a/app/src/App.vue b/app/src/App.vue
index c7b3239b0..5439c3ccf 100644
--- a/app/src/App.vue
+++ b/app/src/App.vue
@@ -1,11 +1,14 @@
@@ -29,9 +41,12 @@ const hour = computed(() => {
/>
-
- {{ request.humanRoute }}
-
+
+
+ {{ request.title }}
+
+ ({{ $t('history.started_by', { caller }) }})
+
@@ -54,8 +69,8 @@ const hour = computed(() => {
-
diff --git a/app/src/components/YToast.vue b/app/src/components/YToast.vue
new file mode 100644
index 000000000..5f673f41d
--- /dev/null
+++ b/app/src/components/YToast.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/components/layouts/MainLayout.vue b/app/src/components/layouts/MainLayout.vue
index 4f0c50f97..fcb0f46e9 100644
--- a/app/src/components/layouts/MainLayout.vue
+++ b/app/src/components/layouts/MainLayout.vue
@@ -9,18 +9,19 @@ import {
ModalError,
ModalReconnecting,
ModalWaiting,
- ModalWarning,
} from '@/components/modals'
import { useInfos } from '@/composables/useInfos'
import { useRequests } from '@/composables/useRequests'
+import { useSSE } from '@/composables/useSSE'
import { useSettings } from '@/composables/useSettings'
import type { CustomRoute, Skeleton, VueClass } from '@/types/commons'
const { t } = useI18n()
const router = useRouter()
const { routerKey, hasSuspenseError } = useInfos()
-const { reconnecting, currentRequest, dismissModal } = useRequests()
+const { currentRequest, dismissModal } = useRequests()
const { transitions, transitionName, dark } = useSettings()
+const { reconnecting } = useSSE()
const RootView = createReusableTemplate<{
Component: VNode
@@ -47,10 +48,7 @@ const modalComponent = computed(() => {
if (reconnecting.value) {
return {
is: ModalReconnecting,
- props: {
- reconnecting: reconnecting.value,
- onDismiss: () => (reconnecting.value = undefined),
- },
+ props: { reconnecting: reconnecting.value },
}
}
@@ -63,11 +61,6 @@ const modalComponent = computed(() => {
is: ModalError,
props: { request, onDismiss: () => dismissModal(request.id) },
}
- } else if (status === 'warning') {
- return {
- is: ModalWarning,
- props: { request, onDismiss: () => dismissModal(request.id) },
- }
} else {
return { is: ModalWaiting, props: { request } }
}
diff --git a/app/src/components/modals/ModalOverlay.vue b/app/src/components/modals/ModalOverlay.vue
index 7630063ee..cf7bd7b0c 100644
--- a/app/src/components/modals/ModalOverlay.vue
+++ b/app/src/components/modals/ModalOverlay.vue
@@ -29,7 +29,7 @@ defineSlots<{
hide-backdrop
no-close-on-backdrop
no-close-on-esc
- :hide-footer="hideFooter"
+ :no-footer="hideFooter"
no-fade
>
diff --git a/app/src/components/modals/ModalReconnecting.vue b/app/src/components/modals/ModalReconnecting.vue
index 3f15e7c8d..3d7c63075 100644
--- a/app/src/components/modals/ModalReconnecting.vue
+++ b/app/src/components/modals/ModalReconnecting.vue
@@ -1,83 +1,33 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
diff --git a/app/src/components/modals/ModalWaiting.vue b/app/src/components/modals/ModalWaiting.vue
index 8549b82b4..42d894c47 100644
--- a/app/src/components/modals/ModalWaiting.vue
+++ b/app/src/components/modals/ModalWaiting.vue
@@ -1,5 +1,6 @@
-
+ {{ title }}
diff --git a/app/src/components/modals/ModalWarning.vue b/app/src/components/modals/ModalWarning.vue
deleted file mode 100644
index bb6af0ae4..000000000
--- a/app/src/components/modals/ModalWarning.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/components/modals/index.ts b/app/src/components/modals/index.ts
index 0da6d6d34..5298a2e7d 100644
--- a/app/src/components/modals/index.ts
+++ b/app/src/components/modals/index.ts
@@ -2,12 +2,5 @@ import ModalOverlay from './ModalOverlay.vue'
import ModalError from './ModalError.vue'
import ModalWaiting from './ModalWaiting.vue'
import ModalReconnecting from './ModalReconnecting.vue'
-import ModalWarning from './ModalWarning.vue'
-export {
- ModalOverlay,
- ModalError,
- ModalWaiting,
- ModalReconnecting,
- ModalWarning,
-}
+export { ModalOverlay, ModalError, ModalWaiting, ModalReconnecting }
diff --git a/app/src/composables/data.ts b/app/src/composables/data.ts
index 93a71dc9b..6e1b364e5 100644
--- a/app/src/composables/data.ts
+++ b/app/src/composables/data.ts
@@ -2,6 +2,7 @@ import { createGlobalState } from '@vueuse/core'
import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue'
import type { RequestMethod } from '@/api/api'
+import api from '@/api/api'
import { isEmptyValue, isObjectLiteral } from '@/helpers/commons'
import { stratify } from '@/helpers/data/tree'
import type { Obj } from '@/types/commons'
@@ -90,6 +91,58 @@ const useData = createGlobalState(() => {
}
}
+ function updateFromAction(action: string, param?: string) {
+ // TODO could be merged somehow with normal 'update' fn?
+ if (action.includes('permission')) {
+ if (Object.keys(permissions.value).length) {
+ api.get({ uri: 'users/permissions?full', cachePath: 'permissions' })
+ }
+ } else if (action.includes('group')) {
+ if (param && Object.keys(groups.value).length) {
+ if (action.includes('create') && param) {
+ groups.value[param] = { members: [], permissions: [] }
+ } else if (action.includes('delete')) {
+ delete groups.value[param]
+ } else if (action.includes('update')) {
+ api.get({
+ uri: 'users/groups?full&include_primary_groups',
+ cachePath: 'groups',
+ })
+ }
+ }
+ } else if (action.includes('user')) {
+ if (param && Object.keys(users.value).length) {
+ if (action.includes('delete')) {
+ delete userDetails.value[param]
+ delete users.value[param]
+ } else if (action.includes('create')) {
+ api.get({
+ uri: 'users?fields=username&fields=fullname&fields=mail&fields=mailbox-quota&fields=groups',
+ cachePath: 'users',
+ cacheForce: true,
+ })
+ } else if (action.includes('update')) {
+ api.get({
+ uri: `users/${param}`,
+ cachePath: `userDetails.${param}`,
+ cacheForce: true,
+ })
+ }
+ }
+ } else if (action.includes('domain') && param) {
+ if (action.includes('main_domain') && mainDomain.value) {
+ mainDomain.value = param
+ } else if (domains.value) {
+ if (action.includes('add')) {
+ domains.value.push(param)
+ } else if (action.includes('remove')) {
+ domains.value?.splice(domains.value.indexOf(param), 1)
+ delete domainDetails.value[param]
+ }
+ }
+ }
+ }
+
return {
users,
userDetails,
@@ -101,6 +154,7 @@ const useData = createGlobalState(() => {
domainDetails,
update,
+ updateFromAction,
}
})
@@ -261,3 +315,12 @@ export function resetCache(keys: DataKeys[]) {
}
}
}
+
+export function updateCacheFromAction(operationId: string) {
+ const [action, param] = operationId.substring(16).split('-') as [
+ string,
+ string | undefined,
+ ]
+
+ useData().updateFromAction(action, param)
+}
diff --git a/app/src/composables/useAutoToast.ts b/app/src/composables/useAutoToast.ts
new file mode 100644
index 000000000..7c306022d
--- /dev/null
+++ b/app/src/composables/useAutoToast.ts
@@ -0,0 +1,22 @@
+import { createGlobalState } from '@vueuse/core'
+import type { OrchestratedToast } from 'bootstrap-vue-next'
+import { useToastController } from 'bootstrap-vue-next'
+import { ref } from 'vue'
+
+import YToast from '@/components/YToast.vue'
+
+type useToastControllerInstance = ReturnType
+
+export const useAutoToast = createGlobalState(() => {
+ const toastController = ref(null)
+
+ function init(controller: useToastControllerInstance) {
+ toastController.value = controller
+ }
+
+ function show(props: OrchestratedToast) {
+ toastController.value?.show?.({ props, component: YToast })
+ }
+
+ return { init, show }
+})
diff --git a/app/src/composables/useInfos.ts b/app/src/composables/useInfos.ts
index 856ae98e4..1cc4c24ee 100644
--- a/app/src/composables/useInfos.ts
+++ b/app/src/composables/useInfos.ts
@@ -13,7 +13,7 @@ import api from '@/api'
import { timeout } from '@/helpers/commons'
import i18n from '@/i18n'
import { useDomains } from './data'
-import { useRequests, type ReconnectingArgs } from './useRequests'
+import { useSSE } from './useSSE'
type BreadcrumbRoutes = {
name: RouteRecordNameGeneric
@@ -150,6 +150,7 @@ export const useInfos = createGlobalState(() => {
await getYunoHostVersion()
connected.value = true
await api.get({ uri: 'domains', cachePath: 'domains' })
+ useSSE().init()
}
function onLogout(route?: RouteLocationNormalizedLoaded) {
@@ -170,7 +171,7 @@ export const useInfos = createGlobalState(() => {
function login(credentials: string) {
return api
- .post({ uri: 'login', data: { credentials }, websocket: false })
+ .post({ uri: 'login', data: { credentials }, isAction: false })
.then(() => _onLogin())
}
@@ -179,10 +180,6 @@ export const useInfos = createGlobalState(() => {
return api.get('logout')
}
- function tryToReconnect(args?: ReconnectingArgs) {
- useRequests().reconnecting.value = args
- }
-
function updateRouterKey(to?: RouteLocationNormalized) {
if (!to) {
// Trick to force a view reload
@@ -221,7 +218,6 @@ export const useInfos = createGlobalState(() => {
onLogout,
login,
logout,
- tryToReconnect,
updateHtmlTitle,
updateRouterKey,
}
diff --git a/app/src/composables/useRequests.ts b/app/src/composables/useRequests.ts
index 33d439adc..e73d8248b 100644
--- a/app/src/composables/useRequests.ts
+++ b/app/src/composables/useRequests.ts
@@ -1,31 +1,35 @@
import { createGlobalState } from '@vueuse/core'
-import { v4 as uuid } from 'uuid'
-import { computed, reactive, ref, shallowRef } from 'vue'
+import { computed, reactive, shallowRef } from 'vue'
import { useRouter } from 'vue-router'
-import type { APIQuery, RequestMethod } from '@/api/api'
+import type { RequestMethod } from '@/api/api'
import { APIErrorLog, type APIError } from '@/api/errors'
-import { isObjectLiteral } from '@/helpers/commons'
import i18n from '@/i18n'
import type { StateVariant } from '@/types/commons'
+import { updateCacheFromAction } from './data'
+import { useAutoToast } from './useAutoToast'
import { useInfos } from './useInfos'
export type RequestStatus = 'pending' | 'success' | 'warning' | 'error'
+export type RequestCaller = 'root' | 'noninteractive' | string | null
export type APIRequest = {
- status: RequestStatus
- method: RequestMethod
- uri: string
id: string
- humanRoute: string
- initial: boolean
+ title: string
date: number
+ method?: RequestMethod
+ uri?: string
+ status: RequestStatus
+ initial: boolean
err?: APIError
action?: APIActionProps
showModal?: boolean
showModalTimeout?: number
}
type APIActionProps = {
+ external: boolean
+ caller?: RequestCaller
+ operationId?: string
messages: RequestMessage[]
errors: number
warnings: number
@@ -41,63 +45,65 @@ export type RequestMessage = {
variant: StateVariant
}
-export type ReconnectingArgs = {
- attemps?: number
- origin?: string
- initialDelay?: number
- delay?: number
-}
-
export const useRequests = createGlobalState(() => {
const router = useRouter()
const requests = shallowRef([])
- const reconnecting = ref()
const currentRequest = computed(() => {
return requests.value.find((r) => r.showModal)
})
const locked = computed(() => currentRequest.value?.showModal)
- const historyList = computed(() => {
+ const historyList = computed<(APIRequest | APIRequestAction)[]>(() => {
return requests.value
- .filter((r) => !!r.action || !!r.err)
+ .filter((r) => (!!r.action && !r.id.startsWith('lock')) || !!r.err)
.reverse() as APIRequestAction[]
})
function startRequest({
- uri,
+ id,
+ date,
+ title,
method,
- humanKey,
- initial,
- websocket,
- showModal,
+ uri,
+ caller,
+ initial = false,
+ isAction = true,
+ showModal = true,
+ external = false,
+ status = 'pending',
}: {
- uri: string
- method: RequestMethod
- humanKey?: APIQuery['humanKey']
- showModal: boolean
- websocket: boolean
- initial: boolean
- }): APIRequest {
- // 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.split('?')[0]}`
-
+ id: string
+ date: number
+ title?: string
+ method?: RequestMethod
+ uri?: string
+ caller?: RequestCaller
+ showModal?: boolean
+ isAction?: boolean
+ initial?: boolean
+ external?: boolean
+ status?: APIRequest['status']
+ }): APIRequest | APIRequestAction {
const request: APIRequest = reactive({
+ id,
+ title:
+ title ||
+ (method && uri
+ ? `[${method}] /${uri!.split('?')[0]}`
+ : i18n.global.t('api.unknown_request')),
+ date,
method,
uri,
- status: 'pending',
- humanRoute,
+ status,
initial,
showModal: false,
- id: uuid(),
- date: Date.now(),
err: undefined,
- action: websocket
+ action: isAction
? {
+ external,
+ caller,
+ // in case of recent history entry
+ operationId: external && status !== 'pending' ? id : undefined,
messages: [],
warnings: 0,
errors: 0,
@@ -123,21 +129,36 @@ export const useRequests = createGlobalState(() => {
request,
success,
showError = false,
+ errorMsg = undefined,
}: {
- request: APIRequest
+ request: APIRequest | APIRequestAction
success: boolean
showError?: boolean
+ errorMsg?: string
}) {
let status: RequestStatus = success ? 'success' : 'error'
- let hideModal = success || !showError
+ const hideModal = success || !showError
if (success && request.action) {
- const { warnings, errors, messages } = request.action
+ const { warnings, errors, messages, external, operationId } =
+ request.action
const msgCount = messages.length
if (msgCount && messages[msgCount - 1].variant === 'warning') {
- hideModal = false
+ useAutoToast().show({
+ body: messages[msgCount - 1].text,
+ variant: 'warning',
+ })
}
if (errors || warnings) status = 'warning'
+
+ if (external && operationId) {
+ updateCacheFromAction(operationId)
+ }
+ } else if (!success && errorMsg) {
+ useAutoToast().show({
+ body: errorMsg,
+ variant: 'danger',
+ })
}
if (request.showModalTimeout) {
@@ -153,7 +174,8 @@ export const useRequests = createGlobalState(() => {
request.showModal = false
// We can remove requests that are not actions or has no errors
requests.value = requests.value.filter(
- (r) => r.showModal || !!r.action || !!r.err,
+ (r) =>
+ r.showModal || (!!r.action && !r.id.startsWith('lock')) || !!r.err,
)
} else if (showError) {
request.showModal = true
@@ -200,7 +222,6 @@ export const useRequests = createGlobalState(() => {
requests,
historyList,
currentRequest,
- reconnecting,
locked,
startRequest,
endRequest,
diff --git a/app/src/composables/useSSE.ts b/app/src/composables/useSSE.ts
new file mode 100644
index 000000000..77ac4f8f5
--- /dev/null
+++ b/app/src/composables/useSSE.ts
@@ -0,0 +1,271 @@
+import { createGlobalState } from '@vueuse/core'
+import { computed, ref } from 'vue'
+
+import { STATUS_VARIANT, isOkStatus } from '@/helpers/yunohostArguments'
+import type { StateStatus } from '@/types/commons'
+import { useAutoToast } from './useAutoToast'
+import type { APIRequest, APIRequestAction, RequestCaller } from './useRequests'
+import { useRequests } from './useRequests'
+
+type SSEEventDataStart = {
+ type: 'start'
+ timestamp: number
+ ref_id: string
+ operation_id: string
+ started_by: RequestCaller
+ title: string
+}
+
+type SSEEventDataEnd = {
+ type: 'end'
+ timestamp: number
+ ref_id: string
+ operation_id: string
+ success: boolean
+ errormsg?: string
+}
+
+type SSEEventDataMsg = {
+ type: 'msg'
+ timestamp: number
+ ref_id: string
+ operation_id: string
+ level: StateStatus
+ msg: string
+}
+
+type SSEEventDataHistory = {
+ type: 'recent_history'
+ operation_id: string
+ title: string
+ started_at: number
+ started_by: RequestCaller
+ success: boolean
+}
+
+type SSEEventDataToast = {
+ type: 'toast'
+ timestamp: number
+ ref_id: string
+ operation_id: string
+ level: StateStatus
+ msg: string
+}
+
+type SSEEventDataHeartbeat = {
+ type: 'heartbeat'
+ timestamp: number
+ current_operation: string | null
+ cmdline: string | null
+}
+
+type AnySSEEventDataAction =
+ | SSEEventDataStart
+ | SSEEventDataEnd
+ | SSEEventDataMsg
+type AnySSEEventData =
+ | AnySSEEventDataAction
+ | SSEEventDataHistory
+ | SSEEventDataToast
+ | SSEEventDataHeartbeat
+
+export type ReconnectionArgs = {
+ origin: 'unknown' | 'reboot' | 'shutdown' | 'upgrade_system'
+ initialDelay?: number
+ delay?: number
+}
+
+export const useSSE = createGlobalState(() => {
+ const sseSource = ref(null)
+ const reconnectionArgs = ref(null)
+ const reconnectTimeout = ref()
+ const { startRequest, endRequest, historyList } = useRequests()
+ const nonOperationWithLock = ref(null)
+
+ const reconnecting = computed(() =>
+ !sseSource.value && reconnectionArgs.value
+ ? reconnectionArgs.value.origin
+ : null,
+ )
+
+ function init(): Promise {
+ return new Promise((resolve, reject) => {
+ if (sseSource.value) resolve()
+
+ const sse = new EventSource(`/yunohost/api/sse`, {
+ withCredentials: true,
+ })
+
+ sse.onopen = () => {
+ sseSource.value = sse
+ reconnectionArgs.value = null
+ resolve()
+ }
+
+ function wrapEvent(
+ name: T['type'],
+ fn: (data: T) => void,
+ ) {
+ sse.addEventListener(name, (e) => {
+ // The server sends at least heartbeats every 10s, try to reconnect if we loose connection
+ tryToReconnect({ initialDelay: 15000, origin: 'reboot' })
+ fn({ type: name, ...JSON.parse(e.data) })
+ })
+ }
+
+ wrapEvent('recent_history', onHistoryEvent)
+ wrapEvent('start', onActionEvent)
+ wrapEvent('msg', onActionEvent)
+ wrapEvent('end', onActionEvent)
+ wrapEvent('heartbeat', onHeartbeatEvent)
+ wrapEvent('toast', onToastEvent)
+
+ sse.onerror = (event) => {
+ console.error('SSE error', event)
+ reject()
+ }
+ })
+ }
+
+ /**
+ * SSE reconnection helper. Resolve when server is reachable.
+ *
+ * @param origin - a i18n key to explain why we're trying to reconnect
+ * @param delay - Delay between calls to the API in ms
+ * @param initialDelay - Delay before calling the API for the first time in ms
+ *
+ * @returns Promise that resolve when connection is successful
+ */
+ function tryToReconnect(args: ReconnectionArgs) {
+ clearTimeout(reconnectTimeout.value)
+
+ return new Promise((resolve) => {
+ function reconnect() {
+ if (!reconnectionArgs.value) {
+ reconnectionArgs.value = args
+ }
+ sseSource.value?.close()
+ sseSource.value = null
+ init()
+ .then(resolve)
+ .catch(() => {
+ reconnectTimeout.value = window.setTimeout(
+ reconnect,
+ args.delay || 3000,
+ )
+ })
+ }
+
+ if (args.initialDelay) {
+ reconnectTimeout.value = window.setTimeout(reconnect, args.initialDelay)
+ } else {
+ reconnect()
+ }
+ })
+ }
+
+ function onActionEvent(data: AnySSEEventDataAction) {
+ let request = historyList.value.findLast(
+ (r: APIRequest) => r.id === data.ref_id,
+ ) as APIRequestAction | undefined
+
+ if (!request) {
+ request = startRequest({
+ id: data.ref_id,
+ title: data.type === 'start' ? data.title : data.operation_id,
+ date: data.timestamp * 1000,
+ external: true,
+ }) as APIRequestAction
+ }
+
+ if (data.type === 'start') {
+ request.action.operationId = data.operation_id
+ request.title = data.title
+ request.action.caller = data.started_by
+ } else if (data.type === 'end' && request.action.external) {
+ // End request on this last message if the action was external
+ // (else default http response will end it)
+ endRequest({ request, success: data.success, errorMsg: data.errormsg })
+ } else if (data.type === 'msg') {
+ let text = data.msg.replaceAll('\n', '
')
+ const progressBar = text.match(/^\[#*\+*\.*\] > /)?.[0]
+ if (progressBar) {
+ text = text.replace(progressBar, '')
+ const progress: Record = { '#': 0, '+': 0, '.': 0 }
+ for (const char of progressBar) {
+ if (char in progress) progress[char] += 1
+ }
+ request.action.progress = Object.values(progress)
+ }
+
+ request.action.messages.push({
+ text,
+ variant: STATUS_VARIANT[data.level],
+ })
+
+ if (!isOkStatus(data.level)) request.action[`${data.level}s`]++
+ }
+ }
+
+ function onHeartbeatEvent(data: SSEEventDataHeartbeat) {
+ if (data.current_operation === null) {
+ // An action may have failed without properly exiting
+ // Ensure that there's no pending external request blocking the view
+ // if server says that there's no current action
+ const request =
+ nonOperationWithLock.value ||
+ historyList.value.findLast(
+ (r: APIRequest) =>
+ r.action?.external === true && r.status === 'pending',
+ )
+
+ if (request) {
+ endRequest({
+ request,
+ success: !!nonOperationWithLock.value,
+ showError: false,
+ })
+ nonOperationWithLock.value = null
+ }
+ } else if (data.current_operation.startsWith('lock')) {
+ // SPECIAL CASE: an operation like `yunohost tools shell` has the lock
+ const timestamp = parseFloat(data.current_operation.split('-')[1])
+ if (!nonOperationWithLock.value) {
+ nonOperationWithLock.value = startRequest({
+ id: data.current_operation,
+ title: data.cmdline!,
+ date: timestamp * 1000,
+ caller: 'cli',
+ external: true,
+ }) as APIRequestAction
+ }
+ }
+ }
+
+ function onHistoryEvent(data: SSEEventDataHistory) {
+ const request = historyList.value.findLast(
+ (r: APIRequest) => r.action?.operationId === data.operation_id,
+ ) as APIRequestAction | undefined
+ // Do not add the request if already in the history (can happen on sse reconnection)
+ if (request) return
+
+ startRequest({
+ id: data.operation_id,
+ title: data.title,
+ date: data.started_at * 1000,
+ caller: data.started_by,
+ showModal: false,
+ external: true,
+ status: data.success ? 'success' : 'error',
+ })
+ }
+
+ function onToastEvent(data: SSEEventDataToast) {
+ useAutoToast().show({
+ body: data.msg,
+ variant: STATUS_VARIANT[data.level],
+ })
+ }
+
+ return { init, reconnecting, tryToReconnect }
+})
diff --git a/app/src/i18n/locales/en.json b/app/src/i18n/locales/en.json
index af550fbfc..4415d6184 100644
--- a/app/src/i18n/locales/en.json
+++ b/app/src/i18n/locales/en.json
@@ -15,6 +15,7 @@
"all": "All",
"all_apps": "All apps",
"api": {
+ "busy": "The server is busy…",
"partial_logs": "[…] (check in history for full logs)",
"processing": "The server is processing the action…",
"query_status": {
@@ -26,14 +27,15 @@
"reconnecting": {
"failed": "Looks like the server is not responding. You can try to reconnect again or try to run `systemctl restart yunohost-api` thru ssh.",
"reason": {
- "reboot": "Your server is rebooting and will not be reachable for some time. A login prompt will be available as soon as the server is reachable.",
- "shutdown": "Your server is shutting down and is no longer reachable. Turn it back on and a login prompt will be available as soon as the server is reachable.",
- "unknown": "Connection with the server has been closed for unknown reasons.",
+ "reboot": "Your server is rebooting and will not be reachable for some time.",
+ "shutdown": "Your server is shutting down and is no longer reachable.",
+ "unknown": "Connection with the server has been closed for unknown reasons. Maybe 'yunohost-api' is down?",
"upgrade_system": "Connection with the server has been closed due to YunoHost upgrade. Waiting for the server to be reachable again…"
},
"session_expired": "The server is now reachable! But it looks like your session expired, please login.",
"title": "Trying to communicate with the server…"
- }
+ },
+ "unknown_request": "Unknown request"
},
"api_error": {
"error_message": "Error message:",
@@ -390,6 +392,7 @@
"groups_and_permissions": "Groups and permissions",
"groups_and_permissions_manage": "Manage groups and permissions",
"history": {
+ "check_logs": "Go to operation's log page.",
"is_empty": "Nothing in history for now.",
"last_action": "Last action:",
"methods": {
@@ -398,6 +401,8 @@
"POST": "create/execute",
"PUT": "modify"
},
+ "no_logs": "No available logs.",
+ "started_by": "started by {caller}",
"title": "History"
},
"home": "Home",
@@ -412,86 +417,6 @@
"hook_data_mail_desc": "Raw emails stored on the server",
"hook_data_xmpp": "XMPP data",
"hook_data_xmpp_desc": "Room and user configurations, file uploads",
- "human_routes": {
- "apps": {
- "action_config": "Run action '{action}' of app '{name}' configuration",
- "change_label": "Change label of '{prevName}' for '{nextName}'",
- "change_url": "Change access URL of '{name}'",
- "dismiss_notification": "Dismiss notification for '{name}'",
- "install": "Install app '{name}'",
- "set_default": "Redirect '{domain}' domain root to '{name}'",
- "uninstall": "Uninstall app '{name}'",
- "update_config": "Update panel '{id}' of app '{name}' configuration"
- },
- "backups": {
- "create": "Create a backup",
- "delete": "Delete backup '{name}'",
- "restore": "Restore backup '{name}'"
- },
- "diagnosis": {
- "ignore": {
- "error": "Ignore an error",
- "warning": "Ignore a warning"
- },
- "run": "Run the diagnosis",
- "run_specific": "Run '{description}' diagnosis",
- "unignore": {
- "error": "Unignore an error",
- "warning": "Unignore a warning"
- }
- },
- "domains": {
- "add": "Add domain '{name}'",
- "cert_install": "Install certificate for '{name}'",
- "cert_renew": "Renew certificate for '{name}'",
- "delete": "Delete domain '{name}'",
- "push_dns_changes": "Push DNS records to registrar for '{name}'",
- "set_default": "Set '{name}' as default domain",
- "update_config": "Update panel '{id}' of domain '{name}' configuration"
- },
- "firewall": {
- "ports": "{action} port {port} ({protocol}, {connection})",
- "upnp": "{action} UPnP"
- },
- "groups": {
- "add": "Add '{user}' to group '{name}'",
- "create": "Create group '{name}'",
- "delete": "Delete group '{name}'",
- "remove": "Remove '{user}' from group '{name}'"
- },
- "migrations": {
- "run": "Run migrations",
- "skip": "Skip migrations"
- },
- "permissions": {
- "add": "Allow '{name}' to access '{perm}'",
- "remove": "Remove '{name}' access to '{perm}'"
- },
- "postinstall": "Run the post-install",
- "reboot": "Reboot the server",
- "reconnecting": "Reconnecting",
- "services": {
- "restart": "Restart the service '{name}'",
- "start": "Start the service '{name}'",
- "stop": "Stop the service '{name}'"
- },
- "settings": {
- "update": "Update '{panel}' global settings"
- },
- "share_logs": "Generate link for log '{name}'",
- "shutdown": "Shutdown the server",
- "update": "Check for updates",
- "upgrade": {
- "app": "Upgrade '{app}' app",
- "apps": "Upgrade all apps",
- "system": "Upgrade the system"
- },
- "users": {
- "create": "Create user '{name}'",
- "delete": "Delete user '{name}'",
- "update": "Update user '{name}'"
- }
- },
"id": "ID",
"ignore": "Ignore",
"ignored": "{count} ignored",
diff --git a/app/src/scss/main.scss b/app/src/scss/main.scss
index 156a86c9c..5c12131fe 100644
--- a/app/src/scss/main.scss
+++ b/app/src/scss/main.scss
@@ -30,7 +30,7 @@
@import '~bootstrap/scss/progress';
@import '~bootstrap/scss/list-group';
@import '~bootstrap/scss/close';
-// @import "~bootstrap/scss/toasts";
+@import '~bootstrap/scss/toasts';
@import '~bootstrap/scss/modal';
@import '~bootstrap/scss/tooltip';
@import '~bootstrap/scss/popover';
diff --git a/app/src/views/PostInstall.vue b/app/src/views/PostInstall.vue
index 10213778b..fbf8ef07b 100644
--- a/app/src/views/PostInstall.vue
+++ b/app/src/views/PostInstall.vue
@@ -8,6 +8,7 @@ import { APIBadRequestError } from '@/api/errors'
import { useForm } from '@/composables/form'
import { useAutoModal } from '@/composables/useAutoModal'
import { useInfos } from '@/composables/useInfos'
+import { useSSE } from '@/composables/useSSE'
import {
alphalownumdot_,
minLength,
@@ -26,6 +27,8 @@ const { installed } = useInfos()
if (installed.value) {
router.push({ name: 'home' })
+} else {
+ useSSE().init()
}
type Steps = 'start' | 'domain' | 'user' | 'rootfsspace-error' | 'login'
@@ -129,11 +132,7 @@ async function performPostInstall(force = false) {
)
api
- .post({
- uri: 'postinstall' + (force ? '?force_diskspace' : ''),
- data,
- humanKey: { key: 'postinstall' },
- })
+ .post({ uri: 'postinstall' + (force ? '?force_diskspace' : ''), data })
.then(() => {
// Display success message and allow the user to login
installed.value = true
diff --git a/app/src/views/_partials/HistoryConsole.vue b/app/src/views/_partials/HistoryConsole.vue
index 0b181af8b..1ae6ae3df 100644
--- a/app/src/views/_partials/HistoryConsole.vue
+++ b/app/src/views/_partials/HistoryConsole.vue
@@ -13,9 +13,7 @@ const rootElem = ref | null>(null)
const historyElem = ref | null>(null)
const open = ref(false)
-const lastAction = computed(() => {
- return historyList.value[historyList.value.length - 1]
-})
+const lastAction = computed(() => historyList.value[0])
async function scrollToAction(actionIndex: number) {
await nextTick()
@@ -175,9 +173,21 @@ function onHistoryBarClick(e: MouseEvent) {
/>
+
+ {{ $t('history.no_logs') }}
+
+
diff --git a/app/src/views/app/AppInfo.vue b/app/src/views/app/AppInfo.vue
index 8eb7cbcff..12dd05e8e 100644
--- a/app/src/views/app/AppInfo.vue
+++ b/app/src/views/app/AppInfo.vue
@@ -145,11 +145,6 @@ const config = coreConfig
? `apps/${props.id}/actions/${action}`
: `apps/${props.id}/config/${panelId}`,
data: isEmptyValue(data) ? {} : { args: objectToParams(data) },
- humanKey: {
- key: `apps.${action ? 'action' : 'update'}_config`,
- id: panelId,
- name: props.id,
- },
})
.then(() => api.refetch())
.catch(onError)
@@ -178,11 +173,6 @@ async function changeLabel(permName: string, i: number) {
label: data.label,
show_tile: data.show_tile ? 'True' : 'False',
},
- humanKey: {
- key: 'apps.change_label',
- prevName: app.label,
- nextName: data.label,
- },
})
// FIXME really need to refetch? permissions store update should be ok
.then(() => api.refetch())
@@ -198,7 +188,6 @@ async function changeUrl() {
.put({
uri: `apps/${props.id}/changeurl`,
data: { domain, path: '/' + path },
- humanKey: { key: 'apps.change_url', name: app.label },
})
// Refetch because some content of this page relies on the url
.then(() => api.refetch())
@@ -209,38 +198,22 @@ async function setAsDefaultDomain(undo = false) {
if (!confirmed) return
api
- .put({
- uri: `apps/${props.id}/default${undo ? '?undo' : ''}`,
- humanKey: {
- key: 'apps.set_default',
- name: app.label,
- domain: app.domain,
- },
- })
+ .put({ uri: `apps/${props.id}/default${undo ? '?undo' : ''}` })
.then(() => (app.isDefault.value = true))
}
async function dismissNotification(name: string) {
api
- .put({
- uri: `apps/${props.id}/dismiss_notification/${name}`,
- humanKey: { key: 'apps.dismiss_notification', name: app.label },
- })
+ .put({ uri: `apps/${props.id}/dismiss_notification/${name}` })
// FIXME no need to refetch i guess, filter the reactive notifs?
.then(() => api.refetch())
}
async function uninstall() {
const data = purge.value === true ? { purge: 1 } : {}
- api
- .delete({
- uri: 'apps/' + props.id,
- data,
- humanKey: { key: 'apps.uninstall', name: app.label },
- })
- .then(() => {
- router.push({ name: 'app-list' })
- })
+ api.delete({ uri: 'apps/' + props.id, data }).then(() => {
+ router.push({ name: 'app-list' })
+ })
}
diff --git a/app/src/views/app/AppInstall.vue b/app/src/views/app/AppInstall.vue
index fca039dbd..7f6deb593 100644
--- a/app/src/views/app/AppInstall.vue
+++ b/app/src/views/app/AppInstall.vue
@@ -119,11 +119,7 @@ const performInstall = onSubmit(async (onError) => {
}
api
- .post<{ notifications: Obj }>({
- uri: 'apps',
- data,
- humanKey: { key: 'apps.install', name: app.name },
- })
+ .post<{ notifications: Obj }>({ uri: 'apps', data })
.then(async (response) => {
const postInstall = formatAppNotifs(response.notifications)
if (postInstall) {
@@ -150,12 +146,7 @@ function onDomainAdd(data: {
install_letsencrypt_cert?: boolean
}) {
api
- .post({
- uri: 'domains',
- cachePath: `domains.${data.domain}`,
- data,
- humanKey: { key: 'domains.add', name: data.domain },
- })
+ .post({ uri: 'domains', cachePath: `domains.${data.domain}`, data })
.then(() => {
form.value.domain = data.domain
showAddDomainModal.value = false
diff --git a/app/src/views/backup/BackupCreate.vue b/app/src/views/backup/BackupCreate.vue
index 4d124b0f2..1134e050f 100644
--- a/app/src/views/backup/BackupCreate.vue
+++ b/app/src/views/backup/BackupCreate.vue
@@ -41,7 +41,7 @@ function toggleSelected(select: boolean, type: 'system' | 'apps') {
function createBackup() {
const data = parseBackupForm(selected.value, system)
- api.post({ uri: 'backups', data, humanKey: 'backups.create' }).then(() => {
+ api.post({ uri: 'backups', data }).then(() => {
router.push({ name: 'backup-list', params: { id: props.id } })
})
}
diff --git a/app/src/views/backup/BackupInfo.vue b/app/src/views/backup/BackupInfo.vue
index e28594630..e6b6059aa 100644
--- a/app/src/views/backup/BackupInfo.vue
+++ b/app/src/views/backup/BackupInfo.vue
@@ -60,7 +60,6 @@ async function restoreBackup() {
uri: `backups/${props.name}/restore`,
// FIXME force?
data: { ...data, force: '' },
- humanKey: { key: 'backups.restore', name: props.name },
})
.then(() => {
// FIXME back to backup list or home ?
@@ -81,14 +80,9 @@ async function deleteBackup() {
)
if (!confirmed) return
- api
- .delete({
- uri: 'backups/' + props.name,
- humanKey: { key: 'backups.delete', name: props.name },
- })
- .then(() => {
- router.push({ name: 'backup-list', params: { id: props.id } })
- })
+ api.delete({ uri: 'backups/' + props.name }).then(() => {
+ router.push({ name: 'backup-list', params: { id: props.id } })
+ })
}
function downloadBackup() {
diff --git a/app/src/views/diagnosis/DiagnosisView.vue b/app/src/views/diagnosis/DiagnosisView.vue
index d4ac8e96d..c5506b59c 100644
--- a/app/src/views/diagnosis/DiagnosisView.vue
+++ b/app/src/views/diagnosis/DiagnosisView.vue
@@ -12,7 +12,6 @@ const reports = await api
{
method: 'PUT',
uri: 'diagnosis/run?except_if_never_ran_yet',
- humanKey: 'diagnosis.run',
},
{ uri: 'diagnosis?full' },
])
@@ -55,10 +54,6 @@ function runDiagnosis(report?: { id: string; description: string }) {
.put({
uri: 'diagnosis/run' + (id ? '?force' : ''),
data: id ? { categories: [id] } : {},
- humanKey: {
- key: 'diagnosis.run' + (id ? '_specific' : ''),
- description: report?.description,
- },
})
.then(() => api.refetch())
}
@@ -73,11 +68,7 @@ function toggleIgnoreIssue(
)
api
- .put({
- uri: 'diagnosis/' + action,
- data: { filter: filterArgs },
- humanKey: `diagnosis.${action}.${item.status}`,
- })
+ .put({ uri: 'diagnosis/' + action, data: { filter: filterArgs } })
.then(() => {
item.ignored = action === 'ignore'
const count = item.ignored ? 1 : -1
diff --git a/app/src/views/domain/DomainAdd.vue b/app/src/views/domain/DomainAdd.vue
index 048603c4e..db6841b93 100644
--- a/app/src/views/domain/DomainAdd.vue
+++ b/app/src/views/domain/DomainAdd.vue
@@ -16,12 +16,7 @@ function onSubmit(data: {
install_letsencrypt_cert?: boolean
}) {
api
- .post({
- uri: 'domains',
- cachePath: `domains.${data.domain}`,
- data,
- humanKey: { key: 'domains.add', name: data.domain },
- })
+ .post({ uri: 'domains', cachePath: `domains.${data.domain}`, data })
.then(() => {
router.push({ name: 'domain-list' })
})
diff --git a/app/src/views/domain/DomainDns.vue b/app/src/views/domain/DomainDns.vue
index acfa52f01..0ed5b95f0 100644
--- a/app/src/views/domain/DomainDns.vue
+++ b/app/src/views/domain/DomainDns.vue
@@ -42,7 +42,7 @@ function getDnsChanges() {
.post({
uri: `domains/${props.name}/dns/push?dry_run`,
showModal: true,
- websocket: false,
+ isAction: false,
})
.then((dnsCategories) => {
let canForce = false
@@ -119,7 +119,6 @@ async function pushDnsChanges() {
api
.post>({
uri: `domains/${props.name}/dns/push${force.value ? '?force' : ''}`,
- humanKey: { key: 'domains.push_dns_changes', name: props.name },
})
.then(async (responseData) => {
await getDnsChanges()
diff --git a/app/src/views/domain/DomainInfo.vue b/app/src/views/domain/DomainInfo.vue
index 9f4bc580d..aad103b8c 100644
--- a/app/src/views/domain/DomainInfo.vue
+++ b/app/src/views/domain/DomainInfo.vue
@@ -42,11 +42,6 @@ const config = useConfigPanels(
? `domain/${props.name}/actions/${action}`
: `domains/${props.name}/config/${panelId}`,
data: { args: objectToParams(data) },
- humanKey: {
- key: `domains.${action ? 'action' : 'update'}_config`,
- id: panelId,
- name: props.name,
- },
})
.then(() => api.refetch())
.catch(onError)
@@ -86,10 +81,6 @@ async function deleteDomain() {
uri: `domains/${props.name}`,
cachePath: `domains.${props.name}`,
data,
- humanKey: {
- key: 'domains.delete',
- name: props.name,
- },
})
.then(() => {
router.push({ name: 'domain-list' })
@@ -112,7 +103,6 @@ async function setAsDefaultDomain() {
uri: `domains/${props.name}/main`,
cachePath: `mainDomain.${props.name}`,
data: {},
- humanKey: { key: 'domains.set_default', name: props.name },
})
}
diff --git a/app/src/views/group/GroupCreate.vue b/app/src/views/group/GroupCreate.vue
index 3a7ab9cfc..ae9cada05 100644
--- a/app/src/views/group/GroupCreate.vue
+++ b/app/src/views/group/GroupCreate.vue
@@ -29,12 +29,7 @@ const { v, onSubmit } = useForm(form, fields)
const onAddGroup = onSubmit((onError) => {
api
- .post({
- uri: 'users/groups',
- cachePath: 'groups',
- data: form.value,
- humanKey: { key: 'groups.create', name: form.value.groupname },
- })
+ .post({ uri: 'users/groups', cachePath: 'groups', data: form.value })
.then(() => {
router.push({ name: 'group-list' })
})
diff --git a/app/src/views/group/GroupList.vue b/app/src/views/group/GroupList.vue
index 8bc28fec3..6e77181fa 100644
--- a/app/src/views/group/GroupList.vue
+++ b/app/src/views/group/GroupList.vue
@@ -116,7 +116,6 @@ async function onPermissionChanged(
.put({
uri: `users/permissions/${perm}/${action}/${name}`,
cachePath: `permissions.${perm}`,
- humanKey: { key: `permissions.${action}`, perm, name },
})
.then(() => applyFn(perm))
}
@@ -136,7 +135,6 @@ async function onUserChanged(
.put({
uri: `users/groups/${name}/${action}/${user}`,
cachePath: `groups.${name}`,
- humanKey: { key: `groups.${action}`, user, name },
})
.then(() => applyFn(user))
}
@@ -146,11 +144,7 @@ async function deleteGroup(name: string) {
if (!confirmed) return
api
- .delete({
- uri: `users/groups/${name}`,
- cachePath: `groups.${name}`,
- humanKey: { key: 'groups.delete', name },
- })
+ .delete({ uri: `users/groups/${name}`, cachePath: `groups.${name}` })
.then(() => {
primaryGroups.value = primaryGroups.value.filter(
(group) => group.name !== name,
diff --git a/app/src/views/service/ServiceInfo.vue b/app/src/views/service/ServiceInfo.vue
index df672e8ba..ea815bf0a 100644
--- a/app/src/views/service/ServiceInfo.vue
+++ b/app/src/views/service/ServiceInfo.vue
@@ -46,12 +46,7 @@ async function updateService(action: 'start' | 'stop' | 'restart') {
)
if (!confirmed) return
- api
- .put({
- uri: `services/${props.name}/${action}`,
- humanKey: { key: `services.${action}`, name: props.name },
- })
- .then(() => api.refetch())
+ api.put({ uri: `services/${props.name}/${action}` }).then(() => api.refetch())
}
function shareLogs() {
diff --git a/app/src/views/tool/ToolFirewall.vue b/app/src/views/tool/ToolFirewall.vue
index 6e73273e9..134ee7335 100644
--- a/app/src/views/tool/ToolFirewall.vue
+++ b/app/src/views/tool/ToolFirewall.vue
@@ -141,17 +141,9 @@ async function togglePort({
)
if (!confirmed) return false
- const actionTrad = t({ allow: 'open', disallow: 'close' }[action])
return api
.put({
uri: `firewall/${protocol}/${action}/${port}?${connection}_only`,
- humanKey: {
- key: 'firewall.ports',
- protocol,
- action: actionTrad,
- port,
- connection,
- },
showModal: false,
})
.then(() => true)
@@ -163,10 +155,7 @@ async function toggleUpnp() {
if (!confirmed) return
api
- .put({
- uri: 'firewall/upnp/' + action,
- humanKey: { key: 'firewall.upnp', action: t(action) },
- })
+ .put({ uri: 'firewall/upnp/' + action })
.then(() => {
// FIXME Couldn't test when it works.
api.refetch()
diff --git a/app/src/views/tool/ToolLog.vue b/app/src/views/tool/ToolLog.vue
index 22a9ecb78..035409af5 100644
--- a/app/src/views/tool/ToolLog.vue
+++ b/app/src/views/tool/ToolLog.vue
@@ -64,11 +64,7 @@ const { description, logs, moreLogsAvailable, info } = await api
function shareLogs() {
api
- .get<{ url: string }>({
- uri: `logs/${props.name}/share`,
- humanKey: { key: 'share_logs', name: props.name },
- websocket: true,
- })
+ .get<{ url: string }>({ uri: `logs/${props.name}/share` })
.then(({ url }) => {
window.open(url, '_blank')
})
diff --git a/app/src/views/tool/ToolMigrations.vue b/app/src/views/tool/ToolMigrations.vue
index 8e43eb707..5c45ae549 100644
--- a/app/src/views/tool/ToolMigrations.vue
+++ b/app/src/views/tool/ToolMigrations.vue
@@ -40,9 +40,7 @@ const { pending, done, checked } = await api
function runMigrations() {
// Check that every migration's disclaimer has been checked.
if (Object.values(checked).every((value) => value === true)) {
- api
- .put({ uri: 'migrations?accept_disclaimer', humanKey: 'migrations.run' })
- .then(() => api.refetch())
+ api.put({ uri: 'migrations?accept_disclaimer' }).then(() => api.refetch())
}
}
@@ -50,11 +48,7 @@ async function skipMigration(id: string) {
const confirmed = await modalConfirm(t('confirm_migrations_skip'))
if (!confirmed) return
api
- .put({
- uri: '/migrations/' + id,
- data: { skip: '', targets: id },
- humanKey: 'migration.skip',
- })
+ .put({ uri: '/migrations/' + id, data: { skip: '', targets: id } })
.then(() => api.refetch())
}
diff --git a/app/src/views/tool/ToolPower.vue b/app/src/views/tool/ToolPower.vue
index a4fd509a8..56c1fc260 100644
--- a/app/src/views/tool/ToolPower.vue
+++ b/app/src/views/tool/ToolPower.vue
@@ -3,19 +3,19 @@ import { useI18n } from 'vue-i18n'
import api from '@/api'
import { useAutoModal } from '@/composables/useAutoModal'
-import { useInfos } from '@/composables/useInfos'
+import { useSSE } from '@/composables/useSSE'
const { t } = useI18n()
const modalConfirm = useAutoModal()
-const { tryToReconnect } = useInfos()
+const { tryToReconnect } = useSSE()
async function triggerAction(action: 'reboot' | 'shutdown') {
const confirmed = await modalConfirm(t('confirm_reboot_action_' + action))
if (!confirmed) return
- api.put({ uri: action + '?force', humanKey: action }).then(() => {
+ api.put({ uri: action + '?force' }).then(() => {
const delay = action === 'reboot' ? 4000 : 10000
- tryToReconnect({ attemps: Infinity, origin: action, delay })
+ tryToReconnect({ origin: action, delay })
})
}
diff --git a/app/src/views/tool/ToolSettings.vue b/app/src/views/tool/ToolSettings.vue
index 35d006a28..989a4211e 100644
--- a/app/src/views/tool/ToolSettings.vue
+++ b/app/src/views/tool/ToolSettings.vue
@@ -17,11 +17,7 @@ const { form, panel, v, routes, onPanelApply } = useConfigPanels(
({ panelId, data }, onError) => {
// FIXME no route for potential action
api
- .put({
- uri: `settings/${panelId}`,
- data: { args: objectToParams(data) },
- humanKey: { key: 'settings.update', panel: panelId },
- })
+ .put({ uri: `settings/${panelId}`, data: { args: objectToParams(data) } })
.then(() => api.refetch())
.catch(onError)
},
diff --git a/app/src/views/update/SystemUpdate.vue b/app/src/views/update/SystemUpdate.vue
index d07f80454..e09692387 100644
--- a/app/src/views/update/SystemUpdate.vue
+++ b/app/src/views/update/SystemUpdate.vue
@@ -5,16 +5,16 @@ import { useI18n } from 'vue-i18n'
import api from '@/api'
import CardCollapse from '@/components/CardCollapse.vue'
import { useAutoModal } from '@/composables/useAutoModal'
-import { useInfos } from '@/composables/useInfos'
+import { useSSE } from '@/composables/useSSE'
import type { SystemUpdate } from '@/types/core/api'
import { formatAppNotifs } from '../app/appData'
const { t } = useI18n()
-const { tryToReconnect } = useInfos()
+const { tryToReconnect } = useSSE()
const modalConfirm = useAutoModal()
const { apps, system, importantYunohostUpgrade, pendingMigrations } = await api
- .put({ uri: 'update/all', humanKey: 'update' })
+ .put({ uri: 'update/all' })
.then(({ apps, system, important_yunohost_upgrade, pending_migrations }) => {
return {
apps: ref(apps),
@@ -46,7 +46,6 @@ async function performAppsUpgrade(ids: string[]) {
const continue_ = await api
.put>({
uri: `apps/${app.id}/upgrade`,
- humanKey: { key: 'upgrade.app', app: app.name },
})
.then((response) => {
const postMessage = formatAppNotifs(response.notifications.POST_UPGRADE)
@@ -79,10 +78,9 @@ async function performSystemUpgrade() {
const confirmed = await modalConfirm(t('confirm_update_system'))
if (!confirmed) return
- api.put({ uri: 'upgrade/system', humanKey: 'upgrade.system' }).then(() => {
+ api.put({ uri: 'upgrade/system' }).then(() => {
if (system.value.some(({ name }) => name.includes('yunohost'))) {
tryToReconnect({
- attemps: 1,
origin: 'upgrade_system',
initialDelay: 2000,
})
diff --git a/app/src/views/user/UserCreate.vue b/app/src/views/user/UserCreate.vue
index 845817ac3..02cbc6d4b 100644
--- a/app/src/views/user/UserCreate.vue
+++ b/app/src/views/user/UserCreate.vue
@@ -109,12 +109,7 @@ const { v, onSubmit } = useForm(form, fields)
const onUserCreate = onSubmit(async (onError) => {
const data = await formatForm(form)
api
- .post({
- uri: 'users',
- cachePath: 'users',
- data,
- humanKey: { key: 'users.create', name: form.value.username },
- })
+ .post({ uri: 'users', cachePath: 'users', data })
.then(() => {
router.push({ name: 'user-list' })
})
diff --git a/app/src/views/user/UserEdit.vue b/app/src/views/user/UserEdit.vue
index 75d2ea128..d47ac14ea 100644
--- a/app/src/views/user/UserEdit.vue
+++ b/app/src/views/user/UserEdit.vue
@@ -216,7 +216,6 @@ const onUserEdit = onSubmit(async (onError, serverErrors) => {
uri: `users/${props.name}`,
cachePath: `userDetails.${props.name}`,
data,
- humanKey: { key: 'users.update', name: props.name },
})
.then(() => {
router.push({ name: 'user-info', params: { name: props.name } })
diff --git a/app/src/views/user/UserInfo.vue b/app/src/views/user/UserInfo.vue
index f40d6c3ec..5627296f9 100644
--- a/app/src/views/user/UserInfo.vue
+++ b/app/src/views/user/UserInfo.vue
@@ -24,7 +24,6 @@ function deleteUser() {
uri: `users/${props.name}`,
cachePath: `userDetails.${props.name}`,
data,
- humanKey: { key: 'users.delete', name: props.name },
})
.then(() => {
router.push({ name: 'user-list' })
diff --git a/app/vite.config.ts b/app/vite.config.ts
index 42535372e..10062601a 100644
--- a/app/vite.config.ts
+++ b/app/vite.config.ts
@@ -126,7 +126,6 @@ export default defineConfig(({ mode }) => {
proxy: {
'/yunohost': {
target: `https://${env.VITE_IP}`,
- ws: true,
logLevel: 'info',
secure: false,
},