Skip to content

Commit

Permalink
GDB-8760: Implement backup and restore monitoring view in WB
Browse files Browse the repository at this point in the history
## What
A new view, 'Backup and Restore' has been added to display running backup or restore operations.

## Why
We aim to provide our clients with the opportunity to easily check if there are ongoing backup or restore operations.

## How
The view displays all currently running backup and restore operations in a table format. The page automatically refreshes every 2 seconds.
  • Loading branch information
boyan-tonchev committed Sep 18, 2023
1 parent ac4593b commit b8b45b6
Show file tree
Hide file tree
Showing 19 changed files with 414 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/css/backup-and-restore.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.backup-and-restore-updater-icon div, .backup-and-restore-updater-icon img {
display: inline-block;
}

.backup-and-restore table td {
vertical-align: top;
}
15 changes: 15 additions & 0 deletions src/i18n/locale-en.json
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,20 @@
"view.namespaces.helpInfo": "The Namespaces view provides an overview of all namespaces defined in a GraphDB repository. Namespaces are essentially shorthand notations for IRIs. Here you can add, remove and modify them.",
"view.query.and.update.monitoring.title": "Query and Update monitoring",
"view.query.and.update.monitoring.helpInfo": "The Queries and Updates monitoring view shows all running queries and updates in a GraphDB repository. A query or update can be terminated by pressing the Abort button.",
"view.monitoring.backup_and_restore.title": "Backup and Restore",
"view.monitoring.backup_and_restore.helpInfo": "The Backup and Restore monitoring view shows running backup or restore operation.",
"view.monitoring.backup_and_restore.id.header": "Id",
"view.monitoring.backup_and_restore.username.header": "Username",
"view.monitoring.backup_and_restore.recovery_operation_type.header": "Recovery operation type",
"view.monitoring.backup_and_restore.affected_repositories.header": "Affected repositories",
"view.monitoring.backup_and_restore.lifetime.header": "Lifetime",
"view.monitoring.backup_and_restore.snapshot_options.header": "Snapshot options",
"view.monitoring.backup_and_restore.node_performing_cluster_backup.header": "Node performing backup",
"view.monitoring.backup_and_restore.CREATE_BACKUP_IN_PROGRESS": "Creating backup",
"view.monitoring.backup_and_restore.RESTORE_BACKUP_IN_PROGRESS": "Restoring backup",
"view.monitoring.backup_and_restore.CREATE_CLOUD_BACKUP_IN_PROGRESS": "Creating cloud backup",
"view.monitoring.backup_and_restore.RESTORE_CLOUD_BACKUP_IN_PROGRESS": "Restoring cloud backup",
"view.monitoring.backup_and_restore.no_running_backup_and_restore": "No running backup or restore.",
"view.rdf.rank.title": "RDF Rank",
"view.rdf.rank.helpInfo": "RDF Rank is an algorithm that identifies the most important or popular entities in the repository by examining their interconnectedness. The popularity of the entities can be used to order the query results. Use this view to configure the RDF rank, recompute it or check the current state.",
"view.repositories.helpInfo": "The Repositories view is used to create, modify and delete repositories and connections to remote GraphDB instances (also known as remote locations).",
Expand Down Expand Up @@ -639,6 +653,7 @@
"menu.sparql.label": "SPARQL",
"menu.monitor.label": "Monitor",
"menu.queries.and.updates.label": "Queries and Updates",
"menu.backup_and_restore.label": "Backup and Restore",
"menu.enableFtsIndex.label": "Enable full-text search (FTS) index",
"menu.ftsIndexes.label": "FTS indexes to build (comma delimited)",
"menu.ftsStringLiteralsIndex.label": "FTS index for xsd:string literals",
Expand Down
15 changes: 15 additions & 0 deletions src/i18n/locale-fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,20 @@
"view.namespaces.helpInfo": "La vue Espaces de noms fournit une vue d'ensemble de tous les espaces de noms définis dans un dépôt GraphDB. Les espaces de noms sont essentiellement des notations abrégées pour les IRI. Vous pouvez y ajouter, supprimer et modifier ces espaces.",
"view.query.and.update.monitoring.title": "Surveillance des requêtes et des mises à jour",
"view.query.and.update.monitoring.helpInfo": "La vue de surveillance des requêtes et des mises à jour montre toutes les requêtes ou mises à jour en cours dans GraphDB. Une requête ou une mise à jour peut être interrompue en appuyant sur le bouton Abandon.",
"view.monitoring.backup_and_restore.title": "Sauvegarde et Restauration",
"view.monitoring.backup_and_restore.helpInfo": "La vue de surveillance de la sauvegarde et de la restauration affiche les opérations de sauvegarde ou de restauration en cours.",
"view.monitoring.backup_and_restore.id.header": "Id",
"view.monitoring.backup_and_restore.username.header": "Nom d'utilisateur",
"view.monitoring.backup_and_restore.recovery_operation_type.header": "Type d'opération de restauration",
"view.monitoring.backup_and_restore.affected_repositories.header": "Dépôts affectés",
"view.monitoring.backup_and_restore.lifetime.header": "Durée de vie",
"view.monitoring.backup_and_restore.snapshot_options.header": "Options de capture instantanée",
"view.monitoring.backup_and_restore.node_performing_cluster_backup.header": "Nœud effectuant la sauvegarde",
"view.monitoring.backup_and_restore.CREATE_BACKUP_IN_PROGRESS": "Création de sauvegarde en cours",
"view.monitoring.backup_and_restore.RESTORE_BACKUP_IN_PROGRESS": "Restauration de sauvegarde en cours",
"view.monitoring.backup_and_restore.CREATE_CLOUD_BACKUP_IN_PROGRESS": "Création de sauvegarde dans le cloud en cours",
"view.monitoring.backup_and_restore.RESTORE_CLOUD_BACKUP_IN_PROGRESS": "Restauration de sauvegarde dans le cloud en cours",
"view.monitoring.backup_and_restore.no_running_backup_and_restore": "Aucune sauvegarde ou restauration en cours.",
"view.rdf.rank.title": "Rang RDF",
"view.rdf.rank.helpInfo": "RDF Rank est un algorithme qui identifie les entités les plus importantes ou les plus populaires dans le dépôt en examinant leur interconnexion. La popularité des entités peut alors être utilisée pour ordonner les résultats des requêtes. Utilisez cette vue pour configurer le RDF Rank, le recalculer ou vérifier l'état actuel.",
"view.repositories.helpInfo": "La vue Dépôts est utilisée pour créer, modifier et supprimer des dépôts et des connexions à des instances GraphDB distantes (également appelées emplacements distants).",
Expand Down Expand Up @@ -642,6 +656,7 @@
"menu.sparql.label": "SPARQL",
"menu.monitor.label": "Surveiller",
"menu.queries.and.updates.label": "Requêtes et mises à jour",
"menu.backup_and_restore.label": "Sauvegarde et Restauration",
"menu.ftsLanguages.label": "Entrez la langue pour la recherche plein texte",
"menu.resources.label": "Système",
"menu.setup.label": "Configurer",
Expand Down
6 changes: 6 additions & 0 deletions src/js/angular/backup-and-restore/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'angular/backup-and-restore/controllers';
const modules = [
'graphdb.framework.monitoring.backupandrestore.controllers'
];

