Fixup port bindings in location view

This commit is contained in:
Johannes Zellner
2025-03-03 21:24:27 +01:00
parent 6ef6caaca4
commit 3d487be59e
4 changed files with 125 additions and 46 deletions

View File

@@ -138,6 +138,16 @@ defineExpose({
tcpPorts.value = a.manifest.tcpPorts;
udpPorts.value = a.manifest.udpPorts;
// ensure we have value property
for (const p in tcpPorts.value) {
tcpPorts.value[p].value = tcpPorts.value[p].value || tcpPorts.value[p].defaultValue;
tcpPorts.value[p].enabled = false;
}
for (const p in udpPorts.value) {
udpPorts.value[p].value = udpPorts.value[p].value || udpPorts.value[p].defaultValue;
udpPorts.value[p].enabled = false;
}
secondaryDomains.value = a.manifest.httpPorts;
for (const p in secondaryDomains.value) {
const port = secondaryDomains.value[p];
@@ -200,7 +210,7 @@ defineExpose({
<TextInput id="upstreamUri" v-model="upstreamUri" />
</FormGroup>
<PortBindings v-model:tcp-ports="tcpPorts" v-model:udp-ports="udpPorts" :error="formError"/>
<PortBindings v-model:tcp="tcpPorts" v-model:udp="udpPorts" :error="formError"/>
<AccessControl v-model:option="accessRestrictionOption" v-model:acl="accessRestrictionAcl" :manifest="manifest"/>
<Button style="margin-top: 15px" @click="submit" icon="fa-solid fa-circle-down" :disabled="!formValid" :loading="busy">Install {{ manifest.title }}</Button>

View File

@@ -2,21 +2,11 @@
import { FormGroup, Checkbox, NumberInput } from 'pankow';
const props = defineProps([ 'tcpPorts', 'udpPorts', 'error' ]);
defineEmits([ 'update:tcpPorts', 'update:udpPorts' ]);
defineProps([ 'error' ]);
// copy value so we can use value as model value
for (const p in props.tcpPorts) {
const port = props.tcpPorts[p];
port.value = port.defaultValue;
port.enabled = true;
}
for (const p in props.udpPorts) {
const port = props.udpPorts[p];
port.value = port.defaultValue;
port.enabled = true;
}
// all ports require a property called 'value' for the model to work
const tcpPorts = defineModel('tcp');
const udpPorts = defineModel('udp');
</script>
@@ -26,7 +16,7 @@ for (const p in props.udpPorts) {
<Checkbox :label="port.title" v-model="port.enabled" />
<small>{{ port.description + '.' + (port.portCount >=1 ? (port.portCount + ' ports. ') : '') }}</small>
<small v-show="port.readOnly">{{ $t('appstore.installDialog.portReadOnly') }}</small>
<small class="has-error" v-show="error.port === port.value">Port already taken</small>
<small class="has-error" v-if="error.port === port.value">Port already taken {{ port }}</small>
<NumberInput v-model="port.value" :disabled="!port.enabled" :min="1"/>
<!-- TODO <p class="text-small text-warning text-bold" ng-show="appInstall.domain.provider === 'cloudflare'">{{ 'appstore.installDialog.cloudflarePortWarning' | tr }} </p> -->
</FormGroup>

View File

@@ -1,7 +1,8 @@
<script setup>
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted } from 'vue';
import { Button, SingleSelect, InputGroup, FormGroup, TextInput } from 'pankow';
import PortBindings from '../PortBindings.vue';
import AppsModel from '../../models/AppsModel.js';
import DomainsModel from '../../models/DomainsModel.js';
@@ -13,31 +14,48 @@ const domainsModel = DomainsModel.create();
const domains = ref([]);
const busy = ref(false);
const errorMessage = ref('');
const errorObject = ref({});
const overwriteDns = ref(false);
const needsOverwriteDns = ref(false);
const domain = ref('');
const subdomain = ref('');
const provider = computed(() => {
const d = domains.value.find(d => d.domain === domain.value);
return d ? d.provider : '';
});
const secondaryDomains = ref({});
const aliasDomains = ref([]);
const aliases = ref([]);
const redirects =ref([]);
const tcpPorts = ref({});
const udpPorts = ref({});
function onAddAliasDomain() {
aliasDomains.value.push({
function isNoopOrManual(domain) {
const tmp = domains.value.find(d => d.domain === domain);
return tmp ? (tmp.provider === 'noop' || tmp.provider === 'manual') : false;
}
function onAddAlias() {
aliases.value.push({
domain: '',
subdomain: ''
});
}
function onRemoveAliasDomain(index) {
aliasDomains.value.splice(index, 1);
function onRemoveAlias(index) {
aliases.value.splice(index, 1);
}
function onAddRedirect() {
redirects.value.push({
domain: '',
subdomain: ''
});
}
function onRemoveRedirect(index) {
redirects.value.splice(index, 1);
}
async function onSubmit() {
busy.value = true;
errorMessage.value = '';
errorObject.value = {};
needsOverwriteDns.value = false;
const checkForDomains = [{
@@ -46,7 +64,8 @@ async function onSubmit() {
}];
for (const d in secondaryDomains.value) checkForDomains.push({ domain: secondaryDomains.value[d].domain, subdomain: secondaryDomains.value[d].subdomain });
for (const d of aliasDomains.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
for (const d of aliases.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
for (const d of redirects.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
for (const d of checkForDomains) {
const [error, result] = await domainsModel.checkRecords(d.domain, d.subdomain);
@@ -58,14 +77,23 @@ async function onSubmit() {
if (result.needsOverwrite) return needsOverwriteDns.value = true;
}
// only use enabled ports
const ports = {};
const portsCombined = Object.assign(tcpPorts.value, udpPorts.value);
for (const env in portsCombined) {
if (portsCombined[env].enabled) {
ports[env] = portsCombined[env].value;
}
}
const data = {
overwriteDns: overwriteDns.value,
subdomain: subdomain.value,
domain: domain.value,
ports: {}, // TODO
ports: ports,
secondaryDomains: secondaryDomains.value,
redirectDomains: [], // TODO
aliasDomains: aliasDomains.value,
redirectDomains: redirects.value,
aliasDomains: aliases.value,
};
const [error] = await appsModel.configure(props.app.id, 'location', data);
@@ -95,7 +123,32 @@ onMounted(async () => {
secondaryDomains.value[d.environmentVariable].subdomain = d.subdomain;
}
aliasDomains.value = props.app.aliasDomains;
aliases.value = props.app.aliasDomains;
redirects.value = props.app.redirectDomains;
tcpPorts.value = props.app.manifest.tcpPorts;
udpPorts.value = props.app.manifest.udpPorts;
// ensure we have value property
for (const p in tcpPorts.value) {
tcpPorts.value[p].value = tcpPorts.value[p].value || tcpPorts.value[p].defaultValue;
tcpPorts.value[p].enabled = false;
}
for (const p in udpPorts.value) {
udpPorts.value[p].value = udpPorts.value[p].value || udpPorts.value[p].defaultValue;
udpPorts.value[p].enabled = false;
}
for (const p in props.app.portBindings) {
if (tcpPorts.value[p]) {
tcpPorts.value[p].value = props.app.portBindings[p].hostPort;
tcpPorts.value[p].enabled = true;
} else if (udpPorts.value[p]) {
udpPorts.value[p].value = props.app.portBindings[p].hostPort;
udpPorts.value[p].enabled = true;
}
else console.error(`Portbinding ${p} not known in manifest!`);
}
});
</script>
@@ -108,40 +161,66 @@ onMounted(async () => {
<FormGroup>
<label>{{ $t('app.location.location') }}</label>
<div class="has-error" v-if="errorMessage">{{ errorMessage }}</div>
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" v-model="subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="domain" option-key="domain" option-label="domain"/>
</InputGroup>
<p class="text-warning" v-if="isNoopOrManual(domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((subdomain ? subdomain + '.' : '') + domain) })"></p>
</FormGroup>
<FormGroup v-for="secondaryDomain in secondaryDomains" :key="secondaryDomain.containerPort">
<label :for="'secondaryDomainInput' + secondaryDomain.containerPort">{{ secondaryDomain.title }}</label>
<small>{{ secondaryDomain.description }}</small>
<FormGroup v-for="item in secondaryDomains" :key="item.containerPort">
<label :for="'secondaryDomainInput' + item.containerPort">{{ item.title }}</label>
<small>{{ item.description }}</small>
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" :id="'secondaryDomainInput' + secondaryDomain.containerPort" v-model="secondaryDomain.subdomain" :placeholder="$t('appstore.installDialog.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="secondaryDomain.domain" option-key="domain" option-label="domain"/>
<TextInput style="flex-grow: 1" :id="'secondaryDomainInput' + item.containerPort" v-model="item.subdomain" :placeholder="$t('appstore.installDialog.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="domain"/>
</InputGroup>
<p class="text-warning" v-if="isNoopOrManual(item.domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((item.subdomain ? item.subdomain + '.' : '') + item.domain) })"></p>
</FormGroup>
<PortBindings v-model:tcp="tcpPorts" v-model:udp="udpPorts" :error="errorObject"/>
<div v-if="app.manifest.multiDomain">
<label>{{ $t('app.location.aliases') }} <sup><a href="https://docs.cloudron.io/apps/#aliases" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<div style="display: flex; gap: 10px; margin-bottom: 10px" v-for="(aliasDomain, index) in aliasDomains" :key="aliasDomain">
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" v-model="aliasDomain.subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="aliasDomain.domain" option-key="domain" option-label="domain"/>
</InputGroup>
<Button danger tool :disabled="busy" icon="fa-solid fa-trash" @click="onRemoveAliasDomain(index)"/>
<div v-if="aliases.length === 0">{{ $t('app.location.noAliases') }}</div>
<div v-for="(item, index) in aliases" :key="item" style="margin-bottom: 10px">
<div style="display: flex; gap: 10px;">
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" v-model="item.subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="domain"/>
</InputGroup>
<Button danger tool :disabled="busy" icon="fa-solid fa-trash" @click="onRemoveAlias(index)"/>
</div>
<p class="text-warning" v-if="isNoopOrManual(item.domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((item.subdomain ? item.subdomain + '.' : '') + item.domain) })"></p>
</div>
<div class="actionable" v-if="!busy" @click="onAddAliasDomain()">{{ $t('app.location.addAliasAction') }}</div>
<div class="actionable" v-if="!busy" @click="onAddAlias()">{{ $t('app.location.addAliasAction') }}</div>
</div>
<div>
<label>{{ $t('app.location.redirections') }} <sup><a href="https://docs.cloudron.io/apps/#redirections" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<div v-if="redirects.length === 0">{{ $t('app.location.noRedirections') }}</div>
<div v-for="(item, index) in redirects" :key="item" style="margin-bottom: 10px;">
<div style="display: flex; gap: 10px;">
<InputGroup style="flex-grow: 1">
<TextInput style="flex-grow: 1" v-model="item.subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="domain"/>
</InputGroup>
<Button danger tool :disabled="busy" icon="fa-solid fa-trash" @click="onRemoveRedirect(index)"/>
</div>
<p class="text-warning" v-if="isNoopOrManual(item.domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((item.subdomain ? item.subdomain + '.' : '') + item.domain) })"></p>
</div>
<div class="actionable" v-if="!busy" @click="onAddRedirect()">{{ $t('app.location.addRedirectionAction') }}</div>
</div>
</fieldset>
</form>
<p class="text-bold text-warning" v-if="provider === 'noop' || provider === 'manual'" v-html="$t('appstore.installDialog.manualWarning', { location: ((subdomain ? subdomain + '.' : '') + domain) })"></p>
<div class="has-error" v-if="errorMessage">{{ errorMessage }}</div>
<br/>