diff options
Diffstat (limited to 'apps/settings/src/store/app-api-store.ts')
-rw-r--r-- | apps/settings/src/store/app-api-store.ts | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/apps/settings/src/store/app-api-store.ts b/apps/settings/src/store/app-api-store.ts new file mode 100644 index 00000000000..769f212ebd7 --- /dev/null +++ b/apps/settings/src/store/app-api-store.ts @@ -0,0 +1,325 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { confirmPassword } from '@nextcloud/password-confirmation' +import { showError, showInfo } from '@nextcloud/dialogs' +import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' +import { generateUrl } from '@nextcloud/router' +import { defineStore } from 'pinia' + +import api from './api' +import logger from '../logger' + +import type { IAppstoreExApp, IDeployDaemon, IDeployOptions, IExAppStatus } from '../app-types.ts' +import Vue from 'vue' + +interface AppApiState { + apps: IAppstoreExApp[] + updateCount: number + loading: Record<string, boolean> + loadingList: boolean + statusUpdater: number | null | undefined + daemonAccessible: boolean + defaultDaemon: IDeployDaemon | null + dockerDaemons: IDeployDaemon[] +} + +export const useAppApiStore = defineStore('app-api-apps', { + state: (): AppApiState => ({ + apps: [], + updateCount: loadState('settings', 'appstoreExAppUpdateCount', 0), + loading: {}, + loadingList: false, + statusUpdater: null, + daemonAccessible: loadState('settings', 'defaultDaemonConfigAccessible', false), + defaultDaemon: loadState('settings', 'defaultDaemonConfig', null), + dockerDaemons: [], + }), + + getters: { + getLoading: (state) => (id: string) => state.loading[id] ?? false, + getAllApps: (state) => state.apps, + getUpdateCount: (state) => state.updateCount, + getDaemonAccessible: (state) => state.daemonAccessible, + getDefaultDaemon: (state) => state.defaultDaemon, + getAppStatus: (state) => (appId: string) => + state.apps.find((app) => app.id === appId)?.status || null, + getStatusUpdater: (state) => state.statusUpdater, + getInitializingOrDeployingApps: (state) => + state.apps.filter((app) => + app?.status?.action + && (app?.status?.action === 'deploy' || app.status.action === 'init' || app.status.action === 'healthcheck') + && app.status.type !== '', + ), + }, + + actions: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appsApiFailure(error: any) { + showError(t('settings', 'An error occurred during the request. Unable to proceed.') + '<br>' + error.error.response.data.data.message, { isHTML: true }) + logger.error(error) + }, + + setLoading(id: string, value: boolean) { + Vue.set(this.loading, id, value) + }, + + setError(appId: string | string[], error: string) { + const appIds = Array.isArray(appId) ? appId : [appId] + appIds.forEach((_id) => { + const app = this.apps.find((app) => app.id === _id) + if (app) { + app.error = error + } + }) + }, + + enableApp(appId: string, daemon: IDeployDaemon, deployOptions: IDeployOptions) { + this.setLoading(appId, true) + this.setLoading('install', true) + return confirmPassword().then(() => { + + return axios.post(generateUrl(`/apps/app_api/apps/enable/${appId}/${daemon.name}`), { deployOptions }) + .then((response) => { + this.setLoading(appId, false) + this.setLoading('install', false) + + const app = this.apps.find((app) => app.id === appId) + if (app) { + if (!app.installed) { + app.installed = true + app.needsDownload = false + app.daemon = daemon + app.status = { + type: 'install', + action: 'deploy', + init: 0, + deploy: 0, + } as IExAppStatus + } + app.active = true + app.canUnInstall = false + app.removable = true + app.error = '' + } + + this.updateAppsStatus() + + return axios.get(generateUrl('apps/files')) + .then(() => { + if (response.data.update_required) { + showInfo( + t('settings', 'The app has been enabled but needs to be updated.'), + { + onClick: () => window.location.reload(), + close: false, + }, + ) + setTimeout(() => { + location.reload() + }, 5000) + } + }) + .catch(() => { + this.setError(appId, t('settings', 'Error: This app cannot be enabled because it makes the server unstable')) + }) + }) + .catch((error) => { + this.setLoading(appId, false) + this.setLoading('install', false) + this.setError(appId, error.response.data.data.message) + this.appsApiFailure({ appId, error }) + }) + }).catch(() => { + this.setLoading(appId, false) + this.setLoading('install', false) + }) + }, + + forceEnableApp(appId: string) { + this.setLoading(appId, true) + this.setLoading('install', true) + return confirmPassword().then(() => { + + return api.post(generateUrl('/apps/app_api/apps/force'), { appId }) + .then(() => { + location.reload() + }) + .catch((error) => { + this.setLoading(appId, false) + this.setLoading('install', false) + this.setError(appId, error.response.data.data.message) + this.appsApiFailure({ appId, error }) + }) + }).catch(() => { + this.setLoading(appId, false) + this.setLoading('install', false) + }) + }, + + disableApp(appId: string) { + this.setLoading(appId, true) + return confirmPassword().then(() => { + + return api.get(generateUrl(`apps/app_api/apps/disable/${appId}`)) + .then(() => { + this.setLoading(appId, false) + const app = this.apps.find((app) => app.id === appId) + if (app) { + app.active = false + if (app.removable) { + app.canUnInstall = true + } + } + return true + }) + .catch((error) => { + this.setLoading(appId, false) + this.appsApiFailure({ appId, error }) + }) + }).catch(() => { + this.setLoading(appId, false) + }) + }, + + uninstallApp(appId: string, removeData: boolean) { + this.setLoading(appId, true) + return confirmPassword().then(() => { + + return api.get(generateUrl(`/apps/app_api/apps/uninstall/${appId}?removeData=${removeData}`)) + .then(() => { + this.setLoading(appId, false) + const app = this.apps.find((app) => app.id === appId) + if (app) { + app.active = false + app.needsDownload = true + app.installed = false + app.canUnInstall = false + app.canInstall = true + app.daemon = null + app.status = {} + if (app.update !== null) { + this.updateCount-- + } + app.update = undefined + } + return true + }) + .catch((error) => { + this.setLoading(appId, false) + this.appsApiFailure({ appId, error }) + }) + }) + }, + + updateApp(appId: string) { + this.setLoading(appId, true) + this.setLoading('install', true) + return confirmPassword().then(() => { + + return api.get(generateUrl(`/apps/app_api/apps/update/${appId}`)) + .then(() => { + this.setLoading(appId, false) + this.setLoading('install', false) + const app = this.apps.find((app) => app.id === appId) + if (app) { + const version = app.update + app.update = undefined + app.version = version || app.version + app.status = { + type: 'update', + action: 'deploy', + init: 0, + deploy: 0, + } as IExAppStatus + app.error = '' + } + this.updateCount-- + this.updateAppsStatus() + return true + }) + .catch((error) => { + this.setLoading(appId, false) + this.setLoading('install', false) + this.appsApiFailure({ appId, error }) + }) + }).catch(() => { + this.setLoading(appId, false) + this.setLoading('install', false) + }) + }, + + async fetchAllApps() { + this.loadingList = true + try { + const response = await api.get(generateUrl('/apps/app_api/apps/list')) + this.apps = response.data.apps + this.loadingList = false + return true + } catch (error) { + logger.error(error as string) + showError(t('settings', 'An error occurred during the request. Unable to proceed.')) + this.loadingList = false + } + }, + + async fetchAppStatus(appId: string) { + return api.get(generateUrl(`/apps/app_api/apps/status/${appId}`)) + .then((response) => { + const app = this.apps.find((app) => app.id === appId) + if (app) { + app.status = response.data + } + const initializingOrDeployingApps = this.getInitializingOrDeployingApps + console.debug('initializingOrDeployingApps after setAppStatus', initializingOrDeployingApps) + if (initializingOrDeployingApps.length === 0) { + console.debug('clearing interval') + clearInterval(this.statusUpdater as number) + this.statusUpdater = null + } + if (Object.hasOwn(response.data, 'error') + && response.data.error !== '' + && initializingOrDeployingApps.length === 1) { + clearInterval(this.statusUpdater as number) + this.statusUpdater = null + } + }) + .catch((error) => { + this.appsApiFailure({ appId, error }) + this.apps = this.apps.filter((app) => app.id !== appId) + this.updateAppsStatus() + }) + }, + + async fetchDockerDaemons() { + try { + const { data } = await axios.get(generateUrl('/apps/app_api/daemons')) + this.defaultDaemon = data.daemons.find((daemon: IDeployDaemon) => daemon.name === data.default_daemon_config) + this.dockerDaemons = data.daemons.filter((daemon: IDeployDaemon) => daemon.accepts_deploy_id === 'docker-install') + } catch (error) { + logger.error('[app-api-store] Failed to fetch Docker daemons', { error }) + return false + } + return true + }, + + updateAppsStatus() { + clearInterval(this.statusUpdater as number) + const initializingOrDeployingApps = this.getInitializingOrDeployingApps + if (initializingOrDeployingApps.length === 0) { + return + } + this.statusUpdater = setInterval(() => { + const initializingOrDeployingApps = this.getInitializingOrDeployingApps + console.debug('initializingOrDeployingApps', initializingOrDeployingApps) + initializingOrDeployingApps.forEach(app => { + this.fetchAppStatus(app.id) + }) + }, 2000) as unknown as number + }, + }, +}) |