Skip to content

Commit

Permalink
feat(app_api): Advanced deploy options
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Borysenko <[email protected]>
  • Loading branch information
andrey18106 committed Jan 17, 2025
1 parent 326120a commit cc2b48a
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 6 deletions.
30 changes: 30 additions & 0 deletions apps/settings/src/app-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,38 @@ export interface IExAppStatus {
type: string
}

export interface IDeployOptions {
environment_variables: IDeployEnv[]

Check failure on line 91 in apps/settings/src/app-types.ts

View workflow job for this annotation

GitHub Actions / NPM lint

'IDeployEnv' was used before it was defined
mounts: IDeployMount[]

Check failure on line 92 in apps/settings/src/app-types.ts

View workflow job for this annotation

GitHub Actions / NPM lint

'IDeployMount' was used before it was defined
ports: IDeployPort[]

Check failure on line 93 in apps/settings/src/app-types.ts

View workflow job for this annotation

GitHub Actions / NPM lint

'IDeployPort' was used before it was defined
}

export interface IDeployEnv {
envName: string
displayName: string
description: string
default?: string
}

export interface IDeployMount {
hostPath: string
containerPath: string
readOnly: boolean
}

export interface IDeployPort {
hostPort: string // 443, 80, 443/tcp, 80/udp, etc.
hostIp: string // 0.0.0.0, 127.0.0.1, localhost, etc.
containerPort: number
}

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[]
}
306 changes: 306 additions & 0 deletions apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<NcModal v-if="show"
label-id="form-name"
@close="() => $emit('update:show', false)">
<div class="modal__content">
<h2 id="form-name">{{ t('settings', 'Advanced deploy options') }}</h2>
<p class="description" style="text-align: center;">
{{ configuredDeployOptions === null ? t('settings', 'Edit ExApp deploy options before installation') : t('settings', 'Configured ExApp deploy options. Can be set only during installation') }}.
<a href="https://docs.nextcloud.com/server/latest/admin_manual/exapps_management/AdvancedDeployOptions.html">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"

Check failure on line 21 in apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue

View workflow job for this annotation

GitHub Actions / NPM lint

':key' should be on a new line
class="deploy-option">

Check failure on line 22 in apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Expected "\t" character, but found " " character
<NcTextField :label="envVar.displayName" :value.sync="deployOptions.environment_variables[envVar.envName]" />
<p class="description">{{ envVar.description }}</p>
</div>
</template>
<template v-else-if="Object.keys(configuredDeployOptions).length > 0">
<p class="description">{{ t('settings', 'ExApp container environment variables (ExApp-reserved envs are excluded)') }}</p>
<ul class="envs">
<li v-for="envVar in Object.keys(configuredDeployOptions.environment_variables)" :key="envVar">
<NcTextField
:label="configuredDeployOptions.environment_variables[envVar].displayName ?? envVar"

Check failure on line 32 in apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Expected no linebreak before this attribute
:value="configuredDeployOptions.environment_variables[envVar].value" readonly />

Check failure on line 33 in apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'readonly' should be on a new line
<p class="description">{{ configuredDeployOptions.environment_variables[envVar].description }}</p>
</li>
</ul>
</template>
<template v-else>
<p class="description">{{ t('settings', 'No environment variables defined') }}</p>
</template>

<h3>{{ t('settings', 'Mounts') }}</h3>
<template v-if="configuredDeployOptions === null">
<p class="description">{{ t('settings', 'Define host folder mounts to bind to ExApp container') }}</p>
<p class="warning">{{ t('settings', 'Must exist on Deploy daemon host prior to installing the ExApp') }}</p>
<div v-for="mount in deployOptions.mounts"

Check failure on line 46 in apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Elements in iteration expect to have 'v-bind:key' directives
class="deploy-option"

Check failure on line 47 in apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Expected "\t" character, but found " " character
style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">

Check failure on line 48 in apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Expected "\t" character, but found " " character
<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="() => {
addingMount = true
$nextTick(() => {
this.$refs.newMountHostPath.focus()
})
}">
<template #icon>
<NcIconSvgWrapper :path="mdiPlus" />
</template>
{{ t('settings', 'Add mount') }}
</NcButton>
</template>
<template v-else-if="configuredDeployOptions.mounts.length > 0">
<p class="description">{{ t('settings', 'ExApp container mounts') }}</p>
<div v-for="mount in configuredDeployOptions.mounts"
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>
<template v-else>
<p class="description">{{ t('settings', 'No mounts defined') }}</p>
</template>

<NcButton v-if="!app.active && (app.canInstall || app.isCompatible) && configuredDeployOptions === null"
:title="enableButtonTooltip"
:aria-label="enableButtonTooltip"
type="primary"
:disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
style="margin-top: 10px;"
@click.stop="() => {
enable(app.id, deployOptions)
$emit('update:show', false)
}">
{{ enableButtonText }}
</NcButton>
</div>
</NcModal>
</template>

<script>
import { computed, ref } from 'vue'

import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'

import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.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: {
NcModal,
NcTextField,
NcButton,
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,
}
},
watch: {
show(newShow) {
if (newShow) {
this.fetchExAppDeployOptions()
} else {
this.configuredDeployOptions = null
}
},
},
data() {
return {
addingMount: false,
newMountPoint: {
hostPath: '',
containerPath: '',
readonly: false,
},
addingPortBinding: false,
configuredDeployOptions: null,
}
},
methods: {
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
})
},
},
}
</script>

<style scoped>
.modal__content {
margin: 40px;
}

.modal__content h2 {
text-align: center;
}

.deploy-option {
margin: calc(var(--default-grid-baseline) * 4) 0;
display: flex;
flex-direction: column;
align-items: flex-start;
}

.envs {
width: 100%;
overflow: auto;
height: 100%;
max-height: 300px;

li {
margin: 10px 0;
}
}

.description {
margin-top: 4px;
font-size: 0.8em;
color: var(--color-text-lighter);
}
</style>
Loading

0 comments on commit cc2b48a

Please sign in to comment.