Feature: Refactor config management interface
This commit is contained in:
parent
16901482d9
commit
1e857b79c1
@ -54,6 +54,7 @@
|
||||
"2005": "Ungültige Landesauswahl!",
|
||||
"3001": "Nichts gelöscht!",
|
||||
"3002": "Konfiguration zurückgesetzt. Starte jetzt neu...",
|
||||
"3003": "Datei erfolgreich gelöscht. Neustarten um Änderungen anzuwenden!",
|
||||
"4001": "@:apiresponse.2001",
|
||||
"4002": "Der Name muss zwischen 1 und {max} Zeichen lang sein!",
|
||||
"4003": "Es werden nur {max} Wechselrichter unterstützt!",
|
||||
@ -556,11 +557,9 @@
|
||||
"YieldDayCorrection": "Tagesertragskorrektur",
|
||||
"YieldDayCorrectionHint": "Summiert den Tagesertrag, auch wenn der Wechselrichter neu gestartet wird. Der Wert wird um Mitternacht zurückgesetzt"
|
||||
},
|
||||
"configadmin": {
|
||||
"fileadmin": {
|
||||
"ConfigManagement": "Konfigurationsverwaltung",
|
||||
"BackupHeader": "Sicherung: Sicherung der Konfigurationsdatei",
|
||||
"BackupConfig": "Sicherung der Konfigurationsdatei",
|
||||
"Backup": "Sichern",
|
||||
"Restore": "Wiederherstellen",
|
||||
"NoFileSelected": "Keine Datei ausgewählt",
|
||||
"RestoreHeader": "Wiederherstellen: Wiederherstellen der Konfigurationsdatei",
|
||||
@ -573,6 +572,12 @@
|
||||
"FactoryReset": "Werksreset",
|
||||
"ResetMsg": "Sind Sie sicher, dass Sie die aktuelle Konfiguration löschen und alle Einstellungen auf die Werkseinstellungen zurücksetzen möchten?",
|
||||
"ResetConfirm": "Werksreset!",
|
||||
"Download": "Herunterladen",
|
||||
"Delete": "Löschen",
|
||||
"DeleteMsg": "Sind Sie sicher, dass Sie die Datei löschen wollen: '{name}'? Es muss manuell neu gestartet werden um die Konfigurationsänderungen zu übernehmen!",
|
||||
"Name": "Name",
|
||||
"Size": "Größe",
|
||||
"Action": "Aktion",
|
||||
"Cancel": "@:base.Cancel"
|
||||
},
|
||||
"login": {
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
"2005": "Invalid country selection!",
|
||||
"3001": "Not deleted anything!",
|
||||
"3002": "Configuration resettet. Rebooting now...",
|
||||
"3003": "File successful deleted. Restart to apply changes!",
|
||||
"4001": "@:apiresponse.2001",
|
||||
"4002": "Name must between 1 and {max} characters long!",
|
||||
"4003": "Only {max} inverters are supported!",
|
||||
@ -556,11 +557,9 @@
|
||||
"YieldDayCorrection": "Yield Day Correction",
|
||||
"YieldDayCorrectionHint": "Sum up daily yield even if the inverter is restarted. Value will be reset at midnight"
|
||||
},
|
||||
"configadmin": {
|
||||
"fileadmin": {
|
||||
"ConfigManagement": "Config Management",
|
||||
"BackupHeader": "Backup: Configuration File Backup",
|
||||
"BackupConfig": "Backup the configuration file",
|
||||
"Backup": "Backup",
|
||||
"Restore": "Restore",
|
||||
"NoFileSelected": "No file selected",
|
||||
"RestoreHeader": "Restore: Restore the Configuration File",
|
||||
@ -573,6 +572,12 @@
|
||||
"FactoryReset": "Factory Reset",
|
||||
"ResetMsg": "Are you sure you want to delete the current configuration and reset all settings to their factory defaults?",
|
||||
"ResetConfirm": "Factory Reset!",
|
||||
"Download": "Download",
|
||||
"Delete": "Delete",
|
||||
"DeleteMsg": "Are you sure you want to delete file: '{name}'? You have to manually reboot the device to apply config changes!",
|
||||
"Name": "Name",
|
||||
"Size": "Size",
|
||||
"Action": "Action",
|
||||
"Cancel": "@:base.Cancel"
|
||||
},
|
||||
"login": {
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
"2005": "Invalid country selection !",
|
||||
"3001": "Rien n'a été supprimé !",
|
||||
"3002": "Configuration réinitialisée. Redémarrage maintenant...",
|
||||
"3003": "File successful deleted. Restart to apply changes!",
|
||||
"4001": "@:apiresponse.2001",
|
||||
"4002": "Le nom doit comporter entre 1 et {max} caractères !",
|
||||
"4003": "Seulement {max} onduleurs sont supportés !",
|
||||
@ -538,11 +539,9 @@
|
||||
"YieldDayCorrection": "Yield Day Correction",
|
||||
"YieldDayCorrectionHint": "Sum up daily yield even if the inverter is restarted. Value will be reset at midnight"
|
||||
},
|
||||
"configadmin": {
|
||||
"fileadmin": {
|
||||
"ConfigManagement": "Gestion de la configuration",
|
||||
"BackupHeader": "Sauvegarder le fichier de configuration",
|
||||
"BackupConfig": "Fichier de configuration",
|
||||
"Backup": "Sauvegarder",
|
||||
"Restore": "Restaurer",
|
||||
"NoFileSelected": "Aucun fichier sélectionné",
|
||||
"RestoreHeader": "Restaurer le fichier de configuration",
|
||||
@ -555,6 +554,12 @@
|
||||
"FactoryReset": "Remise à zéro",
|
||||
"ResetMsg": "Êtes-vous sûr de vouloir supprimer la configuration actuelle et réinitialiser tous les paramètres à leurs valeurs par défaut ?",
|
||||
"ResetConfirm": "Remise à zéro !",
|
||||
"Download": "Download",
|
||||
"Delete": "Supprimer",
|
||||
"DeleteMsg": "Are you sure you want to delete file: '{name}'? You have to manually reboot the device to apply config changes!",
|
||||
"Name": "Name",
|
||||
"Size": "Size",
|
||||
"Action": "Action",
|
||||
"Cancel": "@:base.Cancel"
|
||||
},
|
||||
"login": {
|
||||
|
||||
6
webapp/src/types/Alert.ts
Normal file
6
webapp/src/types/Alert.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface AlertResponse {
|
||||
message: string;
|
||||
type: string;
|
||||
code: number;
|
||||
show: boolean;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
export interface ConfigFileInfo {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ConfigFileList {
|
||||
configs: Array<ConfigFileInfo>;
|
||||
}
|
||||
4
webapp/src/types/File.ts
Normal file
4
webapp/src/types/File.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface FileInfo {
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
@ -1,30 +1,38 @@
|
||||
<template>
|
||||
<BasePage :title="$t('configadmin.ConfigManagement')" :isLoading="loading">
|
||||
<BootstrapAlert v-model="showAlert" dismissible :variant="alertType">
|
||||
{{ alertMessage }}
|
||||
<BasePage :title="$t('fileadmin.ConfigManagement')" :isLoading="loading" :show-reload="true" @reload="getFileList">
|
||||
<BootstrapAlert v-model="alert.show" dismissible :variant="alert.type">
|
||||
{{ alert.message }}
|
||||
</BootstrapAlert>
|
||||
|
||||
<CardElement :text="$t('configadmin.BackupHeader')" textVariant="text-bg-primary" center-content>
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-sm">
|
||||
{{ $t('configadmin.BackupConfig') }}
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<select class="form-select" v-model="backupFileSelect">
|
||||
<option v-for="file in fileList.configs" :key="file.name" :value="file.name">
|
||||
{{ file.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<button class="btn btn-primary" @click="downloadConfig">
|
||||
{{ $t('configadmin.Backup') }}
|
||||
</button>
|
||||
</div>
|
||||
<CardElement :text="$t('fileadmin.BackupHeader')" textVariant="text-bg-primary">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('fileadmin.Name') }}</th>
|
||||
<th>{{ $t('fileadmin.Size') }}</th>
|
||||
<th>{{ $t('fileadmin.Action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ref="fileList">
|
||||
<tr v-for="(file, index) in fileList" :key="index" :value="index">
|
||||
<td>{{ file.name }}</td>
|
||||
<td>{{ $n(file.size, 'byte') }}</td>
|
||||
<td>
|
||||
<a href="#" class="icon text-danger" :title="$t('fileadmin.Delete')">
|
||||
<BIconTrash v-on:click="onOpenModal(modalDelete, file)" /> </a
|
||||
>
|
||||
<a href="#" class="icon" :title="$t('fileadmin.Download')">
|
||||
<BIconDownload v-on:click="downloadFile(file.name)" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardElement>
|
||||
|
||||
<CardElement :text="$t('configadmin.RestoreHeader')" textVariant="text-bg-primary" center-content add-space>
|
||||
<CardElement :text="$t('fileadmin.RestoreHeader')" textVariant="text-bg-primary" add-space center-content>
|
||||
<div v-if="!uploading && UploadError != ''">
|
||||
<p class="h1 mb-2">
|
||||
<BIconExclamationCircleFill />
|
||||
@ -34,17 +42,21 @@
|
||||
</span>
|
||||
<br />
|
||||
<br />
|
||||
<button class="btn btn-light" @click="clear"><BIconArrowLeft /> {{ $t('configadmin.Back') }}</button>
|
||||
<button class="btn btn-light" @click="clearUpload">
|
||||
<BIconArrowLeft /> {{ $t('fileadmin.Back') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!uploading && UploadSuccess">
|
||||
<span class="h1 mb-2">
|
||||
<BIconCheckCircle />
|
||||
</span>
|
||||
<span> {{ $t('configadmin.UploadSuccess') }} </span>
|
||||
<span> {{ $t('fileadmin.UploadSuccess') }} </span>
|
||||
<br />
|
||||
<br />
|
||||
<button class="btn btn-primary" @click="clear"><BIconArrowLeft /> {{ $t('configadmin.Back') }}</button>
|
||||
<button class="btn btn-primary" @click="clearUpload">
|
||||
<BIconArrowLeft /> {{ $t('fileadmin.Back') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!uploading">
|
||||
@ -59,8 +71,8 @@
|
||||
<input class="form-control" type="file" ref="file" accept=".json" />
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<button class="btn btn-primary" @click="uploadConfig">
|
||||
{{ $t('configadmin.Restore') }}
|
||||
<button class="btn btn-primary" @click="onUpload">
|
||||
{{ $t('fileadmin.Restore') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -81,28 +93,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert" v-html="$t('configadmin.RestoreHint')"></div>
|
||||
<div class="alert alert-danger mt-3" role="alert" v-html="$t('fileadmin.RestoreHint')"></div>
|
||||
</CardElement>
|
||||
|
||||
<CardElement :text="$t('configadmin.ResetHeader')" textVariant="text-bg-primary" center-content add-space>
|
||||
<CardElement :text="$t('fileadmin.ResetHeader')" textVariant="text-bg-primary" center-content add-space>
|
||||
<button class="btn btn-danger" @click="onFactoryResetModal">
|
||||
{{ $t('configadmin.FactoryResetButton') }}
|
||||
{{ $t('fileadmin.FactoryResetButton') }}
|
||||
</button>
|
||||
|
||||
<div class="alert alert-danger mt-3" role="alert" v-html="$t('configadmin.ResetHint')"></div>
|
||||
<div class="alert alert-danger mt-3" role="alert" v-html="$t('fileadmin.ResetHint')"></div>
|
||||
</CardElement>
|
||||
</BasePage>
|
||||
|
||||
<ModalDialog
|
||||
modalId="factoryReset"
|
||||
small
|
||||
:title="$t('configadmin.FactoryReset')"
|
||||
:closeText="$t('configadmin.Cancel')"
|
||||
>
|
||||
{{ $t('configadmin.ResetMsg') }}
|
||||
<ModalDialog modalId="fileDelete" small :title="$t('fileadmin.Delete')" :closeText="$t('fileadmin.Cancel')">
|
||||
{{
|
||||
$t('fileadmin.DeleteMsg', {
|
||||
name: selectedFile.name,
|
||||
})
|
||||
}}
|
||||
<template #footer>
|
||||
<button type="button" class="btn btn-danger" @click="onDelete">
|
||||
{{ $t('fileadmin.Delete') }}
|
||||
</button>
|
||||
</template>
|
||||
</ModalDialog>
|
||||
|
||||
<ModalDialog modalId="factoryReset" small :title="$t('fileadmin.FactoryReset')" :closeText="$t('fileadmin.Cancel')">
|
||||
{{ $t('fileadmin.ResetMsg') }}
|
||||
<template #footer>
|
||||
<button type="button" class="btn btn-danger" @click="onFactoryResetPerform">
|
||||
{{ $t('configadmin.ResetConfirm') }}
|
||||
{{ $t('fileadmin.ResetConfirm') }}
|
||||
</button>
|
||||
</template>
|
||||
</ModalDialog>
|
||||
@ -113,10 +133,17 @@ import BasePage from '@/components/BasePage.vue';
|
||||
import BootstrapAlert from '@/components/BootstrapAlert.vue';
|
||||
import CardElement from '@/components/CardElement.vue';
|
||||
import ModalDialog from '@/components/ModalDialog.vue';
|
||||
import type { ConfigFileList } from '@/types/Config';
|
||||
import type { AlertResponse } from '@/types/Alert';
|
||||
import type { FileInfo } from '@/types/File';
|
||||
import { authHeader, handleResponse } from '@/utils/authentication';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
import { BIconArrowLeft, BIconCheckCircle, BIconExclamationCircleFill } from 'bootstrap-icons-vue';
|
||||
import {
|
||||
BIconArrowLeft,
|
||||
BIconCheckCircle,
|
||||
BIconDownload,
|
||||
BIconExclamationCircleFill,
|
||||
BIconTrash,
|
||||
} from 'bootstrap-icons-vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
@ -127,88 +154,93 @@ export default defineComponent({
|
||||
ModalDialog,
|
||||
BIconArrowLeft,
|
||||
BIconCheckCircle,
|
||||
BIconDownload,
|
||||
BIconExclamationCircleFill,
|
||||
BIconTrash,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modalFactoryReset: {} as bootstrap.Modal,
|
||||
alertMessage: '',
|
||||
alertType: 'info',
|
||||
showAlert: false,
|
||||
loading: true,
|
||||
fileList: [] as FileInfo[],
|
||||
selectedFile: {} as FileInfo,
|
||||
file: {} as Blob,
|
||||
modalDelete: {} as bootstrap.Modal,
|
||||
modalFactoryReset: {} as bootstrap.Modal,
|
||||
alert: {} as AlertResponse,
|
||||
uploading: false,
|
||||
progress: 0,
|
||||
UploadError: '',
|
||||
UploadSuccess: false,
|
||||
file: {} as Blob,
|
||||
fileList: {} as ConfigFileList,
|
||||
backupFileSelect: '',
|
||||
restoreFileSelect: 'config.json',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.modalDelete = new bootstrap.Modal('#fileDelete');
|
||||
this.modalFactoryReset = new bootstrap.Modal('#factoryReset');
|
||||
},
|
||||
created() {
|
||||
this.getFileList();
|
||||
},
|
||||
methods: {
|
||||
onFactoryResetModal() {
|
||||
this.modalFactoryReset.show();
|
||||
},
|
||||
onFactoryResetCancel() {
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
onFactoryResetPerform() {
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify({ delete: true }));
|
||||
|
||||
fetch('/api/file/delete', {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((response) => {
|
||||
this.alertMessage = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alertType = response.type;
|
||||
this.showAlert = true;
|
||||
});
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
getFileList() {
|
||||
this.loading = true;
|
||||
fetch('/api/file/list', { headers: authHeader() })
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.fileList = data;
|
||||
if (this.fileList.configs) {
|
||||
this.backupFileSelect = this.fileList.configs[0].name;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
downloadConfig() {
|
||||
fetch('/api/file/get?file=' + this.backupFileSelect, { headers: authHeader() })
|
||||
downloadFile(filename: string) {
|
||||
fetch('/api/file/get?file=' + filename, { headers: authHeader() })
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const file = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = file;
|
||||
a.download = this.backupFileSelect;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
});
|
||||
},
|
||||
uploadConfig() {
|
||||
callFileApiEndpoint(endpoint: string, jsonData: string) {
|
||||
const formData = new FormData();
|
||||
formData.append('data', jsonData);
|
||||
|
||||
fetch('/api/file/' + endpoint, {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((data) => {
|
||||
this.getFileList();
|
||||
this.alert = data;
|
||||
this.alert.message = this.$t('apiresponse.' + data.code, data.param);
|
||||
this.alert.show = true;
|
||||
});
|
||||
},
|
||||
onOpenModal(modal: bootstrap.Modal, file: FileInfo) {
|
||||
// deep copy File object for editing/deleting
|
||||
this.selectedFile = JSON.parse(JSON.stringify(file)) as FileInfo;
|
||||
modal.show();
|
||||
},
|
||||
onCloseModal(modal: bootstrap.Modal) {
|
||||
modal.hide();
|
||||
},
|
||||
onDelete() {
|
||||
this.callFileApiEndpoint('delete', JSON.stringify({ file: this.selectedFile.name }));
|
||||
this.onCloseModal(this.modalDelete);
|
||||
},
|
||||
onUpload() {
|
||||
this.uploading = true;
|
||||
const formData = new FormData();
|
||||
const target = this.$refs.file as HTMLInputElement; // event.target as HTMLInputElement;
|
||||
if (target.files !== null && target.files?.length > 0) {
|
||||
this.file = target.files[0];
|
||||
} else {
|
||||
this.UploadError = this.$t('configadmin.NoFileSelected');
|
||||
this.UploadError = this.$t('fileadmin.NoFileSelected');
|
||||
this.uploading = false;
|
||||
this.progress = 0;
|
||||
return;
|
||||
@ -239,11 +271,34 @@ export default defineComponent({
|
||||
});
|
||||
request.send(formData);
|
||||
},
|
||||
clear() {
|
||||
clearUpload() {
|
||||
this.UploadError = '';
|
||||
this.UploadSuccess = false;
|
||||
this.getFileList();
|
||||
},
|
||||
onFactoryResetModal() {
|
||||
this.modalFactoryReset.show();
|
||||
},
|
||||
onFactoryResetCancel() {
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
onFactoryResetPerform() {
|
||||
const formData = new FormData();
|
||||
formData.append('data', JSON.stringify({ delete: true }));
|
||||
|
||||
fetch('/api/file/delete_all', {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => handleResponse(response, this.$emitter, this.$router))
|
||||
.then((response) => {
|
||||
this.alert.message = this.$t('apiresponse.' + response.code, response.param);
|
||||
this.alert.type = response.type;
|
||||
this.alert.show = true;
|
||||
});
|
||||
this.modalFactoryReset.hide();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user