Browse Source

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

Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
pull/53756/head
Andrey Borysenko 4 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, id: number,
name: string, name: string,
protocol: string, protocol: string,
exAppsCount: number,
} }
export interface IExAppStatus { 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" :aria-label="enableButtonTooltip"
type="primary" type="primary"
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying" :disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
@click.stop="enable(app.id)">
@click.stop="enableButtonAction">
{{ enableButtonText }} {{ enableButtonText }}
</NcButton> </NcButton>
<NcButton v-else-if="!app.active" <NcButton v-else-if="!app.active"
@ -111,6 +111,10 @@
@click.stop="forceEnable(app.id)"> @click.stop="forceEnable(app.id)">
{{ forceEnableButtonText }} {{ forceEnableButtonText }}
</NcButton> </NcButton>
<DaemonSelectionDialog v-if="app?.app_api && showSelectDaemonModal"
:show.sync="showSelectDaemonModal"
:app="app" />
</component> </component>
</component> </component>
</template> </template>
@ -126,6 +130,7 @@ import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { mdiCogOutline } from '@mdi/js' import { mdiCogOutline } from '@mdi/js'
import { useAppApiStore } from '../../store/app-api-store.ts' import { useAppApiStore } from '../../store/app-api-store.ts'
import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
export default { export default {
name: 'AppItem', name: 'AppItem',
@ -134,6 +139,7 @@ export default {
AppScore, AppScore,
NcButton, NcButton,
NcIconSvgWrapper, NcIconSvgWrapper,
DaemonSelectionDialog,
}, },
mixins: [AppManagement, SvgFilterMixin], mixins: [AppManagement, SvgFilterMixin],
props: { props: {
@ -177,6 +183,7 @@ export default {
isSelected: false, isSelected: false,
scrolled: false, scrolled: false,
screenshotLoaded: false, screenshotLoaded: false,
showSelectDaemonModal: false,
} }
}, },
computed: { computed: {
@ -219,6 +226,23 @@ export default {
getDataItemHeaders(columnName) { getDataItemHeaders(columnName) {
return this.useBundleView ? [this.headers, columnName].join(' ') : null 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> </script>

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

@ -152,6 +152,7 @@ import { computed, ref } from 'vue'
import axios from '@nextcloud/axios' import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router' import { generateUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state' import { loadState } from '@nextcloud/initial-state'
import { emit } from '@nextcloud/event-bus'
import NcDialog from '@nextcloud/vue/components/NcDialog' import NcDialog from '@nextcloud/vue/components/NcDialog'
import NcTextField from '@nextcloud/vue/components/NcTextField' import NcTextField from '@nextcloud/vue/components/NcTextField'
@ -277,8 +278,15 @@ export default {
this.configuredDeployOptions = null 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) this.$emit('update:show', false)
}, },
}, },

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

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

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

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

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

@ -25,6 +25,7 @@ interface AppApiState {
statusUpdater: number | null | undefined statusUpdater: number | null | undefined
daemonAccessible: boolean daemonAccessible: boolean
defaultDaemon: IDeployDaemon | null defaultDaemon: IDeployDaemon | null
dockerDaemons: IDeployDaemon[]
} }
export const useAppApiStore = defineStore('app-api-apps', { export const useAppApiStore = defineStore('app-api-apps', {
@ -36,6 +37,7 @@ export const useAppApiStore = defineStore('app-api-apps', {
statusUpdater: null, statusUpdater: null,
daemonAccessible: loadState('settings', 'defaultDaemonConfigAccessible', false), daemonAccessible: loadState('settings', 'defaultDaemonConfigAccessible', false),
defaultDaemon: loadState('settings', 'defaultDaemonConfig', null), defaultDaemon: loadState('settings', 'defaultDaemonConfig', null),
dockerDaemons: [],
}), }),
getters: { 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(appId, true)
this.setLoading('install', true) this.setLoading('install', true)
return confirmPassword().then(() => { 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) => { .then((response) => {
this.setLoading(appId, false) this.setLoading(appId, false)
this.setLoading('install', false) this.setLoading('install', false)
@ -91,7 +93,7 @@ export const useAppApiStore = defineStore('app-api-apps', {
if (!app.installed) { if (!app.installed) {
app.installed = true app.installed = true
app.needsDownload = false app.needsDownload = false
app.daemon = this.defaultDaemon
app.daemon = daemon
app.status = { app.status = {
type: 'install', type: 'install',
action: 'deploy', 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() { updateAppsStatus() {
clearInterval(this.statusUpdater as number) clearInterval(this.statusUpdater as number)
const initializingOrDeployingApps = this.getInitializingOrDeployingApps 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: Tobias Koppers @sokra
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com> SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@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: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Matt Zabriskie SPDX-FileCopyrightText: Matt Zabriskie
SPDX-FileCopyrightText: Joyent SPDX-FileCopyrightText: Joyent
@ -139,6 +140,9 @@ This file is generated from multiple sources. Included packages:
- vue-loader - vue-loader
- version: 15.11.1 - version: 15.11.1
- license: MIT - license: MIT
- vue-material-design-icons
- version: 5.3.1
- license: MIT
- vue-router - vue-router
- version: 3.6.5 - version: 3.6.5
- license: MIT - 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: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com> SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Roeland Jago Douma 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@vorba.ch> (http://paul.vorba.ch)
SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de) SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de)
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
@ -235,6 +236,9 @@ This file is generated from multiple sources. Included packages:
- vue-loader - vue-loader
- version: 15.11.1 - version: 15.11.1
- license: MIT - license: MIT
- vue-material-design-icons
- version: 5.3.1
- license: MIT
- vue-router - vue-router
- version: 3.6.5 - version: 3.6.5
- license: MIT - 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