aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src')
-rw-r--r--apps/settings/src/app-types.ts1
-rw-r--r--apps/settings/src/components/AdminAI.vue22
-rw-r--r--apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue41
-rw-r--r--apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue77
-rw-r--r--apps/settings/src/components/AppAPI/DaemonSelectionList.vue77
-rw-r--r--apps/settings/src/components/AppList/AppItem.vue26
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue12
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue36
-rw-r--r--apps/settings/src/constants/AppstoreCategoryIcons.ts6
-rw-r--r--apps/settings/src/mixins/AppManagement.js4
-rw-r--r--apps/settings/src/store/app-api-store.ts20
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>&nbsp;</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