remove not needed recreate button

This commit is contained in:
usmannasir
2025-04-11 15:17:22 +05:00
parent 7c825d3380
commit d897d146ac
6 changed files with 7182 additions and 4 deletions

View File

@@ -1151,6 +1151,37 @@ class ContainerManager(multi.Thread):
'cpu_usage': container.stats(stream=False)['cpu_stats']['cpu_usage'].get('total_usage', 0) 'cpu_usage': container.stats(stream=False)['cpu_stats']['cpu_usage'].get('total_usage', 0)
} }
# Add N8N specific health metrics if this is an N8N container
if 'n8n' in container.name.lower():
try:
# Get N8N port from environment variables or port bindings
n8n_port = None
for port_config in container_info.get('NetworkSettings', {}).get('Ports', {}).values():
if port_config:
n8n_port = port_config[0].get('HostPort')
break
if n8n_port:
# Try to get N8N health metrics from the API
health_url = f"http://localhost:{n8n_port}/api/v1/health"
response = requests.get(health_url, timeout=5)
if response.status_code == 200:
health_data = response.json()
details['n8nStats'] = {
'dbConnected': health_data.get('db', {}).get('status') == 'ok',
'activeWorkflows': len(health_data.get('activeWorkflows', [])),
'queuedExecutions': health_data.get('executionQueue', {}).get('waiting', 0),
'lastExecution': health_data.get('lastExecution')
}
except:
# If we can't get N8N stats, provide default values
details['n8nStats'] = {
'dbConnected': None,
'activeWorkflows': 0,
'queuedExecutions': 0,
'lastExecution': None
}
data_ret = {'status': 1, 'error_message': 'None', 'data': [1, details]} data_ret = {'status': 1, 'error_message': 'None', 'data': [1, details]}
json_data = json.dumps(data_ret) json_data = json.dumps(data_ret)
return HttpResponse(json_data) return HttpResponse(json_data)
@@ -1302,3 +1333,181 @@ class ContainerManager(multi.Thread):
data_ret = {'removeImageStatus': 0, 'error_message': str(msg)} data_ret = {'removeImageStatus': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret) json_data = json.dumps(data_ret)
return HttpResponse(json_data) return HttpResponse(json_data)
def _get_backup_dir(self, container_id):
"""Helper method to get the backup directory for a container"""
return f"/home/docker/backups/{container_id}"
def createBackup(self, userID=None, data=None):
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
container_id = data['id']
client = docker.from_env()
container = client.containers.get(container_id)
# Create backup directory if it doesn't exist
backup_dir = self._get_backup_dir(container_id)
os.makedirs(backup_dir, exist_ok=True)
# Create timestamp for backup
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_file = f"{backup_dir}/backup_{timestamp}.tar.gz"
# Get N8N data directory from container mounts
n8n_data_dir = None
for mount in container.attrs.get('Mounts', []):
if mount.get('Destination', '').endswith('/.n8n'):
n8n_data_dir = mount.get('Source')
break
if not n8n_data_dir:
raise Exception("N8N data directory not found in container mounts")
# Create backup using tar
backup_cmd = f"tar -czf {backup_file} -C {n8n_data_dir} ."
result = subprocess.run(backup_cmd, shell=True, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"Backup failed: {result.stderr}")
data_ret = {'status': 1, 'error_message': 'None', 'backup_file': backup_file}
return HttpResponse(json.dumps(data_ret))
except Exception as e:
data_ret = {'status': 0, 'error_message': str(e)}
return HttpResponse(json.dumps(data_ret))
def listBackups(self, userID=None, data=None):
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
container_id = data['id']
backup_dir = self._get_backup_dir(container_id)
if not os.path.exists(backup_dir):
backups = []
else:
backups = []
for file in os.listdir(backup_dir):
if file.startswith('backup_') and file.endswith('.tar.gz'):
file_path = os.path.join(backup_dir, file)
file_stat = os.stat(file_path)
backups.append({
'id': file,
'date': datetime.fromtimestamp(file_stat.st_mtime).isoformat(),
'size': file_stat.st_size
})
data_ret = {'status': 1, 'error_message': 'None', 'backups': backups}
return HttpResponse(json.dumps(data_ret))
except Exception as e:
data_ret = {'status': 0, 'error_message': str(e)}
return HttpResponse(json.dumps(data_ret))
def restoreBackup(self, userID=None, data=None):
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
container_id = data['id']
backup_id = data['backup_id']
client = docker.from_env()
container = client.containers.get(container_id)
backup_dir = self._get_backup_dir(container_id)
backup_file = os.path.join(backup_dir, backup_id)
if not os.path.exists(backup_file):
raise Exception("Backup file not found")
# Get N8N data directory from container mounts
n8n_data_dir = None
for mount in container.attrs.get('Mounts', []):
if mount.get('Destination', '').endswith('/.n8n'):
n8n_data_dir = mount.get('Source')
break
if not n8n_data_dir:
raise Exception("N8N data directory not found in container mounts")
# Stop the container
container.stop()
try:
# Clear existing data
shutil.rmtree(n8n_data_dir)
os.makedirs(n8n_data_dir)
# Restore from backup
restore_cmd = f"tar -xzf {backup_file} -C {n8n_data_dir}"
result = subprocess.run(restore_cmd, shell=True, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"Restore failed: {result.stderr}")
# Start the container
container.start()
data_ret = {'status': 1, 'error_message': 'None'}
return HttpResponse(json.dumps(data_ret))
except Exception as e:
# Try to start the container even if restore failed
try:
container.start()
except:
pass
raise e
except Exception as e:
data_ret = {'status': 0, 'error_message': str(e)}
return HttpResponse(json.dumps(data_ret))
def deleteBackup(self, userID=None, data=None):
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
container_id = data['id']
backup_id = data['backup_id']
backup_dir = self._get_backup_dir(container_id)
backup_file = os.path.join(backup_dir, backup_id)
if os.path.exists(backup_file):
os.remove(backup_file)
data_ret = {'status': 1, 'error_message': 'None'}
return HttpResponse(json.dumps(data_ret))
except Exception as e:
data_ret = {'status': 0, 'error_message': str(e)}
return HttpResponse(json.dumps(data_ret))
def downloadBackup(self, userID=None, data=None):
try:
admin = Administrator.objects.get(pk=userID)
if admin.acl.adminStatus != 1:
return ACLManager.loadError()
container_id = data['id']
backup_id = data['backup_id']
backup_dir = self._get_backup_dir(container_id)
backup_file = os.path.join(backup_dir, backup_id)
if not os.path.exists(backup_file):
raise Exception("Backup file not found")
with open(backup_file, 'rb') as f:
response = HttpResponse(f.read(), content_type='application/gzip')
response['Content-Disposition'] = f'attachment; filename="{backup_id}"'
return response
except Exception as e:
data_ret = {'status': 0, 'error_message': str(e)}
return HttpResponse(json.dumps(data_ret))

