Browse Source

WIP: migrate to Pinia, minor fixes

Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
pull/48665/head
Andrey Borysenko 1 year ago
parent
commit
c8b35fa22f
No known key found for this signature in database GPG Key ID: 934CB29F9F59B0D1
  1. 12
      apps/settings/src/components/AppList.vue
  2. 6
      apps/settings/src/components/AppList/AppItem.vue
  3. 8
      apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue
  4. 14
      apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue
  5. 6
      apps/settings/src/composables/useAppIcon.ts
  6. 104
      apps/settings/src/mixins/AppManagement.js
  7. 295
      apps/settings/src/store/app-api-store.ts
  8. 2
      apps/settings/src/store/index.js
  9. 11
      apps/settings/src/views/AppStore.vue
  10. 4
      apps/settings/src/views/AppStoreSidebar.vue

12
apps/settings/src/components/AppList.vue

@ -143,6 +143,7 @@ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import AppItem from './AppList/AppItem.vue'
import pLimit from 'p-limit'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import { useAppApiStore } from '../store/app-api-store'
export default {
name: 'AppList',
@ -158,6 +159,13 @@ export default {
},
},
setup() {
const appApiStore = useAppApiStore()
return {
appApiStore,
}
},
data() {
return {
search: '',
@ -171,7 +179,7 @@ export default {
if (!this.$store.getters['appApiApps/isAppApiEnabled']) {
return this.$store.getters.loading('list')
}
return this.$store.getters.loading('list') || this.$store.getters['appApiApps/loading']('list')
return this.$store.getters.loading('list') || this.appApiStore.getLoading('list')
},
hasPendingUpdate() {
return this.apps.filter(app => app.update).length > 0
@ -181,7 +189,7 @@ export default {
},
apps() {
// Exclude ExApps from the list if AppAPI is disabled
const exApps = this.$store.getters.isAppApiEnabled ? this.$store.getters['appApiApps/getAllApps'] : []
const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : []
const apps = [...this.$store.getters.getAllApps, ...exApps]
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
.sort(function(a, b) {

6
apps/settings/src/components/AppList/AppItem.vue

@ -83,7 +83,7 @@
@click.stop="update(app.id)">
{{ t('settings', 'Update to {update}', {update:app.update}) }}
</NcButton>
<NcButton v-if="app.canUnInstall"
<NcButton v-if="app.canUnInstall || app.canUninstall"
class="uninstall"
type="tertiary"
:disabled="installing || isLoading"
@ -91,7 +91,7 @@
{{ t('settings', 'Remove') }}
</NcButton>
<NcButton v-if="app.active"
:disabled="installing || isLoading || isInitializing || isDeploying"
:disabled="installing || isLoading || isInitializing || isDeploying"
@click.stop="disable(app.id)">
{{ disableButtonText }}
</NcButton>
@ -184,7 +184,7 @@ export default {
return !!this.$route.params.id
},
shouldDisplayDefaultIcon() {
return this.listView && !this.app.preview || !this.listView && !this.screenshotLoaded
return (this.listView && !this.app.preview) || (!this.listView && !this.screenshotLoaded)
},
},
watch: {

8
apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue

@ -16,7 +16,8 @@
<p><b>{{ t('settings', 'Type') }}</b>: {{ app?.daemon.accepts_deploy_id }}</p>
<p><b>{{ t('settings', 'Name') }}</b>: {{ app?.daemon.name }}</p>
<p><b>{{ t('settings', 'Display Name') }}</b>: {{ app?.daemon.display_name }}</p>
<p><b>{{ t('settings', 'GPUs support') }}</b>: {{ app?.daemon.deploy_config?.computeDevice?.id !== 'cpu' || 'false' }}</p>
<p><b>{{ t('settings', 'GPUs support') }}</b>: {{ gpuSupport }}</p>
<p><b>{{ t('settings', 'Compute device') }}</b>: {{ app?.daemon?.deploy_config?.computeDevice?.label }}</p>
</div>
</NcAppSidebarTab>
</template>
@ -28,10 +29,13 @@ import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import { mdiFileChart } from '@mdi/js'
import { ref } from 'vue'
defineProps<{
const props = defineProps<{
app: IAppstoreExApp,
}>()
const gpuSupport = ref(props.app?.daemon?.deploy_config?.computeDevice?.id !== 'cpu' || false)
</script>
<style scoped lang="scss">

14
apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue

@ -49,7 +49,7 @@
:value="t('settings', 'Update to {version}', { version: app.update })"
:disabled="installing || isLoading || isManualInstall"
@click="update(app.id)">
<input v-if="app.canUnInstall"
<input v-if="app.canUnInstall || app.canUninstall"
class="uninstall"
type="button"
:value="t('settings', 'Remove')"
@ -78,7 +78,10 @@
:disabled="installing || isLoading"
@click="forceEnable(app.id)">
</div>
<NcCheckboxRadioSwitch v-if="app.canUnInstall"
<p v-if="!defaultDeployDaemonAccessible" class="warning">
{{ t('settings', 'Default Deploy daemon is not accessible') }}
</p>
<NcCheckboxRadioSwitch v-if="app.canUnInstall || app.canUninstall"
:checked="removeData"
:disabled="installing || isLoading || !defaultDeployDaemonAccessible"
@update:checked="toggleRemoveData">
@ -110,7 +113,7 @@
<NcDateTime :timestamp="lastModified" />
</div>
<div class="app-details__section">
<div v-if="appAuthors" class="app-details__section">
<h4>
{{ t('settings', 'Author') }}
</h4>
@ -119,7 +122,7 @@
</p>
</div>
<div class="app-details__section">
<div v-if="appCategories" class="app-details__section">
<h4>
{{ t('settings', 'Categories') }}
</h4>
@ -194,6 +197,7 @@ import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadi
import AppManagement from '../../mixins/AppManagement.js'
import { mdiBug, mdiFeatureSearch, mdiStar, mdiTextBox, mdiTooltipQuestion } from '@mdi/js'
import { useAppsStore } from '../../store/apps-store'
import { useAppApiStore } from '../../store/app-api-store'
export default {
name: 'AppDetailsTab',
@ -217,9 +221,11 @@ export default {
setup() {
const store = useAppsStore()
const appApiStore = useAppApiStore()
return {
store,
appApiStore,
mdiBug,
mdiFeatureSearch,

6
apps/settings/src/composables/useAppIcon.ts

@ -29,9 +29,9 @@ export function useAppIcon(app: Ref<IAppstoreApp>) {
path = mdiCogOutline
} else {
path = [app.value?.category ?? []].flat()
.map((name) => AppstoreCategoryIcons[name])
.filter((icon) => !!icon)
.at(0)
.map((name) => AppstoreCategoryIcons[name])
.filter((icon) => !!icon)
.at(0)
?? (!app.value?.app_api ? mdiCog : mdiCogOutline)
}
return path ? `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="${path}" /></svg>` : null

104
apps/settings/src/mixins/AppManagement.js

@ -5,33 +5,40 @@
import { showError } from '@nextcloud/dialogs'
import rebuildNavigation from '../service/rebuild-navigation.js'
import { useAppApiStore } from '../store/app-api-store'
export default {
setup() {
const appApiStore = useAppApiStore()
return {
appApiStore,
}
},
computed: {
appGroups() {
return this.app.groups.map(group => { return { id: group, name: group } })
},
installing() {
if (this.app?.app_api) {
return this.app && this.$store.getters['appApiApps/loading']('install')
return this.app && this?.appApiStore.getLoading('install') === true
}
return this.$store.getters.loading('install')
},
isLoading() {
if (this.app?.app_api) {
return this.app && this.$store.getters['appApiApps/loading'](this.app.id)
return this.app && this?.appApiStore.getLoading(this.app.id) === true
}
return this.app && this.$store.getters.loading(this.app.id)
},
isInitializing() {
if (this.app?.app_api) {
return this.app && Object.hasOwn(this.app?.status, 'action') && (this.app.status.action === 'init' || this.app.status.action === 'healthcheck')
return this.app && (this.app?.status?.action === 'init' || this.app?.status?.action === 'healthcheck')
}
return false
},
isDeploying() {
if (this.app?.app_api) {
return this.app && Object.hasOwn(this.app?.status, 'action') && this.app.status.action === 'deploy'
return this.app && this.app?.status?.action === 'deploy'
}
return false
},
@ -90,7 +97,7 @@ export default {
return t('settings', 'Allow untested app')
},
enableButtonTooltip() {
if (this.app.needsDownload) {
if (!this.app?.app_api && this.app.needsDownload) {
return t('settings', 'The app will be downloaded from the App Store')
}
return null
@ -107,10 +114,11 @@ export default {
if (this.app?.daemon && this.app?.daemon?.accepts_deploy_id === 'manual-install') {
return true
}
if (this.app?.daemon?.accepts_deploy_id === 'docker-install') {
return this.$store.getters['appApiApps/getDaemonAccessible'] === true
if (this.app?.daemon?.accepts_deploy_id === 'docker-install'
&& this.appApiStore.getDefaultDaemon?.name === this.app?.daemon?.name) {
return this?.appApiStore.getDaemonAccessible === true
}
return this.$store.getters['appApiApps/getDaemonAccessible']
return this?.appApiStore.getDaemonAccessible
}
return true
},
@ -177,63 +185,73 @@ export default {
this.$store.dispatch('enableApp', { appId: this.app.id, groups: currentGroups })
},
forceEnable(appId) {
let type = 'forceEnableApp'
if (this.app?.app_api) {
type = 'appApiApps/forceEnableApp'
this.appApiStore.forceEnableApp(appId)
.then(() => { rebuildNavigation() })
.catch((error) => { showError(error) })
} else {
this.$store.dispatch('forceEnableApp', { appId, groups: [] })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
}
this.$store.dispatch(type, { appId, groups: [] })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
},
enable(appId) {
let type = 'enableApp'
if (this.app?.app_api) {
type = 'appApiApps/enableApp'
this.appApiStore.enableApp(appId)
.then(() => { rebuildNavigation() })
.catch((error) => { showError(error) })
} else {
this.$store.dispatch('enableApp', { appId, groups: [] })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
}
this.$store.dispatch(type, { appId, groups: [] })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
},
disable(appId) {
let type = 'disableApp'
if (this.app?.app_api) {
type = 'appApiApps/disableApp'
this.appApiStore.disableApp(appId)
.then(() => { rebuildNavigation() })
.catch((error) => { showError(error) })
} else {
this.$store.dispatch('disableApp', { appId })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
}
this.$store.dispatch(type, { appId })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
},
remove(appId, removeData = false) {
let type = 'uninstallApp'
let payload = { appId }
if (this.app?.app_api) {
type = 'appApiApps/uninstallApp'
payload = { appId, removeData }
this.appApiStore.uninstallApp(appId, removeData)
.then(() => { rebuildNavigation() })
.catch((error) => { showError(error) })
} else {
this.$store.dispatch('appApiApps/uninstallApp', { appId, removeData })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
}
this.$store.dispatch(type, payload)
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
},
install(appId) {
let type = 'enableApp'
if (this.app?.app_api) {
type = 'appApiApps/enableApp'
this.appApiStore.enableApp(appId)
.then(() => { rebuildNavigation() })
.catch((error) => { showError(error) })
} else {
this.$store.dispatch('enableApp', { appId })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
}
this.$store.dispatch(type, { appId })
.then((response) => { rebuildNavigation() })
.catch((error) => { showError(error) })
},
update(appId) {
let type = 'updateApp'
if (this.app?.app_api) {
type = 'appApiApps/updateApp'
this.appApiStore.updateApp(appId)
.then(() => { rebuildNavigation() })
.catch((error) => { showError(error) })
} else {
this.$store.dispatch('updateApp', { appId })
.catch((error) => { showError(error) })
.then(() => {
rebuildNavigation()
this.store.updateCount = Math.max(this.store.updateCount - 1, 0)
})
}
this.$store.dispatch(type, { appId })
.catch((error) => { showError(error) })
.then(() => {
rebuildNavigation()
this.store.updateCount = Math.max(this.store.updateCount - 1, 0)
})
},
},
}

295
apps/settings/src/store/app-api-store.ts

@ -0,0 +1,295 @@
/**
* 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, IExAppStatus } from '../app-types'
import { reactive } from 'vue'
interface AppApiState {
apps: IAppstoreExApp[]
updateCount: number
loading: Record<string, boolean>
loadingList: boolean
statusUpdater: number | null | undefined
daemonAccessible: boolean
defaultDaemon: IDeployDaemon | null
}
export const useAppApiStore = defineStore('app-api-apps', {
state: (): AppApiState => ({
apps: [],
updateCount: loadState('settings', 'appstoreExAppUpdateCount', 0),
loading: reactive({}),
loadingList: false,
statusUpdater: null,
daemonAccessible: loadState('settings', 'defaultDaemonConfigAccessible', false),
defaultDaemon: loadState('settings', 'defaultDaemonConfig', null),
}),
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: {
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)
},
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) {
this.loading[appId] = true
this.loading.install = true
return confirmPassword().then(() => {
return axios.post(generateUrl(`/apps/app_api/apps/enable/${appId}`))
.then((response) => {
this.loading[appId] = false
this.loading.install = false
const app = this.apps.find((app) => app.id === appId)
if (app) {
if (!app.installed) {
app.installed = true
app.needsDownload = false
app.daemon = this.defaultDaemon
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.loading[appId] = false
this.loading.install = false
this.setError(appId, error.response.data.data.message)
this.appsApiFailure({ appId, error })
})
})
},
forceEnableApp(appId: string) {
this.loading[appId] = true
this.loading.install = true
return confirmPassword().then(() => {
return api.post(generateUrl('/apps/app_api/apps/force'), { appId })
.then(() => {
location.reload()
})
.catch((error) => {
this.loading[appId] = false
this.loading.install = false
this.setError(appId, error.response.data.data.message)
this.appsApiFailure({ appId, error })
})
})
},
disableApp(appId: string) {
this.loading[appId] = true
return confirmPassword().then(() => {
return api.get(generateUrl(`apps/app_api/apps/disable/${appId}`))
.then(() => {
this.loading[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.loading[appId] = false
this.appsApiFailure({ appId, error })
})
})
},
uninstallApp(appId: string, removeData: boolean) {
this.loading[appId] = true
return confirmPassword().then(() => {
return api.get(generateUrl(`/apps/app_api/apps/uninstall/${appId}?removeData=${removeData}`))
.then(() => {
this.loading[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 = null
}
return true
})
.catch((error) => {
this.loading[appId] = false
this.appsApiFailure({ appId, error })
})
})
},
updateApp(appId: string) {
this.loading[appId] = true
this.loading.install = true
return confirmPassword().then(() => {
return api.get(generateUrl(`/apps/app_api/apps/update/${appId}`))
.then(() => {
this.loading.install = false
this.loading[appId] = false
const app = this.apps.find((app) => app.id === appId)
if (app) {
const version = app.update
app.update = null
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.loading[appId] = false
this.loading.install = false
this.appsApiFailure({ appId, error })
})
})
},
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()
})
},
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
},
},
})

2
apps/settings/src/store/index.js

@ -7,7 +7,6 @@ import Vue from 'vue'
import Vuex, { Store } from 'vuex'
import users from './users.js'
import apps from './apps.js'
import appApiApps from './app_api_apps.js'
import settings from './users-settings.js'
import oc from './oc.js'
import { showError } from '@nextcloud/dialogs'
@ -36,7 +35,6 @@ export const useStore = () => {
modules: {
users,
apps,
appApiApps,
settings,
oc,
},

11
apps/settings/src/views/AppStore.vue

@ -34,9 +34,11 @@ import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import AppList from '../components/AppList.vue'
import AppStoreDiscoverSection from '../components/AppStoreDiscover/AppStoreDiscoverSection.vue'
import { useAppApiStore } from '../store/app-api-store.ts'
const route = useRoute()
const store = useAppsStore()
const appApiStore = useAppApiStore()
/**
* ID of the current active category, default is `discover`
@ -62,15 +64,12 @@ onBeforeMount(() => {
(instance?.proxy as any).$store.dispatch('getAllApps')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((instance?.proxy as any).$store.getters.isAppApiEnabled) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(instance?.proxy as any).$store.dispatch('appApiApps/getAllApps');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(instance?.proxy as any).$store.dispatch('appApiApps/updateAppsStatus')
appApiStore.fetchAllApps()
appApiStore.updateAppsStatus()
}
})
onBeforeUnmount(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
clearInterval((instance?.proxy as any).$store.getters('appApiApps/getStatusUpdater'))
clearInterval(appApiStore.getStatusUpdater)
})
</script>

4
apps/settings/src/views/AppStoreSidebar.vue

@ -56,16 +56,18 @@ import AppLevelBadge from '../components/AppList/AppLevelBadge.vue'
import AppDaemonBadge from '../components/AppList/AppDaemonBadge.vue'
import { useAppIcon } from '../composables/useAppIcon.ts'
import { useStore } from '../store'
import { useAppApiStore } from '../store/app-api-store.ts'
const route = useRoute()
const router = useRouter()
const store = useAppsStore()
const appApiStore = useAppApiStore()
const legacyStore = useStore()
const appId = computed(() => route.params.id ?? '')
const app = computed(() => {
if (legacyStore.getters.isAppApiEnabled) {
const exApp = legacyStore.getters['appApiApps/getAllApps']
const exApp = appApiStore.getAllApps
.find((app) => app.id === appId.value) ?? null
if (exApp) {
return exApp

Loading…
Cancel
Save