diff options
9 files changed, 325 insertions, 10 deletions
diff --git a/apps/settings/src/app-types.ts b/apps/settings/src/app-types.ts index 49f0d5a1709..0c448ca907c 100644 --- a/apps/settings/src/app-types.ts +++ b/apps/settings/src/app-types.ts @@ -75,6 +75,7 @@ export interface IDeployDaemon { id: number, name: string, protocol: string, + exAppsCount: number, } export interface IExAppStatus { diff --git a/apps/settings/src/components/AppAPI/DaemonEnableSelection.vue b/apps/settings/src/components/AppAPI/DaemonEnableSelection.vue new file mode 100644 index 00000000000..e8aac3705d0 --- /dev/null +++ b/apps/settings/src/components/AppAPI/DaemonEnableSelection.vue @@ -0,0 +1,84 @@ +<template> + <div class="daemon"> + <NcListItem :name="itemTitle" + :details="isDefault ? t('settings', 'Default') : ''" + :force-display-actions="true" + :counter-number="daemon.exAppsCount" + :class="{'daemon-default': isDefault }" + counter-type="highlighted" + @click.stop="selectDaemonAndInstall"> + <template #subname> + {{ daemon.accepts_deploy_id }} + </template> + </NcListItem> + </div> +</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: 'DaemonEnableSelection', + components: { + NcListItem, + }, + mixins: [AppManagement], + props: { + daemon: { + type: Object, + required: true, + default: () => {}, + }, + isDefault: { + type: Boolean, + required: true, + default: () => false, + }, + app: { + type: Object, + required: true, + default: () => {}, + }, + deployOptions: { + type: Object, + required: false, + default: () => null, + }, + }, + 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> + +<style lang="scss"> +.daemon-default > .list-item { + background-color: var(--color-background-dark); +} +</style> diff --git a/apps/settings/src/components/AppAPI/DaemonSelectionList.vue b/apps/settings/src/components/AppAPI/DaemonSelectionList.vue new file mode 100644 index 00000000000..7c88c1da81e --- /dev/null +++ b/apps/settings/src/components/AppAPI/DaemonSelectionList.vue @@ -0,0 +1,84 @@ +<template> + <div class="daemon-selection-list"> + <ul v-if="dockerDaemons.length > 0" + :aria-label="t('settings', 'Registered Deploy daemons list')"> + <DaemonEnableSelection 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 + :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> + </NcEmptyContent> + </div> +</template> + +<script> +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import FormatListBullet from 'vue-material-design-icons/FormatListBulleted.vue' +import DaemonEnableSelection from './DaemonEnableSelection.vue' +import { useAppApiStore } from '../../store/app-api-store.ts' +import { useAppsStore } from '../../store/apps-store.ts' + +export default { + name: 'DaemonSelectionList', + components: { + FormatListBullet, + DaemonEnableSelection, + NcEmptyContent, + }, + props: { + app: { + type: Object, + required: true, + }, + deployOptions: { + type: Object, + required: false, + default: () => ({}), + }, + }, + setup() { + const store = useAppsStore() + const appApiStore = useAppApiStore() + + return { + store, + appApiStore, + } + }, + computed: { + dockerDaemons() { + return this.appApiStore.dockerDaemons + }, + defaultDaemon() { + return this.appApiStore.defaultDaemon + }, + }, + methods: { + closeModal() { + this.$emit('close') + }, + }, +} +</script> + +<style scoped lang="scss"> +.daemon-selection-list { + max-height: 300px; + overflow-y: scroll; + padding: 2rem; + + .empty-content { + margin-top: 0; + text-align: center; + } +} +</style> diff --git a/apps/settings/src/components/AppAPI/DaemonSelectionModal.vue b/apps/settings/src/components/AppAPI/DaemonSelectionModal.vue new file mode 100644 index 00000000000..2f77cd8a992 --- /dev/null +++ b/apps/settings/src/components/AppAPI/DaemonSelectionModal.vue @@ -0,0 +1,70 @@ +<template> + <div class="daemon-selection-modal"> + <NcModal :show="show" + :name="t('settings', 'Daemon selection')" + size="normal" + @close="closeModal"> + <div class="select-modal-body"> + <h3>{{ t('settings', 'Choose Deploy Daemon for {appName}', {appName: app.name }) }}</h3> + <DaemonSelectionList :app="app" + :deploy-options="deployOptions" + @close="closeModal" /> + </div> + </NcModal> + </div> +</template> + +<script> +import NcModal from '@nextcloud/vue/components/NcModal' +import DaemonSelectionList from './DaemonSelectionList.vue' +import { useAppsStore } from '../../store/apps-store' +import { useAppApiStore } from '../../store/app-api-store' + +export default { + name: 'DaemonSelectionModal', + components: { + NcModal, + DaemonSelectionList, + }, + props: { + show: { + type: Boolean, + required: true, + default: false, + }, + app: { + type: Object, + required: true, + }, + deployOptions: { + type: Object, + required: false, + default: () => ({}), + }, + }, + setup() { + const store = useAppsStore() + const appApiStore = useAppApiStore() + + return { + store, + appApiStore, + } + }, + data() { + return { + selectDaemonModal: false, + } + }, + methods: { + closeModal() { + this.$emit('update:show', false) + }, + }, +} +</script> +<style scoped> +.select-modal-body h3 { + text-align: center; +} +</style> diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue index d0f39f3c74a..aa995c5b039 100644 --- a/apps/settings/src/components/AppList/AppItem.vue +++ b/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> + + <DaemonSelectionModal 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 DaemonSelectionModal from '../AppAPI/DaemonSelectionModal.vue' export default { name: 'AppItem', @@ -134,6 +139,7 @@ export default { AppScore, NcButton, NcIconSvgWrapper, + DaemonSelectionModal, }, 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> diff --git a/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue b/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue index 04c49827b02..e04d8884b96 100644 --- a/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue +++ b/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue @@ -187,6 +187,10 @@ export default { type: Boolean, required: true, }, + showDaemonSelectionModal: { + type: Function, + required: true, + }, }, setup(props) { // for AppManagement mixin @@ -277,8 +281,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) { + this.showDaemonSelectionModal(this.deployOptions) + } else { + this.enable(this.app.id, this.app.daemon, this.deployOptions) + } this.$emit('update:show', false) }, }, diff --git a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue index 3aa42f1d15a..55f5c2477e9 100644 --- a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue +++ b/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" @@ -194,7 +194,12 @@ <AppDeployOptionsModal v-if="app?.app_api" :show.sync="showDeployOptionsModal" - :app="app" /> + :app="app" + :show-daemon-selection-modal="showSelectionModal" /> + <DaemonSelectionModal v-if="app?.app_api && showSelectDaemonModal" + :show.sync="showSelectDaemonModal" + :app="app" + :deploy-options="deployOptions" /> </div> </NcAppSidebarTab> </template> @@ -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 DaemonSelectionModal from '../AppAPI/DaemonSelectionModal.vue' import AppManagement from '../../mixins/AppManagement.js' import { mdiBug, mdiFeatureSearch, mdiStar, mdiTextBox, mdiTooltipQuestion, mdiToyBrickPlus } from '@mdi/js' @@ -224,6 +230,7 @@ export default { NcSelect, NcCheckboxRadioSwitch, AppDeployOptionsModal, + DaemonSelectionModal, }, mixins: [AppManagement], @@ -256,6 +263,8 @@ export default { groupCheckedAppsData: false, removeData: false, showDeployOptionsModal: false, + showSelectDaemonModal: false, + deployOptions: null, } }, @@ -365,6 +374,9 @@ export default { this.removeData = false }, }, + beforeUnmount() { + this.deployOptions = null + }, mounted() { if (this.app.groups.length > 0) { this.groupCheckedAppsData = true @@ -374,6 +386,24 @@ export default { 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> diff --git a/apps/settings/src/mixins/AppManagement.js b/apps/settings/src/mixins/AppManagement.js index b877b8dd88e..bac170fba6b 100644 --- a/apps/settings/src/mixins/AppManagement.js +++ b/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 { diff --git a/apps/settings/src/store/app-api-store.ts b/apps/settings/src/store/app-api-store.ts index f2f950d6948..deb48d5532d 100644 --- a/apps/settings/src/store/app-api-store.ts +++ b/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,15 @@ export const useAppApiStore = defineStore('app-api-apps', { }) }, + async fetchDockerDaemons() { + return axios.get(generateUrl('/apps/app_api/daemons')) + .then((res) => { + this.defaultDaemon = res.data.daemons.find((daemon: IDeployDaemon) => daemon.name === res.data.default_daemon_config) + this.dockerDaemons = res.data.daemons.filter((daemon: IDeployDaemon) => daemon.accepts_deploy_id === 'docker-install') + return res + }) + }, + updateAppsStatus() { clearInterval(this.statusUpdater as number) const initializingOrDeployingApps = this.getInitializingOrDeployingApps |