View File

@@ -27,7 +27,7 @@ urlpatterns = [
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'), re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),
re_path(r'^installDocker$', views.installDocker, name='installDocker'), re_path(r'^installDocker$', views.installDocker, name='installDocker'),
re_path(r'^images$', views.images, name='containerImage'), re_path(r'^images$', views.images, name='containerImage'),
re_path(r'^view/(?P<name>.+)$', views.viewContainer, name='viewContainer'), re_path(r'^view/(?P<n>.+)$', views.viewContainer, name='viewContainer'),
path('manage/<int:dockerapp>/app', Dockersitehome, name='Dockersitehome'), path('manage/<int:dockerapp>/app', Dockersitehome, name='Dockersitehome'),
path('getDockersiteList', views.getDockersiteList, name='getDockersiteList'), path('getDockersiteList', views.getDockersiteList, name='getDockersiteList'),
@@ -36,4 +36,9 @@ urlpatterns = [
path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'), path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'),
path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'), path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'),
path('StopContainerAPP', views.StopContainerAPP, name='StopContainerAPP'), path('StopContainerAPP', views.StopContainerAPP, name='StopContainerAPP'),
path('createBackup', views.createBackup, name='createBackup'),
path('listBackups', views.listBackups, name='listBackups'),
path('restoreBackup', views.restoreBackup, name='restoreBackup'),
path('deleteBackup', views.deleteBackup, name='deleteBackup'),
path('downloadBackup', views.downloadBackup, name='downloadBackup'),
] ]

