diff --git a/dashboard/src/Index.vue b/dashboard/src/Index.vue index 9a4b928b6..feddc881f 100644 --- a/dashboard/src/Index.vue +++ b/dashboard/src/Index.vue @@ -23,6 +23,7 @@ import AppearanceView from './views/AppearanceView.vue'; import AppstoreView from './views/AppstoreView.vue'; import BackupTargetsView from './views/BackupTargetsView.vue'; import BackupAppArchivesView from './views/BackupAppArchivesView.vue'; +import BackupListView from './views/BackupListView.vue'; import CloudronAccountView from './views/CloudronAccountView.vue'; import DomainsView from './views/DomainsView.vue'; import EmailDomainView from './views/EmailDomainView.vue'; @@ -51,6 +52,7 @@ const VIEWS = { APPS: 'apps', APPSTORE: 'appstore', BACKUP_TARGETS: 'backup-targets', + BACKUP_LIST: 'backup-list', BACKUP_APP_ARCHIVES: 'backup-app-archives', CLOUDRON_ACCOUNT: 'cloudron-account', DOMAINS: 'domains', @@ -173,6 +175,8 @@ function onHashChange() { view.value = VIEWS.APPEARANCE; } else if (v === VIEWS.BACKUP_TARGETS && profile.value.isAtLeastAdmin) { view.value = VIEWS.BACKUP_TARGETS; + } else if (v === VIEWS.BACKUP_LIST && profile.value.isAtLeastAdmin) { + view.value = VIEWS.BACKUP_LIST; } else if (v === VIEWS.BACKUP_APP_ARCHIVES && profile.value.isAtLeastAdmin) { view.value = VIEWS.BACKUP_APP_ARCHIVES; } else if (v === VIEWS.CLOUDRON_ACCOUNT && profile.value.isAtLeastOwner) { @@ -306,7 +310,8 @@ onMounted(async () => { @@ -360,6 +365,7 @@ onMounted(async () => { + diff --git a/dashboard/src/components/BackupList.vue b/dashboard/src/components/BackupList.vue index d4bd4a7b7..f3f602d7f 100644 --- a/dashboard/src/components/BackupList.vue +++ b/dashboard/src/components/BackupList.vue @@ -7,19 +7,17 @@ const t = i18n.t; import { ref, onMounted, useTemplateRef } from 'vue'; import { Button, ButtonGroup, ProgressBar, FormGroup, TextInput, Checkbox, TableView, Dialog } from '@cloudron/pankow'; import { prettyLongDate, copyToClipboard } from '@cloudron/pankow/utils'; -import { TASK_TYPES, SECRET_PLACEHOLDER } from '../constants.js'; +import { TASK_TYPES } from '../constants.js'; import Section from '../components/Section.vue'; import BackupsModel from '../models/BackupsModel.js'; +import BackupTargetsModel from '../models/BackupTargetsModel.js'; import AppsModel from '../models/AppsModel.js'; import TasksModel from '../models/TasksModel.js'; import DashboardModel from '../models/DashboardModel.js'; import { download } from '../utils.js'; -const props = defineProps({ - config: Object -}); - const backupsModel = BackupsModel.create(); +const backupTargetsModel = BackupTargetsModel.create(); const appsModel = AppsModel.create(); const tasksModel = TasksModel.create(); const dashboardModel = DashboardModel.create(); @@ -167,24 +165,23 @@ async function onStopBackup() { } async function onDownloadConfig(backup) { - const [error, result] = await dashboardModel.config(); + const [error, dashboardConfig] = await dashboardModel.config(); if (error) return console.error(error); - // secrets and tokens already come with placeholder characters we remove them + const [backupTargetError, backupTarget] = await backupTargetsModel.get(backup.targetId); + if (backupTargetError) return console.error(backupTargetError); + const tmp = { - remotePath: backup.remotePath, - encrypted: !!props.config.password // we add this just to help the import UI + remotePath: backup.remotePath }; + for (const k of ['provider', 'config', 'limits', 'format', 'encrypted', 'encryptedFilenames']) { + tmp[k] = backupTarget[k]; + } - Object.keys(props.config).forEach((k) => { - if (props.config[k] !== SECRET_PLACEHOLDER) tmp[k] = props.config[k]; - }); - - const filename = `${result.adminFqdn}-backup-config-${(new Date(backup.creationTime)).toISOString().split('T')[0]}.json`; + const filename = `${dashboardConfig.adminFqdn}-backup-config-${(new Date(backup.creationTime)).toISOString().split('T')[0]}.json`; download(filename, JSON.stringify(tmp, null, 4)); } - // backups info dialog const infoDialog = useTemplateRef('infoDialog'); const infoBackup = ref({ contents: [] }); diff --git a/dashboard/src/components/BackupTargetDialog.vue b/dashboard/src/components/BackupTargetDialog.vue index 75abbb0eb..cf79188f5 100644 --- a/dashboard/src/components/BackupTargetDialog.vue +++ b/dashboard/src/components/BackupTargetDialog.vue @@ -17,7 +17,7 @@ const systemModel = SystemModel.create(); const dialog = useTemplateRef('dialog'); const form = useTemplateRef('form'); const target = ref({}); -const label = ref(''); +const name = ref(''); const encrypted = ref(false); const encryptedFilenames = ref(false); const formError = ref({}); @@ -146,7 +146,7 @@ async function onSubmitAdd() { formError.value = {}; busy.value = true; - const [error, targetId] = await backupTargetsModel.add(label.value, format.value, provider.value, data, schedulePattern, retention, limitsConfig, encryptionPassword, encryptedFilenames); + const [error, targetId] = await backupTargetsModel.add(name.value, format.value, provider.value, data, schedulePattern, retention, limitsConfig, encryptionPassword, encryptedFilenames); if (error) { formError.value.generic = error.body ? error.body.message : 'Internal error'; busy.value = false; @@ -168,7 +168,14 @@ async function onSubmitAdd() { async function onSubmitEdit() { // TODO set config (eg. provider config passwords) - // TODO set label somehow? + + // name + let [error] = await backupTargetsModel.setName(target.value.id, name.value); + if (error) { + formError.value.generic = 'Failed to set name'; + busy.value = false; + return console.error(error); + } // limits const limitsConfig = { @@ -180,7 +187,7 @@ async function onSubmitEdit() { // deleteConcurrency: parseInt(providerConfig.value.limits.deleteConcurrency), }; - const [error] = await backupTargetsModel.setLimits(target.value.id, limitsConfig); + [error] = await backupTargetsModel.setLimits(target.value.id, limitsConfig); if (error) { formError.value.generic = 'Failed to set limits'; busy.value = false; @@ -227,7 +234,7 @@ defineExpose({ formError.value = {}; busy.value = false; - label.value = t?.label || ''; + name.value = t?.name || ''; provider.value = t?.provider || ''; format.value = t?.format || ''; isPrimary.value = t?.primary || false; @@ -273,14 +280,14 @@ defineExpose({ - - - - + + + + diff --git a/dashboard/src/models/BackupTargetsModel.js b/dashboard/src/models/BackupTargetsModel.js index e47298463..fd40cf9b8 100644 --- a/dashboard/src/models/BackupTargetsModel.js +++ b/dashboard/src/models/BackupTargetsModel.js @@ -93,6 +93,17 @@ function create() { if (error || result.status !== 200) return [error || result]; return [null]; }, + async setName(id, name) { + let error, result; + try { + result = await fetcher.post(`${API_ORIGIN}/api/v1/backup_targets/${id}/configure/name`, { name }, { access_token: accessToken }); + } catch (e) { + error = e; + } + + if (error || result.status !== 200) return [error || result]; + return [null]; + }, async setLimits(id, limits) { let error, result; try { diff --git a/dashboard/src/views/BackupListView.vue b/dashboard/src/views/BackupListView.vue new file mode 100644 index 000000000..f986dcf9a --- /dev/null +++ b/dashboard/src/views/BackupListView.vue @@ -0,0 +1,11 @@ + + + diff --git a/dashboard/src/views/BackupTargetsView.vue b/dashboard/src/views/BackupTargetsView.vue index ea54bced8..f37af52bb 100644 --- a/dashboard/src/views/BackupTargetsView.vue +++ b/dashboard/src/views/BackupTargetsView.vue @@ -30,12 +30,12 @@ const columns = { status: { width: '30px', }, - provider: { - label: 'Provider', + name: { + label: 'Name', sort: true, }, - label: { - label: 'Label', + provider: { + label: 'Provider', sort: true, }, format: { @@ -164,13 +164,14 @@ onMounted(async () => { + + -