Browse Source

feat(settings): Deploy daemon selection support during ExApp installation

Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
pull/53756/head
Andrey Borysenko 3 months ago
parent
commit
71ef47e70b
No known key found for this signature in database GPG Key ID: 934CB29F9F59B0D1
  1. 1
      apps/settings/src/app-types.ts
  2. 41
      apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue
  3. 77
      apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue
  4. 77
      apps/settings/src/components/AppAPI/DaemonSelectionList.vue
  5. 26
      apps/settings/src/components/AppList/AppItem.vue
  6. 12
      apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue
  7. 36
      apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue
  8. 4
      apps/settings/src/mixins/AppManagement.js
  9. 20
      apps/settings/src/store/app-api-store.ts
  10. 4
      dist/8737-8737.js
  11. 4
      dist/8737-8737.js.license
  12. 2
      dist/8737-8737.js.map
  13. 4
      dist/settings-apps-view-4529.js
  14. 4
      dist/settings-apps-view-4529.js.license
  15. 2
      dist/settings-apps-view-4529.js.map
  16. 4
      dist/settings-vue-settings-apps-users-management.js
  17. 2
      dist/settings-vue-settings-apps-users-management.js.map

1
apps/settings/src/app-types.ts

@ -75,6 +75,7 @@ export interface IDeployDaemon {
id: number,
name: string,
protocol: string,
exAppsCount: number,
}
export interface IExAppStatus {

41
apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue

@ -0,0 +1,41 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcDialog :open="show"
:name="t('settings', 'Choose Deploy Daemon for {appName}', {appName: app.name })"
size="normal"
@update:open="closeModal">
<DaemonSelectionList :app="app"
:deploy-options="deployOptions"
@close="closeModal" />
</NcDialog>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import DaemonSelectionList from './DaemonSelectionList.vue'
defineProps({
show: {
type: Boolean,
required: true,
},
app: {
type: Object,
required: true,
},
deployOptions: {
type: Object,
required: false,
default: () => ({}),
},
})
const emit = defineEmits(['update:show'])
const closeModal = () => {
emit('update:show', false)
}
</script>

77
apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue

@ -0,0 +1,77 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcListItem :name="itemTitle"
:details="isDefault ? t('settings', 'Default') : ''"
:force-display-actions="true"
:counter-number="daemon.exAppsCount"
:active="isDefault"
counter-type="highlighted"
@click.stop="selectDaemonAndInstall">
<template #subname>
{{ daemon.accepts_deploy_id }}
</template>
</NcListItem>
</template>
<script>
import NcListItem from '@nextcloud/vue/components/NcListItem'
import AppManagement from '../../mixins/AppManagement.js'
import { useAppsStore } from '../../store/apps-store'
import { useAppApiStore } from '../../store/app-api-store'
export default {
name: 'DaemonSelectionEntry',
components: {
NcListItem,
},
mixins: [AppManagement], // TODO: Convert to Composition API when AppManagement is refactored
props: {
daemon: {
type: Object,
required: true,
},
isDefault: {
type: Boolean,
required: true,
},
app: {
type: Object,
required: true,
},
deployOptions: {
type: Object,
required: false,
default: () => ({}),
},
},
setup() {
const store = useAppsStore()
const appApiStore = useAppApiStore()
return {
store,
appApiStore,
}
},
computed: {
itemTitle() {
return this.daemon.name + ' - ' + this.daemon.display_name
},
daemons() {
return this.appApiStore.dockerDaemons
},
},
methods: {
closeModal() {
this.$emit('close')
},
selectDaemonAndInstall() {
this.closeModal()
this.enable(this.app.id, this.daemon, this.deployOptions)
},
},
}
</script>

77
apps/settings/src/components/AppAPI/DaemonSelectionList.vue

@ -0,0 +1,77 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="daemon-selection-list">
<ul v-if="dockerDaemons.length > 0"
:aria-label="t('settings', 'Registered Deploy daemons list')">
<DaemonSelectionEntry v-for="daemon in dockerDaemons"
:key="daemon.id"
:daemon="daemon"
:is-default="defaultDaemon.name === daemon.name"
:app="app"
:deploy-options="deployOptions"
@close="closeModal" />
</ul>
<NcEmptyContent v-else
class="daemon-selection-list__empty-content"
:name="t('settings', 'No Deploy daemons configured')"
:description="t('settings', 'Register a custom one or setup from available templates')">
<template #icon>
<FormatListBullet :size="20" />
</template>
<template #action>
<NcButton :href="appApiAdminPage">
{{ t('settings', 'Manage Deploy daemons') }}
</NcButton>
</template>
</NcEmptyContent>
</div>
</template>
<script setup>
import { computed, defineProps } from 'vue'
import { generateUrl } from '@nextcloud/router'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcButton from '@nextcloud/vue/components/NcButton'
import FormatListBullet from 'vue-material-design-icons/FormatListBulleted.vue'
import DaemonSelectionEntry from './DaemonSelectionEntry.vue'
import { useAppApiStore } from '../../store/app-api-store.ts'
defineProps({
app: {
type: Object,
required: true,
},
deployOptions: {
type: Object,
required: false,
default: () => ({}),
},
})
const appApiStore = useAppApiStore()
const dockerDaemons = computed(() => appApiStore.dockerDaemons)
const defaultDaemon = computed(() => appApiStore.defaultDaemon)
const appApiAdminPage = computed(() => generateUrl('/settings/admin/app_api'))
const emit = defineEmits(['close'])
const closeModal = () => {
emit('close')
}
</script>
<style scoped lang="scss">
.daemon-selection-list {
max-height: 350px;
overflow-y: scroll;
padding: 2rem;
&__empty-content {
margin-top: 0;
text-align: center;
}
}
</style>

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

@ -100,7 +100,7 @@
:aria-label="enableButtonTooltip"
type="primary"
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
@click.stop="enable(app.id)">
@click.stop="enableButtonAction">
{{ enableButtonText }}
</NcButton>
<NcButton v-else-if="!app.active"
@ -111,6 +111,10 @@
@click.stop="forceEnable(app.id)">
{{ forceEnableButtonText }}
</NcButton>
<DaemonSelectionDialog v-if="app?.app_api && showSelectDaemonModal"
:show.sync="showSelectDaemonModal"
:app="app" />
</component>
</component>
</template>
@ -126,6 +130,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { mdiCogOutline } from '@mdi/js'
import { useAppApiStore } from '../../store/app-api-store.ts'
import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
export default {
name: 'AppItem',
@ -134,6 +139,7 @@ export default {
AppScore,
NcButton,
NcIconSvgWrapper,
DaemonSelectionDialog,
},
mixins: [AppManagement, SvgFilterMixin],
props: {
@ -177,6 +183,7 @@ export default {
isSelected: false,
scrolled: false,
screenshotLoaded: false,
showSelectDaemonModal: false,
}
},
computed: {
@ -219,6 +226,23 @@ export default {
getDataItemHeaders(columnName) {
return this.useBundleView ? [this.headers, columnName].join(' ') : null
},
showSelectionModal() {
this.showSelectDaemonModal = true
},
async enableButtonAction() {
if (!this.app?.app_api) {
this.enable(this.app.id)
return
}
await this.appApiStore.fetchDockerDaemons()
if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
this.enable(this.app.id, this.appApiStore.dockerDaemons[0])
} else if (this.app.needsDownload) {
this.showSelectionModal()
} else {
this.enable(this.app.id, this.app.daemon)
}
},
},
}
</script>

12
apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue

@ -152,6 +152,7 @@ import { computed, ref } from 'vue'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
import { emit } from '@nextcloud/event-bus'
import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcTextField from '@nextcloud/vue/components/NcTextField'
@ -277,8 +278,15 @@ export default {
this.configuredDeployOptions = null
})
},
submitDeployOptions() {
this.enable(this.app.id, this.deployOptions)
async submitDeployOptions() {
await this.appApiStore.fetchDockerDaemons()
if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
this.enable(this.app.id, this.appApiStore.dockerDaemons[0], this.deployOptions)
} else if (this.app.needsDownload) {
emit('showDaemonSelectionModal', this.deployOptions)
} else {
this.enable(this.app.id, this.app.daemon, this.deployOptions)
}
this.$emit('update:show', false)
},
},

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

