UI for restore configuration is done

This commit is contained in:
Donald Zou 2024-10-25 00:19:27 +08:00
parent 82a472f368
commit a606626053
6 changed files with 614 additions and 24 deletions

View file

@ -462,11 +462,13 @@ class WireguardConfiguration:
self.PostDown: str = ""
self.SaveConfig: bool = True
self.Name = name
self.__configPath = os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], f'{self.Name}.conf')
if name is not None:
self.__parseConfigurationFile()
else:
self.Name = data["ConfigurationName"]
self.__configPath = os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], f'{self.Name}.conf')
for i in dir(self):
if str(i) in data.keys():
if isinstance(getattr(self, i), bool):
@ -484,8 +486,7 @@ class WireguardConfiguration:
"SaveConfig": "true"
}
with open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1],
f"{self.Name}.conf"), "w+") as configFile:
with open(self.__configPath, "w+") as configFile:
self.__parser.write(configFile)
@ -498,7 +499,7 @@ class WireguardConfiguration:
self.getRestrictedPeersList()
def __parseConfigurationFile(self):
self.__parser.read_file(open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], f'{self.Name}.conf')))
self.__parser.read_file(open(self.__configPath))
sections = self.__parser.sections()
if "Interface" not in sections:
raise self.InvalidConfigurationFileException(
@ -510,16 +511,14 @@ class WireguardConfiguration:
setattr(self, i, _strToBool(interfaceConfig[i]))
else:
setattr(self, i, interfaceConfig[i])
if self.PrivateKey:
self.PublicKey = self.__getPublicKey()
self.Status = self.getStatus()
def __dropDatabase(self):
existingTables = sqlSelect(f"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '{self.Name}%'").fetchall()
for t in existingTables:
sqlUpdate(f"DROP TABLE {t['name']}")
sqlUpdate("DROP TABLE '%s'" % t['name'])
existingTables = sqlSelect(f"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '{self.Name}%'").fetchall()
@ -950,8 +949,7 @@ class WireguardConfiguration:
for l in self.__dumpDatabase():
f.write(l + "\n")
def getBackups(self) -> list[dict[str: str, str: str, str: str]]:
def getBackups(self, databaseContent: bool = False) -> list[dict[str: str, str: str, str: str]]:
backups = []
directory = os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup')
@ -963,11 +961,16 @@ class WireguardConfiguration:
if _regexMatch(f"^({self.Name})_(.*)\.(conf)$", f):
s = re.search(f"^({self.Name})_(.*)\.(conf)$", f)
date = s.group(2)
backups.append({
d = {
"filename": f,
"backupDate": date,
"content": open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup', f), 'r').read()
})
}
if f.replace(".conf", ".sql") in list(os.listdir(directory)):
d['database'] = True
if databaseContent:
d['databaseContent'] = open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup', f.replace(".conf", ".sql")), 'r').read()
backups.append(d)
return backups
@ -1047,7 +1050,15 @@ class WireguardConfiguration:
if not status:
return False, msg
return True, ""
def deleteConfiguration(self):
if self.getStatus():
self.toggleConfiguration()
os.remove(self.__configPath)
self.__dropDatabase()
return True
class Peer:
def __init__(self, tableData, configuration: WireguardConfiguration):
self.configuration = configuration
@ -1545,6 +1556,8 @@ def sqlUpdate(statement: str, paramters: tuple = ()) -> sqlite3.Cursor:
with sqldb:
cursor = sqldb.cursor()
try:
statement = statement.rstrip(';')
s = f'BEGIN TRANSACTION;{statement};END TRANSACTION;'
cursor.execute(statement, paramters)
sqldb.commit()
except sqlite3.OperationalError as error:
@ -1748,6 +1761,18 @@ def API_updateWireguardConfiguration():
return ResponseObject(status, message=msg, data=WireguardConfigurations[name])
@app.post(f'{APP_PREFIX}/api/deleteWireguardConfiguration')
def API_deleteWireguardConfiguration():
data = request.get_json()
if "Name" not in data.keys() or data.get("Name") is None or data.get("Name") not in WireguardConfigurations.keys():
return ResponseObject(False, "Please provide the configuration name you want to delete")
status = WireguardConfigurations[data.get("Name")].deleteConfiguration()
if status:
WireguardConfigurations.pop(data.get("Name"))
return ResponseObject(status)
@app.get(f'{APP_PREFIX}/api/getWireguardConfigurationBackup')
def API_getWireguardConfigurationBackup():
configurationName = request.args.get('configurationName')
@ -1755,6 +1780,43 @@ def API_getWireguardConfigurationBackup():
return ResponseObject(False, "Configuration does not exist")
return ResponseObject(data=WireguardConfigurations[configurationName].getBackups())
@app.get(f'{APP_PREFIX}/api/getAllWireguardConfigurationBackup')
def API_getAllWireguardConfigurationBackup():
data = {
"ExistingConfigurations": {},
"NonExistingConfigurations": {}
}
existingConfiguration = WireguardConfigurations.keys()
for i in existingConfiguration:
b = WireguardConfigurations[i].getBackups(True)
if len(b) > 0:
data['ExistingConfigurations'][i] = WireguardConfigurations[i].getBackups(True)
directory = os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup')
files = [(file, os.path.getctime(os.path.join(directory, file)))
for file in os.listdir(directory) if os.path.isfile(os.path.join(directory, file))]
files.sort(key=lambda x: x[1], reverse=True)
for f, ct in files:
if _regexMatch(f"^(.*)_(.*)\.(conf)$", f):
s = re.search(f"^(.*)_(.*)\.(conf)$", f)
name = s.group(1)
if name not in existingConfiguration:
if name not in data['NonExistingConfigurations'].keys():
data['NonExistingConfigurations'][name] = []
date = s.group(2)
d = {
"filename": f,
"backupDate": date,
"content": open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup', f), 'r').read()
}
if f.replace(".conf", ".sql") in list(os.listdir(directory)):
d['database'] = True
d['databaseContent'] = open(os.path.join(DashboardConfig.GetConfig("Server", "wg_conf_path")[1], 'WGDashboard_Backup', f.replace(".conf", ".sql")), 'r').read()
data['NonExistingConfigurations'][name].append(d)
return ResponseObject(data=data)
@app.get(f'{APP_PREFIX}/api/createWireguardConfigurationBackup')
def API_createWireguardConfigurationBackup():
configurationName = request.args.get('configurationName')

View file

@ -0,0 +1,80 @@
<script setup>
import {onMounted, ref} from "vue";
import dayjs from "dayjs";
const props = defineProps({
configurationName: String,
backups: Array,
open: false,
selectedConfigurationBackup: Object
})
const emit = defineEmits(["select"])
const showBackups = ref(props.open)
onMounted(() => {
if (props.selectedConfigurationBackup){
document.querySelector(`#${props.selectedConfigurationBackup.filename.replace('.conf', '')}`).scrollIntoView({
behavior: "smooth"
})
}
})
</script>
<template>
<div class="card rounded-3 shadow-sm">
<a role="button" class="card-body d-flex align-items-center text-decoration-none" @click="showBackups = !showBackups">
<div class="d-flex gap-3 align-items-center">
<h6 class="mb-0">
<samp>
{{configurationName}}
</samp>
</h6>
<small class="text-muted">
{{backups.length}} {{backups.length > 1 ? "Backups": "Backup" }}
</small>
</div>
<h5 class="ms-auto mb-0 dropdownIcon text-muted" :class="{active: showBackups}">
<i class="bi bi-chevron-down"></i>
</h5>
</a>
<div class="card-footer p-3 d-flex flex-column gap-2" v-if="showBackups">
<div class="card rounded-3 shadow-sm animate__animated"
:key="b.filename"
@click="() => {emit('select', b)}"
:id="b.filename.replace('.conf', '')"
role="button" v-for="b in backups">
<div class="card-body d-flex p-3 gap-3 align-items-center">
<small>
<i class="bi bi-file-earmark me-2"></i>
<samp>{{b.filename}}</samp>
</small>
<small>
<i class="bi bi-clock-history me-2"></i>
<samp>{{dayjs(b.backupDate).format("YYYY-MM-DD HH:mm:ss")}}</samp>
</small>
<small >
<i class="bi bi-database me-2"></i>
{{b.database? "Yes" : "No" }}
</small>
<small class="text-muted ms-auto">
<i class="bi bi-chevron-right"></i>
</small>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.dropdownIcon{
transition: all 0.2s ease-in-out;
}
.dropdownIcon.active{
transform: rotate(180deg);
}
</style>

View file

@ -0,0 +1,318 @@
<script setup>
import {computed, onMounted, reactive, ref, watch} from "vue";
import LocaleText from "@/components/text/localeText.vue";
import {WireguardConfigurationsStore} from "@/stores/WireguardConfigurationsStore.js";
import {parse} from "cidr-tools";
const props = defineProps({
selectedConfigurationBackup: Object
})
const newConfiguration = reactive({
ConfigurationName: props.selectedConfigurationBackup.filename.split("_")[0]
})
const lineSplit = props.selectedConfigurationBackup.content.split("\n");
for(let line of lineSplit){
if( line === "[Peer]") break
if (line.length > 0){
let l = line.replace(" = ", "=").split("=")
if (l[0] === "ListenPort"){
newConfiguration[l[0]] = parseInt(l[1])
}else{
newConfiguration[l[0]] = l[1]
}
}
}
const error = ref(false)
const loading = ref(false)
const errorMessage = ref("")
const store = WireguardConfigurationsStore()
const wireguardGenerateKeypair = () => {
const wg = window.wireguard.generateKeypair();
newConfiguration.PrivateKey = wg.privateKey;
newConfiguration.PublicKey = wg.publicKey;
newConfiguration.PresharedKey = wg.presharedKey;
}
const validateConfigurationName = computed(() => {
return /^[a-zA-Z0-9_=+.-]{1,15}$/.test(newConfiguration.ConfigurationName)
&& newConfiguration.ConfigurationName.length > 0
&& !store.Configurations.find(x => x.Name === newConfiguration.ConfigurationName)
})
const validatePrivateKey = computed(() => {
try{
wireguard.generatePublicKey(newConfiguration.PrivateKey)
}catch (e) {
return false
}
return true
})
const validateListenPort = computed(() => {
return newConfiguration.ListenPort > 0
&& newConfiguration.ListenPort <= 65353
&& Number.isInteger(newConfiguration.ListenPort)
&& !store.Configurations.find(x => parseInt(x.ListenPort) === newConfiguration.ListenPort)
})
const validateAddress = computed(() => {
try{
parse(newConfiguration.Address)
return true
}catch (e){
return false
}
})
const validateForm = computed(() => {
return validateAddress.value
&& validateListenPort.value
&& validatePrivateKey.value
&& validateConfigurationName.value
})
onMounted(() => {
document.querySelector("main").scrollTo({
top: 0,
behavior: "smooth"
})
watch(() => validatePrivateKey, (newVal) => {
if (newVal){
newConfiguration.PublicKey = wireguard.generatePublicKey(newConfiguration.PrivateKey)
}
}, {
immediate: true
})
})
const availableIPAddress = computed(() => {
let p;
try{
p = parse(newConfiguration.Address);
}catch (e){
return 0;
}
return p.end - p.start
})
const peersCount = computed(() => {
if (props.selectedConfigurationBackup.database){
let l = props.selectedConfigurationBackup.databaseContent.split("\n")
return l.filter(x => x.search('INSERT INTO "(.*)"') >= 0).length
}
return 0
})
const restrictedPeersCount = computed(() => {
if (props.selectedConfigurationBackup.database){
let l = props.selectedConfigurationBackup.databaseContent.split("\n")
return l.filter(x => x.search('INSERT INTO "(.*)_restrict_access"') >= 0).length
}
return 0
})
</script>
<template>
<div class="d-flex flex-column gap-5" id="confirmBackup">
<form class="d-flex flex-column gap-3">
<div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center gap-3">
<h4 class="mb-0">
<LocaleText t="Configuration File"></LocaleText>
</h4>
</div>
<div>
<label class="text-muted mb-1" for="ConfigurationName"><small>
<LocaleText t="Configuration Name"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" placeholder="ex. wg1" id="ConfigurationName"
v-model="newConfiguration.ConfigurationName"
:class="[validateConfigurationName ? 'is-valid':'is-invalid']"
:disabled="loading"
required>
<div class="invalid-feedback">
<div v-if="error">{{errorMessage}}</div>
<div v-else>
<LocaleText t="Configuration name is invalid. Possible reasons:"></LocaleText>
<ul class="mb-0">
<li>
<LocaleText t="Configuration name already exist."></LocaleText>
</li>
<li>
<LocaleText t="Configuration name can only contain 15 lower/uppercase alphabet, numbers, underscore, equal sign, plus sign, period and hyphen."></LocaleText>
</li>
</ul>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-sm">
<div>
<label class="text-muted mb-1" for="PrivateKey"><small>
<LocaleText t="Private Key"></LocaleText>
</small></label>
<div class="input-group">
<input type="text" class="form-control rounded-start-3" id="PrivateKey" required
:disabled="loading"
:class="[validatePrivateKey ? 'is-valid':'is-invalid']"
v-model="newConfiguration.PrivateKey" disabled
>
<button class="btn btn-outline-primary rounded-end-3" type="button"
title="Regenerate Private Key"
@click="wireguardGenerateKeypair()"
>
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
</div>
<div class="col-sm">
<div>
<label class="text-muted mb-1" for="PublicKey"><small>
<LocaleText t="Public Key"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PublicKey"
v-model="newConfiguration.PublicKey" disabled
>
</div>
</div>
</div>
<div>
<label class="text-muted mb-1" for="ListenPort"><small>
<LocaleText t="Listen Port"></LocaleText>
</small></label>
<input type="number" class="form-control rounded-3" placeholder="0-65353" id="ListenPort"
min="1"
max="65353"
v-model="newConfiguration.ListenPort"
:class="[validateListenPort ? 'is-valid':'is-invalid']"
:disabled="loading"
required>
<div class="invalid-feedback">
<div v-if="error">{{errorMessage}}</div>
<div v-else>
<LocaleText t="Listen Port is invalid. Possible reasons:"></LocaleText>
<ul class="mb-0">
<li>
<LocaleText t="Invalid port."></LocaleText>
</li>
<li>
<LocaleText t="Port is assigned to existing WireGuard Configuration. "></LocaleText>
</li>
</ul>
</div>
</div>
</div>
<div>
<label class="text-muted mb-1 d-flex" for="ListenPort">
<small>
<LocaleText t="IP Address/CIDR"></LocaleText>
</small>
<small class="ms-auto" :class="[availableIPAddress > 0 ? 'text-success':'text-danger']">
{{availableIPAddress}} Available IP Address
</small>
</label>
<input type="text" class="form-control"
placeholder="Ex: 10.0.0.1/24" id="Address"
v-model="newConfiguration.Address"
:class="[validateAddress ? 'is-valid':'is-invalid']"
:disabled="loading"
required>
<div class="invalid-feedback">
<div v-if="error">{{errorMessage}}</div>
<div v-else>
<LocaleText t="IP Address/CIDR is invalid"></LocaleText>
</div>
</div>
</div>
<div class="accordion" id="newConfigurationOptionalAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed rounded-3"
type="button" data-bs-toggle="collapse" data-bs-target="#newConfigurationOptionalAccordionCollapse">
<LocaleText t="Optional Settings"></LocaleText>
</button>
</h2>
<div id="newConfigurationOptionalAccordionCollapse"
class="accordion-collapse collapse "
data-bs-parent="#newConfigurationOptionalAccordion">
<div class="accordion-body d-flex flex-column gap-3">
<div>
<label class="text-muted mb-1" for="PreUp"><small>
<LocaleText t="PreUp"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PreUp" v-model="newConfiguration.PreUp">
</div>
<div>
<label class="text-muted mb-1" for="PreDown"><small>
<LocaleText t="PreDown"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PreDown" v-model="newConfiguration.PreDown">
</div>
<div>
<label class="text-muted mb-1" for="PostUp"><small>
<LocaleText t="PostUp"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PostUp" v-model="newConfiguration.PostUp">
</div>
<div>
<label class="text-muted mb-1" for="PostDown"><small>
<LocaleText t="PostDown"></LocaleText>
</small></label>
<input type="text" class="form-control rounded-3" id="PostDown" v-model="newConfiguration.PostDown">
</div>
</div>
</div>
</div>
</div>
</form>
<div class="d-flex flex-column gap-3">
<div class="d-flex flex-column flex-sm-row align-items-start align-items-sm-center gap-3">
<h4 class="mb-0">
<LocaleText t="Database File"></LocaleText>
</h4>
<h4 class="mb-0 ms-auto" :class="[selectedConfigurationBackup.database ? 'text-success':'text-danger']">
<i class="bi" :class="[selectedConfigurationBackup.database ? 'bi-check-circle-fill':'bi-x-circle-fill']"></i>
</h4>
</div>
<div v-if="selectedConfigurationBackup.database">
<div class="row g-3">
<div class="col-sm">
<div class="card text-bg-success rounded-3">
<div class="card-body">
<i class="bi bi-person-fill me-2"></i> Contain <strong>{{peersCount}}</strong> Peer{{peersCount > 1 ? 's':''}}
</div>
</div>
</div>
<div class="col-sm">
<div class="card text-bg-warning rounded-3">
<div class="card-body">
<i class="bi bi-person-fill-lock me-2"></i> Contain <strong>{{restrictedPeersCount}}</strong> Restricted Peer{{restrictedPeersCount > 1 ? 's':''}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex">
<button class="btn btn-dark btn-brand rounded-3 px-3 py-2 shadow ms-auto"
:disabled="!validateForm"
>
<i class="bi bi-clock-history me-2"></i> Restore
</button>
</div>
</div>
</template>
<style scoped>
</style>

View file

@ -126,14 +126,23 @@ export default {
</script>
<template>
<div class="mt-5">
<div class="mt-5 text-body">
<div class="container mb-4">
<div class="mb-4 d-flex align-items-center gap-4">
<RouterLink to="/" class="text-decoration-none">
<h3 class="mb-0 text-body">
<i class="bi bi-chevron-left me-4"></i>
<LocaleText t="New Configuration"></LocaleText>
</h3>
<RouterLink to="/"
class="btn btn-dark btn-brand p-2 shadow" style="border-radius: 100%">
<h2 class="mb-0" style="line-height: 0">
<i class="bi bi-arrow-left-circle"></i>
</h2>
</RouterLink>
<h2 class="mb-0">
<LocaleText t="New Configuration"></LocaleText>
</h2>
<RouterLink to="/restore_configuration"
class="btn btn-dark btn-brand p-2 shadow ms-auto" style="border-radius: 100%">
<h2 class="mb-0" style="line-height: 0">
<i class="bi bi-clock-history"></i>
</h2>
</RouterLink>
</div>
@ -168,8 +177,7 @@ export default {
</div>
<div class="card rounded-3 shadow">
<div class="card-header">
<LocaleText t="Private Key"></LocaleText> &
<LocaleText t="Public Key"></LocaleText>
<LocaleText t="Private Key"></LocaleText> & <LocaleText t="Public Key"></LocaleText>
</div>
<div class="card-body" style="font-family: var(--bs-font-monospace)">
<div class="mb-2">
@ -286,13 +294,12 @@ export default {
<i class="bi bi-check-circle-fill ms-2"></i>
</span>
<span v-else-if="!this.loading" class="d-flex w-100">
<LocaleText t="Save Configuration"></LocaleText>
<i class="bi bi-save-fill ms-2"></i>
<i class="bi bi-save-fill me-2"></i>
<LocaleText t="Save"></LocaleText>
</span>
<span v-else class="d-flex w-100 align-items-center">
<LocaleText t="Saving..."></LocaleText>
<span class="ms-2 spinner-border spinner-border-sm" role="status">
<!-- <span class="visually-hidden">Loading...</span>-->
</span>
</span>

View file

@ -0,0 +1,123 @@
<script setup>
import LocaleText from "@/components/text/localeText.vue";
import {onMounted, reactive, ref, watch} from "vue";
import {fetchGet} from "@/utilities/fetch.js";
import BackupGroup from "@/components/restoreConfigurationComponents/backupGroup.vue";
import ConfirmBackup from "@/components/restoreConfigurationComponents/confirmBackup.vue";
const backups = ref(undefined)
onMounted(() => {
fetchGet("/api/getAllWireguardConfigurationBackup", {}, (res) => {
backups.value = res.data
})
})
const confirm = ref(false)
const selectedConfigurationBackup = ref(undefined)
const selectedConfiguration = ref("")
</script>
<template>
<div class="mt-5 text-body">
<div class="container mb-4">
<div class="mb-5 d-flex align-items-center gap-4">
<RouterLink to="/new_configuration"
class="btn btn-dark btn-brand p-2 shadow" style="border-radius: 100%">
<h2 class="mb-0" style="line-height: 0">
<i class="bi bi-arrow-left-circle"></i>
</h2>
</RouterLink>
<h2 class="mb-0">
<LocaleText t="Restore Configuration"></LocaleText>
</h2>
</div>
<div name="restore" v-if="backups" >
<div class="d-flex mb-5 align-items-center steps" role="button"
:class="{active: !confirm}"
@click="confirm = false" key="step1">
<div class=" d-flex text-decoration-none text-body flex-grow-1 align-items-center gap-3"
>
<h1 class="mb-0"
style="line-height: 0">
<i class="bi bi-1-circle-fill"></i>
</h1>
<div>
<h4 class="mb-0">Step 1</h4>
<small class="text-muted">
<LocaleText t="Select a backup you want to restore" v-if="!confirm"></LocaleText>
<LocaleText t="Click to change a backup" v-else></LocaleText>
</small>
</div>
</div>
<Transition name="zoomReversed">
<div class="ms-sm-auto" v-if="confirm">
<small class="text-muted">Selected Backup</small>
<h6>
<samp>{{selectedConfigurationBackup.filename}}</samp>
</h6>
</div>
</Transition>
</div>
<div id="step1Detail" v-if="!confirm">
<div class="mb-4">
<h5>Backup of existing WireGuard Configurations</h5>
<hr>
<div class="d-flex gap-3 flex-column">
<BackupGroup
@select="(b) => {selectedConfigurationBackup = b; selectedConfiguration = c; confirm = true}"
:open="selectedConfiguration === c"
:selectedConfigurationBackup="selectedConfigurationBackup"
v-for="c in Object.keys(backups.ExistingConfigurations)"
:configuration-name="c" :backups="backups.ExistingConfigurations[c]"></BackupGroup>
</div>
</div>
<div class="mb-4">
<h5>Backup of non-existing WireGuard Configurations</h5>
<hr>
<div class="d-flex gap-3 flex-column">
<BackupGroup
@select="(b) => {selectedConfigurationBackup = b; selectedConfiguration = c; confirm = true}"
:selectedConfigurationBackup="selectedConfigurationBackup"
:open="selectedConfiguration === c"
v-for="c in Object.keys(backups.NonExistingConfigurations)"
:configuration-name="c" :backups="backups.NonExistingConfigurations[c]"></BackupGroup>
</div>
</div>
</div>
<div class="my-5" key="step2" id="step2">
<div class="steps d-flex text-decoration-none text-body flex-grow-1 align-items-center gap-3"
:class="{active: confirm}"
>
<h1 class="mb-0"
style="line-height: 0">
<i class="bi bi-2-circle-fill"></i>
</h1>
<div>
<h4 class="mb-0">Step 2</h4>
<small class="text-muted">
<LocaleText t="Backup not selected" v-if="!confirm"></LocaleText>
<LocaleText t="Confirm & edit restore information" v-else></LocaleText>
</small>
</div>
</div>
</div>
<ConfirmBackup :selectedConfigurationBackup="selectedConfigurationBackup" v-if="confirm" key="confirm"></ConfirmBackup>
</div>
</div>
</div>
</template>
<style scoped>
.steps{
transition: all 0.3s ease-in-out;
opacity: 0.3;
&.active{
opacity: 1;
}
}
</style>

View file

@ -38,9 +38,9 @@ export default {
<template>
<div class="mt-md-5 mt-3">
<div class="container-md">
<h3 class="mb-3 text-body">
<h2 class="mb-4 text-body">
<LocaleText t="Settings"></LocaleText>
</h3>
</h2>
<div class="card mb-4 shadow rounded-3">
<p class="card-header">