Skip to content

Commit fc483fa

Browse files
committed
feat(app_api): Advanced deploy options
Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
1 parent 326120a commit fc483fa

File tree

5 files changed

+380
-6
lines changed

5 files changed

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

0 commit comments

Comments
 (0)