From 5f58598df8b0874b1b602ba56760ca25fd885ee6 Mon Sep 17 00:00:00 2001 From: "svilen.velikov" Date: Tue, 23 Apr 2024 18:49:44 +0300 Subject: [PATCH] GDB-10162 Cluster node info panel with status change history. ## What Implementation of cluster node info panel with status change history. ## Why ## How --- src/i18n/locale-en.json | 3 + src/js/angular/clustermanagement/app.js | 4 +- .../cluster-management.controller.js | 65 ++++++++++++++++--- .../cluster-configuration.directive.js | 4 +- .../cluster-graphical-view.directive.js | 2 +- .../directives/node-info.directive.js | 63 ++++++++++++++++++ .../services/cluster-drawing.service.js | 17 +++++ .../services/cluster-view-context.service.js | 29 ++++++++- .../templates/node-info.html | 44 +++++++++++++ .../clustermanagement/node-status-info.js | 8 +++ src/js/angular/rest/cluster.rest.service.js | 15 ++++- .../rest/mappers/cluster-management-mapper.js | 7 ++ src/pages/cluster-management/clusterInfo.html | 42 +++++------- 13 files changed, 261 insertions(+), 42 deletions(-) create mode 100644 src/js/angular/clustermanagement/directives/node-info.directive.js create mode 100644 src/js/angular/clustermanagement/templates/node-info.html create mode 100644 src/js/angular/models/clustermanagement/node-status-info.js create mode 100644 src/js/angular/rest/mappers/cluster-management-mapper.js diff --git a/src/i18n/locale-en.json b/src/i18n/locale-en.json index 4c514375f..025ec21e2 100644 --- a/src/i18n/locale-en.json +++ b/src/i18n/locale-en.json @@ -47,6 +47,9 @@ "label": "Cluster configuration", "nodes_list_label": "Nodes list" }, + "node_info": { + "node_state_history": "Node state history" + }, "cluster_status": { "node": { "rpc_address": "RPC address", diff --git a/src/js/angular/clustermanagement/app.js b/src/js/angular/clustermanagement/app.js index 4f6f50adf..6e4966f5a 100644 --- a/src/js/angular/clustermanagement/app.js +++ b/src/js/angular/clustermanagement/app.js @@ -3,6 +3,7 @@ import 'angular/core/directives'; import 'angular/clustermanagement/controllers/cluster-management.controller'; import 'angular/clustermanagement/directives/cluster-graphical-view.directive'; import 'angular/clustermanagement/directives/cluster-configuration.directive'; +import 'angular/clustermanagement/directives/node-info.directive'; import 'angular/core/services/repositories.service'; import 'lib/d3.patch.js'; import 'angular-pageslide-directive/dist/angular-pageslide-directive'; @@ -12,7 +13,8 @@ const modules = [ 'toastr', 'graphdb.framework.clustermanagement.controllers.cluster-management', 'graphdb.framework.clustermanagement.directives.cluster-graphical-view', - 'graphdb.framework.clustermanagement.directives.cluster-configuration' + 'graphdb.framework.clustermanagement.directives.cluster-configuration', + 'graphdb.framework.clustermanagement.directives.node-info' ]; angular.module('graphdb.framework.clustermanagement', modules); diff --git a/src/js/angular/clustermanagement/controllers/cluster-management.controller.js b/src/js/angular/clustermanagement/controllers/cluster-management.controller.js index fa4be82a5..534ec66b0 100644 --- a/src/js/angular/clustermanagement/controllers/cluster-management.controller.js +++ b/src/js/angular/clustermanagement/controllers/cluster-management.controller.js @@ -11,6 +11,7 @@ import 'angular/clustermanagement/controllers/replace-nodes.controller'; import {isString} from "lodash"; import {LinkState, NodeState} from "../../models/clustermanagement/states"; import {DELETE_CLUSTER, UPDATE_CLUSTER} from "../events"; +import {nodeStatusInfoMapper} from "../../rest/mappers/cluster-management-mapper"; const modules = [ 'ui.bootstrap', @@ -55,19 +56,25 @@ function ClusterManagementCtrl($scope, $http, $q, toastr, $repositories, $uibMod $scope.loader = true; $scope.isLeader = false; $scope.currentNode = null; + $scope.nodeStatusInfo = []; $scope.clusterModel = {}; $scope.NodeState = NodeState; $scope.leaderChanged = false; $scope.currentLeader = null; // Holds child context + // TODO: remove this and replace with events and parameters to the child directive $scope.childContext = {}; $scope.showClusterConfigurationPanel = false; + $scope.showNodeInfoPanel = false; // ========================= // Public functions // ========================= - $scope.onopen = $scope.onclose = () => angular.noop(); + $scope.onOpenClusterConfig = () => angular.noop(); + $scope.onCloseClusterConfig = () => angular.noop(); + $scope.onOpenNodeInfo = () => angular.noop(); + $scope.onCloseNodeInfo = () => angular.noop(); $scope.isAdmin = () => { return $jwtAuth.isAuthenticated() && $jwtAuth.isAdmin(); @@ -103,6 +110,7 @@ function ClusterManagementCtrl($scope, $http, $q, toastr, $repositories, $uibMod $scope.getClusterConfiguration = () => { return ClusterRestService.getClusterConfig() .then((response) => { + // console.log(`CLUSTER CONF`, response); $scope.clusterConfiguration = response.data; if (!$scope.currentNode) { return $scope.getCurrentNodeStatus(); @@ -116,6 +124,7 @@ function ClusterManagementCtrl($scope, $http, $q, toastr, $repositories, $uibMod $scope.getClusterStatus = () => { return ClusterRestService.getClusterStatus() .then((response) => { + // console.log(`CLUSTER STATUS`, response); const nodes = response.data.slice(); const leader = nodes.find((node) => node.nodeState === NodeState.LEADER); @@ -177,6 +186,7 @@ function ClusterManagementCtrl($scope, $http, $q, toastr, $repositories, $uibMod $scope.getCurrentNodeStatus = () => { return ClusterRestService.getNodeStatus() .then((response) => { + console.log(`NODE STATUS`, response); $scope.leaderChanged = false; $scope.currentNode = response.data; }) @@ -305,13 +315,42 @@ function ClusterManagementCtrl($scope, $http, $q, toastr, $repositories, $uibMod // Private functions // ========================= + const nodesAreEqual = (node1, node2) => { + return node1.address === node2.address; + }; + const selectNode = (node) => { - if ($scope.selectedNode !== node) { + // no selected node + if (!$scope.selectedNode) { + $scope.selectedNode = node; + $scope.showNodeInfoPanel = true; + ClusterViewContextService.showNodeInfoPanel(); + } else if (!nodesAreEqual(node, $scope.selectedNode)) { + // currently selected node and the newly selected node are different: select the newly selected node $scope.selectedNode = node; + if (!$scope.showNodeInfoPanel) { + $scope.showNodeInfoPanel = true; + ClusterViewContextService.showNodeInfoPanel(); + } + } else if (!$scope.showNodeInfoPanel) { + // panel was closed and selected node is the same: just reopen it + $scope.showNodeInfoPanel = true; + ClusterViewContextService.showNodeInfoPanel(); } else { + // close the panel and reset the selected node $scope.selectedNode = null; + $scope.showNodeInfoPanel = false; + ClusterViewContextService.hideNodeInfoPanel(); } - $scope.$apply(); + ClusterRestService.getNodeStatusHistory($scope.selectedNode.address).then((response) => { + const model = nodeStatusInfoMapper(response); + console.log(`model`, model); + $scope.nodeStatusInfo = model; + }).catch((error) => { + console.log(`error`, error); + }); + // TODO: probably not needed + // $scope.$apply(); }; const updateCluster = (force) => { @@ -431,11 +470,10 @@ function ClusterManagementCtrl($scope, $http, $q, toastr, $repositories, $uibMod }; const mousedownHandler = function (event) { - const target = event.target; - const nodeTooltipElement = document.getElementById('nodeTooltip'); - if ($scope.selectedNode && nodeTooltipElement !== target && !nodeTooltipElement.contains(target)) { - $scope.childContext.selectNode(null); - } + // reimplement this to close the sidebar when clicking outside of it + // if ($scope.selectedNode) { + // $scope.childContext.selectNode(null); + // } }; // ========================= @@ -462,6 +500,14 @@ function ClusterManagementCtrl($scope, $http, $q, toastr, $repositories, $uibMod subscriptions.push($scope.$on(DELETE_CLUSTER, (event, data) => { deleteCluster(data.force); })); + + subscriptions.push(ClusterViewContextService.onShowNodeInfoPanel((show) => { + $scope.showNodeInfoPanel = show; + })); + + subscriptions.push($scope.$on('nodeSelected', (event, node) => { + selectNode(node); + })); }; $scope.$on('$destroy', function () { @@ -476,8 +522,9 @@ function ClusterManagementCtrl($scope, $http, $q, toastr, $repositories, $uibMod const init = () => { subscribeHandlers(); - $scope.childContext.selectNode = selectNode; + // $scope.childContext.selectNode = selectNode; $scope.showClusterConfigurationPanel = ClusterViewContextService.getShowClusterConfigurationPanel(); + $scope.showNodeInfoPanel = ClusterViewContextService.getShowNodeInfoPanel(); loadInitialData() .finally(() => { diff --git a/src/js/angular/clustermanagement/directives/cluster-configuration.directive.js b/src/js/angular/clustermanagement/directives/cluster-configuration.directive.js index 79d438b67..7f6c22557 100644 --- a/src/js/angular/clustermanagement/directives/cluster-configuration.directive.js +++ b/src/js/angular/clustermanagement/directives/cluster-configuration.directive.js @@ -6,9 +6,9 @@ angular .module('graphdb.framework.clustermanagement.directives.cluster-configuration', modules) .directive('clusterConfiguration', ClusterConfiguration); -ClusterConfiguration.$inject = ['$jwtAuth', '$uibModal', '$translate', 'toastr', 'ClusterViewContextService', 'ClusterRestService']; +ClusterConfiguration.$inject = ['$jwtAuth', '$uibModal', '$translate', 'toastr', 'ClusterViewContextService']; -function ClusterConfiguration($jwtAuth, $uibModal, $translate, toastr, ClusterViewContextService, ClusterRestService) { +function ClusterConfiguration($jwtAuth, $uibModal, $translate, toastr, ClusterViewContextService) { return { restrict: 'E', templateUrl: 'js/angular/clustermanagement/templates/cluster-configuration.html', diff --git a/src/js/angular/clustermanagement/directives/cluster-graphical-view.directive.js b/src/js/angular/clustermanagement/directives/cluster-graphical-view.directive.js index 693580f03..a83b98bbf 100644 --- a/src/js/angular/clustermanagement/directives/cluster-graphical-view.directive.js +++ b/src/js/angular/clustermanagement/directives/cluster-graphical-view.directive.js @@ -382,7 +382,7 @@ clusterManagementDirectives.directive('clusterGraphicalView', ['$window', 'Local nodesElements .on('click', (event, d) => { - scope.childContext.selectNode(d); + scope.$emit('nodeSelected', d); // position the tooltip according to the node! const tooltip = d3.select('.nodetooltip'); diff --git a/src/js/angular/clustermanagement/directives/node-info.directive.js b/src/js/angular/clustermanagement/directives/node-info.directive.js new file mode 100644 index 000000000..cb3f8004c --- /dev/null +++ b/src/js/angular/clustermanagement/directives/node-info.directive.js @@ -0,0 +1,63 @@ +import {createSingleHexagon} from "../services/cluster-drawing.service"; + +angular + .module('graphdb.framework.clustermanagement.directives.node-info', []) + .directive('nodeInfo', nodeInfo); + +function nodeInfo() { + const directive = { + restrict: 'E', + templateUrl: 'js/angular/clustermanagement/templates/node-info.html', + scope: { + currentNode: '=', + nodeStatusInfo: '=' + }, + // link: linkFunc, + controller: NodeInfoCtrl, + controllerAs: 'vm', + bindToController: true + }; + + return directive; + + // function linkFunc($scope) { + // $scope.node = $scope.currentNode; + // console.log(`node`, $scope.node); + // } +} + +NodeInfoCtrl.$inject = ['$scope', 'ClusterViewContextService']; + +function NodeInfoCtrl($scope, ClusterViewContextService) { + const vm = this; + + vm.closeNodeInfoPanel = () => { + ClusterViewContextService.hideNodeInfoPanel(); + }; + + const createNodeHexagons = () => { + const nodeHexagons = vm.nodeStatusInfo.map((node, index) => { + return createSingleHexagon(`.item .node-${index}`, 30); + }); + console.log(`nodeHexagons`, nodeHexagons, vm.nodeStatusInfo); + }; + + const subscriptions = []; + + const removeAllListeners = () => { + subscriptions.forEach((subscription) => subscription()); + }; + + $scope.$on('$destroy', function () { + removeAllListeners(); + }); + + const init = () => { + console.log(`%cINIT:`, 'background: red', ); + subscriptions.push(ClusterViewContextService.onShowNodeInfoPanel((show) => { + console.log(`%cOPENED:`, 'background: green', show); + })); + createNodeHexagons(); + }; + init(); +} diff --git a/src/js/angular/clustermanagement/services/cluster-drawing.service.js b/src/js/angular/clustermanagement/services/cluster-drawing.service.js index b7e2f3d2a..63bdbbee3 100644 --- a/src/js/angular/clustermanagement/services/cluster-drawing.service.js +++ b/src/js/angular/clustermanagement/services/cluster-drawing.service.js @@ -448,6 +448,23 @@ function createHexagon(nodeGroup, radius) { .attr("d", d3.line()); } + +export function createSingleHexagon(by, radius) { + const _s32 = (Math.sqrt(3) / 2); + const xDiff = 0; + const yDiff = 0; + const points = [[radius + xDiff, yDiff], [radius / 2 + xDiff, radius * _s32 + yDiff], [-radius / 2 + xDiff, radius * _s32 + yDiff], + [-radius + xDiff, yDiff], + [-radius / 2 + xDiff, -radius * _s32 + yDiff], [radius / 2 + xDiff, -radius * _s32 + yDiff], [radius + xDiff, yDiff], + [radius / 2 + xDiff, radius * _s32 + yDiff]]; + const svg = d3.select(by); + return svg + .data([points]) + .append("path") + .attr('class', 'node member') + .attr("d", d3.line()); +} + export function removeEventListeners() { d3.select(document).selectAll('.node-info-fo').on('.tooltip', null); } diff --git a/src/js/angular/clustermanagement/services/cluster-view-context.service.js b/src/js/angular/clustermanagement/services/cluster-view-context.service.js index 15a2d13f3..a62cfef15 100644 --- a/src/js/angular/clustermanagement/services/cluster-view-context.service.js +++ b/src/js/angular/clustermanagement/services/cluster-view-context.service.js @@ -6,6 +6,7 @@ ClusterViewContextService.$inject = ['EventEmitterService']; function ClusterViewContextService(EventEmitterService) { let _showClusterConfigurationPanel = false; + let _showNodeInfoPanel = false; function getShowClusterConfigurationPanel() { return _showClusterConfigurationPanel; @@ -28,11 +29,37 @@ function ClusterViewContextService(EventEmitterService) { return EventEmitterService.subscribe('showClusterConfigurationPanel', () => callback(getShowClusterConfigurationPanel())); } + function getShowNodeInfoPanel() { + return _showNodeInfoPanel; + } + + function setShowNodeInfoPanel(showNodeInfoPanel) { + _showNodeInfoPanel = showNodeInfoPanel; + EventEmitterService.emit('showNodeInfoPanel', getShowNodeInfoPanel()); + } + + function showNodeInfoPanel() { + setShowNodeInfoPanel(true); + } + + function hideNodeInfoPanel() { + setShowNodeInfoPanel(false); + } + + function onShowNodeInfoPanel(callback) { + return EventEmitterService.subscribe('showNodeInfoPanel', () => callback(getShowNodeInfoPanel())); + } + return { getShowClusterConfigurationPanel, setShowClusterConfigurationPanel, showClusterConfigurationPanel, hideClusterConfigurationPanel, - onShowClusterConfigurationPanel + onShowClusterConfigurationPanel, + getShowNodeInfoPanel, + setShowNodeInfoPanel, + showNodeInfoPanel, + hideNodeInfoPanel, + onShowNodeInfoPanel }; } diff --git a/src/js/angular/clustermanagement/templates/node-info.html b/src/js/angular/clustermanagement/templates/node-info.html new file mode 100644 index 000000000..28168f001 --- /dev/null +++ b/src/js/angular/clustermanagement/templates/node-info.html @@ -0,0 +1,44 @@ +
+ +

