diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48a2e24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +components +build diff --git a/History.md b/History.md new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5c6a32f --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ + +build: components index.js grunt-wpt-page.css template.js + @component build --dev + +template.js: template.html + @component convert $< + +components: component.json + @component install --dev + +clean: + rm -fr build components template.js + +.PHONY: clean diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..573389b --- /dev/null +++ b/Readme.md @@ -0,0 +1,38 @@ + +# grunt-wpt-page + + Keep tracking webpagetest score + +## Installation + + Install with [component(1)](http://component.io): + + $ component install sideroad/grunt-wpt-page + +## API + + + +## License + + The MIT License (MIT) + + Copyright (c) 2014 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. \ No newline at end of file diff --git a/component.json b/component.json new file mode 100644 index 0000000..c87d9c7 --- /dev/null +++ b/component.json @@ -0,0 +1,24 @@ +{ + "name": "grunt-wpt-page", + "description": "Keep tracking webpagetest score", + "version": "0.0.1", + "keywords": [], + "dependencies": { + "moment/moment": "*", + "yyx990803/vue": "*", + "components/jquery": "*", + "components/bootstrap": "*", + "lodash/lodash": "*" + }, + "development": {}, + "license": "MIT", + "scripts": [ + "index.js" + ], + "templates": [ + "index.html" + ], + "styles": [ + "grunt-wpt-page.css" + ] +} \ No newline at end of file diff --git a/grunt-wpt-page.css b/grunt-wpt-page.css new file mode 100644 index 0000000..2514f45 --- /dev/null +++ b/grunt-wpt-page.css @@ -0,0 +1,3 @@ +.sidebar .affix select{ + width: 68%; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..434de59 --- /dev/null +++ b/index.html @@ -0,0 +1,195 @@ + + + + + + + + + Grunt WebPageTest + + + +
+ +
+ +
+
+ +
+ +

Response Time

+

FirstView

+

Average

+
+ +

Median

+
+ +

RepeatView

+

Average

+
+ +

Median

+
+ +

Detail

+

Average

+ + + + + + + + + + + + + + + + + + + + + +
DateIDFirstViewRepeatView
{{$value}}{{$value}}
{{info.completed | convertToDate }}{{info.id}}{{response.data.average.firstView[$key] | ms}}{{response.data.average.repeatView[$key] | ms}}
+ +

Median

+ + + + + + + + + + + + + + + + + + + + + + +
DateIDFirstViewRepeatView
{{$value}}{{$value}}
{{info.completed | convertToDate}}{{info.id}}{{response.data.median.firstView[$key] | ms}}{{response.data.median.repeatView[$key] | ms}}
+ + +

Contents Size

+

FirstView

+
+ +

RepeatView

+
+ +

Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + +
DateIDFirstViewRepeatView
Total{{$value}}Total{{$value}}
{{info.completed | convertToDate}}{{info.id}}{{response.data.median.firstView.breakdown | totalBytes | KB}}{{response.data.median.firstView.breakdown[$key].bytes | KB}}{{response.data.median.repeatView.breakdown | totalBytes | KB}}{{response.data.median.repeatView.breakdown[$key].bytes | KB}}
+ +

Contents Requests

+

FirstView

+
+ +

RepeatView

+
+ +

Detail

