Merge pull request #53756 from nextcloud/feat/settings/app_api/daemon-selection

feat(settings): Deploy daemon selection support during ExApp installation
This commit is contained in:
Daniel 2025-08-13 22:57:13 +02:00 committed by GitHub
commit 9dd661f3d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 302 additions and 18 deletions

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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)
},
},

View file

@ -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>

View file

@ -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 {

View file

@ -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 vendored

File diff suppressed because one or more lines are too long

View file

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long