Milimetric has submitted this change and it was merged.

Change subject: Add metric selector
......................................................................


Add metric selector

Change-Id: I5b7b0dff6d350fc3f01b8b291215b164df0d1b16
---
M bower.json
M src/app/apis/wikimetrics.js
M src/app/config.js
M src/app/require.config.js
A src/components/metric-selector/bindings.js
M src/components/metric-selector/metric-selector.html
M src/components/metric-selector/metric-selector.js
M src/components/time-selector/time-selector.html
M src/components/wikimetrics-layout/wikimetrics-layout.html
M src/components/wikimetrics-layout/wikimetrics-layout.js
M src/css/styles.css
M src/index.html
A stubs/categorizedMetrics.json
M test/app/apis.js
A test/components/metric-selector.js
M test/components/wikimetrics-layout.js
16 files changed, 328 insertions(+), 37 deletions(-)

Approvals:
  Milimetric: Verified; Looks good to me, approved



diff --git a/bower.json b/bower.json
index fc53407..ea0d674 100644
--- a/bower.json
+++ b/bower.json
@@ -12,6 +12,7 @@
     "moment": "^2.7.0",
     "knockout-projections": "~1.1.0-pre",
     "URIjs": "~1.13.2",
-    "typeahead.js":"~0.10.5"
+    "typeahead.js":"~0.10.5",
+    "semantic": "~0.19.0"
   }
 }
diff --git a/src/app/apis/wikimetrics.js b/src/app/apis/wikimetrics.js
index e6021a1..dae97af 100644
--- a/src/app/apis/wikimetrics.js
+++ b/src/app/apis/wikimetrics.js
@@ -3,7 +3,6 @@
  *   reports run by WikimetricsBot on wikimetrics.  Methods commented inline
  */
 define(['config', 'uri/URI', 'uri/URITemplate'], function (siteConfig, uri) {
-
     'use strict';
 
     function WikimetricsApi(config) {
@@ -11,7 +10,7 @@
         this.projectOptions = [];
         this.languageOptions = [];
         this.urlProjectLanguageChoices = config.urlProjectLanguageChoices;
-
+        this.categorizedMetricsUrl = config.categorizedMetricsUrl;
     }
 
 
@@ -85,7 +84,7 @@
 
             }.bind(this));
         }
-    }
+    };
 
     WikimetricsApi.prototype._getJSONConfig = function (url, callback) {
         // callback should execute callback(json)
@@ -94,8 +93,21 @@
             url: url,
             success: callback
         });
+    };
 
 
+    /**
+     * Parameters
+     *   callback : a function to pass returned data to
+     *              (note you can just pass an observable here)
+     *
+     * Returns
+     *   a jquery promise to an array of available metrics, formatted like 
this:
+     *
+     *      {category: 'some category', name: 'some metric'}
+     **/
+    WikimetricsApi.prototype.getCategorizedMetrics = function(callback) {
+        return $.get(this.categorizedMetricsUrl).done(callback);
     };
 
     return new WikimetricsApi(siteConfig);
diff --git a/src/app/config.js b/src/app/config.js
index f77ce0e..736746d 100644
--- a/src/app/config.js
+++ b/src/app/config.js
@@ -12,8 +12,7 @@
             PagesCreated: 'pages_created',
             NamespaceEdits: 'edits'
         },
-        //totally fake for now
-        urlProjectLanguageChoices: 
'../../stubs/fake-wikimetrics/projectLanguageChoices.json'
-
+        urlProjectLanguageChoices: 
'/stubs/fake-wikimetrics/projectLanguageChoices.json',
+        categorizedMetricsUrl: '/stubs/categorizedMetrics.json',
     };
 });
