refactor backup info into separate component
app backups now have the size and duration information
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { ClipboardAction, TableView, Dialog } from '@cloudron/pankow';
|
||||
import { prettyDuration, prettyLongDate, prettyFileSize } from '@cloudron/pankow/utils';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
import BackupsModel from '../models/BackupsModel.js';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const backupsModel = BackupsModel.create();
|
||||
|
||||
const busy = ref(true);
|
||||
|
||||
const backupContentTableColumns = {
|
||||
label: {
|
||||
label: t('backups.listing.contents'),
|
||||
sort: true,
|
||||
},
|
||||
fileCount: {
|
||||
label: t('backup.target.fileCount'),
|
||||
sort(a, b, A, B) {
|
||||
return A.stats?.upload?.fileCount - B.stats?.upload?.fileCount;
|
||||
},
|
||||
},
|
||||
size: {
|
||||
label: t('backup.target.size'),
|
||||
sort(a, b, A , B) {
|
||||
return A.stats?.upload?.size - B.stats?.upload?.size;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const backup = ref({ contents: [], validStats: false });
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
defineExpose({
|
||||
async open(b) {
|
||||
backup.value = JSON.parse(JSON.stringify(b)); // make a copy
|
||||
backup.value.contents = [];
|
||||
backup.value.validStats = false; // old cloudron version had invalid stats
|
||||
busy.value = true;
|
||||
|
||||
dialog.value.open();
|
||||
|
||||
if (backup.value.type === 'app') {
|
||||
backup.value.validStats = backup.value.stats?.upload && backup.value.stats?.copy;
|
||||
busy.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// amend detailed app info
|
||||
const appsById = {};
|
||||
|
||||
const [appsError, apps] = await appsModel.list();
|
||||
if (appsError) console.error('Failed to get apps list:', appsError);
|
||||
|
||||
(apps || []).forEach(function (app) {
|
||||
appsById[app.id] = app;
|
||||
});
|
||||
|
||||
for (const contentId of backup.value.dependsOn) {
|
||||
const match = contentId.match(/(mail|app)_(.*?)_.*/); // *? means non-greedy
|
||||
if (!match) continue;
|
||||
const [error, result] = await backupsModel.get(contentId);
|
||||
if (error) console.error(error);
|
||||
const content = { id: null, label: null, fqdn: null, stats: null };
|
||||
content.stats = result.stats;
|
||||
if (match[1] === 'mail') {
|
||||
content.id = 'mail';
|
||||
content.label = 'Mail Server';
|
||||
} else {
|
||||
const app = appsById[match[2]];
|
||||
if (app) {
|
||||
content.id = app.id;
|
||||
content.label = app.label;
|
||||
content.fqdn = app.fqdn;
|
||||
} else { // uninstalled app
|
||||
content.id = match[2];
|
||||
}
|
||||
}
|
||||
backup.value.contents.push(content);
|
||||
}
|
||||
|
||||
backup.value.validStats = backup.value.stats?.aggregatedUpload && backup.value.stats?.aggregatedCopy;
|
||||
busy.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog ref="dialog"
|
||||
:title="$t('backups.backupDetails.title')"
|
||||
:reject-label="$t('main.dialog.close')"
|
||||
>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.id') }}</div>
|
||||
<div class="info-value">{{ backup.id }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupEdit.label') }}</div>
|
||||
<div class="info-value">{{ backup.label }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupEdit.remotePath') }}</div>
|
||||
<div class="info-value">
|
||||
<div>
|
||||
{{ backup.remotePath }}
|
||||
<ClipboardAction plain :value="backup.remotePath"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.date') }}</div>
|
||||
<div class="info-value">{{ prettyLongDate(backup.creationTime) }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.version') }}</div>
|
||||
<div class="info-value">{{ backup.packageVersion }}</div>
|
||||
</div>
|
||||
<div class="info-row" v-if="backup.validStats">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.size') }}</div>
|
||||
<div v-if="backup.type === 'box'" class="info-value">{{ prettyFileSize(backup.stats.aggregatedUpload.size) }} | {{ backup.stats.aggregatedUpload.fileCount }} file(s)</div>
|
||||
<div v-else class="info-value">{{ prettyFileSize(backup.stats.upload.size) }} | {{ backup.stats.upload.fileCount }} file(s)</div>
|
||||
</div>
|
||||
<div class="info-row" v-if="backup.validStats">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.duration') }}</div>
|
||||
<div v-if="backup.type === 'box'" class="info-value">{{ prettyDuration(backup.stats.aggregatedUpload.duration + backup.stats.aggregatedCopy.duration) }}</div>
|
||||
<div v-else class="info-value">{{ prettyDuration(backup.stats.upload.duration + backup.stats.copy.duration) }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="backup.type === 'box'">
|
||||
<br/>
|
||||
<div>{{ $t('backups.backupDetails.list', { appCount: backup.appCount }) }}:</div>
|
||||
<br/>
|
||||
|
||||
<TableView :columns="backupContentTableColumns" :model="backup.contents" :busy="busy">
|
||||
<template #label="content">
|
||||
<a v-if="content.id === 'mail'" href="/#/mailboxes">{{ content.label }}</a>
|
||||
<a v-else-if="content.fqdn" :href="`/#/app/${content.id}/backups`">{{ content.label || content.fqdn }}</a>
|
||||
<a v-else :href="`/#/system-eventlog?search=${content.id}`">{{ content.id }}</a>
|
||||
</template>
|
||||
<template #fileCount="content">
|
||||
<div v-if="content.stats?.upload" style="text-align: right">{{ content.stats.upload.fileCount }}</div>
|
||||
<div v-else style="text-align: right">-</div>
|
||||
</template>
|
||||
<!-- <td>{{ prettyDuration(content.stats.upload.duration | content.stats.copy.duration) }}</td> -->
|
||||
<template #size="content">
|
||||
<div v-if="content.stats?.upload" style="text-align: right">{{ prettyFileSize(content.stats.upload.size) }}</div>
|
||||
<div v-else style="text-align: right">-</div>
|
||||
</template>
|
||||
</TableView>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -5,20 +5,19 @@ const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { Button, ClipboardAction, Menu, FormGroup, TextInput, Checkbox, TableView, Dialog } from '@cloudron/pankow';
|
||||
import { prettyDuration, prettyLongDate, prettyFileSize } from '@cloudron/pankow/utils';
|
||||
import { Button, Menu, FormGroup, TextInput, Checkbox, TableView, Dialog } from '@cloudron/pankow';
|
||||
import { prettyLongDate, prettyFileSize } from '@cloudron/pankow/utils';
|
||||
import { TASK_TYPES } from '../constants.js';
|
||||
import Section from '../components/Section.vue';
|
||||
import BackupsModel from '../models/BackupsModel.js';
|
||||
import BackupSitesModel from '../models/BackupSitesModel.js';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
import DashboardModel from '../models/DashboardModel.js';
|
||||
import { download } from '../utils.js';
|
||||
import BackupInfoDialog from './BackupInfoDialog.vue';
|
||||
|
||||
const backupsModel = BackupsModel.create();
|
||||
const backupSitesModel = BackupSitesModel.create();
|
||||
const appsModel = AppsModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
const dashboardModel = DashboardModel.create();
|
||||
|
||||
@@ -56,25 +55,6 @@ const columns = {
|
||||
actions: {}
|
||||
};
|
||||
|
||||
const backupContentTableColumns = {
|
||||
label: {
|
||||
label: t('backups.listing.contents'),
|
||||
sort: true,
|
||||
},
|
||||
fileCount: {
|
||||
label: t('backup.target.fileCount'),
|
||||
sort(a, b, A, B) {
|
||||
return A.stats?.upload?.fileCount - B.stats?.upload?.fileCount;
|
||||
},
|
||||
},
|
||||
size: {
|
||||
label: t('backup.target.size'),
|
||||
sort(a, b, A , B) {
|
||||
return A.stats?.upload?.size - B.stats?.upload?.size;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const actionMenuModel = ref([]);
|
||||
const actionMenuElement = useTemplateRef('actionMenuElement');
|
||||
function onActionMenu(backup, event) {
|
||||
@@ -200,51 +180,9 @@ async function onDownloadConfig(backup) {
|
||||
download(filename, JSON.stringify(backupConfig, null, 4));
|
||||
}
|
||||
|
||||
// backups info dialog
|
||||
const infoDialog = useTemplateRef('infoDialog');
|
||||
const infoDialogBusy = ref(true);
|
||||
const infoBackup = ref({ contents: [] });
|
||||
async function onInfo(backup) {
|
||||
infoBackup.value = backup;
|
||||
infoBackup.value.contents = [];
|
||||
infoDialogBusy.value = true;
|
||||
|
||||
infoDialog.value.open();
|
||||
|
||||
// amend detailed app info
|
||||
const appsById = {};
|
||||
|
||||
const [appsError, apps] = await appsModel.list();
|
||||
if (appsError) console.error('Failed to get apps list:', appsError);
|
||||
|
||||
(apps || []).forEach(function (app) {
|
||||
appsById[app.id] = app;
|
||||
});
|
||||
|
||||
for (const contentId of infoBackup.value.dependsOn) {
|
||||
const match = contentId.match(/(mail|app)_(.*?)_.*/); // *? means non-greedy
|
||||
if (!match) continue;
|
||||
const [error, backup] = await backupsModel.get(contentId);
|
||||
if (error) console.error(error);
|
||||
const content = { id: null, label: null, fqdn: null, stats: null };
|
||||
content.stats = backup.stats;
|
||||
if (match[1] === 'mail') {
|
||||
content.id = 'mail';
|
||||
content.label = 'Mail Server';
|
||||
} else {
|
||||
const app = appsById[match[2]];
|
||||
if (app) {
|
||||
content.id = app.id;
|
||||
content.label = app.label;
|
||||
content.fqdn = app.fqdn;
|
||||
} else { // uninstalled app
|
||||
content.id = match[2];
|
||||
}
|
||||
}
|
||||
infoBackup.value.contents.push(content);
|
||||
}
|
||||
|
||||
infoDialogBusy.value = false;
|
||||
infoDialog.value.open(backup);
|
||||
}
|
||||
|
||||
// edit backups dialog
|
||||
@@ -302,65 +240,7 @@ defineExpose({ refresh });
|
||||
<Section :title="$t('backups.listing.title')">
|
||||
<Menu ref="actionMenuElement" :model="actionMenuModel" />
|
||||
|
||||
<Dialog ref="infoDialog"
|
||||
:title="$t('backups.backupDetails.title')"
|
||||
:reject-label="$t('main.dialog.close')"
|
||||
>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.id') }}</div>
|
||||
<div class="info-value">{{ infoBackup.id }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupEdit.label') }}</div>
|
||||
<div class="info-value">{{ infoBackup.label }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupEdit.remotePath') }}</div>
|
||||
<div class="info-value">
|
||||
<div>
|
||||
{{ infoBackup.remotePath }}
|
||||
<ClipboardAction plain :value="infoBackup.remotePath"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.date') }}</div>
|
||||
<div class="info-value">{{ prettyLongDate(infoBackup.creationTime) }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.version') }}</div>
|
||||
<div class="info-value">{{ infoBackup.packageVersion }}</div>
|
||||
</div>
|
||||
<div class="info-row" v-if="infoBackup.stats?.aggregatedUpload">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.size') }}</div>
|
||||
<div class="info-value">{{ prettyFileSize(infoBackup.stats.aggregatedUpload.size) }} | {{ infoBackup.stats.aggregatedUpload.fileCount }} file(s)</div>
|
||||
</div>
|
||||
<div class="info-row" v-if="infoBackup.stats?.aggregatedCopy">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.duration') }}</div>
|
||||
<div class="info-value">{{ prettyDuration(infoBackup.stats.aggregatedUpload.duration + infoBackup.stats.aggregatedCopy.duration) }}</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<div>{{ $t('backups.backupDetails.list', { appCount: infoBackup.appCount }) }}:</div>
|
||||
<br/>
|
||||
|
||||
<TableView :columns="backupContentTableColumns" :model="infoBackup.contents" :busy="infoDialogBusy">
|
||||
<template #label="content">
|
||||
<a v-if="content.id === 'mail'" href="/#/mailboxes">{{ content.label }}</a>
|
||||
<a v-else-if="content.fqdn" :href="`/#/app/${content.id}/backups`">{{ content.label || content.fqdn }}</a>
|
||||
<a v-else :href="`/#/system-eventlog?search=${content.id}`">{{ content.id }}</a>
|
||||
</template>
|
||||
<template #fileCount="content">
|
||||
<div v-if="content.stats?.upload" style="text-align: right">{{ content.stats.upload.fileCount }}</div>
|
||||
<div v-else style="text-align: right">-</div>
|
||||
</template>
|
||||
<!-- <td>{{ prettyDuration(content.stats.upload.duration | content.stats.copy.duration) }}</td> -->
|
||||
<template #size="content">
|
||||
<div v-if="content.stats?.upload" style="text-align: right">{{ prettyFileSize(content.stats.upload.size) }}</div>
|
||||
<div v-else style="text-align: right">-</div>
|
||||
</template>
|
||||
</TableView>
|
||||
</Dialog>
|
||||
<BackupInfoDialog ref="infoDialog" />
|
||||
|
||||
<Dialog ref="editDialog"
|
||||
:title="$t('backups.backupEdit.title')"
|
||||
|
||||
@@ -16,6 +16,7 @@ import AppsModel from '../../models/AppsModel.js';
|
||||
import BackupSitesModel from '../../models/BackupSitesModel.js';
|
||||
import TasksModel from '../../models/TasksModel.js';
|
||||
import { TASK_TYPES } from '../../constants.js';
|
||||
import BackupInfoDialog from '../BackupInfoDialog.vue';
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const backupSitesModel = BackupSitesModel.create();
|
||||
@@ -103,7 +104,6 @@ function onActionMenu(backup, event) {
|
||||
|
||||
const busy = ref(true);
|
||||
const errorMessage = ref('');
|
||||
const infoBackup = ref({});
|
||||
const editBusy = ref(false);
|
||||
const editError = ref('');
|
||||
const editBackup = ref({});
|
||||
@@ -189,8 +189,7 @@ async function onStopBackup() {
|
||||
}
|
||||
|
||||
function onInfo(backup) {
|
||||
infoBackup.value = backup;
|
||||
infoDialog.value.open();
|
||||
infoDialog.value.open(backup);
|
||||
}
|
||||
|
||||
function onEdit(backup) {
|
||||
@@ -302,29 +301,7 @@ onMounted(async () => {
|
||||
<AppRestoreDialog ref="cloneDialog"/>
|
||||
<AppImportDialog ref="importDialog"/>
|
||||
|
||||
<Dialog ref="infoDialog"
|
||||
:title="$t('backups.backupDetails.title')"
|
||||
:reject-label="$t('main.dialog.close')"
|
||||
>
|
||||
<div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.id') }}</div>
|
||||
<div class="info-value">{{ infoBackup.id }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupEdit.remotePath') }}</div>
|
||||
<div class="info-value">{{ infoBackup.remotePath }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.date') }}</div>
|
||||
<div class="info-value">{{ prettyLongDate(infoBackup.creationTime) }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('backups.backupDetails.version') }}</div>
|
||||
<div class="info-value">{{ infoBackup.packageVersion }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<BackupInfoDialog ref="infoDialog" />
|
||||
|
||||
<Dialog ref="editDialog"
|
||||
:title="$t('backups.backupEdit.title')"
|
||||
|
||||
Reference in New Issue
Block a user