259 lines
10 KiB
Vue
259 lines
10 KiB
Vue
<script setup>
|
|
|
|
import { ref, onMounted, computed } from 'vue';
|
|
import { Button, SingleSelect, InputGroup, FormGroup, TextInput, Checkbox } from '@cloudron/pankow';
|
|
import { isValidDomain } from '@cloudron/pankow/utils';
|
|
import { ISTATES } from '../../constants.js';
|
|
import PortBindings from '../PortBindings.vue';
|
|
import AppsModel from '../../models/AppsModel.js';
|
|
import DomainsModel from '../../models/DomainsModel.js';
|
|
|
|
const props = defineProps([ 'app' ]);
|
|
|
|
const appsModel = AppsModel.create();
|
|
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 secondaryDomains = ref({});
|
|
const aliases = ref([]);
|
|
const redirects =ref([]);
|
|
const tcpPorts = ref({});
|
|
const udpPorts = ref({});
|
|
const domainProvider = computed(() => {
|
|
const tmp = domains.value.find((d) => d.domain === domain.value);
|
|
return tmp ? tmp.provider : '';
|
|
});
|
|
|
|
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: domains.value[0].domain,
|
|
subdomain: ''
|
|
});
|
|
}
|
|
|
|
function onRemoveAlias(index) {
|
|
aliases.value.splice(index, 1);
|
|
}
|
|
|
|
function onAddRedirect() {
|
|
redirects.value.push({
|
|
domain: domains.value[0].domain,
|
|
subdomain: ''
|
|
});
|
|
}
|
|
|
|
const formValid = computed(() => {
|
|
if (!domain.value) return false;
|
|
|
|
const checkForDomains = [{
|
|
domain: domain.value,
|
|
subdomain: subdomain.value,
|
|
}];
|
|
|
|
for (const d in secondaryDomains.value) checkForDomains.push({ domain: secondaryDomains.value[d].domain, subdomain: secondaryDomains.value[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 });
|
|
|
|
if (checkForDomains.find(d => !isValidDomain((d.subdomain ? (d.subdomain + '.') : '') + d.domain))) return false;
|
|
|
|
return true;
|
|
});
|
|
|
|
function onRemoveRedirect(index) {
|
|
redirects.value.splice(index, 1);
|
|
}
|
|
|
|
async function onSubmit() {
|
|
busy.value = true;
|
|
errorMessage.value = '';
|
|
errorObject.value = {};
|
|
needsOverwriteDns.value = false;
|
|
|
|
const checkForDomains = [{
|
|
domain: domain.value,
|
|
subdomain: subdomain.value,
|
|
}];
|
|
|
|
for (const d in secondaryDomains.value) checkForDomains.push({ domain: secondaryDomains.value[d].domain, subdomain: secondaryDomains.value[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);
|
|
if (error) {
|
|
errorMessage.value = error.body ? error.body.message : 'Internal error';
|
|
busy.value = false;
|
|
return console.error(error);
|
|
}
|
|
|
|
if (result.needsOverwrite && !overwriteDns.value) {
|
|
busy.value = false;
|
|
needsOverwriteDns.value = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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: ports,
|
|
secondaryDomains: secondaryDomains.value,
|
|
redirectDomains: redirects.value,
|
|
aliasDomains: aliases.value,
|
|
};
|
|
|
|
const [error] = await appsModel.configure(props.app.id, 'location', data);
|
|
if (error) {
|
|
errorMessage.value = error.body ? error.body.message : 'Internal error';
|
|
busy.value = false;
|
|
return console.error(error);
|
|
}
|
|
|
|
busy.value = false;
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const [error, result] = await domainsModel.list();
|
|
if (error) return console.error(error);
|
|
|
|
domains.value = result;
|
|
|
|
subdomain.value = props.app.subdomain;
|
|
domain.value = props.app.domain;
|
|
|
|
// map manifest httpPorts into existing secondary domains from app
|
|
secondaryDomains.value = props.app.manifest.httpPorts || {};
|
|
for (const d of props.app.secondaryDomains) {
|
|
if (!secondaryDomains.value[d.environmentVariable]) continue;
|
|
secondaryDomains.value[d.environmentVariable].domain = d.domain;
|
|
secondaryDomains.value[d.environmentVariable].subdomain = d.subdomain;
|
|
}
|
|
|
|
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>
|
|
|
|
<template>
|
|
<div>
|
|
<form @submit.prevent="onSubmit()" autocomplete="off" novalidate>
|
|
<fieldset :disabled="busy">
|
|
<input type="submit" style="display: none;" :disabled="(app.error && app.error.details.installationState !== ISTATES.PENDING_LOCATION_CHANGE) || app.taskId || !formValid"/>
|
|
|
|
<FormGroup>
|
|
<label>{{ $t('app.location.location') }}</label>
|
|
|
|
<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" :search-threshold="10"/>
|
|
</InputGroup>
|
|
<div class="warning-label" v-if="isNoopOrManual(domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((subdomain ? subdomain + '.' : '') + domain) })"></div>
|
|
</FormGroup>
|
|
|
|
<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' + 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" :search-threshold="10"/>
|
|
</InputGroup>
|
|
<div class="warning-label" v-if="isNoopOrManual(item.domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((item.subdomain ? item.subdomain + '.' : '') + item.domain) })"></div>
|
|
</FormGroup>
|
|
|
|
<PortBindings v-model:tcp="tcpPorts" v-model:udp="udpPorts" :error="errorObject" :domain-provider="domainProvider"/>
|
|
|
|
<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 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" :search-threshold="10"/>
|
|
</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="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" :search-threshold="10"/>
|
|
</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>
|
|
|
|
<div class="has-error" v-if="errorMessage">{{ errorMessage }}</div>
|
|
|
|
<br/>
|
|
<Checkbox v-if="needsOverwriteDns" v-model="overwriteDns" :label="$t('app.location.dnsoverwrite')"/>
|
|
|
|
<Button @click="onSubmit()" :loading="busy" :disabled="busy || (app.error && app.error.details.installationState !== ISTATES.PENDING_LOCATION_CHANGE) || app.taskId || !formValid">{{ $t('app.location.saveAction') }}</Button>
|
|
</div>
|
|
</template>
|