diff --git a/src/app/require.config.js b/src/app/require.config.js
index ccd12bd..73aaa57 100644
--- a/src/app/require.config.js
+++ b/src/app/require.config.js
@@ -5,23 +5,24 @@
 var require = {
     baseUrl: '.',
     paths: {
-        'jquery': 'bower_modules/jquery/dist/jquery',
-        'knockout': 'bower_modules/knockout/dist/knockout',
-        'knockout-projections': 
'bower_modules/knockout-projections/dist/knockout-projections',
-        'text': 'bower_modules/requirejs-text/text',
-        'd3': 'bower_modules/d3/d3',
-        'vega': 'bower_modules/vega/vega',
-        'topojson': 'bower_modules/topojson/topojson',
-        'moment': 'bower_modules/moment/moment',
+        'jquery'                : 'bower_modules/jquery/dist/jquery',
+        'knockout'              : 'bower_modules/knockout/dist/knockout',
+        'knockout-projections'  : 
'bower_modules/knockout-projections/dist/knockout-projections',
+        'text'                  : 'bower_modules/requirejs-text/text',
+        'd3'                    : 'bower_modules/d3/d3',
+        'vega'                  : 'bower_modules/vega/vega',
+        'topojson'              : 'bower_modules/topojson/topojson',
+        'moment'                : 'bower_modules/moment/moment',
+        'semantic-dropdown'     : 
'bower_modules/semantic/build/uncompressed/modules/dropdown',
         // NOTE: if you want functions like uri.expand, you must include both
         // URI and URITemplate like define(['uri/URI', 'uri/URITemplate'] ...
         // because URITemplate modifies URI when it's parsed
-        'uri': 'bower_modules/URIjs/src',
-        'config': 'app/config',
-        'logger': 'lib/logger',
-        'wikimetricsApi': 'app/apis/wikimetrics',
-        'typeahead': 'bower_modules/typeahead.js/dist/typeahead.bundle',
-        'ajaxWrapper': 'lib/ajaxWrapper'
+        'uri'                   : 'bower_modules/URIjs/src',
+        'config'                : 'app/config',
+        'logger'                : 'lib/logger',
+        'wikimetricsApi'        : 'app/apis/wikimetrics',
+        'typeahead'             : 
'bower_modules/typeahead.js/dist/typeahead.bundle',
+        'ajaxWrapper'           : 'lib/ajaxWrapper'
     },
     shim: {
         'ajaxWrapper': {
diff --git a/src/components/metric-selector/bindings.js 
b/src/components/metric-selector/bindings.js
new file mode 100644
index 0000000..ddc1476
--- /dev/null
+++ b/src/components/metric-selector/bindings.js
@@ -0,0 +1,16 @@
+define(function(require) {
+    'use strict';
+
+    var ko              = require('knockout');
+
+    var re = /([a-z])([A-Z])/g;
+    /**
+     * "TurnsSomeMetricNameLikeThis" into "Turns Some Metric Name Like This"
+     */
+    ko.bindingHandlers.metricName = {
+        update: function(element, valueAccessor) {
+            var metric = ko.unwrap(valueAccessor());
+            $(element).text(metric.replace(re, '$1 $2'));
+        }
+    };
+});
diff --git a/src/components/metric-selector/metric-selector.html 
b/src/components/metric-selector/metric-selector.html
index 5c49f2b..ffc95be 100644
--- a/src/components/metric-selector/metric-selector.html
+++ b/src/components/metric-selector/metric-selector.html
@@ -1 +1,37 @@
--- select metrics --
+<div class="ui dropdown purple label" data-bind="click: toggle">
+    <i class="add sign box icon"></i>
+    <!-- ko ifnot: selectedMetric -->
+    <div class="text" data-bind="text: open() ? 'Close' : 'Add Metric'"></div>
+    <!-- /ko -->
+    <i class="dropdown icon vertically" data-bind="css: {flipped: open}"></i>
+</div>
+
+<!-- ko foreach: addedMetrics -->
+<div class="ui large label" data-bind="css: {black: $data === 
$parent.selectedMetric()}">
+    <a data-bind="click: $parent.selectMetric, metricName: $data"></a>
+    <i class="delete icon" data-bind="click: $parent.removeMetric"></i>
+</div>
+<!-- /ko -->
+
+<div class="ui two column grid target" data-bind="css: {open: open}">
+    <div class="column">
+        <div class="ui vertical pointing purple menu">
+        <!-- ko foreach: categories -->
+            <a class="item" data-bind="
+                css: {active: name === $parent.selectedCategory().name},
+                text: name,
+                click: $parent.selectCategory
+            "></a>
+        <!-- /ko -->
+        </div>
+    </div>
+    <div class="column">
+        <div class="ui vertical menu">
+        <!-- ko if: selectedCategory -->
+        <!-- ko foreach: selectedCategory().metrics -->
+            <a class="item" data-bind="metricName: $data, click: 
$parent.addMetric"></a>
+        <!-- /ko -->
+        <!-- /ko -->
+        </div>
+    </div>
+</div>
diff --git a/src/components/metric-selector/metric-selector.js 
b/src/components/metric-selector/metric-selector.js
index cf28915..e781b49 100644
--- a/src/components/metric-selector/metric-selector.js
+++ b/src/components/metric-selector/metric-selector.js
@@ -1,10 +1,101 @@
-define(['knockout', 'text!./metric-selector.html'], function(ko, 
templateMarkup) {
+/**
+ * This component allows the user to select a metric.
+ *
+ * Example usage:
 
+        <metric-selector params="
+            metrics             : array (plain, observable, or 
observableArray) of categories
+                                    [
+                                        {name: 'Something', metrics: 
['one','two'},
+                                        {name: 'Else', metrics: ['three']}
+                                    ]
+            selectedMetric      : an observable of the selected metric
+            defaultSelection    : (optional) array of pre-added metric names
+        "/>
+ */
+define(function(require) {
     'use strict';
 
-    function MetricSelector() {
+    var ko              = require('knockout'),
+        templateMarkup  = require('text!./metric-selector.html');
+
+    require('./bindings');
+
+    function MetricSelector(params) {
         var self = this;
-        self.loading = ko.observable();
+
+        self.open = ko.observable(false);
+        self.toggle = function () {
+            this.open(!this.open());
+        };
+
+        self.selectedMetric = params.selectedMetric;
+        self.addedMetrics = ko.observableArray([]);
+
+        if (ko.isObservable(params.defaultSelection)) {
+            params.defaultSelection.subscribe(function() {
+                self.addedMetrics(ko.unwrap(this));
+            }, params.defaultSelection);
+        }
+        self.addedMetrics(ko.unwrap(params.defaultSelection) || []);
+
+        self.selectedCategory = ko.observable();
+
+        self.categories = ko.computed(function(){
+            var unwrap = ko.unwrap(params.metrics) || [],
+                copy = unwrap.slice(),
+                categories = copy.sort(function(a, b){
+                    return a.name === b.name ?
+                        0 : a.name > b.name ? 1 : -1;
+                });
+
+            categories.splice(0, 0, {
+                name: 'All metrics',
+                metrics: [].concat.apply([], categories.map(function(c) {
+                    return c.metrics;
+                })).sort()
+            });
+
+            if (categories.length) {
+                this.selectedCategory(categories[0]);
+            }
+            return categories;
+        }, self);
+
+        self.selectCategory = function (category) {
+            self.selectedCategory(category);
+        };
+
+        self.addMetric = function (name) {
+            if (self.addedMetrics.indexOf(name) >= 0) {
+                return;
+            }
+            self.addedMetrics.push(name);
+            self.reassignSelected();
+        };
+
+        self.removeMetric = function (name) {
+            self.addedMetrics.remove(name);
+            if (self.selectedMetric() === name) {
+                self.selectedMetric(null);
+                self.reassignSelected();
+            }
+        };
+
+        self.reassignSelected = function () {
+            if (!self.selectedMetric()) {
+                self.selectedMetric(
+                    self.addedMetrics().length ? self.addedMetrics()[0] : null
+                );
+            }
+        };
+
+        self.selectMetric = function (name) {
+            self.selectedMetric(name);
+        };
+
+        // start off with a metric selected, if metrics were pre-added but no 
default was picked
+        self.reassignSelected();
     }
 
     return {
diff --git a/src/components/time-selector/time-selector.html 
b/src/components/time-selector/time-selector.html
index 8ebf646..8d1c8b6 100644
--- a/src/components/time-selector/time-selector.html
+++ b/src/components/time-selector/time-selector.html
@@ -1 +1 @@
--- select time --
+ 
diff --git a/src/components/wikimetrics-layout/wikimetrics-layout.html 
b/src/components/wikimetrics-layout/wikimetrics-layout.html
index 7b7db90..48fcb00 100644
--- a/src/components/wikimetrics-layout/wikimetrics-layout.html
+++ b/src/components/wikimetrics-layout/wikimetrics-layout.html
@@ -8,7 +8,11 @@
     </section>
     <section class="col span_large main-container">
         <time-selector></time-selector>
-        <metric-selector params="selectedMetric: 
selectedMetric"></metric-selector>
+        <metric-selector params="
+            selectedMetric: selectedMetric,
+            metrics: metrics,
+            defaultSelection: defaultMetrics
+        "></metric-selector>
         <wikimetrics-visualizer params="projects: selectedProjects, metric: 
selectedMetric"></wikimetrics-visualizer>
     </section>
 </section>
diff --git a/src/components/wikimetrics-layout/wikimetrics-layout.js 
b/src/components/wikimetrics-layout/wikimetrics-layout.js
index f883cff..cce9223 100644
--- a/src/components/wikimetrics-layout/wikimetrics-layout.js
+++ b/src/components/wikimetrics-layout/wikimetrics-layout.js
@@ -1,5 +1,4 @@
 define(['knockout', 'text!./wikimetrics-layout.html', 'wikimetricsApi'], 
function (ko, templateMarkup, wikimetricsApi) {
-
     'use strict';
 
     /**
@@ -21,6 +20,26 @@
             self.projectOptions(wikimetricsApi.projectOptions);
         });
 
+        self.selectedProjects = ko.observableArray();
+
+        self.metricData = ko.observable();
+        wikimetricsApi.getCategorizedMetrics(self.metricData);
+
+        self.metrics = ko.computed(function() {
+            var configData = this.metricData();
+            if (configData) {
+                return configData.categorizedMetrics;
+            }
+            return [];
+        }, self);
+
+        self.defaultMetrics = ko.computed(function() {
+            var configData = this.metricData();
+            if (configData) {
+                return configData.defaultMetrics;
+            }
+            return [];
+        }, self);
     }
 
     return {
diff --git a/src/css/styles.css b/src/css/styles.css
index 49a128a..64ab3b3 100644
--- a/src/css/styles.css
+++ b/src/css/styles.css
@@ -6,7 +6,7 @@
     bottom: 30px;
 }
 section.main-container {
-    background-color: #EEEEEE;
+    background-color: #f9f9f9;
     padding: 30px;
     right: 30px;
     top: 30px;
@@ -18,7 +18,6 @@
     height: 100%;
     padding: 0;
 }
-
 
 /**
  * Twitter typeahead stuff
@@ -91,3 +90,12 @@
     z-index: 100;
 }
 /* End Twitter Typeahead stuff */
+
+.target {
+    position: absolute!important;
+    z-index: 100!important;
+    display: none!important;
+}
+.target.open {
+    display: block!important;
+}
diff --git a/src/index.html b/src/index.html
index 2d88ede..d41ca4f 100644
--- a/src/index.html
+++ b/src/index.html
@@ -5,6 +5,14 @@
         <meta http-equiv="X-UA-Compatible" content="IE=edge">
         <title>Dashiki</title>
         <!-- build:css -->
+            <!-- always pick and choose semantic ui components, full build 
includes things like chatrooms! -->
+            <link 
href="bower_modules/semantic/build/uncompressed/collections/grid.css" 
rel="stylesheet">
+            <link 
href="bower_modules/semantic/build/uncompressed/collections/menu.css" 
rel="stylesheet">
+            <link 
href="bower_modules/semantic/build/uncompressed/modules/dropdown.css" 
rel="stylesheet">
+            <link 
href="bower_modules/semantic/build/uncompressed/elements/label.css" 
rel="stylesheet">
+            <link 
href="bower_modules/semantic/build/uncompressed/elements/icon.css" 
rel="stylesheet">
+            <!-- end semantic ui -->
+
             <link href="css/layouts.css" rel="stylesheet">
             <link href="css/styles.css" rel="stylesheet">
         <!-- endbuild -->
diff --git a/stubs/categorizedMetrics.json b/stubs/categorizedMetrics.json
new file mode 100644
index 0000000..5928782
--- /dev/null
+++ b/stubs/categorizedMetrics.json
@@ -0,0 +1,8 @@
+{
+    "defaultMetrics": ["RollingActiveEditor"],
+    "categorizedMetrics": [
+        {"name":"Community", "metrics":["NamespaceEdits","PagesCreated"]},
+        {"name":"Acquisition", "metrics":["NewlyRegistered"]},
+        {"name":"Retention", "metrics":["RollingActiveEditor"]}
+    ]
+}
diff --git a/test/app/apis.js b/test/app/apis.js
index 3c84fd9..585e20d 100644
--- a/test/app/apis.js
+++ b/test/app/apis.js
@@ -1,11 +1,17 @@
 define(['wikimetricsApi', 'jquery'], function (wikimetrics, $) {
 
-    describe('Wikimetrics API', function () {
-        beforeEach(function () {
-            sinon.stub($, 'get');
+    describe('Wikimetrics API', function() {
+        var getJSONConfigStub;
+
+        beforeEach(function() {
+            var deferred = new $.Deferred();
+            deferred.resolveWith('not important');
+            sinon.stub($, 'get').returns(deferred);
+            getJSONConfigStub = sinon.stub(wikimetrics, '_getJSONConfig');
         });
         afterEach(function () {
             $.get.restore();
+            getJSONConfigStub.restore();
         });
 
         it('should fetch the correct URL', function () {
@@ -18,17 +24,20 @@
 
         it('should not retrieve option file if project choices are already set 
', function () {
             wikimetrics.root = 'something';
-            var stub = sinon.stub(wikimetrics, '_getJSONConfig')
             var callback = sinon.stub();
             wikimetrics.getProjectAndLanguageChoices(callback);
-            expect(stub.called).toBe(true);
+            expect(getJSONConfigStub.called).toBe(true);
 
             wikimetrics.projectOptions = ['some option'];
             wikimetrics.languageOptions = ['some other option'];
             wikimetrics.getProjectAndLanguageChoices(callback);
             // ajax call was not done the second time
-            expect(stub.calledOnce).toBe(true);
+            expect(getJSONConfigStub.calledOnce).toBe(true);
         });
 
+        it('should get metrics configuration', function() {
+            wikimetrics.getCategorizedMetrics();
+            
expect($.get.calledWith(wikimetrics.categorizedMetricsUrl)).toBe(true);
+        });
     });
 });
diff --git a/test/components/metric-selector.js 
b/test/components/metric-selector.js
new file mode 100644
index 0000000..4bf9483
--- /dev/null
+++ b/test/components/metric-selector.js
@@ -0,0 +1,76 @@
+define(['components/metric-selector/metric-selector', 'knockout'], 
function(component, ko) {
+    var MetricSelector = component.viewModel;
+
+    describe('MetricSelector view model', function() {
+
+        it('should add two binding handlers to knockout', function() {
+            expect(ko.bindingHandlers.metricName).not.toBe(undefined);
+        });
+
+        it('should process params', function() {
+            var metricsConfig = [
+                {name: 'Something', metrics: ['a','b']},
+                {name: 'Else', metrics: ['c']}
+            ];
+            var params = {
+                metrics: metricsConfig,
+                selectedMetric: ko.observable()
+            };
+
+            // **** initialization
+            // defaultSelection can be unset
+            var instance = new MetricSelector(params);
+            expect(instance.selectedMetric).toBe(params.selectedMetric);
+            expect(instance.selectedMetric()).toBeNull();
+            expect(instance.categories()[0].name).toBe('All metrics');
+            expect(instance.categories()[0].metrics).toEqual(['a','b','c']);
+            // don't clobber what's passed in
+            expect(metricsConfig[0].name).not.toBe('All metrics');
+
+            // defaultSelection can be set
+            params.defaultSelection = ['b'];
+            instance = new MetricSelector(params);
+            expect(instance.addedMetrics()).toEqual(['b']);
+
+            // defaultSelection can be observable, and the instance reacts to 
changes
+            params.defaultSelection = ko.observable();
+            instance = new MetricSelector(params);
+            params.defaultSelection(['b']);
+            expect(instance.addedMetrics()).toEqual(['b']);
+
+            // metrics can be observable
+            params.metrics = ko.observableArray(metricsConfig);
+            instance = new MetricSelector(params);
+            expect(instance.categories()[0].metrics).toEqual(['a','b','c']);
+
+            // **** internal methods
+            // adding a metric
+            instance.addMetric('c');
+            expect(instance.addedMetrics()).toEqual(['b','c']);
+            expect(instance.selectedMetric()).toEqual('b');
+
+            // removing an un-selected metric
+            instance.removeMetric('c');
+            expect(instance.selectedMetric()).toEqual('b');
+
+            // removing the selected metric selects the next one
+            instance.addMetric('c');
+            instance.removeMetric('b');
+            expect(instance.selectedMetric()).toEqual('c');
+
+            // removing all metrics clears selectedMetric
+            instance.removeMetric('c');
+            expect(instance.selectedMetric()).toBeNull();
+
+            // adding a metric selects it
+            instance.addMetric('c');
+            expect(instance.selectedMetric()).toEqual('c');
+
+            // categories work properly
+            instance = new MetricSelector(params);
+            expect(instance.selectedCategory().name).toEqual('All metrics');
+            instance.selectCategory(metricsConfig[1]);
+            expect(instance.selectedCategory().name).toEqual('Else');
+        });
+    });
+});
diff --git a/test/components/wikimetrics-layout.js 
b/test/components/wikimetrics-layout.js
index b6f0ea3..1d65a11 100644
--- a/test/components/wikimetrics-layout.js
+++ b/test/components/wikimetrics-layout.js
@@ -4,11 +4,14 @@
 
     describe('WikimetricsLayout view model', function() {
 
-        it('should create a loading observable', function() {
+        it('should create observables needed by others', function() {
             var layout = new WikimetricsLayout();
 
             expect(typeof(layout.selectedMetric)).toEqual('function');
             expect(typeof(layout.selectedProjects)).toEqual('function');
+
+            expect(typeof(layout.metrics)).toEqual('function');
+            expect(typeof(layout.defaultMetrics)).toEqual('function');
         });
     });
 });

-- 
To view, visit https://gerrit.wikimedia.org/r/157170
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I5b7b0dff6d350fc3f01b8b291215b164df0d1b16
Gerrit-PatchSet: 8
Gerrit-Project: analytics/dashiki
Gerrit-Branch: master
Gerrit-Owner: Milimetric <dandree...@wikimedia.org>
Gerrit-Reviewer: Milimetric <dandree...@wikimedia.org>
Gerrit-Reviewer: Nuria <nu...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to