angular.module('graphdb.framework.monitoring.backupandrestore', modules);
65 changes: 65 additions & 0 deletions src/js/angular/backup-and-restore/controllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const modules = [];
const UPDATE_BACKUP_AND_RESTORE_INFO_DATA_TIME_INTERVAL = 2000;

angular
.module('graphdb.framework.monitoring.backupandrestore.controllers', modules)
.controller('BackupAndRestoreCtrl', BackupAndRestoreCtrl);

BackupAndRestoreCtrl.$inject = ['$scope', '$interval', 'MonitoringRestService'];

function BackupAndRestoreCtrl($scope, $interval, MonitoringRestService) {

$scope.loading = false;
$scope.initialized = false;

/**
* @type {BackupAndRestoreInfo[]}
*/
$scope.backupAndRestoreInfos = undefined;
$scope.hasClusterOperation = false;
let timer = undefined;

// =========================
// Public functions
// =========================
$scope.hasToShowValue = (value) => {
if (typeof value === 'boolean') {
return true;
}
return !!value;
};

// =========================
// Private functions
// =========================
const loadBackupAndRestoreData = () => {
if ($scope.loading) {
return;
}
$scope.loading = true;
MonitoringRestService.monitorBackup()
.then((backupAndRestoreInfos) => {
$scope.backupAndRestoreInfos = backupAndRestoreInfos;
$scope.initialized = true;
$scope.hasClusterOperation = $scope.backupAndRestoreInfos.some((backupAndRestoreInfo) => backupAndRestoreInfo.nodePerformingClusterBackup);
})
.finally(() => $scope.loading = false);
};

const removeAllListeners = () => {
if (timer) {
$interval.cancel(timer);
}
};

// =========================
// Event handlers
// =========================
$scope.$on('$destroy', removeAllListeners);

// =========================
// Initializes controller.
// =========================
loadBackupAndRestoreData();
timer = $interval(() => loadBackupAndRestoreData(), UPDATE_BACKUP_AND_RESTORE_INFO_DATA_TIME_INTERVAL);
}
23 changes: 23 additions & 0 deletions src/js/angular/backup-and-restore/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
PluginRegistry.add('route', {
'url': '/monitor/backup-and-restore',
'module': 'graphdb.framework.monitoring.backupandrestore',
'path': 'backup-and-restore/app',
'chunk': 'monitor-backup-and-restore',
'controller': 'BackupAndRestoreCtrl',
'templateUrl': 'pages/monitor/backup-and-restore.html',
'title': 'view.monitoring.backup_and_restore.title',
'helpInfo': 'view.monitoring.backup_and_restore.helpInfo'
});

