Skip to content

Commit 9dd661f

Browse files
authored
Merge pull request #53756 from nextcloud/feat/settings/app_api/daemon-selection
feat(settings): Deploy daemon selection support during ExApp installation
2 parents d658b9b + 71ef47e commit 9dd661f

17 files changed

+302
-18
lines changed

apps/settings/src/app-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export interface IDeployDaemon {
7575
id: number,
7676
name: string,
7777
protocol: string,
78+
exAppsCount: number,
7879
}
7980

8081
export interface IExAppStatus {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<NcDialog :open="show"
7+
:name="t('settings', 'Choose Deploy Daemon for {appName}', {appName: app.name })"
8+
size="normal"
9+
@update:open="closeModal">
10+
<DaemonSelectionList :app="app"
11+
:deploy-options="deployOptions"
12+
@close="closeModal" />
13+
</NcDialog>
14+
</template>
15+
16+
<script setup>
17+
import { defineProps, defineEmits } from 'vue'
18+
import NcDialog from '@nextcloud/vue/components/NcDialog'
19+
import DaemonSelectionList from './DaemonSelectionList.vue'
20+
21+
defineProps({
22+
show: {
23+
type: Boolean,
24+
required: true,
25+
},
26+
app: {
27+
type: Object,
28+
required: true,
29+
},
30+
deployOptions: {
31+
type: Object,
32+
required: false,
33+
default: () => ({}),
34+
},
35+
})
36+
37+
const emit = defineEmits(['update:show'])
38+
const closeModal = () => {
39+
emit('update:show', false)
40+
}
41+
</script>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<NcListItem :name="itemTitle"
7+
:details="isDefault ? t('settings', 'Default') : ''"
8+
:force-display-actions="true"
9+
:counter-number="daemon.exAppsCount"
10+
:active="isDefault"
11+
counter-type="highlighted"
12+
@click.stop="selectDaemonAndInstall">
13+
<template #subname>
14+
{{ daemon.accepts_deploy_id }}
15+
</template>
16+
</NcListItem>
17+
</template>
18+
19+
<script>
20+
import NcListItem from '@nextcloud/vue/components/NcListItem'
21+
import AppManagement from '../../mixins/AppManagement.js'
22+
import { useAppsStore } from '../../store/apps-store'
23+
import { useAppApiStore } from '../../store/app-api-store'
24+
25+
export default {
26+
name: 'DaemonSelectionEntry',
27+
components: {
28+
NcListItem,
29+
},
30+
mixins: [AppManagement], // TODO: Convert to Composition API when AppManagement is refactored
31+
props: {
32+
daemon: {
33+
type: Object,
34+
required: true,
35+
},
36+
isDefault: {
37+
type: Boolean,
38+
required: true,
39+
},
40+
app: {
41+
type: Object,
42+
required: true,
43+
},
44+
deployOptions: {
45+
type: Object,
46+
required: false,
47+
default: () => ({}),
48+
},
49+
},
50+
setup() {
51+
const store = useAppsStore()
52+
const appApiStore = useAppApiStore()
53+
54+
return {
55+
store,
56+
appApiStore,
57+
}
58+
},
59+
computed: {
60+
itemTitle() {
61+
return this.daemon.name + ' - ' + this.daemon.display_name
62+
},
63+
daemons() {
64+
return this.appApiStore.dockerDaemons
65+
},
66+
},
67+
methods: {
68+
closeModal() {
69+
this.$emit('close')
70+
},
71+
selectDaemonAndInstall() {
72+
this.closeModal()
73+
this.enable(this.app.id, this.daemon, this.deployOptions)
74+
},
75+
},
76+
}
77+
</script>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<div class="daemon-selection-list">
7+
<ul v-if="dockerDaemons.length > 0"
8+
:aria-label="t('settings', 'Registered Deploy daemons list')">
9+
<DaemonSelectionEntry v-for="daemon in dockerDaemons"
10+
:key="daemon.id"
11+
:daemon="daemon"
12+
:is-default="defaultDaemon.name === daemon.name"
13+
:app="app"
14+
:deploy-options="deployOptions"
15+
@close="closeModal" />
16+
</ul>
17+
<NcEmptyContent v-else
18+
class="daemon-selection-list__empty-content"
19+
:name="t('settings', 'No Deploy daemons configured')"
20+
:description="t('settings', 'Register a custom one or setup from available templates')">
21+
<template #icon>
22+
<FormatListBullet :size="20" />
23+
</template>
24+
<template #action>
25+
<NcButton :href="appApiAdminPage">
26+
{{ t('settings', 'Manage Deploy daemons') }}
27+
</NcButton>
28+
</template>
29+
</NcEmptyContent>
30+
</div>
31+
</template>
32+
33+
<script setup>
34+
import { computed, defineProps } from 'vue'
35+
import { generateUrl } from '@nextcloud/router'
36+
37+
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
38+
import NcButton from '@nextcloud/vue/components/NcButton'
39+
import FormatListBullet from 'vue-material-design-icons/FormatListBulleted.vue'
40+
import DaemonSelectionEntry from './DaemonSelectionEntry.vue'
41+
import { useAppApiStore } from '../../store/app-api-store.ts'
42+
43+
defineProps({
44+
app: {
45+
type: Object,
46+
required: true,
47+
},
48+
deployOptions: {
49+
type: Object,
50+
required: false,
51+
default: () => ({}),
52+
},
53+
})
54+
55+
const appApiStore = useAppApiStore()
56+
57+
const dockerDaemons = computed(() => appApiStore.dockerDaemons)
58+
const defaultDaemon = computed(() => appApiStore.defaultDaemon)
59+
const appApiAdminPage = computed(() => generateUrl('/settings/admin/app_api'))
60+
const emit = defineEmits(['close'])
61+
const closeModal = () => {
62+
emit('close')
63+
}
64+
</script>
65+
66+
<style scoped lang="scss">
67+
.daemon-selection-list {
68+
max-height: 350px;
69+
overflow-y: scroll;
70+
padding: 2rem;
71+
72+
&__empty-content {
73+
margin-top: 0;
74+
text-align: center;
75+
}
76+
}
77+
</style>

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
:aria-label="enableButtonTooltip"
101101
type="primary"
102102
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
103-
@click.stop="enable(app.id)">
103+
@click.stop="enableButtonAction">
104104
{{ enableButtonText }}
105105
</NcButton>
106106
<NcButton v-else-if="!app.active"
@@ -111,6 +111,10 @@
111111
@click.stop="forceEnable(app.id)">
112112
{{ forceEnableButtonText }}
113113
</NcButton>
114+
115+
<DaemonSelectionDialog v-if="app?.app_api && showSelectDaemonModal"
116+
:show.sync="showSelectDaemonModal"
117+
:app="app" />
114118
</component>
115119
</component>
116120
</template>
@@ -126,6 +130,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
126130
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
127131
import { mdiCogOutline } from '@mdi/js'
128132
import { useAppApiStore } from '../../store/app-api-store.ts'
133+
import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
129134
130135
export default {
131136
name: 'AppItem',
@@ -134,6 +139,7 @@ export default {
134139
AppScore,
135140
NcButton,
136141
NcIconSvgWrapper,
142+
DaemonSelectionDialog,
137143
},
138144
mixins: [AppManagement, SvgFilterMixin],
139145
props: {
@@ -177,6 +183,7 @@ export default {
177183
isSelected: false,
178184
scrolled: false,
179185
screenshotLoaded: false,
186+
showSelectDaemonModal: false,
180187
}
181188
},
182189
computed: {
@@ -219,6 +226,23 @@ export default {
219226
getDataItemHeaders(columnName) {
220227
return this.useBundleView ? [this.headers, columnName].join(' ') : null
221228
},
229+
showSelectionModal() {
230+
this.showSelectDaemonModal = true
231+
},
232+
async enableButtonAction() {
233+
if (!this.app?.app_api) {
234+
this.enable(this.app.id)
235+
return
236+
}
237+
await this.appApiStore.fetchDockerDaemons()
238+
if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
239+
this.enable(this.app.id, this.appApiStore.dockerDaemons[0])
240+
} else if (this.app.needsDownload) {
241+
this.showSelectionModal()
242+
} else {
243+
this.enable(this.app.id, this.app.daemon)
244+
}
245+
},
222246
},
223247
}
224248
</script>

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ import { computed, ref } from 'vue'
152152
import axios from '@nextcloud/axios'
153153
import { generateUrl } from '@nextcloud/router'
154154
import { loadState } from '@nextcloud/initial-state'
155+
import { emit } from '@nextcloud/event-bus'
155156
156157
import NcDialog from '@nextcloud/vue/components/NcDialog'
157158
import NcTextField from '@nextcloud/vue/components/NcTextField'
@@ -277,8 +278,15 @@ export default {
277278
this.configuredDeployOptions = null
278279
})
279280
},
280-
submitDeployOptions() {
281-
this.enable(this.app.id, this.deployOptions)
281+
async submitDeployOptions() {
282+
await this.appApiStore.fetchDockerDaemons()
283+
if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
284+
this.enable(this.app.id, this.appApiStore.dockerDaemons[0], this.deployOptions)
285+
} else if (this.app.needsDownload) {
286+
emit('showDaemonSelectionModal', this.deployOptions)
287+
} else {
288+
this.enable(this.app.id, this.app.daemon, this.deployOptions)
289+
}
282290
this.$emit('update:show', false)
283291
},
284292
},

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
type="button"
6969
:value="enableButtonText"
7070
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
71-
@click="enable(app.id)">
71+
@click="enableButtonAction">
7272
<input v-else-if="!app.active && !app.canInstall"
7373
:title="forceEnableButtonTooltip"
7474
:aria-label="forceEnableButtonTooltip"
@@ -195,18 +195,24 @@
195195
<AppDeployOptionsModal v-if="app?.app_api"
196196
:show.sync="showDeployOptionsModal"
197197
:app="app" />
198+
<DaemonSelectionDialog v-if="app?.app_api"
199+
:show.sync="showSelectDaemonModal"
200+
:app="app"
201+
:deploy-options="deployOptions" />
198202
</div>
199203
</NcAppSidebarTab>
200204
</template>
201205

