-
Notifications
You must be signed in to change notification settings - Fork 238
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/develop' into feature/ui_traffic…
…_management_plugin
- Loading branch information
Showing
39 changed files
with
2,212 additions
and
214 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
#Wasabi UI Plugin Demo | ||
|
||
##Overview | ||
|
||
The Wasabi Admin UI has a (simple) plugin architecture that allows you to add | ||
features to the dialogs for editing a Draft experiment or a non-Draft (e.g., Running, Stopped, etc.) | ||
experiment. Since the interaction for editing Draft experiments is different from | ||
editing experiments in other states, there are different dialogs for each. Because | ||
of that, the plugins are separated into plugins for the Draft dialog and plugins | ||
for the non-Draft dialog. | ||
|
||
The Wasabi Admin UI is built using Angular JS. For this reason, the best way to build | ||
a plugin is to also use Angular JS. If you are not familiar with Angular JS, you should | ||
look at the example and read enough to understand what it is doing. Programming a UI using | ||
Angular JS can be quite powerful, but also very foreign if you are not used to it. | ||
|
||
##Lifecycle of a Plugin | ||
|
||
Support for a plugin is built in to the Wasabi UI. When the UI starts in a browser, | ||
e.g., the user goes to the main page, the initialization code of the application | ||
(in app.js) is executed. Within that is some code that looks at a global variable | ||
created by the scripts/plugins.js file. That file is always included by the index.html, | ||
but by default, it creates a variable named wasabiUIPlugins in the global namespace | ||
that is an empty array. If any plugins are defined, they will be described by | ||
an object in this array. For example: | ||
|
||
``` | ||
var wasabiUIPlugins = [ | ||
{ | ||
"pluginType": "contributeDraftTab", | ||
"displayName": "Get/Set Priority", | ||
"ctrlName": "DemoPluginCtrl", | ||
"ctrl": "plugins/demo-plugin/ctrl.js", | ||
"templateUrl": "plugins/demo-plugin/template.html" | ||
} | ||
]; | ||
``` | ||
|
||
This is part of the definition of the plugins in this demo. | ||
|
||
Before examining what each of these properties does, we need to examine how the plugins | ||
manifest in the UI. | ||
|
||
If there are any plugins that modify the dialog used to edit Draft experiments (pluginType of contributeDraftTab), | ||
then they will appear on the Plugins tab of the Draft dialog. Each plugin appears as | ||
a button when you select that tab. If you click on that button, the UI defined by the | ||
plugin is displayed in a modal dialog. | ||
|
||
For that to work, since we are using Angular JS, we need to have loaded the controller code | ||
so that the controller can be used with the template when the plugin dialog is displayed. That is | ||
done automatically when the UI starts up by the initialization code mentioned above. The file | ||
specified by the ctrl property of the plugin definition (plugins/demo-plugin/ctrl.js in the example above) | ||
is loaded into the browser (basically by dynamically constructing a script tag and inserting it into the | ||
DOM to cause the browser to load and interpret the file). If this is successful, | ||
the controller will now be known and available to Angular JS. | ||
|
||
The other properties define the string displayed on the button on the Plugins tab (displayName), | ||
the name of the controller created as described above (ctrlName, this is used by the Draft dialog when | ||
it displays the plugin modal dialog), and the URL used to load the template of the | ||
plugin UI (templateUrl). | ||
|
||
Similarly, if you want to contribute a plugin to the dialog used to edit non-Draft | ||
experiments, the plugin definition in the plugins.js file will look like this: | ||
|
||
``` | ||
{ | ||
"pluginType": "contributeDetailsTab", | ||
"displayName": "Get/Set Priority", | ||
"ctrlName": "DemoPluginDetailsCtrl", | ||
"ctrl": "plugins/demo-plugin/detailsCtrl.js", | ||
"templateUrl": "plugins/demo-plugin/detailsTemplate.html" | ||
} | ||
``` | ||
|
||
This is used in exactly the same way as the Draft plugin definitions. This plugin configuration | ||
results in a button being added to the Plugins tab of the non-Draft dialog. The | ||
main difference is in how the plugin works, not in how the plugin is defined. | ||
|
||
When the user clicks on the button for a plugin, a modal dialog is created and | ||
the template is displayed and the defined code is used as the controller. | ||
|
||
##The Demo Plugin | ||
|
||
The example plugins simply allow the user to adjust the priority of the experiment from | ||
the experiment dialog itself, which is otherwise only achievable by going to the | ||
Priority tab. To do this, when the controller is loaded, it will use one of the | ||
factory objects that already exists in the UI to retrieve the priorities of all | ||
experiments in the same application as this experiment. It will then save and | ||
display the priority of this experiment. | ||
|
||
In the case of the Draft dialog, if the user changes the priority and clicks on | ||
the Save button, the savePriority() function is called, which updates the priority | ||
of the experiment, and the dialog is dismissed. | ||
|
||
In the case of the non-Draft dialog, the paradigm for changing values on the dialog | ||
is different. Rather than saving things when the dialog is saved, changes are saved | ||
each time an area of the dialog is edited. | ||
While it wouldn't have been necessary for the plugin UI to follow that paradigm, | ||
we have done so with this example to show how it might be done. This consists of | ||
using the dynamic-edit directive to control when the widgets are editable. See the | ||
detailsTemplate.html and the detailsCtrl.js for details. | ||
|
||
##Your Plugin | ||
These should give you an example of how you would be able to create and install | ||
your own plugin. Basically, you have access to the experiment from whose | ||
dialog your UI was launched. That would allow you to extract information or | ||
use the Wasabi APIs to implement your specific feature. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
'use strict'; | ||
|
||
angular.module('wasabi.controllers'). | ||
controllerProvider.register('DemoPluginCtrl', ['$scope', 'experiment', 'UtilitiesFactory', '$modalInstance', 'PrioritiesFactory', | ||
function ($scope, experiment, UtilitiesFactory, $modalInstance, PrioritiesFactory) { | ||
$scope.experiment = experiment; | ||
$scope.experimentPriority = 1; | ||
$scope.orderedExperiments = []; | ||
|
||
// Get the initial value of the priority as well as the other experiments in the application and their priorities. | ||
PrioritiesFactory.query({applicationName: $scope.experiment.applicationName}).$promise.then(function (priorities) { | ||
$scope.orderedExperiments = priorities; | ||
|
||
for (var i = 0; i < $scope.orderedExperiments.length; i++) { | ||
if ($scope.orderedExperiments[i].id === $scope.experiment.id) { | ||
$scope.experimentPriority = i + 1; | ||
} | ||
} | ||
}, function(response) { | ||
UtilitiesFactory.handleGlobalError(response, 'The list of priorities could not be retrieved.'); | ||
}); | ||
|
||
|
||
// This function sets up an array of experiment IDs ordered in priority order, moving the experiment | ||
// to the desired position in the list. The array of IDs is what the priority API expects to have | ||
// passed to it. | ||
$scope.savePriority = function() { | ||
// Do some minimal validation on the priority number. NOTE: we would probably handle this in the template | ||
// but are doing it here to simplify the example. | ||
var newPri = parseInt($scope.experimentPriority); | ||
if (isNaN(newPri)) { | ||
alert('Priority must be a number.'); | ||
return; | ||
} | ||
if (newPri < 1 || newPri > $scope.orderedExperiments.length) { | ||
// Out of range, just return | ||
alert('Priority must be between 1 and ' + ($scope.orderedExperiments.length + 1)); | ||
return; | ||
} | ||
|
||
// Extract the IDs for all the experiments, in order, and save the new priority order. | ||
var orderedIds = []; | ||
for (var i = 0; i < $scope.orderedExperiments.length; i++) { | ||
if (orderedIds.length + 1 === parseInt($scope.experimentPriority)) { | ||
// We want to put this experiment here in prioritized order. | ||
orderedIds.push($scope.experiment.id); | ||
} | ||
|
||
// Otherwise, just add the next prioritized experiment to the list | ||
if ($scope.orderedExperiments[i].id !== $scope.experiment.id) { | ||
orderedIds.push($scope.orderedExperiments[i].id); | ||
} | ||
} | ||
PrioritiesFactory.update({ | ||
'applicationName': $scope.experiment.applicationName, | ||
'experimentIDs': orderedIds | ||
}).$promise.then(function () { | ||
// Nothing needs doing here after we've reordered the experiment priorities | ||
}, function(response) { | ||
UtilitiesFactory.handleGlobalError(response, 'Your experiment priorities could not be changed.'); | ||
}); | ||
|
||
$modalInstance.close(); | ||
}; | ||
|
||
$scope.cancel = function() { | ||
$modalInstance.close(); | ||
}; | ||
} | ||
]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
'use strict'; | ||
|
||
angular.module('wasabi.controllers'). | ||
controllerProvider.register('DemoPluginDetailsCtrl', ['$scope', 'experiment', 'UtilitiesFactory', '$modalInstance', 'PrioritiesFactory', | ||
function ($scope, experiment, UtilitiesFactory, $modalInstance, PrioritiesFactory) { | ||
$scope.experiment = experiment; | ||
$scope.experimentPriority = 1; | ||
$scope.savedPriority = 1; | ||
$scope.orderedExperiments = []; | ||
|
||
$scope.data = { | ||
disableFields: true // This causes the input field to become disabled. | ||
}; | ||
|
||
// Get the initial value of the priority as well as the other experiments in the application and their priorities. | ||
PrioritiesFactory.query({applicationName: $scope.experiment.applicationName}).$promise.then(function (priorities) { | ||
$scope.orderedExperiments = priorities; | ||
|
||
for (var i = 0; i < $scope.orderedExperiments.length; i++) { | ||
if ($scope.orderedExperiments[i].id === $scope.experiment.id) { | ||
$scope.experimentPriority = i + 1; | ||
} | ||
} | ||
}, function(response) { | ||
UtilitiesFactory.handleGlobalError(response, 'The list of priorities could not be retrieved.'); | ||
}); | ||
|
||
// This function is used by the DynamicEditDirective to put the widgets into editing mode. This is the widget | ||
// that changes from a pencil icon to three controls, the save icon (checkmark) and the cancel icon (red X) | ||
// and the disabled pencil icon. If the user clicks on the pencil icon, this function is called and the | ||
// directive displays the other icons. If the save icon is clicked, the savePriority() function is called. | ||
// If the cancel icon is clicked, the cancelPriority() function is called. | ||
// | ||
// One of the responsibilities of this function is to save the value that will be restored if the cancel | ||
// icon is clicked. In this case, it is a simple integer. In some other cases, it could be a stringified | ||
// JSON object with several values. | ||
$scope.editPriority = function() { | ||
$scope.data.disableFields = false; | ||
$scope.$apply(); // Needed to poke Angular to update the fields based on that variable. | ||
$scope.savedPriority = $scope.experimentPriority; | ||
return $scope.savedPriority; // This saved by the directive for potentially being provided to the cancel function. | ||
}; | ||
|
||
$scope.cancelPriority = function(tempValue) { | ||
$scope.experimentPriority = tempValue; // Restore the previous value. | ||
$scope.data.disableFields = true; | ||
$scope.$apply(); | ||
}; | ||
|
||
// This function is called if the user clicks on the save icon. It sets up an array of experiment | ||
// IDs ordered in priority order, moving the experiment to the desired position in the list. The | ||
// array of IDs is what the priority API expects to have passed to it. | ||
$scope.savePriority = function() { | ||
// Do some minimal validation on the priority number. NOTE: we would probably handle this in the template | ||
// but are doing it here to simplify the example. | ||
var newPri = parseInt($scope.experimentPriority); | ||
if (isNaN(newPri)) { | ||
alert('Priority must be a number.'); | ||
|
||
// Handle the problem that the dynamic edit widgets (the pencil, etc., buttons) collapse | ||
// when you do a save...even if there is an error. In the error case, we want them to show. | ||
$('#priorityToolbar').data('dynamicEdit').displayWidgets($('#priorityToolbar .dynamicEdit'), false); | ||
return; | ||
} | ||
if (newPri < 1 || newPri > $scope.orderedExperiments.length) { | ||
// Out of range, just return | ||
alert('Priority must be between 1 and ' + ($scope.orderedExperiments.length + 1)); | ||
|
||
// Handle the problem that the dynamic edit widgets (the pencil, etc., buttons) collapse | ||
// when you do a save...even if there is an error. In the error case, we want them to show. | ||
$('#priorityToolbar').data('dynamicEdit').displayWidgets($('#priorityToolbar .dynamicEdit'), false); | ||
return; | ||
} | ||
|
||
// Extract the IDs for all the experiments, in order, and save the new priority order. | ||
var orderedIds = []; | ||
for (var i = 0; i < $scope.orderedExperiments.length; i++) { | ||
if (orderedIds.length + 1 === parseInt($scope.experimentPriority)) { | ||
// We want to put this experiment here in prioritized order. | ||
orderedIds.push($scope.experiment.id); | ||
} | ||
|
||
// Otherwise, just add the next prioritized experiment to the list. | ||
if ($scope.orderedExperiments[i].id !== $scope.experiment.id) { | ||
orderedIds.push($scope.orderedExperiments[i].id); | ||
} | ||
} | ||
PrioritiesFactory.update({ | ||
'applicationName': $scope.experiment.applicationName, | ||
'experimentIDs': orderedIds | ||
}).$promise.then(function () { | ||
// Nothing needs doing here after we've reordered the experiment priorities | ||
}, function(response) { | ||
UtilitiesFactory.handleGlobalError(response, 'Your experiment priorities could not be changed.'); | ||
}); | ||
|
||
$scope.data.disableFields = true; | ||
}; | ||
|
||
$scope.cancel = function() { | ||
$modalInstance.close(); | ||
}; | ||
} | ||
]); |
27 changes: 27 additions & 0 deletions
27
contrib/demo-plugin/plugins/demo-plugin/detailsTemplate.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<div id="priorityDetailsModal" class="modalDialog" style="width: 700px; left: 0;"> | ||
<h1>Priority</h1> | ||
<form name="priorityForm"> | ||
<div class="dialogContent"> | ||
<div> | ||
<div id="priorityToolbar" ng-show="!readOnly" dynamic-edit input-tag="experimentPriority" select-function="savePriority" edit-function="editPriority" cancel-function="cancelPriority" ng-model="experimentPriority" class="dynamicToolbar" style="top: 43px; left: 330px;"></div> | ||
|
||
<ul class="formLayout oneCol" ng-show="!readOnly"> | ||
<li class="layout8020"> | ||
<div style="width: 320px;"> | ||
<label ng-class="{disabled: data.disableFields}">Priority</label> | ||
<input id="priority" name="priority" ng-model="experimentPriority" class="form-control text" ng-disabled="data.disableFields" ng-class="{disabled: data.disableFields}"/> | ||
</div> | ||
</li> | ||
</ul> | ||
<div ng-show="readOnly"> | ||
<label>Priority is {{experimentPriority}}</label> | ||
</div> | ||
|
||
<div class="buttonBar"> | ||
<button id="btnSavePriorityCancel" class="blue cancel" onclick="return false;" ng-click="cancel();">Close</button> | ||
</div> | ||
</div> | ||
</div> | ||
</form> | ||
|
||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<div id="priorityModal" class="modalDialog" style="width: 700px; left: 0;"> | ||
<h1>Priority</h1> | ||
<form name="priorityForm" novalidate ng-submit="savePriority();"> | ||
<div class="dialogContent"> | ||
<div> | ||
<ul class="formLayout" ng-show="!readOnly"> | ||
<li class="layout8020"> | ||
<div style="width: 320px;"> | ||
<label>Priority</label> | ||
<input id="priority" name="priority" ng-model="experimentPriority" class="form-control text"/> | ||
</div> | ||
</li> | ||
</ul> | ||
<div ng-show="readOnly"> | ||
<label>Priority is {{experimentPriority}}</label> | ||
</div> | ||
</div> | ||
<div class="buttonBar"> | ||
<button id="btnSavePriority" class="blue cancel">Save</button> | ||
<button id="btnSavePriorityCancel" class="cancel" onclick="return false;" ng-click="cancel();">Cancel</button> | ||
</div> | ||
</div> | ||
</form> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
var wasabiUIPlugins = [ | ||
{ | ||
"pluginType": "contributeDraftTab", | ||
"displayName": "Get/Set Priority", | ||
"ctrlName": "DemoPluginCtrl", | ||
"ctrl": "plugins/demo-plugin/ctrl.js", | ||
"templateUrl": "plugins/demo-plugin/template.html" | ||
}, | ||
{ | ||
"pluginType": "contributeDetailsTab", | ||
"displayName": "Get/Set Priority", | ||
"ctrlName": "DemoPluginDetailsCtrl", | ||
"ctrl": "plugins/demo-plugin/detailsCtrl.js", | ||
"templateUrl": "plugins/demo-plugin/detailsTemplate.html" | ||
} | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.