diff --git a/src/css/aclmanagement.css b/src/css/aclmanagement.css index f58d374d9..b5d68f713 100644 --- a/src/css/aclmanagement.css +++ b/src/css/aclmanagement.css @@ -1,4 +1,9 @@ +.acl-management-view .acl-rules { + table-layout: fixed; +} + .acl-management-view .acl-rules .toolbar { + height: 35px; padding: 0; position: relative; } @@ -8,8 +13,16 @@ background-color: var(--color-info-light); } -.acl-management-view .acl-rules .actions-column { - min-width: 107px; +.acl-management-view .acl-rules .preview-rule-row td.data { + overflow-wrap: break-word; +} + +.acl-management-view .acl-rules .index-column { + width: 2%; +} + +.acl-management-view .acl-rules .reorder-column { + width: 25px; } .acl-management-view .acl-rules .reorder-cell button { @@ -17,11 +30,65 @@ cursor: pointer; } +.acl-management-view .acl-rules .data input { + width: 95%; + display: inline-block; +} + +.acl-management-view .acl-rules .data input.ng-invalid ~ em:after { + content: "*"; + color: red; + display: inline; + position: absolute; + margin: 4px 0 0 3px; +} + +.acl-management-view .acl-rules .subject-column { + width: 18%; +} + +.acl-management-view .acl-rules .predicate-column { + width: 18%; +} + +.acl-management-view .acl-rules .object-column { + width: 18%; +} + +.acl-management-view .acl-rules .context-column { + width: 18%; +} + +.acl-management-view .acl-rules .role-column { + width: 18%; +} + +.acl-management-view .acl-rules .policy-column { + width: 100px; +} + +.acl-management-view .acl-rules .actions-column { + width: 107px; +} + +.acl-management-view .acl-rules .actions-cell { + text-align: right; +} + .acl-management-view .acl-rules .actions-cell button { padding: 0; cursor: pointer; } .acl-management-view .acl-rules .selected { - outline: 1px solid var(--secondary-color); + animation: highlight 2s 1; +} + +@keyframes highlight { + 0% { + background-color: rgba(255, 255, 153, .8); + } + 100% { + background-color: rgba(255, 255, 255, 0); + } } diff --git a/src/i18n/locale-en.json b/src/i18n/locale-en.json index 6eb09feb0..03d1f529e 100644 --- a/src/i18n/locale-en.json +++ b/src/i18n/locale-en.json @@ -138,15 +138,28 @@ "role": "Role", "policy": "Policy" }, + "field_placeholders": { + "subject": "Subject", + "predicate": "Predicate", + "object": "Object", + "context": "Context", + "role": "Role" + }, "actions": { "move_up": "Move the rule up", "move_down": "Move the rule down", "delete_rule": "Delete rule", "add_rule": "Add new rule", - "edit_rule": "Edit rule" + "edit_rule": "Edit rule", + "save_rule": "Save rule", + "cancel_rule_editing": "Cancel rule editing", + "save_acl": "Save ACL", + "cancel_acl_saving": "Cancel" + }, "messages": { - "no_data": "There are no rules defined in current repository" + "no_data": "There are no rules defined in current repository", + "invalid_form": "Fill in valid data in all fields before saving rule" } }, "errors": { diff --git a/src/i18n/locale-fr.json b/src/i18n/locale-fr.json index a5b4d30aa..8456317c0 100644 --- a/src/i18n/locale-fr.json +++ b/src/i18n/locale-fr.json @@ -138,15 +138,27 @@ "role": "Rôle", "policy": "Politique" }, + "field_placeholders": { + "subject": "Sujet", + "predicate": "Prédicat", + "object": "Objet", + "context": "Contexte", + "role": "Politique" + }, "actions": { "move_up": "Déplacer la règle vers le haut", "move_down": "Déplacer la règle vers le bas", "delete_rule": "Supprimer la règle", "add_rule": "Ajouter une nouvelle règle", - "edit_rule": "Modifier la règle" + "edit_rule": "Modifier la règle", + "save_rule": "Enregistrer la règle", + "cancel_rule_editing": "Annuler la modification des règles", + "save_acl": "Enregistrer la liste de contrôle d'accès", + "cancel_acl_saving": "Annuler" }, "messages": { - "no_data": "Aucune règle n'est définie dans le référentiel actuel" + "no_data": "Aucune règle n'est définie dans le référentiel actuel", + "invalid_form": "Remplissez des données valides dans tous les champs avant d'enregistrer la règle" } }, "errors": { diff --git a/src/js/angular/aclmanagement/controllers.js b/src/js/angular/aclmanagement/controllers.js index 594923df3..7996f2894 100644 --- a/src/js/angular/aclmanagement/controllers.js +++ b/src/js/angular/aclmanagement/controllers.js @@ -20,6 +20,12 @@ function AclManagementCtrl($scope, toastr, AclManagementRestService, $repositori // Public fields // + /** + * The model for the rule editing form. + * @type {Object} The edited rule form model. + */ + $scope.ruleData = {}; + /** * Flag controlling the loading indicator while some http operation is in progress. * @type {boolean} @@ -31,6 +37,7 @@ function AclManagementCtrl($scope, toastr, AclManagementRestService, $repositori * @type {undefined|ACListModel} */ $scope.rulesModel = undefined; + /** * A copy of the list with ACL rules that will be managed in this view. This is needed for restoring functionality. * @type {undefined|ACListModel} @@ -43,16 +50,23 @@ function AclManagementCtrl($scope, toastr, AclManagementRestService, $repositori */ $scope.selectedRule = undefined; + /** + * The index of a rule which is opened for edit or create. + * @type {undefined|number} + */ + $scope.editedRuleIndex = undefined; + // // Public functions // /** - * Adds a new rule at a given index. + * Adds a new rule at a given index in the rulesModel. * @param {number} index */ $scope.addRule= (index) => { - // TODO: implement + $scope.rulesModel.addRule(index); + $scope.editedRuleIndex = index; }; /** @@ -71,6 +85,22 @@ function AclManagementCtrl($scope, toastr, AclManagementRestService, $repositori // TODO: implement }; + /** + * Saves a rule at a given index in the rulesModel. + */ + $scope.saveRule= () => { + $scope.editedRuleIndex = undefined; + }; + + /** + * Cancels the editing operation of a rule at a given index. + * @param {number} index + */ + $scope.cancelEditing = (index) => { + $scope.rulesModel.removeRule(index); + $scope.editedRuleIndex = undefined; + }; + /** * Moves a rule on the given index in the rulesModel one position up by swapping it with the rule above. * @param {number} index @@ -89,6 +119,14 @@ function AclManagementCtrl($scope, toastr, AclManagementRestService, $repositori $scope.selectedRule = index + 1; }; + $scope.saveAcl = () => { + // TODO: implement + }; + + $scope.cancelAclSave = () => { + // TODO: implement + }; + // // Private functions // @@ -115,7 +153,6 @@ function AclManagementCtrl($scope, toastr, AclManagementRestService, $repositori /** * Watching for repository changes and reload the rules, because they are stored per repository. - * TODO: later when we have the create/edit operations we would need a confirmation before repo change in order to prevent data loss for the user. */ $scope.$watch(function () { return $repositories.getActiveRepository(); diff --git a/src/js/angular/aclmanagement/model.js b/src/js/angular/aclmanagement/model.js index 37a1f8c3d..30e11054b 100644 --- a/src/js/angular/aclmanagement/model.js +++ b/src/js/angular/aclmanagement/model.js @@ -11,6 +11,14 @@ export class ACListModel { return this.aclRules.length; } + addRule(index) { + this.aclRules.splice(index, 0, new ACRuleModel()); + } + + removeRule(index) { + this.aclRules.splice(index, 1); + } + moveUp(index) { const previousRule = this.aclRules[index - 1]; this.aclRules[index - 1] = this.aclRules[index]; @@ -42,7 +50,7 @@ export class ACRuleModel { * @param {string} role * @param {string} policy */ - constructor(subject, predicate, object, context, role, policy) { + constructor(subject = '', predicate = '', object= '', context= '', role= '', policy= ACL_POLICY.ALLOW) { this._subject = subject; this._predicate = predicate; this._object = object; diff --git a/src/pages/aclmanagement.html b/src/pages/aclmanagement.html index e269c68b3..1d4472ab0 100644 --- a/src/pages/aclmanagement.html +++ b/src/pages/aclmanagement.html @@ -14,79 +14,162 @@

- - - - - - - - - - - - - - - + + +
{{'acl_management.rulestable.column.index' | translate}}{{'acl_management.rulestable.column.subject' | translate}}{{'acl_management.rulestable.column.predicate' | translate}}{{'acl_management.rulestable.column.object' | translate}}{{'acl_management.rulestable.column.context' | translate}}{{'acl_management.rulestable.column.role' | translate}}{{'acl_management.rulestable.column.policy' | translate}}
-
- + + +
+ +
+ +
+ +
diff --git a/test-cypress/fixtures/locale-en.json b/test-cypress/fixtures/locale-en.json index 1b05a4ef7..03d1f529e 100644 --- a/test-cypress/fixtures/locale-en.json +++ b/test-cypress/fixtures/locale-en.json @@ -26,7 +26,15 @@ "node_state_restricted": "Restricted", "link_state_in_sync": "In sync", "link_state_syncing": "Syncing", - "link_state_out_of_sync": "Out of sync" + "link_state_out_of_sync": "Out of sync", + "recovery_state": { + "searching_for_node": "Searching for node", + "applying_snapshot": "Applying a snapshot", + "building_snapshot": "Building a snapshot for {{node}}", + "waiting_for_snapshot": "Waiting for snapshot from node {{node}}", + "sending_snapshot": "Sending a snapshot to node {{node}}", + "recording_snapshot": "Recording a snapshot from node {{node}}" + } }, "cluster_configuration": { "label": "Cluster configuration", @@ -130,15 +138,28 @@ "role": "Role", "policy": "Policy" }, + "field_placeholders": { + "subject": "Subject", + "predicate": "Predicate", + "object": "Object", + "context": "Context", + "role": "Role" + }, "actions": { "move_up": "Move the rule up", "move_down": "Move the rule down", "delete_rule": "Delete rule", "add_rule": "Add new rule", - "edit_rule": "Edit rule" + "edit_rule": "Edit rule", + "save_rule": "Save rule", + "cancel_rule_editing": "Cancel rule editing", + "save_acl": "Save ACL", + "cancel_acl_saving": "Cancel" + }, "messages": { - "no_data": "There are no rules defined in current repository" + "no_data": "There are no rules defined in current repository", + "invalid_form": "Fill in valid data in all fields before saving rule" } }, "errors": { diff --git a/test-cypress/integration/setup/aclmanagement.spec.js b/test-cypress/integration/setup/aclmanagement.spec.js index 2e85b16ee..7f7892996 100644 --- a/test-cypress/integration/setup/aclmanagement.spec.js +++ b/test-cypress/integration/setup/aclmanagement.spec.js @@ -53,9 +53,97 @@ describe('ACL Management page', () => { AclManagementSteps.moveRuleDown(1); checkRules([ACL[0], ACL[2], ACL[1], ACL[3], ACL[4]]); }); + + it('Should add a new rule in the beginning of the list and cancel operation', () => { + AclManagementSteps.addRuleInBeginning(); + AclManagementSteps.getAclRules().should('have.length', 6); + AclManagementSteps.cancelRuleEditing(0); + AclManagementSteps.getAclRules().should('have.length', 5); + }); + + it('Should add a new rule in the list and cancel operation', () => { + AclManagementSteps.addRule(1); + AclManagementSteps.getAclRules().should('have.length', 6); + AclManagementSteps.cancelRuleEditing(2); + AclManagementSteps.getAclRules().should('have.length', 5); + }); + + it('Should hide all unnecessary actions during rule editing', () => { + // When there is no rule opened for edit + // Then I expect that move up, move down, edit rule, create rule, delete rule buttons to be visible + AclManagementSteps.getMoveUpButtons().should('have.length', 4); + AclManagementSteps.getMoveDownButtons().should('have.length', 4); + AclManagementSteps.deleteRuleButtons().should('have.length', 5); + AclManagementSteps.editRuleButtons().should('have.length', 5); + AclManagementSteps.createRuleButtons().should('have.length', 6); + // When a rule is in edit mode + AclManagementSteps.addRule(1); + // Then I expect that move up, move down, edit rule, create rule, delete rule buttons to be hidden + AclManagementSteps.getMoveUpButtons().should('have.length', 0); + AclManagementSteps.getMoveDownButtons().should('have.length', 0); + AclManagementSteps.deleteRuleButtons().should('have.length', 0); + AclManagementSteps.editRuleButtons().should('have.length', 0); + AclManagementSteps.createRuleButtons().should('have.length', 0); + }); + + it('Should add a new rule in the list', () => { + // When I add a new rule + AclManagementSteps.addRule(1); + // Then I expect that the save rule button should be disabled because there mandatory fields in the new rule form + checkIfRuleSavingIsForbidden(2); + // When I fill in the subject field + AclManagementSteps.fillSubject(2, ''); + // Then I expect that the save rule button should still be disabled + checkIfRuleSavingIsForbidden(2); + // When I fill in the predicate field + AclManagementSteps.fillPredicate(2, '*'); + // Then I expect that the save rule button should still be disabled + checkIfRuleSavingIsForbidden(2); + // When I fill in the object field + AclManagementSteps.fillObject(2, '*'); + // Then I expect that the save rule button should still be disabled + checkIfRuleSavingIsForbidden(2); + // When I fill in the context field + AclManagementSteps.fillContext(2, '*'); + // Then I expect that the save rule button should still be disabled + checkIfRuleSavingIsForbidden(2); + // When I fill in the role field + AclManagementSteps.fillRole(2, 'ROLE1'); + // Then I expect that the save rule button should be enabled + checkIfRuleSavingIsAllowed(2); + // When I change the policy + AclManagementSteps.selectPolicy(2, 'deny'); + // Then I expect that the save rule button should be enabled + checkIfRuleSavingIsAllowed(2); + // When I save the rule + AclManagementSteps.saveRule(2); + // Then the rule should be saved + AclManagementSteps.getAclRules().should('have.length', 6); + const newRule = { + "subject": "", + "predicate": "*", + "object": "*", + "context": "*", + "role": "ROLE1", + "policy": "deny", + "moveUp": true, + "moveDown": true + }; + checkRules([ACL[0], ACL[1], newRule, ACL[2], ACL[3], ACL[4]]); + }); }); }); +function checkIfRuleSavingIsForbidden(index) { + AclManagementSteps.getSaveRuleButton(index).should('not.exist'); + AclManagementSteps.getSaveRuleDisabledButton(index).should('be.visible'); +} + +function checkIfRuleSavingIsAllowed(index) { + AclManagementSteps.getSaveRuleButton(index).should('be.visible'); + AclManagementSteps.getSaveRuleDisabledButton(index).should('not.exist'); +} + function checkRules(rules = []) { rules.forEach((rule, index) => { AclManagementSteps.getRule(index).within(() => { diff --git a/test-cypress/steps/setup/acl-management-steps.js b/test-cypress/steps/setup/acl-management-steps.js index f17e8f12a..18c0664aa 100644 --- a/test-cypress/steps/setup/acl-management-steps.js +++ b/test-cypress/steps/setup/acl-management-steps.js @@ -56,6 +56,106 @@ export class AclManagementSteps { static getNoDataMessage() { return this.getAclTable().find('.no-data'); } + + static addRuleInBeginning() { + this.getAddFirstRuleButton().click(); + } + + static getAddRuleButton(index) { + return this.getRule(index).find('.add-rule-btn'); + } + + static addRule(index) { + this.getAddRuleButton(index).click(); + } + + static getCancelRuleEditingButton(index) { + return this.getAclRules().eq(index).find('.cancel-rule-editing-btn'); + } + + static cancelRuleEditing(index) { + this.getCancelRuleEditingButton(index).click(); + } + + static getSaveRuleButton(index) { + return this.getRule(index).find('.save-rule-btn'); + } + + static getSaveRuleDisabledButton(index) { + return this.getRule(index).find('.save-rule-disabled-btn'); + } + + static saveRule(index) { + this.getSaveRuleButton(index).click(); + } + + static getSubjectField(index) { + return this.getRule(index).find('.subject-cell input'); + } + + static fillSubject(index, value) { + this.getSubjectField(index).type(value); + } + + static getPredicateField(index) { + return this.getRule(index).find('.predicate-cell input'); + } + + static fillPredicate(index, value) { + this.getPredicateField(index).type(value); + } + + static getObjectField(index) { + return this.getRule(index).find('.object-cell input'); + } + + static fillObject(index, value) { + this.getObjectField(index).type(value); + } + + static getContextField(index) { + return this.getRule(index).find('.context-cell input'); + } + + static fillContext(index, value) { + this.getContextField(index).type(value); + } + + static getRoleField(index) { + return this.getRule(index).find('.role-cell input'); + } + + static fillRole(index, value) { + this.getRoleField(index).type(value); + } + + static getPolicySelectorField(index) { + return this.getRule(index).find('.policy-cell select'); + } + + static selectPolicy(index, value) { + this.getPolicySelectorField(index).select(value); + } + + static getMoveUpButtons() { + return this.getAclTable().find('.move-up-btn'); + } + + static getMoveDownButtons() { + return this.getAclTable().find('.move-down-btn'); + } + + static deleteRuleButtons() { + return this.getAclTable().find('.delete-rule-btn'); + } + + static editRuleButtons() { + return this.getAclTable().find('.edit-rule-btn'); + } + + static createRuleButtons() { + return this.getAclTable().find('.add-rule-btn'); + } } const ACL = [