202206
<script>
207+
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
203208
import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
204209
import NcButton from '@nextcloud/vue/components/NcButton'
205210
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
206211
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
207212
import NcSelect from '@nextcloud/vue/components/NcSelect'
208213
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
209214
import AppDeployOptionsModal from './AppDeployOptionsModal.vue'
215+
import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
210216
211217
import AppManagement from '../../mixins/AppManagement.js'
212218
import { mdiBugOutline, mdiFeatureSearchOutline, mdiStar, mdiTextBoxOutline, mdiTooltipQuestionOutline, mdiToyBrickPlusOutline } from '@mdi/js'
@@ -224,6 +230,7 @@ export default {
224230
NcSelect,
225231
NcCheckboxRadioSwitch,
226232
AppDeployOptionsModal,
233+
DaemonSelectionDialog,
227234
},
228235
mixins: [AppManagement],
229236
@@ -256,6 +263,8 @@ export default {
256263
groupCheckedAppsData: false,
257264
removeData: false,
258265
showDeployOptionsModal: false,
266+
showSelectDaemonModal: false,
267+
deployOptions: null,
259268
}
260269
},
261270
@@ -365,15 +374,40 @@ export default {
365374
this.removeData = false
366375
},
367376
},
377+
beforeUnmount() {
378+
this.deployOptions = null
379+
unsubscribe('showDaemonSelectionModal')
380+
},
368381
mounted() {
369382
if (this.app.groups.length > 0) {
370383
this.groupCheckedAppsData = true
371384
}
385+
subscribe('showDaemonSelectionModal', (deployOptions) => {
386+
this.showSelectionModal(deployOptions)
387+
})
372388
},
373389
methods: {
374390
toggleRemoveData() {
375391
this.removeData = !this.removeData
376392
},
393+
showSelectionModal(deployOptions = null) {
394+
this.deployOptions = deployOptions
395+
this.showSelectDaemonModal = true
396+
},
397+
async enableButtonAction() {
398+
if (!this.app?.app_api) {
399+
this.enable(this.app.id)
400+
return
401+
}
402+
await this.appApiStore.fetchDockerDaemons()
403+
if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
404+
this.enable(this.app.id, this.appApiStore.dockerDaemons[0])
405+
} else if (this.app.needsDownload) {
406+
this.showSelectionModal()
407+
} else {
408+
this.enable(this.app.id, this.app.daemon)
409+
}
410+
},
377411
},
378412
}
379413
</script>

0 commit comments

Comments
 (0)