View File

@@ -536,3 +536,93 @@ def StopContainerAPP(request):
return coreResult return coreResult
except KeyError: except KeyError:
return redirect(loadLoginPage) return redirect(loadLoginPage)
@preDockerRun
def createBackup(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.createBackup(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def listBackups(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.listBackups(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def restoreBackup(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.restoreBackup(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def deleteBackup(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.deleteBackup(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def downloadBackup(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.downloadBackup(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)

View File

@@ -0,0 +1,349 @@
app.controller('DockerContainerManager', function ($scope, $http) {
$scope.cyberpanelLoading = true;
$scope.conatinerview = true;
$scope.ContainerList = [];
$('#cyberpanelLoading').hide();
// Format bytes to human readable
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
$scope.getcontainer = function () {
$('#cyberpanelLoading').show();
url = "/docker/getDockersiteList";
var data = {'name': $('#sitename').html()};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
$scope.cyberpanelLoading = true;
var finalData = JSON.parse(response.data.data[1]);
$scope.ContainerList = finalData;
$("#listFail").hide();
} else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
}
}
function cantLoadInitialData(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
new PNotify({
title: 'Operation Failed!',
text: 'Connection disrupted, refresh the page.',
type: 'error'
});
}
};
$scope.Lunchcontainer = function (containerid) {
$scope.cyberpanelLoading = false;
$('#cyberpanelLoading').show();
var url = "/docker/getContainerAppinfo";
var data = {
'name': $('#sitename').html(),
'id': containerid
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
var containerInfo = response.data.data[1];
// Find the container in the list and update its information
for (var i = 0; i < $scope.ContainerList.length; i++) {
if ($scope.ContainerList[i].id === containerid) {
// Basic Information
$scope.ContainerList[i].status = containerInfo.status;
$scope.ContainerList[i].created = new Date(containerInfo.created);
$scope.ContainerList[i].uptime = containerInfo.uptime;
// Resource Usage
var memoryBytes = containerInfo.memory_usage;
$scope.ContainerList[i].memoryUsage = formatBytes(memoryBytes);
$scope.ContainerList[i].memoryUsagePercent = (memoryBytes / (1024 * 1024 * 1024)) * 100; // Assuming 1GB limit
$scope.ContainerList[i].cpuUsagePercent = (containerInfo.cpu_usage / 10000000000) * 100; // Normalize to percentage
// Network & Ports
$scope.ContainerList[i].ports = containerInfo.ports;
// Volumes
$scope.ContainerList[i].volumes = containerInfo.volumes;
// Environment Variables
$scope.ContainerList[i].environment = containerInfo.environment;
// N8N Stats
$scope.ContainerList[i].n8nStats = containerInfo.n8nStats || {
dbConnected: null,
activeWorkflows: 0,
queuedExecutions: 0,
lastExecution: null
};
// Load backups
$scope.refreshBackups($scope.ContainerList[i].id);
break;
}
}
}
}
function cantLoadInitialData(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
new PNotify({
title: 'Operation Failed!',
text: response.data.error_message || 'Could not connect to server',
type: 'error'
});
}
};
$scope.createBackup = function(containerId) {
$scope.cyberpanelLoading = false;
$('#cyberpanelLoading').show();
var url = "/docker/createBackup";
var data = {
'name': $('#sitename').html(),
'id': containerId
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
// Refresh backups list
$scope.refreshBackups(containerId);
new PNotify({
title: 'Success!',
text: 'Backup created successfully.',
type: 'success'
});
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
new PNotify({
title: 'Error!',
text: 'Could not connect to server.',
type: 'error'
});
});
};
$scope.refreshBackups = function(containerId) {
var url = "/docker/listBackups";
var data = {
'name': $('#sitename').html(),
'id': containerId
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.status === 1) {
// Find the container and update its backups
for (var i = 0; i < $scope.ContainerList.length; i++) {
if ($scope.ContainerList[i].id === containerId) {
$scope.ContainerList[i].backups = response.data.backups;
break;
}
}
}
});
};
$scope.restoreBackup = function(containerId, backupId) {
if (!confirm("Are you sure you want to restore this backup? The container will be stopped during restoration.")) {
return;
}
$scope.cyberpanelLoading = false;
$('#cyberpanelLoading').show();
var url = "/docker/restoreBackup";
var data = {
'name': $('#sitename').html(),
'id': containerId,
'backup_id': backupId
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
if (response.data.status === 1) {
new PNotify({
title: 'Success!',
text: 'Backup restored successfully.',
type: 'success'
});
// Refresh container info
$scope.Lunchcontainer(containerId);
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}, function(error) {
$scope.cyberpanelLoading = true;
$('#cyberpanelLoading').hide();
new PNotify({
title: 'Error!',
text: 'Could not connect to server.',
type: 'error'
});
});
};
$scope.deleteBackup = function(containerId, backupId) {
if (!confirm("Are you sure you want to delete this backup?")) {
return;
}
var url = "/docker/deleteBackup";
var data = {
'name': $('#sitename').html(),
'id': containerId,
'backup_id': backupId
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.status === 1) {
new PNotify({
title: 'Success!',
text: 'Backup deleted successfully.',
type: 'success'
});
// Refresh backups list
$scope.refreshBackups(containerId);
} else {
new PNotify({
title: 'Error!',
text: response.data.error_message,
type: 'error'
});
}
}, function(error) {
new PNotify({
title: 'Error!',
text: 'Could not connect to server.',
type: 'error'
});
});
};
$scope.downloadBackup = function(containerId, backupId) {
window.location.href = "/docker/downloadBackup?name=" + encodeURIComponent($('#sitename').html()) +
"&id=" + encodeURIComponent(containerId) +
"&backup_id=" + encodeURIComponent(backupId);
};
$scope.openN8NEditor = function(container) {
// Find the N8N port from the container's port bindings
var n8nPort = null;
if (container.ports) {
for (var port in container.ports) {
if (container.ports[port] && container.ports[port].length > 0) {
n8nPort = container.ports[port][0].HostPort;
break;
}
}
}
if (n8nPort) {
window.open("http://localhost:" + n8nPort, "_blank");
} else {
new PNotify({
title: 'Error!',
text: 'Could not find N8N port configuration.',
type: 'error'
});
}
};
$scope.getcontainerlog = function (containerid) {
var url = "/docker/getContainerApplog";
var data = {
'name': $('#sitename').html(),
'id': containerid
};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.status === 1) {
// Find the container and update its logs
for (var i = 0; i < $scope.ContainerList.length; i++) {
if ($scope.ContainerList[i].id === containerid) {
$scope.ContainerList[i].logs = response.data.data[1];
break;
}
}
}
});
};
});

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,8 @@
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} --> <!-- Current language: {{ LANGUAGE_CODE }} -->
<script src="{% static 'websiteFunctions/dockerController.js' %}"></script>
<style> <style>
.info-box { .info-box {
background: #fff; background: #fff;
@@ -147,7 +149,7 @@
</script> </script>
<div class="container" ng-controller="ListDockersitecontainer"> <div class="container" ng-controller="DockerContainerManager">
<div id="page-title"> <div id="page-title">
<h2 id="domainNamePage">{% trans "Containers" %} <img id="cyberpanelLoading" ng-hide="cyberpanelLoading" <h2 id="domainNamePage">{% trans "Containers" %} <img id="cyberpanelLoading" ng-hide="cyberpanelLoading"
@@ -308,6 +310,27 @@
<p class="text-muted"> <p class="text-muted">
{% trans "Container ID" %}: <code>{$ web.id $}</code> {% trans "Container ID" %}: <code>{$ web.id $}</code>
</p> </p>
<div class="action-buttons mb-4">
<button class="btn btn-primary me-2" ng-click="openN8NEditor(web)">
<i class="fa fa-external-link"></i> Open N8N Editor
</button>
<button class="btn btn-success me-2" ng-click="startContainer(web.id)" ng-if="web.status !== 'running'">
<i class="fa fa-play"></i> Start
</button>
<button class="btn btn-warning me-2" ng-click="restartContainer(web.id)" ng-if="web.status === 'running'">
<i class="fa fa-refresh"></i> Restart
</button>
<button class="btn btn-danger me-2" ng-click="stopContainer(web.id)" ng-if="web.status === 'running'">
<i class="fa fa-stop"></i> Stop
</button>
<button class="btn btn-info me-2" data-toggle="modal" data-target="#settings">
<i class="fa fa-cog"></i> Settings
</button>
<button class="btn btn-secondary" ng-click="showProcesses(web.id)">
<i class="fa fa-tasks"></i> Processes
</button>
</div>
</div> </div>
<div class="container-fluid p-0"> <div class="container-fluid p-0">
@@ -360,6 +383,80 @@
</div> </div>
</div> </div>
</div> </div>
<div class="info-box shadow-sm mt-4">
<h4 class="border-bottom pb-2 mb-3">N8N Health Status</h4>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="mb-0">Active Workflows</label>
<span class="badge bg-primary">{$ web.n8nStats.activeWorkflows || 0 $}</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="mb-0">Queued Executions</label>
<span class="badge bg-warning">{$ web.n8nStats.queuedExecutions || 0 $}</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="mb-0">Database Connection</label>
<span class="badge" ng-class="{'bg-success': web.n8nStats.dbConnected, 'bg-danger': !web.n8nStats.dbConnected}">
{$ web.n8nStats.dbConnected ? 'Connected' : 'Disconnected' $}
</span>
</div>
<div class="d-flex justify-content-between align-items-center">
<label class="mb-0">Last Execution</label>
<span>{$ web.n8nStats.lastExecution | date:'medium' $}</span>
</div>
</div>
</div>
<div class="info-box shadow-sm mt-4">
<h4 class="border-bottom pb-2 mb-3">Backup Management</h4>
<div class="mb-3">
<div class="row mb-3">
<div class="col-md-8">
<button class="btn btn-primary" ng-click="createBackup(web.id)">
<i class="fa fa-download"></i> Create New Backup
</button>
</div>
<div class="col-md-4 text-end">
<button class="btn btn-outline-primary" ng-click="refreshBackups(web.id)">
<i class="fa fa-refresh"></i>
</button>
</div>
</div>
<div class="table-responsive" ng-if="web.backups && web.backups.length > 0">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>Date</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="backup in web.backups">
<td>{$ backup.date | date:'medium' $}</td>
<td>{$ backup.size | bytes $}</td>
<td>
<button class="btn btn-sm btn-success me-1" ng-click="restoreBackup(web.id, backup.id)">
<i class="fa fa-upload"></i> Restore
</button>
<button class="btn btn-sm btn-primary me-1" ng-click="downloadBackup(web.id, backup.id)">
<i class="fa fa-download"></i> Download
</button>
<button class="btn btn-sm btn-danger" ng-click="deleteBackup(web.id, backup.id)">
<i class="fa fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div ng-if="!web.backups || web.backups.length === 0" class="text-muted">
<p class="mb-0">No backups available</p>
</div>
</div>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">