diff options
Diffstat (limited to 'apps/settings/src')
11 files changed, 305 insertions, 17 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/AdminAI.vue b/apps/settings/src/components/AdminAI.vue index 044ebd9183e..0d3e9154bb9 100644 --- a/apps/settings/src/components/AdminAI.vue +++ b/apps/settings/src/components/AdminAI.vue @@ -11,16 +11,17 @@ @update:modelValue="saveChanges"> {{ t('settings', 'Allow AI usage for guest users') }} </NcCheckboxRadioSwitch> + <h3>{{ t('settings', 'Provider for Task types') }}</h3> <template v-for="type in taskProcessingTaskTypes"> - <div :key="type"> - <h3>{{ t('settings', 'Task:') }} {{ type.name }}</h3> - <p>{{ type.description }}</p> + <div :key="type" class="tasktype-item"> + <p class="tasktype-name"> + {{ type.name }} + </p> <NcCheckboxRadioSwitch v-model="settings['ai.taskprocessing_type_preferences'][type.id]" type="switch" @update:modelValue="saveChanges"> {{ t('settings', 'Enable') }} - </NcCheckboxRadioSwitch> - <NcSelect v-model="settings['ai.taskprocessing_provider_preferences'][type.id]" + </NcCheckboxRadioSwitch><NcSelect v-model="settings['ai.taskprocessing_provider_preferences'][type.id]" class="provider-select" :clearable="false" :disabled="!settings['ai.taskprocessing_type_preferences'][type.id]" @@ -33,7 +34,6 @@ {{ taskProcessingProviders.find(p => p.id === label)?.name }} </template> </NcSelect> - <p> </p> </div> </template> <template v-if="!hasTaskProcessing"> @@ -244,4 +244,14 @@ export default { .provider-select { min-width: 350px !important; } + +.tasktype-item { + display: flex; + align-items: center; + gap: 8px; + .tasktype-name { + flex: 1; + margin: 0; + } +} </style> diff --git a/apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue b/apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue new file mode 100644 index 00000000000..696c77d19ce --- /dev/null +++ b/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> diff --git a/apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue b/apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue new file mode 100644 index 00000000000..6b1cefde032 --- /dev/null +++ b/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> diff --git a/apps/settings/src/components/AppAPI/DaemonSelectionList.vue b/apps/settings/src/components/AppAPI/DaemonSelectionList.vue new file mode 100644 index 00000000000..701a17dbe24 --- /dev/null +++ b/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> diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue index d0f39f3c74a..95a98a93cde 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> + + <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> diff --git a/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue b/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue index 67d4afa6566..0544c3848be 100644 --- a/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue +++ b/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue @@ -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) }, }, diff --git a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue index 8a387b55ecf..eb66d8f3e3a 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" @@ -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> diff --git a/apps/settings/src/constants/AppstoreCategoryIcons.ts b/apps/settings/src/constants/AppstoreCategoryIcons.ts index 24bb0faea6d..989ffe79c22 100644 --- a/apps/settings/src/constants/AppstoreCategoryIcons.ts +++ b/apps/settings/src/constants/AppstoreCategoryIcons.ts @@ -3,14 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { - mdiAccountOutline, mdiAccountMultipleOutline, + mdiAccountOutline, mdiArchiveOutline, mdiCheck, mdiClipboardFlowOutline, mdiClose, mdiCogOutline, mdiControllerClassicOutline, + mdiCreationOutline, mdiDownload, mdiFileDocumentEdit, mdiFolder, @@ -42,7 +43,8 @@ export default Object.freeze({ featured: mdiStar, updates: mdiDownload, - // generic categories + // generic category + ai: mdiCreationOutline, auth: mdiKeyOutline, customization: mdiCogOutline, dashboard: mdiViewColumnOutline, diff --git a/apps/settings/src/mixins/AppManagement.js b/apps/settings/src/mixins/AppManagement.js index b877b8dd88e..3822658589d 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..769f212ebd7 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,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 |