Skip to content

Commit 69ee366

Browse files
authored
Merge pull request #50230 from nextcloud/feat/settings/advanced-deploy-options
feat(settings): advanced deploy options
2 parents 5ba9ece + 7e5f5f3 commit 69ee366

File tree

107 files changed

+523
-159
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+523
-159
lines changed

apps/settings/src/app-types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,31 @@ export interface IExAppStatus {
8787
type: string
8888
}
8989

90+
export interface IDeployEnv {
91+
envName: string
92+
displayName: string
93+
description: string
94+
default?: string
95+
}
96+
97+
export interface IDeployMount {
98+
hostPath: string
99+
containerPath: string
100+
readOnly: boolean
101+
}
102+
103+
export interface IDeployOptions {
104+
environment_variables: IDeployEnv[]
105+
mounts: IDeployMount[]
106+
}
107+
108+
export interface IAppstoreExAppRelease extends IAppstoreAppRelease {
109+
environmentVariables?: IDeployEnv[]
110+
}
111+
90112
export interface IAppstoreExApp extends IAppstoreApp {
91113
daemon: IDeployDaemon | null | undefined
92114
status: IExAppStatus | Record<string, never>
93115
error: string
116+
releases: IAppstoreExAppRelease[]
94117
}
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<NcDialog :open="show"
8+
size="normal"
9+
:name="t('settings', 'Advanced deploy options')"
10+
@update:open="$emit('update:show', $event)">
11+
<div class="modal__content">
12+
<p class="deploy-option__hint">
13+
{{ configuredDeployOptions === null ? t('settings', 'Edit ExApp deploy options before installation') : t('settings', 'Configured ExApp deploy options. Can be set only during installation') }}.
14+
<a v-if="deployOptionsDocsUrl" :href="deployOptionsDocsUrl">
15+
{{ t('settings', 'Learn more') }}
16+
</a>
17+
</p>
18+
<h3 v-if="environmentVariables.length > 0 || (configuredDeployOptions !== null && configuredDeployOptions.environment_variables.length > 0)">
19+
{{ t('settings', 'Environment variables') }}
20+
</h3>
21+
<template v-if="configuredDeployOptions === null">
22+
<div v-for="envVar in environmentVariables"
23+
:key="envVar.envName"
24+
class="deploy-option">
25+
<NcTextField :label="envVar.displayName" :value.sync="deployOptions.environment_variables[envVar.envName]" />
26+
<p class="deploy-option__hint">
27+
{{ envVar.description }}
28+
</p>
29+
</div>
30+
</template>
31+
<fieldset v-else-if="Object.keys(configuredDeployOptions).length > 0"
32+
class="envs">
33+
<legend class="deploy-option__hint">
34+
{{ t('settings', 'ExApp container environment variables') }}
35+
</legend>
36+
<NcTextField v-for="(value, key) in configuredDeployOptions.environment_variables"
37+
:key="key"
38+
:label="value.displayName ?? key"
39+
:helper-text="value.description"
40+
:value="value.value"
41+
readonly />
42+
</fieldset>
43+
<template v-else>
44+
<p class="deploy-option__hint">
45+
{{ t('settings', 'No environment variables defined') }}
46+
</p>
47+
</template>
48+
49+
<h3>{{ t('settings', 'Mounts') }}</h3>
50+
<template v-if="configuredDeployOptions === null">
51+
<p class="deploy-option__hint">
52+
{{ t('settings', 'Define host folder mounts to bind to the ExApp container') }}
53+
</p>
54+
<NcNoteCard type="info" :text="t('settings', 'Must exist on the Deploy daemon host prior to installing the ExApp')" />
55+
<div v-for="mount in deployOptions.mounts"
56+
:key="mount.hostPath"
57+
class="deploy-option"
58+
style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
59+
<NcTextField :label="t('settings', 'Host path')" :value.sync="mount.hostPath" />
60+
<NcTextField :label="t('settings', 'Container path')" :value.sync="mount.containerPath" />
61+
<NcCheckboxRadioSwitch :checked.sync="mount.readonly">
62+
{{ t('settings', 'Read-only') }}
63+
</NcCheckboxRadioSwitch>
64+
<NcButton :aria-label="t('settings', 'Remove mount')"
65+
style="margin-top: 6px;"
66+
@click="removeMount(mount)">
67+
<template #icon>
68+
<NcIconSvgWrapper :path="mdiDelete" />
69+
</template>
70+
</NcButton>
71+
</div>
72+
<div v-if="addingMount" class="deploy-option">
73+
<h4>
74+
{{ t('settings', 'New mount') }}
75+
</h4>
76+
<div style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
77+
<NcTextField ref="newMountHostPath"
78+
:label="t('settings', 'Host path')"
79+
:aria-label="t('settings', 'Enter path to host folder')"
80+
:value.sync="newMountPoint.hostPath" />
81+
<NcTextField :label="t('settings', 'Container path')"
82+
:aria-label="t('settings', 'Enter path to container folder')"
83+
:value.sync="newMountPoint.containerPath" />
84+
<NcCheckboxRadioSwitch :checked.sync="newMountPoint.readonly"
85+
:aria-label="t('settings', 'Toggle read-only mode')">
86+
{{ t('settings', 'Read-only') }}
87+
</NcCheckboxRadioSwitch>
88+
</div>
89+
<div style="display: flex; align-items: center; margin-top: 4px;">
90+
<NcButton :aria-label="t('settings', 'Confirm adding new mount')"
91+
@click="addMountPoint">
92+
<template #icon>
93+
<NcIconSvgWrapper :path="mdiCheck" />
94+
</template>
95+
{{ t('settings', 'Confirm') }}
96+
</NcButton>
97+
<NcButton :aria-label="t('settings', 'Cancel adding mount')"
98+
style="margin-left: 4px;"
99+
@click="cancelAddMountPoint">
100+
<template #icon>
101+
<NcIconSvgWrapper :path="mdiClose" />
102+
</template>
103+
{{ t('settings', 'Cancel') }}
104+
</NcButton>
105+
</div>
106+
</div>
107+
<NcButton v-if="!addingMount"
108+
:aria-label="t('settings', 'Add mount')"
109+
style="margin-top: 5px;"
110+
@click="startAddingMount">
111+
<template #icon>
112+
<NcIconSvgWrapper :path="mdiPlus" />
113+
</template>
114+
{{ t('settings', 'Add mount') }}
115+
</NcButton>
116+
</template>
117+
<template v-else-if="configuredDeployOptions.mounts.length > 0">
118+
<p class="deploy-option__hint">
119+
{{ t('settings', 'ExApp container mounts') }}
120+
</p>
121+
<div v-for="mount in configuredDeployOptions.mounts"
122+
:key="mount.hostPath"
123+
class="deploy-option"
124+
style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
125+
<NcTextField :label="t('settings', 'Host path')" :value.sync="mount.hostPath" readonly />
126+
<NcTextField :label="t('settings', 'Container path')" :value.sync="mount.containerPath" readonly />
127+
<NcCheckboxRadioSwitch :checked.sync="mount.readonly" disabled>
128+
{{ t('settings', 'Read-only') }}
129+
</NcCheckboxRadioSwitch>
130+
</div>
131+
</template>
132+
<p v-else class="deploy-option__hint">
133+
{{ t('settings', 'No mounts defined') }}
134+
</p>
135+
</div>
136+
137+
<template v-if="!app.active && (app.canInstall || app.isCompatible) && configuredDeployOptions === null" #actions>
138+
<NcButton :title="enableButtonTooltip"
139+
:aria-label="enableButtonTooltip"
140+
type="primary"
141+
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
142+
@click.stop="submitDeployOptions">
143+
{{ enableButtonText }}
144+
</NcButton>
145+
</template>
146+
</NcDialog>
147+
</template>
148+
149+
<script>
150+
import { computed, ref } from 'vue'
151+
152+
import axios from '@nextcloud/axios'
153+
import { generateUrl } from '@nextcloud/router'
154+
import { loadState } from '@nextcloud/initial-state'
155+
156+
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
157+
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
158+
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
159+
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
160+
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
161+
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
162+
163+
import { mdiPlus, mdiCheck, mdiClose, mdiDelete } from '@mdi/js'
164+
165+
import { useAppApiStore } from '../../store/app-api-store.ts'
166+
import { useAppsStore } from '../../store/apps-store.ts'
167+
168+
import AppManagement from '../../mixins/AppManagement.js'
169+
170+
export default {
171+
name: 'AppDeployOptionsModal',
172+
components: {
173+
NcDialog,
174+
NcTextField,
175+
NcButton,
176+
NcNoteCard,
177+
NcCheckboxRadioSwitch,
178+
NcIconSvgWrapper,
179+
},
180+
mixins: [AppManagement],
181+
props: {
182+
app: {
183+
type: Object,
184+
required: true,
185+
},
186+
show: {
187+
type: Boolean,
188+
required: true,
189+
},
190+
},
191+
setup(props) {
192+
// for AppManagement mixin
193+
const store = useAppsStore()
194+
const appApiStore = useAppApiStore()
195+
196+
const environmentVariables = computed(() => {
197+
if (props.app?.releases?.length === 1) {
198+
return props.app?.releases[0]?.environmentVariables || []
199+
}
200+
return []
201+
})
202+
203+
const deployOptions = ref({
204+
environment_variables: environmentVariables.value.reduce((acc, envVar) => {
205+
acc[envVar.envName] = envVar.default || ''
206+
return acc
207+
}, {}),
208+
mounts: [],
209+
})
210+
211+
return {
212+
environmentVariables,
213+
deployOptions,
214+
store,
215+
appApiStore,
216+
mdiPlus,
217+
mdiCheck,
218+
mdiClose,
219+
mdiDelete,
220+
}
221+
},
222+
data() {
223+
return {
224+
addingMount: false,
225+
newMountPoint: {
226+
hostPath: '',
227+
containerPath: '',
228+
readonly: false,
229+
},
230+
addingPortBinding: false,
231+
configuredDeployOptions: null,
232+
deployOptionsDocsUrl: loadState('settings', 'deployOptionsDocsUrl', null),
233+
}
234+
},
235+
watch: {
236+
show(newShow) {
237+
if (newShow) {
238+
this.fetchExAppDeployOptions()
239+
} else {
240+
this.configuredDeployOptions = null
241+
}
242+
},
243+
},
244+
methods: {
245+
startAddingMount() {
246+
this.addingMount = true
247+
this.$nextTick(() => {
248+
this.$refs.newMountHostPath.focus()
249+
})
250+
},
251+
addMountPoint() {
252+
this.deployOptions.mounts.push(this.newMountPoint)
253+
this.newMountPoint = {
254+
hostPath: '',
255+
containerPath: '',
256+
readonly: false,
257+
}
258+
this.addingMount = false
259+
},
260+
cancelAddMountPoint() {
261+
this.newMountPoint = {
262+
hostPath: '',
263+
containerPath: '',
264+
readonly: false,
265+
}
266+
this.addingMount = false
267+
},
268+
removeMount(mountToRemove) {
269+
this.deployOptions.mounts = this.deployOptions.mounts.filter(mount => mount !== mountToRemove)
270+
},
271+
async fetchExAppDeployOptions() {
272+
return axios.get(generateUrl(`/apps/app_api/apps/deploy-options/${this.app.id}`))
273+
.then(response => {
274+
this.configuredDeployOptions = response.data
275+
})
276+
.catch(() => {
277+
this.configuredDeployOptions = null
278+
})
279+
},
280+
submitDeployOptions() {
281+
this.enable(this.app.id, this.deployOptions)
282+
this.$emit('update:show', false)
283+
},
284+
},
285+
}
286+
</script>
287+
288+
<style scoped>
289+
.deploy-option {
290+
margin: calc(var(--default-grid-baseline) * 4) 0;
291+
display: flex;
292+
flex-direction: column;
293+
align-items: flex-start;
294+
295+
&__hint {
296+
margin-top: 4px;
297+
font-size: 0.8em;
298+
color: var(--color-text-maxcontrast);
299+
}
300+
}
301+
302+
.envs {
303+
width: 100%;
304+
overflow: auto;
305+
height: 100%;
306+
max-height: 300px;
307+
308+
li {
309+
margin: 10px 0;
310+
}
311+
}
312+
</style>

0 commit comments

Comments
 (0)