+ + + + + + + + + + + + + + + + + + + + + + + + + +
DateIDFirstViewRepeatView
Total{{$value}}Total{{$value}}
{{info.completed | convertToDate}}{{info.id}}{{response.data.median.firstView.breakdown | totalRequests }}{{response.data.median.firstView.breakdown[$key].requests }}{{response.data.median.repeatView.breakdown | totalRequests }}{{response.data.median.repeatView.breakdown[$key].requests }}
+ +
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..3b7e3e1 --- /dev/null +++ b/index.js @@ -0,0 +1,195 @@ +(function( Morris ){ + + var $ = require('jquery'), + _ = require('lodash'), + moment = require('moment'), + bootstrap = require('components-bootstrap'), + Vue = require('vue'), + renderMorris = function(data){ + $("#"+data.element).html(''); + Morris.Area({ + element: data.element, + data: data.data, + xkey: 'date', + ykeys: data.keys, + labels: data.labels, + behaveLikeLine: true + }); + }; + + $.when( + $.ajax({ + url: 'tests/results.json', + dataType: 'json' + }), + $.ajax({ + url: 'tests/locations.json', + dataType: 'json' + }) + ).done(function(resultsAjax, locationsAjax){ + var results = resultsAjax[0], + locations = locationsAjax[0]; + + new Vue({ + el: '#app', + data: { + results: results, + locations: locations, + tests: {}, + labels: { + responseTime: { + median: { + domContentLoadedEventStart: 'DOM Content Ready Start', + domContentLoadedEventEnd: 'DOM Content Ready End', + loadTime: 'Document Complete', + loadEventStart: 'Load Event Start', + loadEventEnd: 'Load Event End', + fullyLoaded: 'Fully Loaded' + }, + average: { + loadTime: 'Document Complete', + fullyLoaded: 'Fully Loaded' + } + }, + contents: { + 'html': 'HTML', + 'css': 'CSS', + 'image': 'Image', + 'flash': 'Flash', + 'js': 'JavaScript', + 'font': 'Font', + 'other': 'Other' + } + } + }, + ready: function(){ + this.location = _.chain(this.locations).keys().first().value(); + this.url = _.chain(this.urls).keys().first().value(); + this.renderGraph(); + }, + computed: { + urls: function(){ + return this.results[this.location]||{}; + }, + testIds: function(){ + return this.urls[this.url]; + } + }, + filters: { + convertToDate: function(time){ + return moment(time*1000).format('LLL') + }, + totalBytes: function(data){ + var total = _.reduce(data, function(memo, val, key){ + return memo + (val.bytes||0); + }, 0); + + return total; + }, + totalRequests: function(data){ + var total = _.reduce(data, function(memo, val, key){ + return memo + (val.requests||0); + }, 0); + + return total; + }, + ms: function(num){ + return String(num).replace(/(\d{1,3})(?=(?:\d{3})+$)/g,"$1,")+' ms'; + }, + KB: function(num){ + return String((num / 1000).toFixed(1)).replace(/(\d{1,3})(?=(?:\d{3})+$)/g,"$1,")+' KB'; + } + }, + methods: { + renderGraph: function(){ + var dummy = new $.Deferred(), + requests = [dummy], + that = this; + + dummy.resolve([]); + + _(this.testIds).each(function(testId){ + requests.push($.ajax({ + url: 'tests/'+testId+'.json', + dataType: 'json', + cache: true + })); + }); + + $.when.apply( $, requests ).done(function(){ + var tests = _.map(arguments, function(arr){ + return arr[0]; + }); + + // Remove dummy deferred object + tests.shift(); + + that.$set('tests', tests); + + that.renderResponseTimeGraph( tests, 'average', 'first' ); + that.renderResponseTimeGraph( tests, 'median', 'first' ); + that.renderResponseTimeGraph( tests, 'average', 'repeat' ); + that.renderResponseTimeGraph( tests, 'median', 'repeat' ); + that.renderContentsSizeGraph( tests, 'first' ); + that.renderContentsSizeGraph( tests, 'repeat' ); + that.renderContentsRequestsGraph( tests, 'first' ); + that.renderContentsRequestsGraph( tests, 'repeat' ); + }); + + }, + renderResponseTimeGraph: function(tests, type, view){ + renderMorris({ + data: _.map(tests, function(test){ + var obj = test.response.data[type][view+'View'] || {}; + obj.date = new Date( test.info.completed*1000 ).getTime(); + return obj; + }), + keys: _(this.labels.responseTime[type]).keys().value(), + labels: _(this.labels.responseTime[type]).values().value(), + element: $.camelCase( view + '-' + type) + }); + }, + renderContentsSizeGraph: function(tests, view){ + renderMorris({ + data: _.map(tests, function(test){ + var obj = {}; + var tmp = 0; + _.each(test.response.data.median[view+'View'].breakdown, function(val, key){ + obj[key] = ( val.bytes / 1000).toFixed(1); + tmp += Number(obj[key]); + }); + obj.total = _.reduce(obj, function(memo, val, key){ + return memo + Number(val||0); + }, 0).toFixed(1); + obj.date = new Date( test.info.completed*1000 ).getTime(); + return obj; + }), + keys: _(this.labels.contents).keys().value().concat(['total']), + labels: _(this.labels.contents).values().value().concat(['Total']), + element: view + 'ContentsSize' + }); + }, + renderContentsRequestsGraph: function(tests, view){ + renderMorris({ + data: _.map(tests, function(test){ + var obj = {}; + var tmp = 0; + _.each(test.response.data.median[view+'View'].breakdown, function(val, key){ + obj[key] = Number(val.requests); + }); + obj.total = _.reduce(obj, function(memo, val, key){ + return memo + Number(val||0); + }, 0); + obj.date = new Date( test.info.completed*1000 ).getTime(); + return obj; + }), + keys: _(this.labels.contents).keys().value().concat(['total']), + labels: _(this.labels.contents).values().value().concat(['Total']), + element: view + 'ContentsRequests' + }); + } + } + }); + }); + +})(Morris); \ No newline at end of file diff --git a/tests/locations.json b/tests/locations.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/tests/locations.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/results.json b/tests/results.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/tests/results.json @@ -0,0 +1 @@ +{} \ No newline at end of file