@ -68,7 +68,7 @@
type="button"
:value="enableButtonText"
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
@click="enable(app.id)">
@click="enableButtonAction">
<input v-else-if="!app.active && !app.canInstall"
:title="forceEnableButtonTooltip"
:aria-label="forceEnableButtonTooltip"
@ -195,11 +195,16 @@
<AppDeployOptionsModal v-if="app?.app_api"
:show.sync="showDeployOptionsModal"
:app="app" />
<DaemonSelectionDialog v-if="app?.app_api"
:show.sync="showSelectDaemonModal"
:app="app"
:deploy-options="deployOptions" />
</div>
</NcAppSidebarTab>
</template>
<script>
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
@ -207,6 +212,7 @@ import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import AppDeployOptionsModal from './AppDeployOptionsModal.vue'
import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
import AppManagement from '../../mixins/AppManagement.js'
import { mdiBugOutline, mdiFeatureSearchOutline, mdiStar, mdiTextBoxOutline, mdiTooltipQuestionOutline, mdiToyBrickPlusOutline } from '@mdi/js'
@ -224,6 +230,7 @@ export default {
NcSelect,
NcCheckboxRadioSwitch,
AppDeployOptionsModal,
DaemonSelectionDialog,
},
mixins: [AppManagement],
@ -256,6 +263,8 @@ export default {
groupCheckedAppsData: false,
removeData: false,
showDeployOptionsModal: false,
showSelectDaemonModal: false,
deployOptions: null,
}
},
@ -365,15 +374,40 @@ export default {
this.removeData = false
},
},
beforeUnmount() {
this.deployOptions = null
unsubscribe('showDaemonSelectionModal')
},
mounted() {
if (this.app.groups.length > 0) {
this.groupCheckedAppsData = true
}
subscribe('showDaemonSelectionModal', (deployOptions) => {
this.showSelectionModal(deployOptions)
})
},
methods: {
toggleRemoveData() {
this.removeData = !this.removeData
},
showSelectionModal(deployOptions = null) {
this.deployOptions = deployOptions
this.showSelectDaemonModal = true
},
async enableButtonAction() {
if (!this.app?.app_api) {
this.enable(this.app.id)
return
}
await this.appApiStore.fetchDockerDaemons()
if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
this.enable(this.app.id, this.appApiStore.dockerDaemons[0])
} else if (this.app.needsDownload) {
this.showSelectionModal()
} else {
this.enable(this.app.id, this.app.daemon)
}
},
},
}
</script>

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