PluginRegistry.add('main.menu', {
'items': [
{
label: 'Backup and Restore',
labelKey: 'menu.backup_and_restore.label',
href: 'monitor/backup-and-restore',
order: 2,
parent: 'Monitor',
guideSelector: 'sub-menu-backup-and-restore'
}
]
});
15 changes: 15 additions & 0 deletions src/js/angular/models/monitoring/backup-and-restore-info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export class BackupAndRestoreInfo {
constructor() {
this.id = undefined;
this.username = '';

/**
* @type {string} - The value must be one of the @{see BackupAndRestoreOperationType} values.
*/
this.operation = '';
this.affectedRepositories = [];
this.secondsSinceCreated = 0;
this.snapshotOptions = [];
this.nodePerformingClusterBackup = '';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const BackupAndRestoreOperationType = {
'CREATE_BACKUP_IN_PROGRESS': 'CREATE_BACKUP_IN_PROGRESS',
'RESTORE_BACKUP_IN_PROGRESS': 'RESTORE_BACKUP_IN_PROGRESS',
'CREATE_CLOUD_BACKUP_IN_PROGRESS': 'CREATE_CLOUD_BACKUP_IN_PROGRESS',
'RESTORE_CLOUD_BACKUP_IN_PROGRESS': 'RESTORE_CLOUD_BACKUP_IN_PROGRESS'
};
11 changes: 11 additions & 0 deletions src/js/angular/models/monitoring/snapshot-option-info.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class SnapshotOptionInfo {
constructor() {
this.withRepositoryData = false;
this.withSystemData = false;
this.withClusterData = false;
this.cleanDataDir = false;
this.removeCluster = false;
this.repositories = [];
this.replicationTimeoutMs = 0;
}
}
2 changes: 1 addition & 1 deletion src/js/angular/resources/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ PluginRegistry.add('main.menu', {
href: 'monitor/system',
// Added role requirement here to assert that users cannot see Resources menu item
role: 'ROLE_MONITORING',
order: 2,
order: 3,
parent: 'Monitor',
guideSelector: 'sub-menu-resources'
}
Expand Down
30 changes: 30 additions & 0 deletions src/js/angular/rest/mappers/monitor-backup-and-restore-mapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {BackupAndRestoreInfo} from "../../models/monitoring/backup-and-restore-info";
import {SnapshotOptionInfo} from "../../models/monitoring/snapshot-option-info";

export const mapBackupAndRestoreResponseToModel = (responseData = []) => {
const backupAndRestoreInfos = [];
responseData.forEach((backupAndRestoreResponseData) => {
const backupAndRestoreInfo = new BackupAndRestoreInfo();
backupAndRestoreInfo.id = backupAndRestoreResponseData.id;
backupAndRestoreInfo.username = backupAndRestoreResponseData.username;
backupAndRestoreInfo.operation = backupAndRestoreResponseData.operation;
backupAndRestoreInfo.affectedRepositories = backupAndRestoreResponseData.affectedRepositories;
backupAndRestoreInfo.secondsSinceCreated = backupAndRestoreResponseData.msSinceCreated / 1000;
backupAndRestoreInfo.snapshotOptions = mapSnapshotOptionsResponseToModel(backupAndRestoreResponseData.snapshotOptions);
backupAndRestoreInfo.nodePerformingClusterBackup = backupAndRestoreResponseData.nodePerformingClusterBackup;
backupAndRestoreInfos.push(backupAndRestoreInfo);
});
return backupAndRestoreInfos;
};

export const mapSnapshotOptionsResponseToModel = ((snapshotOption) => {
const snapshotOptionInfo = new SnapshotOptionInfo();
snapshotOptionInfo.withRepositoryData = snapshotOption.withRepositoryData;
snapshotOptionInfo.withSystemData = snapshotOption.withSystemData;
snapshotOptionInfo.withClusterData = snapshotOption.withClusterData;
snapshotOptionInfo.cleanDataDir = snapshotOption.cleanDataDir;
snapshotOptionInfo.removeCluster = snapshotOption.removeCluster;
snapshotOptionInfo.repositories = snapshotOption.repositories;
snapshotOptionInfo.replicationTimeoutMs = snapshotOption.replicationTimeoutMs;
return snapshotOptionInfo;
});
13 changes: 13 additions & 0 deletions src/js/angular/rest/monitoring.rest.service.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {mapBackupAndRestoreResponseToModel} from "./mappers/monitor-backup-and-restore-mapper";

angular
.module('graphdb.framework.rest.monitoring.service', [])
.factory('MonitoringRestService', MonitoringRestService);
Expand All @@ -6,6 +8,7 @@ MonitoringRestService.$inject = ['$http'];

const MONITORING_ENDPOINT = 'rest/monitor';
const QUERY_MONITORING_ENDPOINT = `${MONITORING_ENDPOINT}/repository`;
const BACKUP_MONITORING_ENDPOINT = `${MONITORING_ENDPOINT}/backup`;

function MonitoringRestService($http) {
return {
Expand All @@ -14,6 +17,7 @@ function MonitoringRestService($http) {
monitorCluster,
monitorGC,
monitorQuery,
monitorBackup,
deleteQuery,
getQueryCount,
checkAutocompleteStatus,
Expand Down Expand Up @@ -44,6 +48,15 @@ function MonitoringRestService($http) {
return $http.get(`${QUERY_MONITORING_ENDPOINT}/${repositoryID}/query/active`);
}

/**
*
* @return {Promise<BackupAndRestoreInfo>}
*/
function monitorBackup() {
return $http.get(BACKUP_MONITORING_ENDPOINT)
.then((response) => mapBackupAndRestoreResponseToModel(response.data));
}

function deleteQuery(queryId, repositoryID) {
return $http.delete(`${QUERY_MONITORING_ENDPOINT}/${repositoryID}/query?query=${encodeURIComponent(queryId)}`);
}
Expand Down
74 changes: 74 additions & 0 deletions src/pages/monitor/backup-and-restore.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<div>
<title>${messages.app.title} : ${messages.jmx.QueryMonitoringPage.title}</title>

<link href="css/backup-and-restore.css?v=[AIV]{version}[/AIV]" rel="stylesheet"/>

<h1>
{{title}}
<span class="btn btn-link"
uib-popover-template="'js/angular/templates/titlePopoverTemplate.html'"
popover-trigger="mouseenter"
popover-placement="bottom-right"
popover-append-to-body="true">
<span class="icon-info text-tertiary"></span>
</span>
<span onto-loader-new class="backup-and-restore-updater-icon" size="25">
</span>
</h1>

<div core-errors ontop></div>

<div ng-if="initialized" class="backup-and-restore">
<table ng-if="backupAndRestoreInfos && backupAndRestoreInfos.length > 0" class="table table-striped table-bordered"
aria-describedby="Monitor backup and restore">
<thead>
<tr>
<th class="id-header">{{'view.monitoring.backup_and_restore.id.header' | translate}}</th>
<th class="username-header">{{'view.monitoring.backup_and_restore.username.header' | translate}}</th>
<th class="recovery-operation-header">{{'view.monitoring.backup_and_restore.recovery_operation_type.header' | translate}}
</th>
<th class="affected-repository-header">{{'view.monitoring.backup_and_restore.affected_repositories.header' | translate}}
</th>
<th class="lifetime-header">{{'view.monitoring.backup_and_restore.lifetime.header' | translate}}</th>
<th class="snapshot-options-header">{{'view.monitoring.backup_and_restore.snapshot_options.header' | translate}}</th>
<th ng-if="hasClusterOperation" class="node-performing-cluster-backup-header">
{{'view.monitoring.backup_and_restore.node_performing_cluster_backup.header' | translate}}
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="backupAndRestoreInfo in backupAndRestoreInfos">
<td>{{backupAndRestoreInfo.id}}</td>
<td>{{backupAndRestoreInfo.username}}</td>
<td>{{('view.monitoring.backup_and_restore.' + backupAndRestoreInfo.operation) | translate}}</td>
<td>
<div ng-repeat="repository in backupAndRestoreInfo.affectedRepositories">
{{repository}}
</div>
</td>
<td>{{getHumanReadableSeconds(backupAndRestoreInfo.secondsSinceCreated)}}</td>
<td class="snapshot-options-data">
<div ng-repeat="(key, value) in backupAndRestoreInfo.snapshotOptions">
<div ng-if="hasToShowValue(value)">
<b>{{key}}</b> : {{value}}
</div>
</div>
</td>
<td ng-if="hasClusterOperation">{{backupAndRestoreInfo.nodePerformingClusterBackup}}</td>
</tr>
</tbody>
</table>

<div class="alert alert-info no-icon no-running-backup-and-restore-alert"
ng-if="!backupAndRestoreInfos || !backupAndRestoreInfos.length || backupAndRestoreInfos.length === 0">
{{'view.monitoring.backup_and_restore.no_running_backup_and_restore' | translate}}
</div>
</div>

<div ng-if="!initialized && loading">
<div onto-loader-new
ng-if="loading"
size="75">
</div>
</div>
</div>
Loading

0 comments on commit b8b45b6

Please sign in to comment.