|
|
|
|
@@ -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/>
|
|
|
|
|
|
|
|
|
|
|