@ -188,9 +188,9 @@ export default {
.catch((error) => { showError(error) })
}
},
enable(appId, deployOptions = []) {
enable(appId, daemon = null, deployOptions = {}) {
if (this.app?.app_api) {
this.appApiStore.enableApp(appId, deployOptions)
this.appApiStore.enableApp(appId, daemon, deployOptions)
.then(() => { rebuildNavigation() })
.catch((error) => { showError(error) })
} else {

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

@ -25,6 +25,7 @@ interface AppApiState {
statusUpdater: number | null | undefined
daemonAccessible: boolean
defaultDaemon: IDeployDaemon | null
dockerDaemons: IDeployDaemon[]
}
export const useAppApiStore = defineStore('app-api-apps', {
@ -36,6 +37,7 @@ export const useAppApiStore = defineStore('app-api-apps', {
statusUpdater: null,
daemonAccessible: loadState('settings', 'defaultDaemonConfigAccessible', false),
defaultDaemon: loadState('settings', 'defaultDaemonConfig', null),
dockerDaemons: [],
}),
getters: {
@ -76,12 +78,12 @@ export const useAppApiStore = defineStore('app-api-apps', {
})
},
enableApp(appId: string, deployOptions: IDeployOptions[] = []) {
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}`), { deployOptions })
return axios.post(generateUrl(`/apps/app_api/apps/enable/${appId}/${daemon.name}`), { deployOptions })
.then((response) => {
this.setLoading(appId, false)
this.setLoading('install', false)
@ -91,7 +93,7 @@ export const useAppApiStore = defineStore('app-api-apps', {
if (!app.installed) {
app.installed = true
app.needsDownload = false
app.daemon = this.defaultDaemon
app.daemon = daemon
app.status = {
type: 'install',
action: 'deploy',
@ -293,6 +295,18 @@ export const useAppApiStore = defineStore('app-api-apps', {
})
},
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

4
dist/8737-8737.js
File diff suppressed because it is too large
View File

4
dist/8737-8737.js.license

@ -12,6 +12,7 @@ SPDX-FileCopyrightText: Varun A P
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Rob Cresswell <robcresswell@pm.me>
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Matt Zabriskie
SPDX-FileCopyrightText: Joyent
@ -139,6 +140,9 @@ This file is generated from multiple sources. Included packages:
- vue-loader
- version: 15.11.1
- license: MIT
- vue-material-design-icons
- version: 5.3.1
- license: MIT
- vue-router
- version: 3.6.5
- license: MIT

2
dist/8737-8737.js.map
File diff suppressed because it is too large
View File

4
dist/settings-apps-view-4529.js
File diff suppressed because it is too large
View File

4
dist/settings-apps-view-4529.js.license

@ -17,6 +17,7 @@ SPDX-FileCopyrightText: Thorsten Lünborg
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Roeland Jago Douma
SPDX-FileCopyrightText: Rob Cresswell <robcresswell@pm.me>
SPDX-FileCopyrightText: Paul Vorbach <paul@vorba.ch> (http://paul.vorba.ch)
SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de)
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
@ -235,6 +236,9 @@ This file is generated from multiple sources. Included packages:
- vue-loader
- version: 15.11.1
- license: MIT
- vue-material-design-icons
- version: 5.3.1
- license: MIT
- vue-router
- version: 3.6.5
- license: MIT

2
dist/settings-apps-view-4529.js.map
File diff suppressed because it is too large
View File

4
dist/settings-vue-settings-apps-users-management.js
File diff suppressed because it is too large
View File

2
dist/settings-vue-settings-apps-users-management.js.map
File diff suppressed because it is too large
View File

Loading…
Cancel
Save