mirror of
https://github.com/nextcloud/server.git
synced 2026-04-15 22:11:17 -04:00
feat(app_api): Advanced deploy options
Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
This commit is contained in:
parent
5ba9ece039
commit
73c138b0f3
5 changed files with 370 additions and 6 deletions
|
|
@ -87,8 +87,31 @@ export interface IExAppStatus {
|
|||
type: string
|
||||
}
|
||||
|
||||
export interface IDeployEnv {
|
||||
envName: string
|
||||
displayName: string
|
||||
description: string
|
||||
default?: string
|
||||
}
|
||||
|
||||
export interface IDeployMount {
|
||||
hostPath: string
|
||||
containerPath: string
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
export interface IDeployOptions {
|
||||
environment_variables: IDeployEnv[]
|
||||
mounts: IDeployMount[]
|
||||
}
|
||||
|
||||
export interface IAppstoreExAppRelease extends IAppstoreAppRelease {
|
||||
environmentVariables?: IDeployEnv[]
|
||||
}
|
||||
|
||||
export interface IAppstoreExApp extends IAppstoreApp {
|
||||
daemon: IDeployDaemon | null | undefined
|
||||
status: IExAppStatus | Record<string, never>
|
||||
error: string
|
||||
releases: IAppstoreExAppRelease[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<NcDialog :open="show"
|
||||
size="normal"
|
||||
:name="t('settings', 'Advanced deploy options')"
|
||||
@update:open="$emit('update:show', $event)">
|
||||
<div class="modal__content">
|
||||
<p class="deploy-option__hint">
|
||||
{{ configuredDeployOptions === null ? t('settings', 'Edit ExApp deploy options before installation') : t('settings', 'Configured ExApp deploy options. Can be set only during installation') }}.
|
||||
<a v-if="deployOptionsDocsUrl" :href="deployOptionsDocsUrl">
|
||||
{{ t('settings', 'Learn more') }}
|
||||
</a>
|
||||
</p>
|
||||
<h3 v-if="environmentVariables.length > 0 || (configuredDeployOptions !== null && configuredDeployOptions.environment_variables.length > 0)">
|
||||
{{ t('settings', 'Environment variables') }}
|
||||
</h3>
|
||||
<template v-if="configuredDeployOptions === null">
|
||||
<div v-for="envVar in environmentVariables"
|
||||
:key="envVar.envName"
|
||||
class="deploy-option">
|
||||
<NcTextField :label="envVar.displayName" :value.sync="deployOptions.environment_variables[envVar.envName]" />
|
||||
<p class="deploy-option__hint">
|
||||
{{ envVar.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<fieldset v-else-if="Object.keys(configuredDeployOptions).length > 0"
|
||||
class="envs">
|
||||
<legend class="deploy-option__hint">
|
||||
{{ t('settings', 'ExApp container environment variables') }}
|
||||
</legend>
|
||||
<NcTextField v-for="(value, key) in configuredDeployOptions.environment_variables"
|
||||
:key="key"
|
||||
:label="value.displayName ?? key"
|
||||
:helper-text="value.description"
|
||||
:value="value.value"
|
||||
readonly />
|
||||
</fieldset>
|
||||
<template v-else>
|
||||
<p class="deploy-option__hint">
|
||||
{{ t('settings', 'No environment variables defined') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<h3>{{ t('settings', 'Mounts') }}</h3>
|
||||
<template v-if="configuredDeployOptions === null">
|
||||
<p class="deploy-option__hint">
|
||||
{{ t('settings', 'Define host folder mounts to bind to the ExApp container') }}
|
||||
</p>
|
||||
<NcNoteCard type="info" :text="t('settings', 'Must exist on the Deploy daemon host prior to installing the ExApp')" />
|
||||
<div v-for="mount in deployOptions.mounts"
|
||||
:key="mount.hostPath"
|
||||
class="deploy-option"
|
||||
style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
|
||||
<NcTextField :label="t('settings', 'Host path')" :value.sync="mount.hostPath" />
|
||||
<NcTextField :label="t('settings', 'Container path')" :value.sync="mount.containerPath" />
|
||||
<NcCheckboxRadioSwitch :checked.sync="mount.readonly">
|
||||
{{ t('settings', 'Read-only') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcButton :aria-label="t('settings', 'Remove mount')"
|
||||
style="margin-top: 6px;"
|
||||
@click="removeMount(mount)">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiDelete" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<div v-if="addingMount" class="deploy-option">
|
||||
<h4>
|
||||
{{ t('settings', 'New mount') }}
|
||||
</h4>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
|
||||
<NcTextField ref="newMountHostPath"
|
||||
:label="t('settings', 'Host path')"
|
||||
:aria-label="t('settings', 'Enter path to host folder')"
|
||||
:value.sync="newMountPoint.hostPath" />
|
||||
<NcTextField :label="t('settings', 'Container path')"
|
||||
:aria-label="t('settings', 'Enter path to container folder')"
|
||||
:value.sync="newMountPoint.containerPath" />
|
||||
<NcCheckboxRadioSwitch :checked.sync="newMountPoint.readonly"
|
||||
:aria-label="t('settings', 'Toggle read-only mode')">
|
||||
{{ t('settings', 'Read-only') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; margin-top: 4px;">
|
||||
<NcButton :aria-label="t('settings', 'Confirm adding new mount')"
|
||||
@click="addMountPoint">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiCheck" />
|
||||
</template>
|
||||
{{ t('settings', 'Confirm') }}
|
||||
</NcButton>
|
||||
<NcButton :aria-label="t('settings', 'Cancel adding mount')"
|
||||
style="margin-left: 4px;"
|
||||
@click="cancelAddMountPoint">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiClose" />
|
||||
</template>
|
||||
{{ t('settings', 'Cancel') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
<NcButton v-if="!addingMount"
|
||||
:aria-label="t('settings', 'Add mount')"
|
||||
style="margin-top: 5px;"
|
||||
@click="startAddingMount">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiPlus" />
|
||||
</template>
|
||||
{{ t('settings', 'Add mount') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
<template v-else-if="configuredDeployOptions.mounts.length > 0">
|
||||
<p class="deploy-option__hint">
|
||||
{{ t('settings', 'ExApp container mounts') }}
|
||||
</p>
|
||||
<div v-for="mount in configuredDeployOptions.mounts"
|
||||
:key="mount.hostPath"
|
||||
class="deploy-option"
|
||||
style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
|
||||
<NcTextField :label="t('settings', 'Host path')" :value.sync="mount.hostPath" readonly />
|
||||
<NcTextField :label="t('settings', 'Container path')" :value.sync="mount.containerPath" readonly />
|
||||
<NcCheckboxRadioSwitch :checked.sync="mount.readonly" disabled>
|
||||
{{ t('settings', 'Read-only') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="deploy-option__hint">
|
||||
{{ t('settings', 'No mounts defined') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-if="!app.active && (app.canInstall || app.isCompatible) && configuredDeployOptions === null" #actions>
|
||||
<NcButton :title="enableButtonTooltip"
|
||||
:aria-label="enableButtonTooltip"
|
||||
type="primary"
|
||||
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
|
||||
@click.stop="submitDeployOptions">
|
||||
{{ enableButtonText }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
|
||||
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
|
||||
import { mdiPlus, mdiCheck, mdiClose, mdiDelete } from '@mdi/js'
|
||||
|
||||
import { useAppApiStore } from '../../store/app-api-store.ts'
|
||||
import { useAppsStore } from '../../store/apps-store.ts'
|
||||
|
||||
import AppManagement from '../../mixins/AppManagement.js'
|
||||
|
||||
export default {
|
||||
name: 'AppDeployOptionsModal',
|
||||
components: {
|
||||
NcDialog,
|
||||
NcTextField,
|
||||
NcButton,
|
||||
NcNoteCard,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcIconSvgWrapper,
|
||||
},
|
||||
mixins: [AppManagement],
|
||||
props: {
|
||||
app: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// for AppManagement mixin
|
||||
const store = useAppsStore()
|
||||
const appApiStore = useAppApiStore()
|
||||
|
||||
const environmentVariables = computed(() => {
|
||||
if (props.app?.releases?.length === 1) {
|
||||
return props.app?.releases[0]?.environmentVariables || []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const deployOptions = ref({
|
||||
environment_variables: environmentVariables.value.reduce((acc, envVar) => {
|
||||
acc[envVar.envName] = envVar.default || ''
|
||||
return acc
|
||||
}, {}),
|
||||
mounts: [],
|
||||
})
|
||||
|
||||
return {
|
||||
environmentVariables,
|
||||
deployOptions,
|
||||
store,
|
||||
appApiStore,
|
||||
mdiPlus,
|
||||
mdiCheck,
|
||||
mdiClose,
|
||||
mdiDelete,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
addingMount: false,
|
||||
newMountPoint: {
|
||||
hostPath: '',
|
||||
containerPath: '',
|
||||
readonly: false,
|
||||
},
|
||||
addingPortBinding: false,
|
||||
configuredDeployOptions: null,
|
||||
deployOptionsDocsUrl: loadState('settings', 'deployOptionsDocsUrl', null),
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(newShow) {
|
||||
if (newShow) {
|
||||
this.fetchExAppDeployOptions()
|
||||
} else {
|
||||
this.configuredDeployOptions = null
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
startAddingMount() {
|
||||
this.addingMount = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.newMountHostPath.focus()
|
||||
})
|
||||
},
|
||||
addMountPoint() {
|
||||
this.deployOptions.mounts.push(this.newMountPoint)
|
||||
this.newMountPoint = {
|
||||
hostPath: '',
|
||||
containerPath: '',
|
||||
readonly: false,
|
||||
}
|
||||
this.addingMount = false
|
||||
},
|
||||
cancelAddMountPoint() {
|
||||
this.newMountPoint = {
|
||||
hostPath: '',
|
||||
containerPath: '',
|
||||
readonly: false,
|
||||
}
|
||||
this.addingMount = false
|
||||
},
|
||||
removeMount(mountToRemove) {
|
||||
this.deployOptions.mounts = this.deployOptions.mounts.filter(mount => mount !== mountToRemove)
|
||||
},
|
||||
async fetchExAppDeployOptions() {
|
||||
return axios.get(generateUrl(`/apps/app_api/apps/deploy-options/${this.app.id}`))
|
||||
.then(response => {
|
||||
this.configuredDeployOptions = response.data
|
||||
})
|
||||
.catch(() => {
|
||||
this.configuredDeployOptions = null
|
||||
})
|
||||
},
|
||||
submitDeployOptions() {
|
||||
this.enable(this.app.id, this.deployOptions)
|
||||
this.$emit('update:show', false)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.deploy-option {
|
||||
margin: calc(var(--default-grid-baseline) * 4) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
&__hint {
|
||||
margin-top: 4px;
|
||||
font-size: 0.8em;
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
|
||||
.envs {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
max-height: 300px;
|
||||
|
||||
li {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -77,6 +77,15 @@
|
|||
:value="forceEnableButtonText"
|
||||
:disabled="installing || isLoading"
|
||||
@click="forceEnable(app.id)">
|
||||
<NcButton v-if="app?.app_api && (app.canInstall || app.isCompatible)"
|
||||
:aria-label="t('settings', 'Advanced deploy options')"
|
||||
type="secondary"
|
||||
@click="() => showDeployOptionsModal = true">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiToyBrickPlus" />
|
||||
</template>
|
||||
{{ t('settings', 'Deploy options') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
<p v-if="!defaultDeployDaemonAccessible" class="warning">
|
||||
{{ t('settings', 'Default Deploy daemon is not accessible') }}
|
||||
|
|
@ -182,6 +191,10 @@
|
|||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppDeployOptionsModal v-if="app?.app_api"
|
||||
:show.sync="showDeployOptionsModal"
|
||||
:app="app" />
|
||||
</div>
|
||||
</NcAppSidebarTab>
|
||||
</template>
|
||||
|
|
@ -193,9 +206,10 @@ import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js'
|
|||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
|
||||
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import AppDeployOptionsModal from './AppDeployOptionsModal.vue'
|
||||
|
||||
import AppManagement from '../../mixins/AppManagement.js'
|
||||
import { mdiBug, mdiFeatureSearch, mdiStar, mdiTextBox, mdiTooltipQuestion } from '@mdi/js'
|
||||
import { mdiBug, mdiFeatureSearch, mdiStar, mdiTextBox, mdiTooltipQuestion, mdiToyBrickPlus } from '@mdi/js'
|
||||
import { useAppsStore } from '../../store/apps-store'
|
||||
import { useAppApiStore } from '../../store/app-api-store'
|
||||
|
||||
|
|
@ -209,6 +223,7 @@ export default {
|
|||
NcIconSvgWrapper,
|
||||
NcSelect,
|
||||
NcCheckboxRadioSwitch,
|
||||
AppDeployOptionsModal,
|
||||
},
|
||||
mixins: [AppManagement],
|
||||
|
||||
|
|
@ -232,6 +247,7 @@ export default {
|
|||
mdiStar,
|
||||
mdiTextBox,
|
||||
mdiTooltipQuestion,
|
||||
mdiToyBrickPlus,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -239,6 +255,7 @@ export default {
|
|||
return {
|
||||
groupCheckedAppsData: false,
|
||||
removeData: false,
|
||||
showDeployOptionsModal: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -370,6 +387,7 @@ export default {
|
|||
&-manage {
|
||||
// if too many, shrink them and ellipsis
|
||||
display: flex;
|
||||
align-items: center;
|
||||
input {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
|
|
|
|||
|
|
@ -188,9 +188,9 @@ export default {
|
|||
.catch((error) => { showError(error) })
|
||||
}
|
||||
},
|
||||
enable(appId) {
|
||||
enable(appId, deployOptions = []) {
|
||||
if (this.app?.app_api) {
|
||||
this.appApiStore.enableApp(appId)
|
||||
this.appApiStore.enableApp(appId, deployOptions)
|
||||
.then(() => { rebuildNavigation() })
|
||||
.catch((error) => { showError(error) })
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { defineStore } from 'pinia'
|
|||
import api from './api'
|
||||
import logger from '../logger'
|
||||
|
||||
import type { IAppstoreExApp, IDeployDaemon, IExAppStatus } from '../app-types'
|
||||
import type { IAppstoreExApp, IDeployDaemon, IDeployOptions, IExAppStatus } from '../app-types.ts'
|
||||
import Vue from 'vue'
|
||||
|
||||
interface AppApiState {
|
||||
|
|
@ -76,12 +76,12 @@ export const useAppApiStore = defineStore('app-api-apps', {
|
|||
})
|
||||
},
|
||||
|
||||
enableApp(appId: string) {
|
||||
enableApp(appId: string, deployOptions: IDeployOptions[] = []) {
|
||||
this.setLoading(appId, true)
|
||||
this.setLoading('install', true)
|
||||
return confirmPassword().then(() => {
|
||||
|
||||
return axios.post(generateUrl(`/apps/app_api/apps/enable/${appId}`))
|
||||
return axios.post(generateUrl(`/apps/app_api/apps/enable/${appId}`), { deployOptions })
|
||||
.then((response) => {
|
||||
this.setLoading(appId, false)
|
||||
this.setLoading('install', false)
|
||||
|
|
@ -132,6 +132,9 @@ export const useAppApiStore = defineStore('app-api-apps', {
|
|||
this.setError(appId, error.response.data.data.message)
|
||||
this.appsApiFailure({ appId, error })
|
||||
})
|
||||
}).catch(() => {
|
||||
this.setLoading(appId, false)
|
||||
this.setLoading('install', false)
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -150,6 +153,9 @@ export const useAppApiStore = defineStore('app-api-apps', {
|
|||
this.setError(appId, error.response.data.data.message)
|
||||
this.appsApiFailure({ appId, error })
|
||||
})
|
||||
}).catch(() => {
|
||||
this.setLoading(appId, false)
|
||||
this.setLoading('install', false)
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -173,6 +179,8 @@ export const useAppApiStore = defineStore('app-api-apps', {
|
|||
this.setLoading(appId, false)
|
||||
this.appsApiFailure({ appId, error })
|
||||
})
|
||||
}).catch(() => {
|
||||
this.setLoading(appId, false)
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -237,6 +245,9 @@ export const useAppApiStore = defineStore('app-api-apps', {
|
|||
this.setLoading('install', false)
|
||||
this.appsApiFailure({ appId, error })
|
||||
})
|
||||
}).catch(() => {
|
||||
this.setLoading(appId, false)
|
||||
this.setLoading('install', false)
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue