Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feature/ui_traffic…
Browse files Browse the repository at this point in the history
…_management_plugin
  • Loading branch information
shoeffner committed Nov 9, 2016
2 parents ada9ca5 + 8a9fc2a commit 0cd7267
Show file tree
Hide file tree
Showing 39 changed files with 2,212 additions and 214 deletions.
107 changes: 107 additions & 0 deletions contrib/demo-plugin/README.md
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.
70 changes: 70 additions & 0 deletions contrib/demo-plugin/plugins/demo-plugin/ctrl.js
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();
};
}
]);
104 changes: 104 additions & 0 deletions contrib/demo-plugin/plugins/demo-plugin/detailsCtrl.js
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 contrib/demo-plugin/plugins/demo-plugin/detailsTemplate.html
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}"/>&nbsp;&nbsp;
</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>
24 changes: 24 additions & 0 deletions contrib/demo-plugin/plugins/demo-plugin/template.html
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"/>&nbsp;&nbsp;
</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>
16 changes: 16 additions & 0 deletions contrib/demo-plugin/scripts/plugins.js
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"
}
];
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ public class Parameters implements Cloneable {
private Mode mode = Mode.PRODUCTION;
@ApiModelProperty(value = "context of the experiment, eg \"QA\", \"PROD\"", dataType = "String")
private Context context = Context.valueOf("PROD");

//derived parameters
/**
* This field is set by calling the {@link #parse()} method that calculates the concrete instance.
*/
@JsonIgnore
private BinomialMetric metricImpl;
@ApiModelProperty(value = "time zone")
Expand Down Expand Up @@ -163,7 +167,7 @@ public BinomialMetric getMetricImpl() {
}

/**
* Calculates derived parameters.
* Calculates the derived instance of a {@link BinomialMetric} from the other parameters.
*/
public void parse() {
if (singleShot) {
Expand Down
Loading

0 comments on commit 0cd7267

Please sign in to comment.