Reuse app restore dialog for app clone

This commit is contained in:
Johannes Zellner
2025-04-25 00:00:06 +02:00
parent 28372af5a9
commit 4c662a30ef
5 changed files with 132 additions and 83 deletions

View File

@@ -6,21 +6,24 @@ import { ref, useTemplateRef, computed } from 'vue';
import { InputGroup, FormGroup, TextInput, SingleSelect, Dialog } from 'pankow';
import { prettyLongDate } from 'pankow/utils';
import PortBindings from '../components/PortBindings.vue';
import AppsModel from '../models/AppsModel.js';
import ArchivesModel from '../models/ArchivesModel.js';
import DomainsModel from '../models/DomainsModel.js';
const appsModel = AppsModel.create();
const archivesModel = ArchivesModel.create();
const domainsModel = DomainsModel.create();
const restoreDialog = useTemplateRef('restoreDialog');
const appId = ref(null);
const dialog = useTemplateRef('dialog');
const restoreArchive = ref({});
const restoreFqdn = ref('');
const fqdn = ref('');
const restoreManifest = ref({});
const restoreLocation = ref('');
const restoreDomain = ref('');
const restoreNeedsOverwrite = ref(false);
const restoreBusy = ref(false);
const restoreError = ref({});
const needsOverwrite = ref(false);
const busy = ref(false);
const formError = ref({});
const restoreTcpPorts = ref({});
const restoreUdpPorts = ref({});
const domains = ref([]);
@@ -30,10 +33,85 @@ const restoreDomainProvider = computed(() => {
return tmp ? tmp.provider : '';
});
async function onSubmit() {
busy.value = true;
formError.value = {};
const secondaryDomains = {};
for (const env in restoreSecondaryDomains.value) {
secondaryDomains[env] = {
subdomain: restoreSecondaryDomains.value[env].subdomain,
domain: restoreSecondaryDomains.value[env].domain
};
}
// only use enabled ports
const finalPorts = {};
for (const env in restoreTcpPorts.value) {
if (restoreTcpPorts.value[env].enabled) {
finalPorts[env] = restoreTcpPorts.value[env].value;
}
}
for (const env in restoreUdpPorts.value) {
if (restoreUdpPorts.value[env].enabled) {
finalPorts[env] = restoreUdpPorts.value[env].value;
}
}
const data = {
subdomain: restoreLocation.value,
domain: restoreDomain.value,
secondaryDomains,
ports: finalPorts,
overwriteDns: needsOverwrite.value,
};
// we could come from restoring an app archive or cloning an app from a backup
if (appId.value) {
console.log('we are cloning!!!', restoreArchive.value, appId.value);
data.backupId = restoreArchive.value.id;
const [error] = await appsModel.clone(appId.value, data);
if (error) {
if (error.type === 'externally_exists') {
formError.value.dnsInUse = 'Some DNS records exist. Submit again to overwrite.';
needsOverwrite.value = true;
} else if (error.body) {
formError.value.generic = error.body.message;
} else {
formError.value.generic = 'Internal error';
console.error(error);
}
busy.value = false;
return;
}
} else {
const [error] = await archivesModel.restore(restoreArchive.value.id, data);
if (error) {
if (error.type === 'externally_exists') {
formError.value.dnsInUse = 'Some DNS records exist. Submit again to overwrite.';
needsOverwrite.value = true;
} else if (error.body) {
formError.value.generic = error.body.message;
} else {
formError.value.generic = 'Internal error';
console.error(error);
}
busy.value = false;
return;
}
}
busy.value = false;
dialog.value.close();
window.location.href = '/#/apps';
}
defineExpose({
async open(archive) {
restoreBusy.value = false;
restoreError.value = {};
async open(archive, cloneFromAppId = null) {
appId.value = cloneFromAppId;
busy.value = false;
formError.value = {};
const [error, result] = await domainsModel.list();
if (error) return console.error(error);
@@ -54,9 +132,9 @@ defineExpose({
const d = domains.value.find(function (d) { return app.domain === d.domain; });
restoreDomain.value = d ? d.domain : domains.value[0].domain; // try to pre-select the app's domain
restoreSecondaryDomains.value = {};
restoreNeedsOverwrite.value = false;
needsOverwrite.value = false;
restoreArchive.value = archive;
restoreFqdn.value = archive.appConfig?.fqdn || '-';
fqdn.value = archive.appConfig?.fqdn || '-';
restoreManifest.value = manifest;
const httpPorts = manifest.httpPorts || {};
@@ -105,83 +183,30 @@ defineExpose({
}
}
restoreDialog.value.open();
dialog.value.open();
}
});
async function onRestoreSubmit() {
restoreBusy.value = true;
restoreError.value = {};
const secondaryDomains = {};
for (const env in restoreSecondaryDomains.value) {
secondaryDomains[env] = {
subdomain: restoreSecondaryDomains.value[env].subdomain,
domain: restoreSecondaryDomains.value[env].domain
};
}
// only use enabled ports
const finalPorts = {};
for (const env in restoreTcpPorts.value) {
if (restoreTcpPorts.value[env].enabled) {
finalPorts[env] = restoreTcpPorts.value[env].value;
}
}
for (const env in restoreUdpPorts.value) {
if (restoreUdpPorts.value[env].enabled) {
finalPorts[env] = restoreUdpPorts.value[env].value;
}
}
const data = {
subdomain: restoreLocation.value,
domain: restoreDomain.value,
secondaryDomains,
ports: finalPorts,
overwriteDns: restoreNeedsOverwrite.value,
};
const [error] = await archivesModel.restore(restoreArchive.value.id, data);
if (error) {
if (error.type === 'externally_exists') {
restoreError.value.dnsInUse = 'Some DNS records exist. Submit again to overwrite.';
restoreNeedsOverwrite.value = true;
} else if (error.body) {
restoreError.value.generic = error.body.message;
} else {
restoreError.value.generic = 'Internal error';
console.error(error);
}
restoreBusy.value = false;
return;
}
restoreBusy.value = false;
restoreDialog.value.close();
window.location.href = '/#/apps';
}
</script>
<template>
<Dialog ref="restoreDialog"
:title="$t('backups.restoreArchiveDialog.title')"
<Dialog ref="dialog"
:title="appId ? $t('backups.restoreArchiveDialog.title') : $t('app.cloneDialog.title', { app: fqdn })"
reject-style="secondary"
:reject-label="restoreBusy ? '' : $t('main.dialog.cancel')"
:confirm-label="$t(restoreNeedsOverwrite ? 'backups.restoreArchiveDialog.restoreActionOverwrite' : 'backups.restoreArchiveDialog.restoreAction')"
:confirm-busy="restoreBusy"
:confirm-active="!restoreBusy"
@confirm="onRestoreSubmit()"
:reject-label="busy ? '' : $t('main.dialog.cancel')"
:confirm-label="appId ? $t(needsOverwrite ? 'backups.restoreArchiveDialog.restoreActionOverwrite' : 'backups.restoreArchiveDialog.restoreAction') : $t(needsOverwrite ? 'app.restoreDialog.cloneActionOverwrite' : 'app.restoreDialog.cloneAction')"
:confirm-busy="busy"
:confirm-active="!busy"
@confirm="onSubmit()"
>
<p v-html="$t('backups.restoreArchiveDialog.description', { appId: restoreManifest.id, fqdn: restoreFqdn, creationTime: prettyLongDate(restoreArchive.creationTime) })"></p>
<p v-if="appId" v-html="$t('backups.restoreArchiveDialog.description', { appId: restoreManifest.id, fqdn, creationTime: prettyLongDate(restoreArchive.creationTime) })"></p>
<p v-else v-html="$t('app.cloneDialog.description', { creationTime: prettyLongDate(restoreArchive.creationTime), packageVersion: restoreArchive.packageVersion })"></p>
<div class="error-label" v-show="restoreError.generic">{{ restoreError.generic }}</div>
<div class="error-label" v-show="restoreError.dnsInUse">{{ restoreError.dnsInUse }}</div>
<div class="error-label" v-show="restoreError.port">{{ restoreError.port }}</div>
<div class="error-label" v-show="formError.generic">{{ formError.generic }}</div>
<div class="error-label" v-show="formError.dnsInUse">{{ formError.dnsInUse }}</div>
<div class="error-label" v-show="formError.port">{{ formError.port }}</div>
<form @submit.prevent="onRestoreSubmit()" autocomplete="off">
<form @submit.prevent="onSubmit()" autocomplete="off">
<fieldset>
<FormGroup>
<label for="locationInput">{{ $t('app.cloneDialog.location') }}</label>
@@ -201,7 +226,7 @@ async function onRestoreSubmit() {
</InputGroup>
</FormGroup>
<PortBindings v-model:tcp="restoreTcpPorts" v-model:udp="restoreUdpPorts" :error="restoreError" :domain-provider="restoreDomainProvider"/>
<PortBindings v-model:tcp="restoreTcpPorts" v-model:udp="restoreUdpPorts" :error="formError" :domain-provider="restoreDomainProvider"/>
</fieldset>
</form>
</Dialog>

View File

@@ -9,6 +9,7 @@ import { Icon, Button, Switch, Checkbox, FormGroup, TextInput, TableView, Button
import { prettyLongDate } from 'pankow/utils';
import { API_ORIGIN, SECRET_PLACEHOLDER } from '../../constants.js';
import { download } from '../../utils.js';
import AppRestoreDialog from '../AppRestoreDialog.vue';
import SettingsItem from '../SettingsItem.vue';
import AppsModel from '../../models/AppsModel.js';
import BackupsModel from '../../models/BackupsModel.js';
@@ -79,8 +80,6 @@ async function onCreate() {
const [error, result] = await appsModel.backup(props.app.id);
if (error) return console.error(error);
console.log(result)
setTimeout(() => createBusy.value = false, 2000);
await waitForTask(result);
@@ -163,8 +162,9 @@ function onImport() {
// TODO
}
const cloneDialog = useTemplateRef('cloneDialog');
function onClone(backup) {
// TODO this is essentially the same as app archive restore!
cloneDialog.value.open(backup, props.app.id);
}
async function refresh() {
@@ -186,6 +186,7 @@ onMounted(async () => {
<template>
<div>
<AppRestoreDialog ref="cloneDialog"/>
<Dialog ref="editDialog"
:title="$t('backups.backupEdit.title')"
:reject-label="$t('main.dialog.cancel')"