+ {{vm.currentNode.endpoint}} +

+ +
+
+
{{'cluster_management.cluster_status.node.rpc_address' | translate}}
+
{{vm.currentNode.address}}
+
+
+
{{'cluster_management.cluster_status.node.state' | translate}}
+
{{vm.currentNode.nodeState}}
+
+
+
{{'cluster_management.cluster_status.node.term' | translate}}
+
{{vm.currentNode.term}}
+
+
+
{{'cluster_management.cluster_status.node.last_log_term' | translate}}
+
{{vm.currentNode.lastLogTerm}}
+
+
+
{{'cluster_management.cluster_status.node.last_log_index' | translate}}
+
{{vm.currentNode.lastLogIndex}}
+
+
+ +
+
{{'cluster_management.node_info.node_state_history' | translate}}
+ +
+
+
+
icon: {{nodeInfo.status}}
+
+
{{nodeInfo.timestamp}}
+
{{nodeInfo.message}}
+
+
+
+
+
diff --git a/src/js/angular/models/clustermanagement/node-status-info.js b/src/js/angular/models/clustermanagement/node-status-info.js new file mode 100644 index 000000000..46b2ca890 --- /dev/null +++ b/src/js/angular/models/clustermanagement/node-status-info.js @@ -0,0 +1,8 @@ +export class NodeStatusInfo { + constructor(address, status, message, timestamp) { + this.address = address; + this.status = status; + this.message = message; + this.timestamp = timestamp; + } +} diff --git a/src/js/angular/rest/cluster.rest.service.js b/src/js/angular/rest/cluster.rest.service.js index ce68d6ad0..2c0686ac2 100644 --- a/src/js/angular/rest/cluster.rest.service.js +++ b/src/js/angular/rest/cluster.rest.service.js @@ -16,7 +16,8 @@ function ClusterRestService($http) { replaceNodesInCluster, removeNodesFromCluster, getClusterStatus, - getNodeStatus + getNodeStatus, + getNodeStatusHistory }; /** @@ -65,4 +66,16 @@ function ClusterRestService($http) { function getNodeStatus() { return $http.get(`${CLUSTER_GROUP_ENDPOINT}/node/status`); } + + function getNodeStatusHistory() { + return Promise.resolve({ + data: [ + {"address": "svelikov-desktop:7300", "status": "IN_SYNC", "message": "In sync", "timestamp": "1664716800000"}, + {"address": "svelikov-desktop:7300", "status": "REQUESTING_SNAPSHOT", "message": "Requesting snapshot", "timestamp": "1664713200000"}, + {"address": "svelikov-desktop:7300", "status": "SEARCHING", "message": "Searching", "timestamp": "1664709600000"}, + {"address": "svelikov-desktop:7300", "status": "OUT_OF_SYNC", "message": "Out of sync", "timestamp": "1664706000000"}, + {"address": "svelikov-desktop:7300", "status": "IN_SYNC", "message": "In sync", "timestamp": "1664702400000"} + ] + }); + } } diff --git a/src/js/angular/rest/mappers/cluster-management-mapper.js b/src/js/angular/rest/mappers/cluster-management-mapper.js new file mode 100644 index 000000000..a1f58a057 --- /dev/null +++ b/src/js/angular/rest/mappers/cluster-management-mapper.js @@ -0,0 +1,7 @@ +import {NodeStatusInfo} from "../../models/clustermanagement/node-status-info"; + +export const nodeStatusInfoMapper = (response) => { + if (response && response.data) { + return response.data.map((nodeInfo) => new NodeStatusInfo(nodeInfo.address, nodeInfo.status, nodeInfo.message, nodeInfo.timestamp)); + } +}; diff --git a/src/pages/cluster-management/clusterInfo.html b/src/pages/cluster-management/clusterInfo.html index 7351f62ef..4965d3f6f 100644 --- a/src/pages/cluster-management/clusterInfo.html +++ b/src/pages/cluster-management/clusterInfo.html @@ -47,31 +47,6 @@

tooltip-placement="left"> -
- {{selectedNode.endpoint}} -
-
-
{{'cluster_management.cluster_status.node.rpc_address' | translate}}
-
{{selectedNode.address}}
-
-
-
{{'cluster_management.cluster_status.node.state' | translate}}
-
{{selectedNode.nodeState}}
-
-
-
{{'cluster_management.cluster_status.node.term' | translate}}
-
{{selectedNode.term}}
-
-
-
{{'cluster_management.cluster_status.node.last_log_term' | translate}}
-
{{selectedNode.lastLogTerm}}
-
-
-
{{'cluster_management.cluster_status.node.last_log_index' | translate}}
-
{{selectedNode.lastLogIndex}}
-
-
-
@@ -83,13 +58,26 @@

+showNodeInfoPanel={{showNodeInfoPanel}} + + +