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