mirror of
https://github.com/nextcloud/server.git
synced 2026-03-07 07:50:57 -05:00
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:
commit
9dd661f3d8
17 changed files with 302 additions and 18 deletions
|
|
@ -75,6 +75,7 @@ export interface IDeployDaemon {
|
|||
id: number,
|
||||
name: string,
|
||||
protocol: string,
|
||||
exAppsCount: number,
|
||||
}
|
||||
|
||||
export interface IExAppStatus {
|
||||
|
|
|
|||
|
|
@ -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
Normal file
77
apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue
Normal 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>
|
||||
77
apps/settings/src/components/AppAPI/DaemonSelectionList.vue
Normal file
77
apps/settings/src/components/AppAPI/DaemonSelectionList.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
4
dist/8737-8737.js
vendored
File diff suppressed because one or more lines are too long
4
dist/8737-8737.js.license
vendored
4
dist/8737-8737.js.license
vendored
|
|
@ -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
vendored
2
dist/8737-8737.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-apps-view-4529.js
vendored
4
dist/settings-apps-view-4529.js
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-apps-view-4529.js.license
vendored
4
dist/settings-apps-view-4529.js.license
vendored
|
|
@ -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
vendored
2
dist/settings-apps-view-4529.js.map
vendored
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
Loading…
Reference in a new issue