Skip to content

Commit

Permalink
WIP: Implementing client side capabilities
Browse files Browse the repository at this point in the history
This commit adds architecture to support client side capabilities.

With it are two important custom directives: capability-if (hide or show
DOM based on a model capability) and capability-enabled (apply a
capability-enabled or capability-disabled class to DOM based on a model
capability).

capability-if appears to work, however capability-enabled is only
partially working.
  • Loading branch information
atruskie committed Nov 18, 2016
1 parent fa9b999 commit 850a658
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 20 deletions.
2 changes: 1 addition & 1 deletion src/app/analysisResults/fileList/fileList.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<i class="fa fa-file-archive-o"></i>
Download this folder
</a>

<ribbon type="building"></ribbon>
</li>
</layout>
<div id="content" class="analysis-job-details">
Expand Down
1 change: 1 addition & 0 deletions src/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ angular.module("baw",
"bawApp.configuration", /* a mapping of all static path configurations
and a module that contains all app configuration */
"bawApp.predictiveCacheDefaultProfiles",
"bawApp.diagnostics",

"http-auth-interceptor", /* the auth module */
"angular-auth", /* the auth module */
Expand Down
22 changes: 22 additions & 0 deletions src/app/eventDiagnostics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//http://plnkr.co/edit/cn3MZynbpTYIcKUWmsBi?p=preview
angular
.module("bawApp.diagnostics", []);
// .config(["$provide", function($provide) {
// $provide.decorator("$rootScope", ["$delegate", function($delegate) {
// var scope = $delegate.constructor;
// var origBroadcast = scope.prototype.$broadcast;
// var origEmit = scope.prototype.$emit;
//
// scope.prototype.$broadcast = function() {
// console.debug("$broadcast was called on $scope " + scope.$id + " with arguments:",
// arguments);
// return origBroadcast.apply(this, arguments);
// };
// scope.prototype.$emit = function() {
// console.debug("$emit was called on $scope " + scope.$id + " with arguments:",
// arguments);
// return origEmit.apply(this, arguments);
// };
// return $delegate;
// }]);
// }]);
25 changes: 13 additions & 12 deletions src/app/jobs/details/jobDetails.tpl.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<layout content-for="action-items">
<layout content-for="action-items" >
<!--<li>-->
<!--<a href="{{ jobsList.newAnalysisJobRoute }}">-->
<!--<span class="fa fa-pencil"></span>-->
Expand All @@ -20,37 +20,38 @@
<li>
<hr />
</li>
<li>

<li capability-if="update">
<button class="btn btn-block btn-default" type="button"
uib-tooltip="Attempt to run all failed items again."
ng-click="jobDetails.analysisJob.retry()" ng-disabled="!jobDetails.analysisJob.canRetry">
ng-click="jobDetails.analysisJob.retry()"
capability-enabled="retry" >
<i class="fa fa-fw fa-repeat"></i>
Retry failed items
</button>
<ribbon type="beta"></ribbon>
<ribbon type="building"></ribbon>

</li>
<li>
<li capability-if="update" >

<button class="btn btn-block btn-default" type="button"
uib-tooltip="Attempt to run all failed items again."
ng-click="jobDetails.analysisJob.retry()" ng-disabled="!jobDetails.analysisJob.canRetry">
<i class="fa fa-fw fa-pause"></i>
ng-click="jobDetails.analysisJob.retry()"
capability-enabled="can('pause') || can('resume')" >
<i class="fa fa-fw fa-pause-circle"></i>
<!--<i class="fa fa-fw fa-play-circle"></i>-->
Pause processing
</button>

<ribbon type="building"></ribbon>
</li>
<li>
<li capability-if="destroy">

<button class="btn btn-block btn-default" type="button"
uib-tooltip="Attempt to run all failed items again."
ng-click="jobDetails.analysisJob.retry()" ng-disabled="!jobDetails.analysisJob.canRetry">
ng-click="jobDetails.analysisJob.retry()" >
<i class="fa fa-fw fa-trash"></i>
Delete this job
</button>
<ribbon type="new"></ribbon>
<ribbon type="building"></ribbon>
</li>
</layout>
<div id="content" class="analysis-job-details">
Expand Down
1 change: 1 addition & 0 deletions src/app/jobs/list/jobsList.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<span class="fa fa-plus"></span>
New analysis job
</a>
<ribbon type="new"></ribbon>
</li>
</layout>
<div id="content" class="analysis-jobs-list">
Expand Down
2 changes: 1 addition & 1 deletion src/app/navigation/rightNavBar.tpl.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ <h4>

<hr/>

<ul class="nav nav-pills nav-stacked" layout render-for="action-items">
<ul class="nav nav-pills nav-stacked" layout render-for="action-items" capability-model="$ctrl.activeResource">
</ul>

<br>
Expand Down
1 change: 1 addition & 0 deletions src/app/uiHints/_cornerRibbon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ ribbon, .ribbon-box {
background: linear-gradient(
to bottom,
transparent,
rgba(256, 256, 256, $darken-amount / 100%),
rgba(0, 0, 0, $darken-amount / 100%)
),
repeating-linear-gradient(
Expand Down
154 changes: 154 additions & 0 deletions src/app/uiHints/capability.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
angular
.module("bawApp.uiHints.capability", [])
.directive(
"capabilityModel",
[
function() {
return {
restrict: "EA",
multiElement: true,
controller: [
"$scope",
"$element",
"$attrs",
function CapabilityController(scope, element, attributes) {
this.model = undefined;
this.modelUpdateEvent = "bawApp.uiHints.capability.capabilityModelUpdate";

// we need root scope because sometimes elements can be transcluded and
// the event hierarchy breaks - i think!
scope.$watch(attributes.capabilityModel, (newValue) => {
if (newValue && !newValue.can) {
throw new Error("The capability-model must have a `can` method.");
}

this.model = newValue;
scope.$root.$broadcast(this.modelUpdateEvent, this.model);
});

this.createEvaluator = function(action) {
if (this.model) {
var model = this.model;
return function() {
return model.can(action);
};
} else {
return () => {};
}
};
}
]
};
}
])
/**
* Using model capabilities, selectively keep or remove elements from the DOM.
* Best used for removing UI actions that a user does not have access to.
*/
.directive(
"capabilityIf",
[
"ngIfDirective",
function(ngIfDirective) {
// based off http://stackoverflow.com/a/29010910/224512

var ngIf = ngIfDirective[0];

return {
transclude: ngIf.transclude,
priority: ngIf.priority - 1,
terminal: ngIf.terminal,
restrict: "A",
require: "^capabilityModel",
link: function (scope, element, attributes, capabilityController) {

var initialNgIf,
ifEvaluator;

scope.$watch(attributes.capabilityIf, update);
scope.$on(capabilityController.modelUpdateEvent, update);

let capability = () => {};
let capabilityWrapper = function() {
return capability();
};

function update() {
capability = capabilityController.createEvaluator(attributes.capabilityIf);
}

// find the initial ng-if attribute
initialNgIf = attributes.ngIf;
// if it exists, evaluates ngIf && capability
if (initialNgIf) {
ifEvaluator = function () {
return scope.$eval(initialNgIf) && capabilityWrapper();
};
} else {
// if there's no ng-if, process normally
ifEvaluator = capabilityWrapper;
}
attributes.ngIf = ifEvaluator;

ngIf.link.apply(ngIf, arguments);
}
};
}])
/**
* Using model capabilities, selectively apply the `.capability-enabled` or `capability-disabled` class.
* Best used for disabling UI actions that use should not be allowed to use at the present time.
* Can be used for security restricted actions that should be seen, or to disable actions that are invalid
* for the current state of the model.
*/
.directive(
"capabilityEnabled",
[
function() {
var capabilityEnabled = "capability-enabled",
capabilityDisabled = "capability-disabled";

return {
restrict: "A",
require: "^capabilityModel",
link: function (scope, element, attributes, capabilityController) {

scope.$watch(attributes.capabilityEnabled, update);
scope.$on(capabilityController.modelUpdateEvent, update);
attributes.$observe("class", function(value) {
update(scope.$eval(attributes.capabilityEnabled));
});

var oldVal;

function addClasses(classes) {
classes.forEach(attributes.$addClass, attributes);
}

function toClasses(expression) {
// can is defined on the ApiBase class
let capability = capabilityController.createEvaluator(expression);

var result = capability();
if (result) {
return [capabilityEnabled];
}
else {
return [capabilityDisabled];
}
}

function update(newVal) {
var newClasses = toClasses(newVal || "");
if (!oldVal) {
addClasses(newClasses);
} else if (!angular.equals(newVal,oldVal)) {
attributes.$updateClass(newClasses, oldVal);
}

oldVal = newVal;
}
}

};
}]
);
3 changes: 2 additions & 1 deletion src/app/uiHints/uiHints.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ angular
.module(
"bawApp.uiHints",
[
"bawApp.uiHints.ribbon"
"bawApp.uiHints.ribbon",
"bawApp.uiHints.capability"
]);
30 changes: 27 additions & 3 deletions src/components/models/analysisJob.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ angular
return this.isNew || this.isPreparing || this.isProcessing;
}

get canRetry() {
return this.isCompleted && this.successfulRatio < 1.0;
}


get completedRatio() {
return (
Expand Down Expand Up @@ -191,6 +189,32 @@ angular
this.scriptId = value && value.id || null;
}

// capabilities (mocked here until API capabilities are real
static get capabilities() {
//return this.isCompleted && this.successfulRatio < 1.0;
return {
"update": {
can: true
},
"destroy": {
can: true
},
"create": {
can: true
},
"pause": {
can: false
},
"resume": {
can: true
},
"retry": {
can: false
}
};
}


toJSON() {
return {
name: this.name,
Expand Down
Loading

0 comments on commit 850a658

Please sign in to comment.