├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── README.md ├── client ├── application │ ├── layout.html │ └── layout.styl ├── home │ ├── home.coffee │ ├── home.html │ └── home.styl ├── includes │ ├── access_denied.html │ ├── errors.html │ ├── errors.js │ ├── loading.html │ └── styles.styl ├── lib │ └── flotr2.js ├── main.html ├── plugins │ ├── example.coffee │ ├── graph.coffee │ └── meteor.coffee └── setup │ ├── setup.coffee │ ├── setup.html │ └── setup.styl ├── collections ├── apiTokens.coffee ├── dashboard.coffee ├── demoSandstorm.coffee ├── githubData.coffee ├── googleData.coffee ├── logData.coffee ├── mailchimpData.coffee ├── oasisSandstorm.coffee ├── preorders.coffee ├── sandcats.coffee ├── sandstormData.coffee ├── sandstormUserData.coffee └── twitterData.coffee ├── export.py ├── export.sh ├── lib ├── config.coffee └── router.coffee ├── packages ├── .gitignore ├── npm-imports │ ├── .gitignore │ ├── .npm │ │ └── package │ │ │ ├── .gitignore │ │ │ ├── README │ │ │ └── npm-shrinkwrap.json │ ├── import.js │ ├── package.js │ └── versions.json └── oauth-helper │ ├── .gitignore │ ├── client.js │ ├── package.js │ ├── server.js │ └── versions.json ├── public ├── css │ └── freeboard.min.css ├── img │ ├── dropdown-arrow.png │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png ├── js │ ├── freeboard+plugins.min.js │ └── freeboard.thirdparty.min.js └── plugins │ ├── freeboard │ ├── freeboard.datasources.js │ └── freeboard.widgets.js │ └── thirdparty │ ├── jquery.sparkline.min.js │ ├── justgage.1.0.1.js │ └── raphael.2.1.0.min.js ├── server ├── github.coffee ├── google.coffee ├── log.coffee ├── mailchimp.coffee ├── methods.coffee ├── preorders.coffee ├── publications.coffee ├── routes.coffee ├── sandcats.coffee ├── sandstorm-build.coffee ├── sandstorm.coffee ├── sandstormUser.coffee ├── startup.coffee └── twitter.coffee └── settings.example.json /.gitignore: -------------------------------------------------------------------------------- 1 | .meteor-spk 2 | *.spk 3 | settings.json 4 | .meteor/local 5 | -------------------------------------------------------------------------------- /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | s4js1o7ehls1477koe 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # 3 | # 'meteor add' and 'meteor remove' will edit this file for you, 4 | # but you can also edit it by hand. 5 | 6 | standard-app-packages 7 | iron:router 8 | sacha:spin 9 | coffeescript 10 | stylus 11 | npm-imports 12 | twitter 13 | oauth-helper 14 | accounts-ui 15 | chrismbeckett:fontawesome4 16 | accounts-google 17 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.2.1 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.1.3 2 | accounts-google@1.0.3 3 | accounts-oauth@1.1.3 4 | accounts-ui@1.1.4 5 | accounts-ui-unstyled@1.1.5 6 | application-configuration@1.0.4 7 | autoupdate@1.1.4 8 | base64@1.0.2 9 | binary-heap@1.0.2 10 | blaze@2.0.4 11 | blaze-tools@1.0.2 12 | boilerplate-generator@1.0.2 13 | callback-hook@1.0.2 14 | check@1.0.3 15 | chrismbeckett:fontawesome4@4.2.2 16 | coffeescript@1.0.5 17 | ddp@1.0.13 18 | deps@1.0.6 19 | ejson@1.0.5 20 | fastclick@1.0.2 21 | follower-livedata@1.0.3 22 | geojson-utils@1.0.2 23 | google@1.1.3 24 | html-tools@1.0.3 25 | htmljs@1.0.3 26 | http@1.0.9 27 | id-map@1.0.2 28 | iron:controller@1.0.7 29 | iron:core@1.0.7 30 | iron:dynamic-template@1.0.7 31 | iron:layout@1.0.7 32 | iron:location@1.0.7 33 | iron:middleware-stack@1.0.7 34 | iron:router@1.0.7 35 | iron:url@1.0.7 36 | jquery@1.0.2 37 | json@1.0.2 38 | launch-screen@1.0.1 39 | less@1.0.12 40 | livedata@1.0.12 41 | localstorage@1.0.2 42 | logging@1.0.6 43 | meteor@1.1.4 44 | meteor-platform@1.2.1 45 | minifiers@1.1.3 46 | minimongo@1.0.6 47 | mobile-status-bar@1.0.2 48 | mongo@1.0.11 49 | npm-imports@0.0.0 50 | oauth@1.1.3 51 | oauth-helper@0.0.0 52 | oauth1@1.1.3 53 | oauth2@1.1.2 54 | observe-sequence@1.0.4 55 | ordered-dict@1.0.2 56 | random@1.0.2 57 | reactive-dict@1.0.5 58 | reactive-var@1.0.4 59 | reload@1.1.2 60 | retry@1.0.2 61 | routepolicy@1.0.3 62 | sacha:spin@2.0.4 63 | service-configuration@1.0.3 64 | session@1.0.5 65 | spacebars@1.0.4 66 | spacebars-compiler@1.0.4 67 | standard-app-packages@1.0.4 68 | stylus@1.0.6 69 | templating@1.0.10 70 | tracker@1.0.4 71 | twitter@1.1.3 72 | ui@1.0.5 73 | underscore@1.0.2 74 | url@1.0.3 75 | webapp@1.1.5 76 | webapp-hashing@1.0.2 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sandstorm Dashboard 2 | 3 | This is a meteor app. Start the dev server with `meteor` 4 | -------------------------------------------------------------------------------- /client/application/layout.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /client/application/layout.styl: -------------------------------------------------------------------------------- 1 | #main 2 | padding: 40px 15px 3 | -------------------------------------------------------------------------------- /client/home/home.coffee: -------------------------------------------------------------------------------- 1 | Template.home.rendered = -> 2 | head.js "js/freeboard+plugins.min.js", => 3 | $ => 4 | loadMeteorPlugin() 5 | loadGraphWidget() 6 | 7 | freeboard.initialize true, => 8 | theFreeboardModel.loadDashboard(@data) 9 | 10 | updateDashboard = -> 11 | Meteor.call 'updateDashboard', theFreeboardModel.serialize(), (err) -> 12 | if err 13 | console.log err 14 | 15 | Template.home.events 16 | 'click #saveDashboard': -> 17 | updateDashboard() 18 | 19 | # Meteor.setInterval updateDashboard, 5000 20 | 21 | clickToggle = -> 22 | $('#toggle-header').click() 23 | Meteor.setTimeout clickToggle, 5000 24 | -------------------------------------------------------------------------------- /client/home/home.html: -------------------------------------------------------------------------------- 1 | 76 | -------------------------------------------------------------------------------- /client/home/home.styl: -------------------------------------------------------------------------------- 1 | #admin 2 | text-align: center 3 | 4 | .fa-white 5 | color: white 6 | 7 | .graph 8 | height: 100% 9 | -------------------------------------------------------------------------------- /client/includes/access_denied.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /client/includes/errors.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | -------------------------------------------------------------------------------- /client/includes/errors.js: -------------------------------------------------------------------------------- 1 | // Local (client-only) collection 2 | Errors = new Meteor.Collection(null); 3 | 4 | throwError = function(message) { 5 | Errors.insert({message: message, seen: false}) 6 | } 7 | 8 | clearErrors = function() { 9 | Errors.remove({seen: true}); 10 | } 11 | 12 | Template.errors.helpers({ 13 | errors: function() { 14 | return Errors.find(); 15 | } 16 | }); 17 | 18 | Template.error.rendered = function() { 19 | var error = this.data; 20 | Meteor.defer(function() { 21 | Errors.update(error._id, {$set: {seen: true}}); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /client/includes/loading.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/includes/styles.styl: -------------------------------------------------------------------------------- 1 | body 2 | background #eee 3 | color #666666 4 | 5 | #spinner 6 | height 300px 7 | 8 | -------------------------------------------------------------------------------- /client/main.html: -------------------------------------------------------------------------------- 1 | 2 | Sandstorm Dashboard 3 | 4 | 5 | 6 | 41 | 42 | -------------------------------------------------------------------------------- /client/plugins/example.coffee: -------------------------------------------------------------------------------- 1 | myDatasourcePlugin = (settings, updateCallback) -> 2 | # This is some function where I'll get my data from somewhere 3 | getData = -> 4 | newData = hello: Math.random() 5 | # Get my data from somewhere and populate newData with it... Probably a JSON API or something. 6 | 7 | # ... 8 | updateCallback newData 9 | return 10 | createRefreshTimer = (interval) -> 11 | clearInterval refreshTimer if refreshTimer 12 | refreshTimer = setInterval(-> 13 | getData() 14 | return 15 | , interval) 16 | return 17 | self = this 18 | currentSettings = settings 19 | refreshTimer = undefined 20 | self.onSettingsChanged = (newSettings) -> 21 | clearInterval refreshTimer 22 | currentSettings = newSettings 23 | createRefreshTimer currentSettings.refresh_time 24 | return 25 | 26 | self.updateNow = -> 27 | getData() 28 | return 29 | 30 | self.onDispose = -> 31 | clearInterval refreshTimer 32 | refreshTimer = `undefined` 33 | return 34 | 35 | Meteor.setTimeout(-> 36 | current_time = currentSettings.past_time 37 | while current_time > 0 38 | self.updateNow() 39 | current_time -= currentSettings.refresh_time 40 | , 3000) 41 | 42 | createRefreshTimer currentSettings.refresh_time 43 | return 44 | 45 | @loadExamplePlugin = -> 46 | freeboard.loadDatasourcePlugin 47 | type_name: "my_datasource_plugin" 48 | display_name: "Datasource Plugin Example" 49 | description: "Some sort of description with optional html!" 50 | settings: [ 51 | { 52 | name: "past_time" 53 | display_name: "Historical Time To Display" 54 | type: "text" 55 | description: "In milliseconds" 56 | default_value: 500000 57 | } 58 | { 59 | name: "refresh_time" 60 | display_name: "Refresh Time" 61 | type: "text" 62 | description: "In milliseconds" 63 | default_value: 5000 64 | } 65 | ] 66 | newInstance: (settings, newInstanceCallback, updateCallback) -> 67 | newInstanceCallback new myDatasourcePlugin(settings, updateCallback) 68 | return 69 | 70 | -------------------------------------------------------------------------------- /client/plugins/graph.coffee: -------------------------------------------------------------------------------- 1 | zip = (arrays) -> 2 | arrays[0].map (_, i) -> 3 | arrays.map (array) -> 4 | array[i] 5 | 6 | findBounds = (array) -> 7 | min = array[0] 8 | max = array[0] 9 | 10 | for elem in array 11 | if elem < min 12 | min = elem 13 | if elem > max 14 | max = elem 15 | 16 | return { 17 | min: min 18 | max: max 19 | } 20 | 21 | myWidgetPlugin = (settings) -> 22 | self = this 23 | currentSettings = settings 24 | myTextElement = $("
") 25 | graph = null 26 | data = {} 27 | self.render = (containerElement) -> 28 | $(containerElement).append myTextElement 29 | return 30 | 31 | self.getHeight = -> 32 | if currentSettings.size is "xxl" 33 | 8 34 | if currentSettings.size is "xl" 35 | 4 36 | else if currentSettings.size is "big" 37 | 2 38 | else 39 | 1 40 | 41 | self.onSettingsChanged = (newSettings) -> 42 | currentSettings = newSettings 43 | return 44 | 45 | drawGraph = -> 46 | yaxis = 47 | tickDecimals: 0 48 | trackformatter = Flotr.defaultTrackFormatter 49 | if currentSettings.y_logarithmic 50 | base = 10 51 | baseLog = Math.log base 52 | yaxis.tickDecimals = 10 53 | yaxis.scaling = 'logarithmic' 54 | bounds = findBounds(data.y_axis) 55 | val = 1 56 | yaxis.ticks = while val < bounds.max 57 | val *= 10 58 | [Math.log(val) / baseLog, val] 59 | yaxis.ticks.push([(Math.log(bounds.min) / baseLog) || 0, bounds.min]) 60 | yaxis.ticks.push([Math.log(bounds.max) / baseLog, bounds.max]) 61 | data_y_axis = data.y_axis.map (elem) -> 62 | Math.log(elem) / baseLog 63 | zipped_data = [ zip([data.x_axis, data_y_axis]) ] 64 | trackformatter = (obj) -> 65 | "(#{obj.x.toString()}, #{data.y_axis[obj.index]})" 66 | else 67 | zipped_data = [ zip([data.x_axis, data.y_axis]) ] 68 | graph = Flotr.draw myTextElement[0], zipped_data, 69 | xaxis: 70 | mode: if currentSettings.x_axis.indexOf('time') != -1 then 'time' else 'normal' 71 | timeMode: 'local' 72 | tickDecimals: 0 73 | yaxis: yaxis 74 | grid: 75 | verticalLines: false 76 | horizontalLines: false 77 | mouse: 78 | track: true 79 | trackAll: true 80 | trackFormatter: trackformatter 81 | 82 | self.onCalculatedValueChanged = (settingName, newValue) -> 83 | data[settingName] = newValue 84 | 85 | if data.x_axis and data.y_axis 86 | setTimeout drawGraph, 500 87 | return 88 | 89 | self.onDispose = -> 90 | return 91 | 92 | return 93 | 94 | @loadGraphWidget = -> 95 | freeboard.loadWidgetPlugin 96 | type_name: "my_widget_plugin" 97 | display_name: "Graph" 98 | description: "Graphing widget" 99 | fill_size: false 100 | settings: [ 101 | { 102 | name: "x_axis" 103 | display_name: "X Axis" 104 | type: "calculated" 105 | } 106 | { 107 | name: "y_axis" 108 | display_name: "Y Axis" 109 | type: "calculated" 110 | } 111 | { 112 | name: "y_logarithmic" 113 | display_name: "Scale Y Axis Logarithmically" 114 | type: "boolean" 115 | } 116 | { 117 | name: "size" 118 | display_name: "Size" 119 | type: "option" 120 | options: [ 121 | { 122 | name: "Regular" 123 | value: "regular" 124 | } 125 | { 126 | name: "Large" 127 | value: "big" 128 | } 129 | { 130 | name: "Extra Large" 131 | value: "xl" 132 | } 133 | { 134 | name: "Extra Extra Large" 135 | value: "xxl" 136 | } 137 | ] 138 | } 139 | ] 140 | newInstance: (settings, newInstanceCallback) -> 141 | newInstanceCallback new myWidgetPlugin(settings) 142 | return 143 | -------------------------------------------------------------------------------- /client/plugins/meteor.coffee: -------------------------------------------------------------------------------- 1 | meteorPlugin = (settings, updateCallback) -> 2 | # This is some function where I'll get my data from somewhere 3 | getData = -> 4 | source = currentSettings.source_name 5 | source = source.charAt(0).toUpperCase() + source.slice(1); 6 | 7 | if currentSettings.single_result 8 | methodName = "fetchLatest#{source}" 9 | else 10 | methodName = "fetch#{source}" 11 | Meteor.call methodName, (err, data) -> 12 | if err 13 | console.log err 14 | else 15 | updateCallback data 16 | 17 | return 18 | createRefreshTimer = (interval) -> 19 | interval = interval * 1000 20 | clearInterval refreshTimer if refreshTimer 21 | refreshTimer = setInterval(-> 22 | getData() 23 | return 24 | , interval) 25 | return 26 | self = this 27 | currentSettings = settings 28 | refreshTimer = undefined 29 | self.onSettingsChanged = (newSettings) -> 30 | clearInterval refreshTimer 31 | currentSettings = newSettings 32 | createRefreshTimer currentSettings.refresh_time 33 | return 34 | 35 | self.updateNow = -> 36 | getData() 37 | return 38 | 39 | self.onDispose = -> 40 | clearInterval refreshTimer 41 | refreshTimer = `undefined` 42 | return 43 | 44 | createRefreshTimer currentSettings.refresh_time 45 | return 46 | 47 | @loadMeteorPlugin = -> 48 | freeboard.loadDatasourcePlugin 49 | type_name: "meteor_plugin" 50 | display_name: "Meteor Data" 51 | description: "This is a data source for meteor collections" 52 | settings: [ 53 | { 54 | name: "source_name" 55 | display_name: "Source Name" 56 | type: "text" 57 | description: "The name of the data source to use. Must be twitter|mailchimp|google|github|sandstorm|log|demoSandstorm|preorders|oasisMonitorData|sandstormUserData" 58 | default_value: 'twitter' 59 | } 60 | { 61 | name: "single_result" 62 | display_name: "Return Only Latest" 63 | type: "boolean" 64 | description: "Should we return only the latest data" 65 | } 66 | { 67 | name: "refresh_time" 68 | display_name: "Refresh Time" 69 | type: "text" 70 | description: "In seconds" 71 | default_value: 300 72 | } 73 | ] 74 | newInstance: (settings, newInstanceCallback, updateCallback) -> 75 | newInstanceCallback new meteorPlugin(settings, updateCallback) 76 | return 77 | 78 | -------------------------------------------------------------------------------- /client/setup/setup.coffee: -------------------------------------------------------------------------------- 1 | setupService = (serviceName) -> 2 | window[serviceName].requestCredential {requestOfflineToken: true}, (token) -> 3 | Meteor.call "setup#{serviceName}", token, OAuthRetrieveSecret(token), (err) -> 4 | if err 5 | console.log err 6 | 7 | Template.setup.events 8 | 'click #setupTwitter': -> 9 | setupService 'Twitter' 10 | -------------------------------------------------------------------------------- /client/setup/setup.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /client/setup/setup.styl: -------------------------------------------------------------------------------- 1 | .clickable 2 | cursor: pointer 3 | 4 | .green 5 | color: green 6 | 7 | .red 8 | color: red 9 | -------------------------------------------------------------------------------- /collections/apiTokens.coffee: -------------------------------------------------------------------------------- 1 | @ApiTokens = new Meteor.Collection 'apiTokens' 2 | -------------------------------------------------------------------------------- /collections/dashboard.coffee: -------------------------------------------------------------------------------- 1 | @Dashboards = new Meteor.Collection 'dashboards' 2 | # Dashboard object stored per user 3 | 4 | @Cache = new Meteor.Collection 'cache' 5 | -------------------------------------------------------------------------------- /collections/demoSandstorm.coffee: -------------------------------------------------------------------------------- 1 | @DemoSandstormData = new Meteor.Collection 'demoSandstormData' 2 | 3 | if Meteor.isServer 4 | DemoSandstormData._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/githubData.coffee: -------------------------------------------------------------------------------- 1 | @GithubData = new Meteor.Collection 'githubData' 2 | 3 | if Meteor.isServer 4 | GithubData._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/googleData.coffee: -------------------------------------------------------------------------------- 1 | @GoogleData = new Meteor.Collection 'googleData' 2 | 3 | if Meteor.isServer 4 | GoogleData._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/logData.coffee: -------------------------------------------------------------------------------- 1 | @LogData = new Meteor.Collection 'logData' 2 | 3 | if Meteor.isServer 4 | LogData._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/mailchimpData.coffee: -------------------------------------------------------------------------------- 1 | @MailchimpData = new Meteor.Collection 'mailchimpData' 2 | 3 | if Meteor.isServer 4 | MailchimpData._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/oasisSandstorm.coffee: -------------------------------------------------------------------------------- 1 | @OasisSandstormData = new Meteor.Collection 'oasisSandstormData' 2 | @OasisMonitorData = new Meteor.Collection 'oasisMonitorData' 3 | 4 | if Meteor.isServer 5 | OasisSandstormData._ensureIndex( {timestamp: 1} ) 6 | OasisMonitorData._ensureIndex( {timestamp: 1, number: 1} ) 7 | -------------------------------------------------------------------------------- /collections/preorders.coffee: -------------------------------------------------------------------------------- 1 | @Preorders = new Meteor.Collection 'preorders' 2 | 3 | if Meteor.isServer 4 | Preorders._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/sandcats.coffee: -------------------------------------------------------------------------------- 1 | @Sandcats = new Meteor.Collection 'sandcats' 2 | 3 | if Meteor.isServer 4 | Sandcats._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/sandstormData.coffee: -------------------------------------------------------------------------------- 1 | @SandstormData = new Meteor.Collection 'sandstormData' 2 | 3 | if Meteor.isServer 4 | SandstormData._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/sandstormUserData.coffee: -------------------------------------------------------------------------------- 1 | @SandstormUserData = new Meteor.Collection 'sandstormUserData' 2 | 3 | if Meteor.isServer 4 | SandstormUserData._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /collections/twitterData.coffee: -------------------------------------------------------------------------------- 1 | @TwitterData = new Meteor.Collection 'twitterData' 2 | 3 | if Meteor.isServer 4 | TwitterData._ensureIndex( {timestamp: 1} ) 5 | -------------------------------------------------------------------------------- /export.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pandas as pd 3 | import numpy 4 | import shutil 5 | 6 | 7 | def handleCumulativeData(filename): 8 | d = pd.read_csv('original/' + filename) 9 | d.timestamp = pd.to_datetime(d.timestamp, unit='ms') 10 | d.index = d.timestamp 11 | d.sort_index() 12 | d.resample('D', how='max').to_csv(filename, index=False) 13 | 14 | 15 | def handleGoogleData(filename): 16 | t = '[' + ','.join(open("original/" + filename + ".json").readlines()) + ']' 17 | d = pd.read_json(t) 18 | d.timestamp = d.timestamp.apply(lambda x: x["$date"]) 19 | d.timestamp = pd.to_datetime(d.timestamp, unit='ms') 20 | d.index = d.timestamp 21 | d.sort_index() 22 | d = d.resample('D', how=numpy.sum) 23 | d["timestamp"] = d.index 24 | d.to_csv(filename + ".csv", index=False) 25 | 26 | 27 | def main(): 28 | handleCumulativeData("alpha.csv") 29 | handleCumulativeData("oasis.csv") 30 | handleCumulativeData("demoSandstorm.csv") 31 | handleCumulativeData("github.csv") 32 | handleCumulativeData("twitter.csv") 33 | handleCumulativeData("mailchimp.csv") 34 | handleCumulativeData("preorders.csv") 35 | handleGoogleData("googleAnalytics") 36 | shutil.copyfile("original/logData.csv", "logData.csv") 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /export.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mongoexport --port 7081 --collection githubData --db meteor --csv --fields timestamp,subscribers_count,stargazers_count --out github.csv 4 | mongoexport --port 7081 --collection demoSandstormData --db meteor --csv --fields timestamp,dailyActiveUsers,dailyAppDemoUsers,dailyActiveGrains --out demoSandstorm.csv 5 | mongoexport --port 7081 --collection logData --db meteor --csv --fields 'timestamp,url,method,status_code,client,channel,from,type' --out logData.csv 6 | mongoexport --port 7081 --collection mailchimpData --db meteor --csv --fields 'stats_member_count,stats_unsubscribe_count,stats_member_count_since_send,stats_unsubscribe_count_since_send,stats_open_rate,stats_click_rate,timestamp' --out mailchimp.csv 7 | mongoexport --port 7081 --collection oasisSandstormData --db meteor --csv --fields timestamp,dailyActiveUsers,dailyActiveGrains --out oaisis.csv 8 | mongoexport --port 7081 --collection sandstormData --db meteor --csv --fields timestamp,dailyActiveUsers,dailyActiveGrains --out alpha.csv 9 | mongoexport --port 7081 --collection preorders --db meteor --csv --fields timestamp,count --out preorders.csv 10 | mongoexport --port 7081 --collection twitterData --db meteor --csv --fields timestamp,followers_count,listed_count,favourites_count,statuses_count --out twitter.csv 11 | 12 | mongoexport --port 7081 --collection googleData --db meteor --out googleAnalytics.json 13 | -------------------------------------------------------------------------------- /lib/config.coffee: -------------------------------------------------------------------------------- 1 | Accounts.config( 2 | forbidClientAccountCreation : false 3 | ) 4 | -------------------------------------------------------------------------------- /lib/router.coffee: -------------------------------------------------------------------------------- 1 | if Meteor.isClient 2 | Meteor.subscribe 'userData' 3 | 4 | Router.configure 5 | layoutTemplate: "layout" 6 | loadingTemplate: "loading" 7 | 8 | Router.map -> 9 | @route "home", 10 | path: "/" 11 | waitOn: -> 12 | return Meteor.subscribe('userDashboard') 13 | data: -> 14 | return Dashboards.findOne() 15 | @route "setup", 16 | path: "/setup" 17 | data: -> 18 | return Meteor.user() 19 | @route "uploadLog", 20 | where: "server" 21 | path: "/uploadLog/:tokenId" 22 | action: -> 23 | tokenId = @params.tokenId 24 | 25 | if tokenId != Meteor.settings.logToken 26 | @response.writeHead 403, 27 | "Content-Type": "text/plain" 28 | 29 | @response.write "Wrong token" 30 | @response.end() 31 | return 32 | 33 | if @request.method != "POST" 34 | @response.writeHead 405, 35 | "Content-Type": "text/plain" 36 | 37 | @response.write "You can only POST here." 38 | @response.end() 39 | return 40 | 41 | try 42 | Meteor.bindEnvironment(doLogUpload(@request)) 43 | @response.writeHead 200, 44 | "Content-Type": "text/plain" 45 | 46 | @response.end() 47 | catch error 48 | console.error error.stack 49 | @response.writeHead 500, 50 | "Content-Type": "text/plain" 51 | 52 | @response.write error.stack 53 | @response.end() 54 | 55 | return 56 | @route "uploadSandcats", 57 | where: "server" 58 | path: "/uploadSandcats/:tokenId" 59 | action: -> 60 | tokenId = @params.tokenId 61 | 62 | if tokenId != Meteor.settings.sandcatsToken 63 | @response.writeHead 403, 64 | "Content-Type": "text/plain" 65 | 66 | @response.write "Wrong token" 67 | @response.end() 68 | return 69 | 70 | if @request.method != "POST" 71 | @response.writeHead 405, 72 | "Content-Type": "text/plain" 73 | 74 | @response.write "You can only POST here." 75 | @response.end() 76 | return 77 | 78 | try 79 | Meteor.bindEnvironment(doSandcatsUpload(@request)) 80 | @response.writeHead 200, 81 | "Content-Type": "text/plain" 82 | 83 | @response.end() 84 | catch error 85 | console.error error.stack 86 | @response.writeHead 500, 87 | "Content-Type": "text/plain" 88 | 89 | @response.write error.stack 90 | @response.end() 91 | 92 | return 93 | 94 | @userIsAdmin = (user) -> 95 | try 96 | user.services.google.email.split('@').slice(-1)[0] == Meteor.settings.public.domain 97 | catch error 98 | return false 99 | 100 | requireAdmin = (pause) -> 101 | if Meteor.user() 102 | if userIsAdmin(Meteor.user()) 103 | @next() 104 | else 105 | @render "accessDenied" 106 | else 107 | if Meteor.loggingIn() 108 | @render @loadingTemplate 109 | else 110 | @render "accessDenied" 111 | 112 | nonMeteorRoutes = ['uploadLog', 'uploadSandcats', 'fetchDemo', 'fetchTrials'] 113 | 114 | Router.onBeforeAction requireAdmin, {except: nonMeteorRoutes} 115 | -------------------------------------------------------------------------------- /packages/.gitignore: -------------------------------------------------------------------------------- 1 | /bootstrap-3 2 | /iron-router 3 | /iron-layout 4 | /blaze-layout 5 | /iron-core 6 | /iron-dynamic-template 7 | /spin 8 | /iron-router-progress 9 | /accounts-sandstorm 10 | /jquery-datatables 11 | /luma-component 12 | /reactive-table 13 | /just-i18n 14 | /autoform 15 | /simple-schema 16 | /collection2 17 | /moment 18 | /collection-hooks 19 | /fontawesome4 20 | -------------------------------------------------------------------------------- /packages/npm-imports/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /packages/npm-imports/.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/npm-imports/.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /packages/npm-imports/.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "csv": { 4 | "version": "0.4.0", 5 | "dependencies": { 6 | "csv-generate": { 7 | "version": "0.0.4" 8 | }, 9 | "csv-parse": { 10 | "version": "0.0.6" 11 | }, 12 | "stream-transform": { 13 | "version": "0.0.6" 14 | }, 15 | "csv-stringify": { 16 | "version": "0.0.3" 17 | } 18 | } 19 | }, 20 | "googleapis": { 21 | "version": "1.0.13", 22 | "dependencies": { 23 | "async": { 24 | "version": "0.9.0" 25 | }, 26 | "gapitoken": { 27 | "version": "0.1.3", 28 | "dependencies": { 29 | "jws": { 30 | "version": "0.0.2", 31 | "dependencies": { 32 | "tap": { 33 | "version": "0.3.3", 34 | "dependencies": { 35 | "inherits": { 36 | "version": "1.0.0" 37 | }, 38 | "yamlish": { 39 | "version": "0.0.5" 40 | }, 41 | "slide": { 42 | "version": "1.1.6" 43 | }, 44 | "runforcover": { 45 | "version": "0.0.2", 46 | "dependencies": { 47 | "bunker": { 48 | "version": "0.1.2", 49 | "dependencies": { 50 | "burrito": { 51 | "version": "0.2.12", 52 | "dependencies": { 53 | "traverse": { 54 | "version": "0.5.2" 55 | }, 56 | "uglify-js": { 57 | "version": "1.1.1" 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | "nopt": { 66 | "version": "2.2.1", 67 | "dependencies": { 68 | "abbrev": { 69 | "version": "1.0.5" 70 | } 71 | } 72 | }, 73 | "mkdirp": { 74 | "version": "0.3.5" 75 | }, 76 | "difflet": { 77 | "version": "0.2.6", 78 | "dependencies": { 79 | "traverse": { 80 | "version": "0.6.6" 81 | }, 82 | "charm": { 83 | "version": "0.1.2" 84 | }, 85 | "deep-is": { 86 | "version": "0.1.3" 87 | } 88 | } 89 | }, 90 | "deep-equal": { 91 | "version": "0.0.0" 92 | }, 93 | "buffer-equal": { 94 | "version": "0.0.1" 95 | } 96 | } 97 | }, 98 | "base64url": { 99 | "version": "0.0.3" 100 | } 101 | } 102 | } 103 | } 104 | }, 105 | "multipart-stream": { 106 | "version": "1.0.0", 107 | "dependencies": { 108 | "sandwich-stream": { 109 | "version": "0.0.4" 110 | }, 111 | "inherits": { 112 | "version": "2.0.1" 113 | } 114 | } 115 | }, 116 | "request": { 117 | "version": "2.40.0", 118 | "dependencies": { 119 | "qs": { 120 | "version": "1.0.2" 121 | }, 122 | "json-stringify-safe": { 123 | "version": "5.0.0" 124 | }, 125 | "mime-types": { 126 | "version": "1.0.2" 127 | }, 128 | "forever-agent": { 129 | "version": "0.5.2" 130 | }, 131 | "node-uuid": { 132 | "version": "1.4.1" 133 | }, 134 | "tough-cookie": { 135 | "version": "0.12.1", 136 | "dependencies": { 137 | "punycode": { 138 | "version": "1.3.1" 139 | } 140 | } 141 | }, 142 | "form-data": { 143 | "version": "0.1.4", 144 | "dependencies": { 145 | "combined-stream": { 146 | "version": "0.0.5", 147 | "dependencies": { 148 | "delayed-stream": { 149 | "version": "0.0.5" 150 | } 151 | } 152 | }, 153 | "mime": { 154 | "version": "1.2.11" 155 | } 156 | } 157 | }, 158 | "tunnel-agent": { 159 | "version": "0.4.0" 160 | }, 161 | "http-signature": { 162 | "version": "0.10.0", 163 | "dependencies": { 164 | "assert-plus": { 165 | "version": "0.1.2" 166 | }, 167 | "asn1": { 168 | "version": "0.1.11" 169 | }, 170 | "ctype": { 171 | "version": "0.5.2" 172 | } 173 | } 174 | }, 175 | "oauth-sign": { 176 | "version": "0.3.0" 177 | }, 178 | "hawk": { 179 | "version": "1.1.1", 180 | "dependencies": { 181 | "hoek": { 182 | "version": "0.9.1" 183 | }, 184 | "boom": { 185 | "version": "0.4.2" 186 | }, 187 | "cryptiles": { 188 | "version": "0.2.2" 189 | }, 190 | "sntp": { 191 | "version": "0.2.4" 192 | } 193 | } 194 | }, 195 | "aws-sign2": { 196 | "version": "0.5.0" 197 | }, 198 | "stringstream": { 199 | "version": "0.0.4" 200 | } 201 | } 202 | } 203 | } 204 | }, 205 | "oauth": { 206 | "version": "0.9.12" 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /packages/npm-imports/import.js: -------------------------------------------------------------------------------- 1 | this.csv = Npm.require("csv"); 2 | this.oauth = Npm.require("oauth"); 3 | this.googleapis = Npm.require('googleapis'); 4 | -------------------------------------------------------------------------------- /packages/npm-imports/package.js: -------------------------------------------------------------------------------- 1 | Npm.depends({ 2 | 'csv': '0.4.0', 3 | 'oauth': '0.9.12', 4 | 'googleapis': '1.0.13' 5 | }); 6 | 7 | Package.on_use(function (api) { 8 | api.add_files('import.js', 'server'); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/npm-imports/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "meteor", 5 | "1.1.2" 6 | ], 7 | [ 8 | "underscore", 9 | "1.0.1" 10 | ] 11 | ], 12 | "pluginDependencies": [], 13 | "toolVersion": "meteor-tool@1.0.34", 14 | "format": "1.0" 15 | } -------------------------------------------------------------------------------- /packages/oauth-helper/.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /packages/oauth-helper/client.js: -------------------------------------------------------------------------------- 1 | this.OAuthRetrieveSecret = OAuth._retrieveCredentialSecret; 2 | -------------------------------------------------------------------------------- /packages/oauth-helper/package.js: -------------------------------------------------------------------------------- 1 | Package.on_use(function (api) { 2 | api.use('oauth', ['client', 'server']); 3 | api.use('oauth1', ['client', 'server']); 4 | api.use('oauth2', ['client', 'server']); 5 | api.add_files('server.js', 'server'); 6 | api.add_files('client.js', 'client'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/oauth-helper/server.js: -------------------------------------------------------------------------------- 1 | this.OauthRetrieveCredential = Oauth.retrieveCredential; 2 | -------------------------------------------------------------------------------- /packages/oauth-helper/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "accounts-base", 5 | "1.1.2" 6 | ], 7 | [ 8 | "application-configuration", 9 | "1.0.3" 10 | ], 11 | [ 12 | "base64", 13 | "1.0.1" 14 | ], 15 | [ 16 | "binary-heap", 17 | "1.0.1" 18 | ], 19 | [ 20 | "blaze", 21 | "2.0.2" 22 | ], 23 | [ 24 | "blaze-tools", 25 | "1.0.1" 26 | ], 27 | [ 28 | "boilerplate-generator", 29 | "1.0.1" 30 | ], 31 | [ 32 | "callback-hook", 33 | "1.0.1" 34 | ], 35 | [ 36 | "check", 37 | "1.0.2" 38 | ], 39 | [ 40 | "ddp", 41 | "1.0.10" 42 | ], 43 | [ 44 | "deps", 45 | "1.0.5" 46 | ], 47 | [ 48 | "ejson", 49 | "1.0.4" 50 | ], 51 | [ 52 | "follower-livedata", 53 | "1.0.2" 54 | ], 55 | [ 56 | "geojson-utils", 57 | "1.0.1" 58 | ], 59 | [ 60 | "html-tools", 61 | "1.0.2" 62 | ], 63 | [ 64 | "htmljs", 65 | "1.0.2" 66 | ], 67 | [ 68 | "http", 69 | "1.0.7" 70 | ], 71 | [ 72 | "id-map", 73 | "1.0.1" 74 | ], 75 | [ 76 | "jquery", 77 | "1.0.1" 78 | ], 79 | [ 80 | "json", 81 | "1.0.1" 82 | ], 83 | [ 84 | "localstorage", 85 | "1.0.1" 86 | ], 87 | [ 88 | "logging", 89 | "1.0.4" 90 | ], 91 | [ 92 | "meteor", 93 | "1.1.2" 94 | ], 95 | [ 96 | "minifiers", 97 | "1.1.1" 98 | ], 99 | [ 100 | "minimongo", 101 | "1.0.4" 102 | ], 103 | [ 104 | "mongo", 105 | "1.0.7" 106 | ], 107 | [ 108 | "oauth", 109 | "1.1.1" 110 | ], 111 | [ 112 | "oauth1", 113 | "1.1.1" 114 | ], 115 | [ 116 | "oauth2", 117 | "1.1.1" 118 | ], 119 | [ 120 | "observe-sequence", 121 | "1.0.3" 122 | ], 123 | [ 124 | "ordered-dict", 125 | "1.0.1" 126 | ], 127 | [ 128 | "random", 129 | "1.0.1" 130 | ], 131 | [ 132 | "reactive-var", 133 | "1.0.3" 134 | ], 135 | [ 136 | "reload", 137 | "1.1.1" 138 | ], 139 | [ 140 | "retry", 141 | "1.0.1" 142 | ], 143 | [ 144 | "routepolicy", 145 | "1.0.2" 146 | ], 147 | [ 148 | "service-configuration", 149 | "1.0.2" 150 | ], 151 | [ 152 | "spacebars", 153 | "1.0.3" 154 | ], 155 | [ 156 | "spacebars-compiler", 157 | "1.0.3" 158 | ], 159 | [ 160 | "templating", 161 | "1.0.8" 162 | ], 163 | [ 164 | "tracker", 165 | "1.0.3" 166 | ], 167 | [ 168 | "ui", 169 | "1.0.4" 170 | ], 171 | [ 172 | "underscore", 173 | "1.0.1" 174 | ], 175 | [ 176 | "url", 177 | "1.0.1" 178 | ], 179 | [ 180 | "webapp", 181 | "1.1.3" 182 | ], 183 | [ 184 | "webapp-hashing", 185 | "1.0.1" 186 | ] 187 | ], 188 | "pluginDependencies": [], 189 | "toolVersion": "meteor-tool@1.0.34", 190 | "format": "1.0" 191 | } -------------------------------------------------------------------------------- /public/img/dropdown-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandstorm-io/sandstorm-dashboard/7358e755fc228a513ac5ab4465dd424300909927/public/img/dropdown-arrow.png -------------------------------------------------------------------------------- /public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandstorm-io/sandstorm-dashboard/7358e755fc228a513ac5ab4465dd424300909927/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandstorm-io/sandstorm-dashboard/7358e755fc228a513ac5ab4465dd424300909927/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/plugins/freeboard/freeboard.datasources.js: -------------------------------------------------------------------------------- 1 | // ┌────────────────────────────────────────────────────────────────────┐ \\ 2 | // │ F R E E B O A R D │ \\ 3 | // ├────────────────────────────────────────────────────────────────────┤ \\ 4 | // │ Copyright © 2013 Jim Heising (https://github.com/jheising) │ \\ 5 | // │ Copyright © 2013 Bug Labs, Inc. (http://buglabs.net) │ \\ 6 | // ├────────────────────────────────────────────────────────────────────┤ \\ 7 | // │ Licensed under the MIT license. │ \\ 8 | // └────────────────────────────────────────────────────────────────────┘ \\ 9 | 10 | (function () { 11 | var jsonDatasource = function (settings, updateCallback) { 12 | var self = this; 13 | var updateTimer = null; 14 | var currentSettings = settings; 15 | var errorStage = 0; // 0 = try standard request 16 | // 1 = try JSONP 17 | // 2 = try thingproxy.freeboard.io 18 | var lockErrorStage = false; 19 | 20 | function updateRefresh(refreshTime) { 21 | if (updateTimer) { 22 | clearInterval(updateTimer); 23 | } 24 | 25 | updateTimer = setInterval(function () { 26 | self.updateNow(); 27 | }, refreshTime); 28 | } 29 | 30 | updateRefresh(currentSettings.refresh * 1000); 31 | 32 | this.updateNow = function () { 33 | if ((errorStage > 1 && !currentSettings.use_thingproxy) || errorStage > 2) // We've tried everything, let's quit 34 | { 35 | return; // TODO: Report an error 36 | } 37 | 38 | var requestURL = currentSettings.url; 39 | 40 | if (errorStage == 2 && currentSettings.use_thingproxy) { 41 | requestURL = (location.protocol == "https:" ? "https:" : "http:") + "//thingproxy.freeboard.io/fetch/" + encodeURI(currentSettings.url); 42 | } 43 | 44 | var body = currentSettings.body; 45 | 46 | // Can the body be converted to JSON? 47 | if (body) { 48 | try { 49 | body = JSON.parse(body); 50 | } 51 | catch (e) { 52 | } 53 | } 54 | 55 | $.ajax({ 56 | url: requestURL, 57 | dataType: (errorStage == 1) ? "JSONP" : "JSON", 58 | type: currentSettings.method || "GET", 59 | data: body, 60 | beforeSend: function (xhr) { 61 | try { 62 | _.each(currentSettings.headers, function (header) { 63 | var name = header.name; 64 | var value = header.value; 65 | 66 | if (!_.isUndefined(name) && !_.isUndefined(value)) { 67 | xhr.setRequestHeader(name, value); 68 | } 69 | }); 70 | } 71 | catch (e) { 72 | } 73 | }, 74 | success: function (data) { 75 | lockErrorStage = true; 76 | updateCallback(data); 77 | }, 78 | error: function (xhr, status, error) { 79 | if (!lockErrorStage) { 80 | // TODO: Figure out a way to intercept CORS errors only. The error message for CORS errors seems to be a standard 404. 81 | errorStage++; 82 | self.updateNow(); 83 | } 84 | } 85 | }); 86 | } 87 | 88 | this.onDispose = function () { 89 | clearInterval(updateTimer); 90 | updateTimer = null; 91 | } 92 | 93 | this.onSettingsChanged = function (newSettings) { 94 | lockErrorStage = false; 95 | errorStage = 0; 96 | 97 | currentSettings = newSettings; 98 | updateRefresh(currentSettings.refresh * 1000); 99 | self.updateNow(); 100 | } 101 | }; 102 | 103 | freeboard.loadDatasourcePlugin({ 104 | type_name: "JSON", 105 | settings: [ 106 | { 107 | name: "url", 108 | display_name: "URL", 109 | type: "text" 110 | }, 111 | { 112 | name: "use_thingproxy", 113 | display_name: "Try thingproxy", 114 | description: 'A direct JSON connection will be tried first, if that fails, a JSONP connection will be tried. If that fails, you can use thingproxy, which can solve many connection problems to APIs. More information.', 115 | type: "boolean", 116 | default_value: true 117 | }, 118 | { 119 | name: "refresh", 120 | display_name: "Refresh Every", 121 | type: "number", 122 | suffix: "seconds", 123 | default_value: 5 124 | }, 125 | { 126 | name: "method", 127 | display_name: "Method", 128 | type: "option", 129 | options: [ 130 | { 131 | name: "GET", 132 | value: "GET" 133 | }, 134 | { 135 | name: "POST", 136 | value: "POST" 137 | }, 138 | { 139 | name: "PUT", 140 | value: "PUT" 141 | }, 142 | { 143 | name: "DELETE", 144 | value: "DELETE" 145 | } 146 | ] 147 | }, 148 | { 149 | name: "body", 150 | display_name: "Body", 151 | type: "text", 152 | description: "The body of the request. Normally only used if method is POST" 153 | }, 154 | { 155 | name: "headers", 156 | display_name: "Headers", 157 | type: "array", 158 | settings: [ 159 | { 160 | name: "name", 161 | display_name: "Name", 162 | type: "text" 163 | }, 164 | { 165 | name: "value", 166 | display_name: "Value", 167 | type: "text" 168 | } 169 | ] 170 | } 171 | ], 172 | newInstance: function (settings, newInstanceCallback, updateCallback) { 173 | newInstanceCallback(new jsonDatasource(settings, updateCallback)); 174 | } 175 | }); 176 | 177 | var openWeatherMapDatasource = function (settings, updateCallback) { 178 | var self = this; 179 | var updateTimer = null; 180 | var currentSettings = settings; 181 | 182 | function updateRefresh(refreshTime) { 183 | if (updateTimer) { 184 | clearInterval(updateTimer); 185 | } 186 | 187 | updateTimer = setInterval(function () { 188 | self.updateNow(); 189 | }, refreshTime); 190 | } 191 | 192 | function toTitleCase(str) { 193 | return str.replace(/\w\S*/g, function (txt) { 194 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 195 | }); 196 | } 197 | 198 | updateRefresh(currentSettings.refresh * 1000); 199 | 200 | this.updateNow = function () { 201 | $.ajax({ 202 | url: "http://api.openweathermap.org/data/2.5/weather?q=" + encodeURIComponent(currentSettings.location) + "&units=" + currentSettings.units, 203 | dataType: "JSONP", 204 | success: function (data) { 205 | // Rejigger our data into something easier to understand 206 | var newData = { 207 | place_name: data.name, 208 | sunrise: (new Date(data.sys.sunrise * 1000)).toLocaleTimeString(), 209 | sunset: (new Date(data.sys.sunset * 1000)).toLocaleTimeString(), 210 | conditions: toTitleCase(data.weather[0].description), 211 | current_temp: data.main.temp, 212 | high_temp: data.main.temp_max, 213 | low_temp: data.main.temp_min, 214 | pressure: data.main.pressure, 215 | humidity: data.main.humidity, 216 | wind_speed: data.wind.speed, 217 | wind_direction: data.wind.deg 218 | }; 219 | 220 | updateCallback(newData); 221 | }, 222 | error: function (xhr, status, error) { 223 | } 224 | }); 225 | } 226 | 227 | this.onDispose = function () { 228 | clearInterval(updateTimer); 229 | updateTimer = null; 230 | } 231 | 232 | this.onSettingsChanged = function (newSettings) { 233 | currentSettings = newSettings; 234 | self.updateNow(); 235 | updateRefresh(currentSettings.refresh * 1000); 236 | } 237 | }; 238 | 239 | freeboard.loadDatasourcePlugin({ 240 | type_name: "openweathermap", 241 | display_name: "Open Weather Map API", 242 | settings: [ 243 | { 244 | name: "location", 245 | display_name: "Location", 246 | type: "text", 247 | description: "Example: London, UK" 248 | }, 249 | { 250 | name: "units", 251 | display_name: "Units", 252 | type: "option", 253 | default: "imperial", 254 | options: [ 255 | { 256 | name: "Imperial", 257 | value: "imperial" 258 | }, 259 | { 260 | name: "Metric", 261 | value: "metric" 262 | } 263 | ] 264 | }, 265 | { 266 | name: "refresh", 267 | display_name: "Refresh Every", 268 | type: "number", 269 | suffix: "seconds", 270 | default_value: 5 271 | } 272 | ], 273 | newInstance: function (settings, newInstanceCallback, updateCallback) { 274 | newInstanceCallback(new openWeatherMapDatasource(settings, updateCallback)); 275 | } 276 | }); 277 | 278 | var dweetioDatasource = function (settings, updateCallback) { 279 | var self = this; 280 | var currentSettings = settings; 281 | 282 | function onNewDweet(dweet) { 283 | updateCallback(dweet); 284 | } 285 | 286 | this.updateNow = function () { 287 | dweetio.get_latest_dweet_for(currentSettings.thing_id, function (err, dweet) { 288 | if (err) { 289 | //onNewDweet({}); 290 | } 291 | else { 292 | onNewDweet(dweet[0].content); 293 | } 294 | }); 295 | } 296 | 297 | this.onDispose = function () { 298 | 299 | } 300 | 301 | this.onSettingsChanged = function (newSettings) { 302 | dweetio.stop_listening(); 303 | 304 | currentSettings = newSettings; 305 | 306 | dweetio.listen_for(currentSettings.thing_id, function (dweet) { 307 | onNewDweet(dweet.content); 308 | }); 309 | } 310 | 311 | self.onSettingsChanged(settings); 312 | }; 313 | 314 | freeboard.loadDatasourcePlugin({ 315 | "type_name": "dweet_io", 316 | "display_name": "Dweet.io", 317 | "external_scripts": [ 318 | "http://dweet.io/client/dweet.io.min.js" 319 | ], 320 | "settings": [ 321 | { 322 | name: "thing_id", 323 | display_name: "Thing Name", 324 | "description": "Example: salty-dog-1", 325 | type: "text" 326 | } 327 | ], 328 | newInstance: function (settings, newInstanceCallback, updateCallback) { 329 | newInstanceCallback(new dweetioDatasource(settings, updateCallback)); 330 | } 331 | }); 332 | 333 | var playbackDatasource = function (settings, updateCallback) { 334 | var self = this; 335 | var currentSettings = settings; 336 | var currentDataset = []; 337 | var currentIndex = 0; 338 | var currentTimeout; 339 | 340 | function moveNext() { 341 | if (currentDataset.length > 0) { 342 | if (currentIndex < currentDataset.length) { 343 | updateCallback(currentDataset[currentIndex]); 344 | currentIndex++; 345 | } 346 | 347 | if (currentIndex >= currentDataset.length && currentSettings.loop) { 348 | currentIndex = 0; 349 | } 350 | 351 | if (currentIndex < currentDataset.length) { 352 | currentTimeout = setTimeout(moveNext, currentSettings.refresh * 1000); 353 | } 354 | } 355 | else { 356 | updateCallback({}); 357 | } 358 | } 359 | 360 | function stopTimeout() { 361 | currentDataset = []; 362 | currentIndex = 0; 363 | 364 | if (currentTimeout) { 365 | clearTimeout(currentTimeout); 366 | currentTimeout = null; 367 | } 368 | } 369 | 370 | this.updateNow = function () { 371 | stopTimeout(); 372 | 373 | $.ajax({ 374 | url: currentSettings.datafile, 375 | dataType: (currentSettings.is_jsonp) ? "JSONP" : "JSON", 376 | success: function (data) { 377 | if (_.isArray(data)) { 378 | currentDataset = data; 379 | } 380 | else { 381 | currentDataset = []; 382 | } 383 | 384 | currentIndex = 0; 385 | 386 | moveNext(); 387 | }, 388 | error: function (xhr, status, error) { 389 | } 390 | }); 391 | } 392 | 393 | this.onDispose = function () { 394 | stopTimeout(); 395 | } 396 | 397 | this.onSettingsChanged = function (newSettings) { 398 | currentSettings = newSettings; 399 | self.updateNow(); 400 | } 401 | }; 402 | 403 | freeboard.loadDatasourcePlugin({ 404 | "type_name": "playback", 405 | "display_name": "Playback", 406 | "settings": [ 407 | { 408 | "name": "datafile", 409 | "display_name": "Data File URL", 410 | "type": "text", 411 | "description": "A link to a JSON array of data." 412 | }, 413 | { 414 | name: "is_jsonp", 415 | display_name: "Is JSONP", 416 | type: "boolean" 417 | }, 418 | { 419 | "name": "loop", 420 | "display_name": "Loop", 421 | "type": "boolean", 422 | "description": "Rewind and loop when finished" 423 | }, 424 | { 425 | "name": "refresh", 426 | "display_name": "Refresh Every", 427 | "type": "number", 428 | "suffix": "seconds", 429 | "default_value": 5 430 | } 431 | ], 432 | newInstance: function (settings, newInstanceCallback, updateCallback) { 433 | newInstanceCallback(new playbackDatasource(settings, updateCallback)); 434 | } 435 | }); 436 | 437 | var clockDatasource = function (settings, updateCallback) { 438 | var self = this; 439 | var currentSettings = settings; 440 | var timer; 441 | 442 | function stopTimer() { 443 | if (timer) { 444 | clearTimeout(timer); 445 | timer = null; 446 | } 447 | } 448 | 449 | function updateTimer() { 450 | stopTimer(); 451 | timer = setInterval(self.updateNow, currentSettings.refresh * 1000); 452 | } 453 | 454 | this.updateNow = function () { 455 | var date = new Date(); 456 | 457 | var data = { 458 | numeric_value: date.getTime(), 459 | full_string_value: date.toLocaleString(), 460 | date_string_value: date.toLocaleDateString(), 461 | time_string_value: date.toLocaleTimeString(), 462 | date_object: date 463 | }; 464 | 465 | updateCallback(data); 466 | } 467 | 468 | this.onDispose = function () { 469 | stopTimer(); 470 | } 471 | 472 | this.onSettingsChanged = function (newSettings) { 473 | currentSettings = newSettings; 474 | updateTimer(); 475 | } 476 | 477 | updateTimer(); 478 | }; 479 | 480 | freeboard.loadDatasourcePlugin({ 481 | "type_name": "clock", 482 | "display_name": "Clock", 483 | "settings": [ 484 | { 485 | "name": "refresh", 486 | "display_name": "Refresh Every", 487 | "type": "number", 488 | "suffix": "seconds", 489 | "default_value": 1 490 | } 491 | ], 492 | newInstance: function (settings, newInstanceCallback, updateCallback) { 493 | newInstanceCallback(new clockDatasource(settings, updateCallback)); 494 | } 495 | }); 496 | 497 | }()); -------------------------------------------------------------------------------- /public/plugins/freeboard/freeboard.widgets.js: -------------------------------------------------------------------------------- 1 | // ┌────────────────────────────────────────────────────────────────────┐ \\ 2 | // │ F R E E B O A R D │ \\ 3 | // ├────────────────────────────────────────────────────────────────────┤ \\ 4 | // │ Copyright © 2013 Jim Heising (https://github.com/jheising) │ \\ 5 | // │ Copyright © 2013 Bug Labs, Inc. (http://buglabs.net) │ \\ 6 | // ├────────────────────────────────────────────────────────────────────┤ \\ 7 | // │ Licensed under the MIT license. │ \\ 8 | // └────────────────────────────────────────────────────────────────────┘ \\ 9 | 10 | (function () { 11 | var SPARKLINE_HISTORY_LENGTH = 100; 12 | var valueStyle = freeboard.getStyleString("values"); 13 | 14 | valueStyle += 15 | "overflow: hidden;" + 16 | "text-overflow: ellipsis;" + 17 | "display: inline;"; 18 | 19 | // Add some styles to our sheet 20 | freeboard.addStyle('.text-widget-unit', 'padding-left: 5px;display:inline;'); 21 | freeboard.addStyle('.text-widget-regular-value', valueStyle + "font-size:30px;"); 22 | freeboard.addStyle('.text-widget-big-value', valueStyle + "font-size:75px;"); 23 | 24 | freeboard.addStyle('.gauge-widget-wrapper', "width: 100%;text-align: center;"); 25 | freeboard.addStyle('.gauge-widget', "width:200px;height:160px;display:inline-block;"); 26 | 27 | freeboard.addStyle('.sparkline', "width:100%;height: 75px;"); 28 | freeboard.addStyle('.sparkline-inline', "width:50%;float:right;height:30px;"); 29 | 30 | freeboard.addStyle('.indicator-light', "border-radius:50%;width:22px;height:22px;border:2px solid #3d3d3d;margin-top:5px;float:left;background-color:#222;margin-right:10px;"); 31 | freeboard.addStyle('.indicator-light.on', "background-color:#FFC773;box-shadow: 0px 0px 15px #FF9900;border-color:#FDF1DF;"); 32 | freeboard.addStyle('.indicator-text', "margin-top:10px;"); 33 | 34 | freeboard.addStyle('div.pointer-value', "position:absolute;height:95px;margin: auto;top: 0px;bottom: 0px;width: 100%;text-align:center;"); 35 | 36 | function easeTransitionText(currentValue, newValue, textElement, duration) { 37 | if (currentValue == newValue) 38 | return; 39 | 40 | if ($.isNumeric(newValue) && $.isNumeric(currentValue)) { 41 | var numParts = newValue.toString().split('.'); 42 | var endingPrecision = 0; 43 | 44 | if (numParts.length > 1) { 45 | endingPrecision = numParts[1].length; 46 | } 47 | 48 | numParts = currentValue.toString().split('.'); 49 | var startingPrecision = 0; 50 | 51 | if (numParts.length > 1) { 52 | startingPrecision = numParts[1].length; 53 | } 54 | 55 | jQuery({transitionValue: Number(currentValue), precisionValue: startingPrecision}).animate({transitionValue: Number(newValue), precisionValue: endingPrecision}, { 56 | duration: duration, 57 | step: function () { 58 | $(textElement).text(this.transitionValue.toFixed(this.precisionValue)); 59 | }, 60 | done: function () { 61 | $(textElement).text(newValue); 62 | } 63 | }); 64 | } 65 | else { 66 | $(textElement).text(newValue); 67 | } 68 | } 69 | 70 | function addValueToSparkline(element, value) { 71 | var values = $(element).data().values; 72 | 73 | if (!values) { 74 | values = []; 75 | } 76 | 77 | if (values.length >= SPARKLINE_HISTORY_LENGTH) { 78 | values.shift(); 79 | } 80 | 81 | //if(_.isNumber(value)) 82 | //{ 83 | values.push(Number(value)); 84 | 85 | $(element).data().values = values; 86 | 87 | $(element).sparkline(values, { 88 | type: "line", 89 | height: "100%", 90 | width: "100%", 91 | fillColor: false, 92 | lineColor: "#FF9900", 93 | lineWidth: 2, 94 | spotRadius: 3, 95 | spotColor: false, 96 | minSpotColor: "#78AB49", 97 | maxSpotColor: "#78AB49", 98 | highlightSpotColor: "#9D3926", 99 | highlightLineColor: "#9D3926" 100 | }); 101 | //} 102 | } 103 | 104 | var textWidget = function (settings) { 105 | var self = this; 106 | 107 | var currentSettings = settings; 108 | var titleElement = $('

'); 109 | var valueElement = $('
'); 110 | var unitsElement = $('
'); 111 | var sparklineElement = $(''); 112 | var currentValue; 113 | 114 | this.render = function (element) { 115 | $(element).append(titleElement).append(valueElement).append(unitsElement).append(sparklineElement); 116 | } 117 | 118 | this.onSettingsChanged = function (newSettings) { 119 | currentSettings = newSettings; 120 | titleElement.html((_.isUndefined(newSettings.title) ? "" : newSettings.title)); 121 | 122 | valueElement 123 | .toggleClass("text-widget-regular-value", (newSettings.size == "regular")) 124 | .toggleClass("text-widget-big-value", (newSettings.size == "big")); 125 | 126 | unitsElement.html((_.isUndefined(newSettings.units) ? "" : newSettings.units)); 127 | 128 | if (newSettings.sparkline) { 129 | sparklineElement.show(); 130 | } 131 | else { 132 | delete sparklineElement.data().values; 133 | sparklineElement.empty(); 134 | sparklineElement.hide(); 135 | } 136 | } 137 | 138 | this.onCalculatedValueChanged = function (settingName, newValue) { 139 | if (settingName == "value") { 140 | if (currentSettings.animate) { 141 | easeTransitionText(currentValue, newValue, valueElement, 500); 142 | } 143 | else { 144 | valueElement.text(newValue); 145 | } 146 | 147 | if (currentSettings.sparkline) { 148 | addValueToSparkline(sparklineElement, newValue); 149 | } 150 | 151 | currentValue = newValue; 152 | } 153 | } 154 | 155 | this.onDispose = function () { 156 | 157 | } 158 | 159 | this.getHeight = function () { 160 | if (currentSettings.size == "big") { 161 | return 2; 162 | } 163 | else { 164 | return 1; 165 | } 166 | } 167 | 168 | this.onSettingsChanged(settings); 169 | }; 170 | 171 | freeboard.loadWidgetPlugin({ 172 | type_name: "text_widget", 173 | display_name: "Text", 174 | "external_scripts" : [ 175 | "plugins/thirdparty/jquery.sparkline.min.js" 176 | ], 177 | settings: [ 178 | { 179 | name: "title", 180 | display_name: "Title", 181 | type: "text" 182 | }, 183 | { 184 | name: "size", 185 | display_name: "Size", 186 | type: "option", 187 | options: [ 188 | { 189 | name: "Regular", 190 | value: "regular" 191 | }, 192 | { 193 | name: "Big", 194 | value: "big" 195 | } 196 | ] 197 | }, 198 | { 199 | name: "value", 200 | display_name: "Value", 201 | type: "calculated" 202 | }, 203 | { 204 | name: "sparkline", 205 | display_name: "Include Sparkline", 206 | type: "boolean" 207 | }, 208 | { 209 | name: "animate", 210 | display_name: "Animate Value Changes", 211 | type: "boolean", 212 | default_value: true 213 | }, 214 | { 215 | name: "units", 216 | display_name: "Units", 217 | type: "text" 218 | } 219 | ], 220 | newInstance: function (settings, newInstanceCallback) { 221 | newInstanceCallback(new textWidget(settings)); 222 | } 223 | }); 224 | 225 | var gaugeID = 0; 226 | 227 | var gaugeWidget = function (settings) { 228 | var self = this; 229 | 230 | var thisGaugeID = "gauge-" + gaugeID++; 231 | var titleElement = $('

'); 232 | var gaugeElement = $('
'); 233 | 234 | var gaugeObject; 235 | var rendered = false; 236 | 237 | var currentSettings = settings; 238 | 239 | function createGauge() { 240 | if (!rendered) { 241 | return; 242 | } 243 | 244 | gaugeElement.empty(); 245 | 246 | gaugeObject = new JustGage({ 247 | id: thisGaugeID, 248 | value: (_.isUndefined(currentSettings.min_value) ? 0 : currentSettings.min_value), 249 | min: (_.isUndefined(currentSettings.min_value) ? 0 : currentSettings.min_value), 250 | max: (_.isUndefined(currentSettings.max_value) ? 0 : currentSettings.max_value), 251 | label: currentSettings.units, 252 | showInnerShadow: false, 253 | valueFontColor: "#d3d4d4" 254 | }); 255 | } 256 | 257 | this.render = function (element) { 258 | rendered = true; 259 | $(element).append(titleElement).append($('
').append(gaugeElement)); 260 | createGauge(); 261 | } 262 | 263 | this.onSettingsChanged = function (newSettings) { 264 | if (newSettings.min_value != currentSettings.min_value || newSettings.max_value != currentSettings.max_value || newSettings.units != currentSettings.units) { 265 | currentSettings = newSettings; 266 | createGauge(); 267 | } 268 | else { 269 | currentSettings = newSettings; 270 | } 271 | 272 | titleElement.html(newSettings.title); 273 | } 274 | 275 | this.onCalculatedValueChanged = function (settingName, newValue) { 276 | if (!_.isUndefined(gaugeObject)) { 277 | gaugeObject.refresh(Number(newValue)); 278 | } 279 | } 280 | 281 | this.onDispose = function () { 282 | } 283 | 284 | this.getHeight = function () { 285 | return 3; 286 | } 287 | 288 | this.onSettingsChanged(settings); 289 | }; 290 | 291 | freeboard.loadWidgetPlugin({ 292 | type_name: "gauge", 293 | display_name: "Gauge", 294 | "external_scripts" : [ 295 | "plugins/thirdparty/raphael.2.1.0.min.js", 296 | "plugins/thirdparty/justgage.1.0.1.js" 297 | ], 298 | settings: [ 299 | { 300 | name: "title", 301 | display_name: "Title", 302 | type: "text" 303 | }, 304 | { 305 | name: "value", 306 | display_name: "Value", 307 | type: "calculated" 308 | }, 309 | { 310 | name: "units", 311 | display_name: "Units", 312 | type: "text" 313 | }, 314 | { 315 | name: "min_value", 316 | display_name: "Minimum", 317 | type: "text", 318 | default_value: 0 319 | }, 320 | { 321 | name: "max_value", 322 | display_name: "Maximum", 323 | type: "text", 324 | default_value: 100 325 | } 326 | ], 327 | newInstance: function (settings, newInstanceCallback) { 328 | newInstanceCallback(new gaugeWidget(settings)); 329 | } 330 | }); 331 | 332 | var sparklineWidget = function (settings) { 333 | var self = this; 334 | 335 | var titleElement = $('

'); 336 | var sparklineElement = $('
'); 337 | 338 | this.render = function (element) { 339 | $(element).append(titleElement).append(sparklineElement); 340 | } 341 | 342 | this.onSettingsChanged = function (newSettings) { 343 | titleElement.html((_.isUndefined(newSettings.title) ? "" : newSettings.title)); 344 | } 345 | 346 | this.onCalculatedValueChanged = function (settingName, newValue) { 347 | addValueToSparkline(sparklineElement, newValue); 348 | } 349 | 350 | this.onDispose = function () { 351 | } 352 | 353 | this.getHeight = function () { 354 | return 2; 355 | } 356 | 357 | this.onSettingsChanged(settings); 358 | }; 359 | 360 | freeboard.loadWidgetPlugin({ 361 | type_name: "sparkline", 362 | display_name: "Sparkline", 363 | "external_scripts" : [ 364 | "plugins/thirdparty/jquery.sparkline.min.js" 365 | ], 366 | settings: [ 367 | { 368 | name: "title", 369 | display_name: "Title", 370 | type: "text" 371 | }, 372 | { 373 | name: "value", 374 | display_name: "Value", 375 | type: "calculated" 376 | } 377 | ], 378 | newInstance: function (settings, newInstanceCallback) { 379 | newInstanceCallback(new sparklineWidget(settings)); 380 | } 381 | }); 382 | 383 | var pointerWidget = function (settings) { 384 | var self = this; 385 | var paper; 386 | var strokeWidth = 3; 387 | var triangle; 388 | var width, height; 389 | var currentValue = 0; 390 | var valueDiv = $('
'); 391 | var unitsDiv = $('
'); 392 | 393 | function polygonPath(points) { 394 | if (!points || points.length < 2) 395 | return []; 396 | var path = []; //will use path object type 397 | path.push(['m', points[0], points[1]]); 398 | for (var i = 2; i < points.length; i += 2) { 399 | path.push(['l', points[i], points[i + 1]]); 400 | } 401 | path.push(['z']); 402 | return path; 403 | } 404 | 405 | this.render = function (element) { 406 | width = $(element).width(); 407 | height = $(element).height(); 408 | 409 | var radius = Math.min(width, height) / 2 - strokeWidth * 2; 410 | 411 | paper = Raphael($(element).get()[0], width, height); 412 | var circle = paper.circle(width / 2, height / 2, radius); 413 | circle.attr("stroke", "#FF9900"); 414 | circle.attr("stroke-width", strokeWidth); 415 | 416 | triangle = paper.path(polygonPath([width / 2, (height / 2) - radius + strokeWidth, 15, 20, -30, 0])); 417 | triangle.attr("stroke-width", 0); 418 | triangle.attr("fill", "#fff"); 419 | 420 | $(element).append($('
').append(valueDiv).append(unitsDiv)); 421 | } 422 | 423 | this.onSettingsChanged = function (newSettings) { 424 | unitsDiv.html(newSettings.units); 425 | } 426 | 427 | this.onCalculatedValueChanged = function (settingName, newValue) { 428 | if (settingName == "direction") { 429 | if (!_.isUndefined(triangle)) { 430 | var direction = "r"; 431 | 432 | var oppositeCurrent = currentValue + 180; 433 | 434 | if (oppositeCurrent < newValue) { 435 | //direction = "l"; 436 | } 437 | 438 | triangle.animate({transform: "r" + newValue + "," + (width / 2) + "," + (height / 2)}, 250, "bounce"); 439 | } 440 | 441 | currentValue = newValue; 442 | } 443 | else if (settingName == "value_text") { 444 | valueDiv.html(newValue); 445 | } 446 | } 447 | 448 | this.onDispose = function () { 449 | } 450 | 451 | this.getHeight = function () { 452 | return 4; 453 | } 454 | 455 | this.onSettingsChanged(settings); 456 | }; 457 | 458 | freeboard.loadWidgetPlugin({ 459 | type_name: "pointer", 460 | display_name: "Pointer", 461 | "external_scripts" : [ 462 | "plugins/thirdparty/raphael.2.1.0.min.js" 463 | ], 464 | settings: [ 465 | { 466 | name: "direction", 467 | display_name: "Direction", 468 | type: "calculated", 469 | description: "In degrees" 470 | }, 471 | { 472 | name: "value_text", 473 | display_name: "Value Text", 474 | type: "calculated" 475 | }, 476 | { 477 | name: "units", 478 | display_name: "Units", 479 | type: "text" 480 | } 481 | ], 482 | newInstance: function (settings, newInstanceCallback) { 483 | newInstanceCallback(new pointerWidget(settings)); 484 | } 485 | }); 486 | 487 | var pictureWidget = function(settings) 488 | { 489 | var self = this; 490 | var widgetElement; 491 | var timer; 492 | var imageURL; 493 | 494 | function stopTimer() 495 | { 496 | if(timer) 497 | { 498 | clearInterval(timer); 499 | timer = null; 500 | } 501 | } 502 | 503 | function updateImage() 504 | { 505 | if(widgetElement && imageURL) 506 | { 507 | var cacheBreakerURL = imageURL + (imageURL.indexOf("?") == -1 ? "?" : "&") + Date.now(); 508 | 509 | $(widgetElement).css({ 510 | "background-image" : "url(" + cacheBreakerURL + ")" 511 | }); 512 | } 513 | } 514 | 515 | this.render = function(element) 516 | { 517 | $(element).css({ 518 | width : "100%", 519 | height: "100%", 520 | "background-size" : "cover", 521 | "background-position" : "center" 522 | }); 523 | 524 | widgetElement = element; 525 | } 526 | 527 | this.onSettingsChanged = function(newSettings) 528 | { 529 | stopTimer(); 530 | 531 | if(newSettings.refresh && newSettings.refresh > 0) 532 | { 533 | timer = setInterval(updateImage, Number(newSettings.refresh) * 1000); 534 | } 535 | } 536 | 537 | this.onCalculatedValueChanged = function(settingName, newValue) 538 | { 539 | if(settingName == "src") 540 | { 541 | imageURL = newValue; 542 | } 543 | 544 | updateImage(); 545 | } 546 | 547 | this.onDispose = function() 548 | { 549 | stopTimer(); 550 | } 551 | 552 | this.getHeight = function() 553 | { 554 | return 4; 555 | } 556 | 557 | this.onSettingsChanged(settings); 558 | }; 559 | 560 | freeboard.loadWidgetPlugin({ 561 | type_name: "picture", 562 | display_name: "Picture", 563 | fill_size: true, 564 | settings: [ 565 | { 566 | name: "src", 567 | display_name: "Image URL", 568 | type: "calculated" 569 | }, 570 | { 571 | "type": "number", 572 | "display_name": "Refresh every", 573 | "name": "refresh", 574 | "suffix": "seconds", 575 | "description":"Leave blank if the image doesn't need to be refreshed" 576 | } 577 | ], 578 | newInstance: function (settings, newInstanceCallback) { 579 | newInstanceCallback(new pictureWidget(settings)); 580 | } 581 | }); 582 | 583 | var indicatorWidget = function (settings) { 584 | var self = this; 585 | var titleElement = $('

'); 586 | var stateElement = $('
'); 587 | var indicatorElement = $('
'); 588 | var currentSettings = settings; 589 | var isOn = false; 590 | 591 | function updateState() { 592 | indicatorElement.toggleClass("on", isOn); 593 | 594 | if (isOn) { 595 | stateElement.text((_.isUndefined(currentSettings.on_text) ? "" : currentSettings.on_text)); 596 | } 597 | else { 598 | stateElement.text((_.isUndefined(currentSettings.off_text) ? "" : currentSettings.off_text)); 599 | } 600 | } 601 | 602 | this.render = function (element) { 603 | $(element).append(titleElement).append(indicatorElement).append(stateElement); 604 | } 605 | 606 | this.onSettingsChanged = function (newSettings) { 607 | currentSettings = newSettings; 608 | titleElement.html((_.isUndefined(newSettings.title) ? "" : newSettings.title)); 609 | updateState(); 610 | } 611 | 612 | this.onCalculatedValueChanged = function (settingName, newValue) { 613 | if (settingName == "value") { 614 | isOn = Boolean(newValue); 615 | } 616 | 617 | updateState(); 618 | } 619 | 620 | this.onDispose = function () { 621 | } 622 | 623 | this.getHeight = function () { 624 | return 1; 625 | } 626 | 627 | this.onSettingsChanged(settings); 628 | }; 629 | 630 | freeboard.loadWidgetPlugin({ 631 | type_name: "indicator", 632 | display_name: "Indicator Light", 633 | settings: [ 634 | { 635 | name: "title", 636 | display_name: "Title", 637 | type: "text" 638 | }, 639 | { 640 | name: "value", 641 | display_name: "Value", 642 | type: "calculated" 643 | }, 644 | { 645 | name: "on_text", 646 | display_name: "On Text", 647 | type: "calculated" 648 | }, 649 | { 650 | name: "off_text", 651 | display_name: "Off Text", 652 | type: "calculated" 653 | } 654 | ], 655 | newInstance: function (settings, newInstanceCallback) { 656 | newInstanceCallback(new indicatorWidget(settings)); 657 | } 658 | }); 659 | 660 | freeboard.addStyle('.gm-style-cc a', "text-shadow:none;"); 661 | 662 | var googleMapWidget = function (settings) { 663 | var self = this; 664 | var currentSettings = settings; 665 | var map; 666 | var marker; 667 | var currentPosition = {}; 668 | 669 | function updatePosition() { 670 | if (map && marker && currentPosition.lat && currentPosition.lon) { 671 | var newLatLon = new google.maps.LatLng(currentPosition.lat, currentPosition.lon); 672 | marker.setPosition(newLatLon); 673 | map.panTo(newLatLon); 674 | } 675 | } 676 | 677 | this.render = function (element) { 678 | function initializeMap() { 679 | var mapOptions = { 680 | zoom: 13, 681 | center: new google.maps.LatLng(37.235, -115.811111), 682 | disableDefaultUI: true, 683 | draggable: false, 684 | styles: [ 685 | {"featureType": "water", "elementType": "geometry", "stylers": [ 686 | {"color": "#2a2a2a"} 687 | ]}, 688 | {"featureType": "landscape", "elementType": "geometry", "stylers": [ 689 | {"color": "#000000"}, 690 | {"lightness": 20} 691 | ]}, 692 | {"featureType": "road.highway", "elementType": "geometry.fill", "stylers": [ 693 | {"color": "#000000"}, 694 | {"lightness": 17} 695 | ]}, 696 | {"featureType": "road.highway", "elementType": "geometry.stroke", "stylers": [ 697 | {"color": "#000000"}, 698 | {"lightness": 29}, 699 | {"weight": 0.2} 700 | ]}, 701 | {"featureType": "road.arterial", "elementType": "geometry", "stylers": [ 702 | {"color": "#000000"}, 703 | {"lightness": 18} 704 | ]}, 705 | {"featureType": "road.local", "elementType": "geometry", "stylers": [ 706 | {"color": "#000000"}, 707 | {"lightness": 16} 708 | ]}, 709 | {"featureType": "poi", "elementType": "geometry", "stylers": [ 710 | {"color": "#000000"}, 711 | {"lightness": 21} 712 | ]}, 713 | {"elementType": "labels.text.stroke", "stylers": [ 714 | {"visibility": "on"}, 715 | {"color": "#000000"}, 716 | {"lightness": 16} 717 | ]}, 718 | {"elementType": "labels.text.fill", "stylers": [ 719 | {"saturation": 36}, 720 | {"color": "#000000"}, 721 | {"lightness": 40} 722 | ]}, 723 | {"elementType": "labels.icon", "stylers": [ 724 | {"visibility": "off"} 725 | ]}, 726 | {"featureType": "transit", "elementType": "geometry", "stylers": [ 727 | {"color": "#000000"}, 728 | {"lightness": 19} 729 | ]}, 730 | {"featureType": "administrative", "elementType": "geometry.fill", "stylers": [ 731 | {"color": "#000000"}, 732 | {"lightness": 20} 733 | ]}, 734 | {"featureType": "administrative", "elementType": "geometry.stroke", "stylers": [ 735 | {"color": "#000000"}, 736 | {"lightness": 17}, 737 | {"weight": 1.2} 738 | ]} 739 | ] 740 | }; 741 | 742 | map = new google.maps.Map(element, mapOptions); 743 | 744 | google.maps.event.addDomListener(element, 'mouseenter', function (e) { 745 | e.cancelBubble = true; 746 | if (!map.hover) { 747 | map.hover = true; 748 | map.setOptions({zoomControl: true}); 749 | } 750 | }); 751 | 752 | google.maps.event.addDomListener(element, 'mouseleave', function (e) { 753 | if (map.hover) { 754 | map.setOptions({zoomControl: false}); 755 | map.hover = false; 756 | } 757 | }); 758 | 759 | marker = new google.maps.Marker({map: map}); 760 | 761 | updatePosition(); 762 | } 763 | 764 | if (window.google && window.google.maps) { 765 | initializeMap(); 766 | } 767 | else { 768 | window.gmap_initialize = initializeMap; 769 | head.js("https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false&callback=gmap_initialize"); 770 | } 771 | } 772 | 773 | this.onSettingsChanged = function (newSettings) { 774 | currentSettings = newSettings; 775 | } 776 | 777 | this.onCalculatedValueChanged = function (settingName, newValue) { 778 | if (settingName == "lat") { 779 | currentPosition.lat = newValue; 780 | } 781 | else if (settingName == "lon") { 782 | currentPosition.lon = newValue; 783 | } 784 | 785 | updatePosition(); 786 | } 787 | 788 | this.onDispose = function () { 789 | } 790 | 791 | this.getHeight = function () { 792 | return 4; 793 | } 794 | 795 | this.onSettingsChanged(settings); 796 | }; 797 | 798 | freeboard.loadWidgetPlugin({ 799 | type_name: "google_map", 800 | display_name: "Google Map", 801 | fill_size: true, 802 | settings: [ 803 | { 804 | name: "lat", 805 | display_name: "Latitude", 806 | type: "calculated" 807 | }, 808 | { 809 | name: "lon", 810 | display_name: "Longitude", 811 | type: "calculated" 812 | } 813 | ], 814 | newInstance: function (settings, newInstanceCallback) { 815 | newInstanceCallback(new googleMapWidget(settings)); 816 | } 817 | }); 818 | 819 | freeboard.addStyle('.html-widget', "white-space:normal;width:100%;height:100%"); 820 | 821 | var htmlWidget = function (settings) { 822 | var self = this; 823 | var htmlElement = $('
'); 824 | var currentSettings = settings; 825 | 826 | this.render = function (element) { 827 | $(element).append(htmlElement); 828 | } 829 | 830 | this.onSettingsChanged = function (newSettings) { 831 | currentSettings = newSettings; 832 | } 833 | 834 | this.onCalculatedValueChanged = function (settingName, newValue) { 835 | if (settingName == "html") { 836 | htmlElement.html(newValue); 837 | } 838 | } 839 | 840 | this.onDispose = function () { 841 | } 842 | 843 | this.getHeight = function () { 844 | return Number(currentSettings.height); 845 | } 846 | 847 | this.onSettingsChanged(settings); 848 | }; 849 | 850 | freeboard.loadWidgetPlugin({ 851 | "type_name": "html", 852 | "display_name": "HTML", 853 | "fill_size": true, 854 | "settings": [ 855 | { 856 | "name": "html", 857 | "display_name": "HTML", 858 | "type": "calculated", 859 | "description": "Can be literal HTML, or javascript that outputs HTML." 860 | }, 861 | { 862 | "name": "height", 863 | "display_name": "Height Blocks", 864 | "type": "number", 865 | "default_value": 4, 866 | "description": "A height block is around 60 pixels" 867 | } 868 | ], 869 | newInstance: function (settings, newInstanceCallback) { 870 | newInstanceCallback(new htmlWidget(settings)); 871 | } 872 | }); 873 | 874 | }()); 875 | -------------------------------------------------------------------------------- /public/plugins/thirdparty/jquery.sparkline.min.js: -------------------------------------------------------------------------------- 1 | /* jquery.sparkline 2.1.2 - http://omnipotent.net/jquery.sparkline/ 2 | ** Licensed under the New BSD License - see above site for details */ 3 | 4 | (function(a,b,c){(function(a){typeof define=="function"&&define.amd?define(["../../lib/js/thirdparty/jquery"],a):jQuery&&!jQuery.fn.sparkline&&a(jQuery)})(function(d){"use strict";var e={},f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L=0;f=function(){return{common:{type:"line",lineColor:"#00f",fillColor:"#cdf",defaultPixelsPerValue:3,width:"auto",height:"auto",composite:!1,tagValuesAttribute:"values",tagOptionsPrefix:"spark",enableTagOptions:!1,enableHighlight:!0,highlightLighten:1.4,tooltipSkipNull:!0,tooltipPrefix:"",tooltipSuffix:"",disableHiddenCheck:!1,numberFormatter:!1,numberDigitGroupCount:3,numberDigitGroupSep:",",numberDecimalMark:".",disableTooltips:!1,disableInteraction:!1},line:{spotColor:"#f80",highlightSpotColor:"#5f5",highlightLineColor:"#f22",spotRadius:1.5,minSpotColor:"#f80",maxSpotColor:"#f80",lineWidth:1,normalRangeMin:c,normalRangeMax:c,normalRangeColor:"#ccc",drawNormalOnTop:!1,chartRangeMin:c,chartRangeMax:c,chartRangeMinX:c,chartRangeMaxX:c,tooltipFormat:new h(' {{prefix}}{{y}}{{suffix}}')},bar:{barColor:"#3366cc",negBarColor:"#f44",stackedBarColor:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],zeroColor:c,nullColor:c,zeroAxis:!0,barWidth:4,barSpacing:1,chartRangeMax:c,chartRangeMin:c,chartRangeClip:!1,colorMap:c,tooltipFormat:new h(' {{prefix}}{{value}}{{suffix}}')},tristate:{barWidth:4,barSpacing:1,posBarColor:"#6f6",negBarColor:"#f44",zeroBarColor:"#999",colorMap:{},tooltipFormat:new h(' {{value:map}}'),tooltipValueLookups:{map:{"-1":"Loss",0:"Draw",1:"Win"}}},discrete:{lineHeight:"auto",thresholdColor:c,thresholdValue:0,chartRangeMax:c,chartRangeMin:c,chartRangeClip:!1,tooltipFormat:new h("{{prefix}}{{value}}{{suffix}}")},bullet:{targetColor:"#f33",targetWidth:3,performanceColor:"#33f",rangeColors:["#d3dafe","#a8b6ff","#7f94ff"],base:c,tooltipFormat:new h("{{fieldkey:fields}} - {{value}}"),tooltipValueLookups:{fields:{r:"Range",p:"Performance",t:"Target"}}},pie:{offset:0,sliceColors:["#3366cc","#dc3912","#ff9900","#109618","#66aa00","#dd4477","#0099c6","#990099"],borderWidth:0,borderColor:"#000",tooltipFormat:new h(' {{value}} ({{percent.1}}%)')},box:{raw:!1,boxLineColor:"#000",boxFillColor:"#cdf",whiskerColor:"#000",outlierLineColor:"#333",outlierFillColor:"#fff",medianColor:"#f00",showOutliers:!0,outlierIQR:1.5,spotRadius:1.5,target:c,targetColor:"#4a2",chartRangeMax:c,chartRangeMin:c,tooltipFormat:new h("{{field:fields}}: {{value}}"),tooltipFormatFieldlistKey:"field",tooltipValueLookups:{fields:{lq:"Lower Quartile",med:"Median",uq:"Upper Quartile",lo:"Left Outlier",ro:"Right Outlier",lw:"Left Whisker",rw:"Right Whisker"}}}}},E='.jqstooltip { position: absolute;left: 0px;top: 0px;visibility: hidden;background: rgb(0, 0, 0) transparent;background-color: rgba(0,0,0,0.6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000);-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#99000000, endColorstr=#99000000)";color: white;font: 10px arial, san serif;text-align: left;white-space: nowrap;padding: 5px;border: 1px solid white;z-index: 10000;}.jqsfield { color: white;font: 10px arial, san serif;text-align: left;}',g=function(){var a,b;return a=function(){this.init.apply(this,arguments)},arguments.length>1?(arguments[0]?(a.prototype=d.extend(new arguments[0],arguments[arguments.length-1]),a._super=arguments[0].prototype):a.prototype=arguments[arguments.length-1],arguments.length>2&&(b=Array.prototype.slice.call(arguments,1,-1),b.unshift(a.prototype),d.extend.apply(d,b))):a.prototype=arguments[0],a.prototype.cls=a,a},d.SPFormatClass=h=g({fre:/\{\{([\w.]+?)(:(.+?))?\}\}/g,precre:/(\w+)\.(\d+)/,init:function(a,b){this.format=a,this.fclass=b},render:function(a,b,d){var e=this,f=a,g,h,i,j,k;return this.format.replace(this.fre,function(){var a;return h=arguments[1],i=arguments[3],g=e.precre.exec(h),g?(k=g[2],h=g[1]):k=!1,j=f[h],j===c?"":i&&b&&b[i]?(a=b[i],a.get?b[i].get(j)||j:b[i][j]||j):(n(j)&&(d.get("numberFormatter")?j=d.get("numberFormatter")(j):j=s(j,k,d.get("numberDigitGroupCount"),d.get("numberDigitGroupSep"),d.get("numberDecimalMark"))),j)})}}),d.spformat=function(a,b){return new h(a,b)},i=function(a,b,c){return ac?c:a},j=function(a,c){var d;return c===2?(d=b.floor(a.length/2),a.length%2?a[d]:(a[d-1]+a[d])/2):a.length%2?(d=(a.length*c+c)/4,d%1?(a[b.floor(d)]+a[b.floor(d)-1])/2:a[d-1]):(d=(a.length*c+2)/4,d%1?(a[b.floor(d)]+a[b.floor(d)-1])/2:a[d-1])},k=function(a){var b;switch(a){case"undefined":a=c;break;case"null":a=null;break;case"true":a=!0;break;case"false":a=!1;break;default:b=parseFloat(a),a==b&&(a=b)}return a},l=function(a){var b,c=[];for(b=a.length;b--;)c[b]=k(a[b]);return c},m=function(a,b){var c,d,e=[];for(c=0,d=a.length;c0;h-=c)a.splice(h,0,e);return a.join("")},o=function(a,b,c){var d;for(d=b.length;d--;){if(c&&b[d]===null)continue;if(b[d]!==a)return!1}return!0},p=function(a){var b=0,c;for(c=a.length;c--;)b+=typeof a[c]=="number"?a[c]:0;return b},r=function(a){return d.isArray(a)?a:[a]},q=function(b){var c;a.createStyleSheet?a.createStyleSheet().cssText=b:(c=a.createElement("style"),c.type="text/css",a.getElementsByTagName("head")[0].appendChild(c),c[typeof a.body.style.WebkitAppearance=="string"?"innerText":"innerHTML"]=b)},d.fn.simpledraw=function(b,e,f,g){var h,i;if(f&&(h=this.data("_jqs_vcanvas")))return h;if(d.fn.sparkline.canvas===!1)return!1;if(d.fn.sparkline.canvas===c){var j=a.createElement("canvas");if(!j.getContext||!j.getContext("2d")){if(!a.namespaces||!!a.namespaces.v)return d.fn.sparkline.canvas=!1,!1;a.namespaces.add("v","urn:schemas-microsoft-com:vml","#default#VML"),d.fn.sparkline.canvas=function(a,b,c,d){return new J(a,b,c)}}else d.fn.sparkline.canvas=function(a,b,c,d){return new I(a,b,c,d)}}return b===c&&(b=d(this).innerWidth()),e===c&&(e=d(this).innerHeight()),h=d.fn.sparkline.canvas(b,e,this,g),i=d(this).data("_jqs_mhandler"),i&&i.registerCanvas(h),h},d.fn.cleardraw=function(){var a=this.data("_jqs_vcanvas");a&&a.reset()},d.RangeMapClass=t=g({init:function(a){var b,c,d=[];for(b in a)a.hasOwnProperty(b)&&typeof b=="string"&&b.indexOf(":")>-1&&(c=b.split(":"),c[0]=c[0].length===0?-Infinity:parseFloat(c[0]),c[1]=c[1].length===0?Infinity:parseFloat(c[1]),c[2]=a[b],d.push(c));this.map=a,this.rangelist=d||!1},get:function(a){var b=this.rangelist,d,e,f;if((f=this.map[a])!==c)return f;if(b)for(d=b.length;d--;){e=b[d];if(e[0]<=a&&e[1]>=a)return e[2]}return c}}),d.range_map=function(a){return new t(a)},u=g({init:function(a,b){var c=d(a);this.$el=c,this.options=b,this.currentPageX=0,this.currentPageY=0,this.el=a,this.splist=[],this.tooltip=null,this.over=!1,this.displayTooltips=!b.get("disableTooltips"),this.highlightEnabled=!b.get("disableHighlight")},registerSparkline:function(a){this.splist.push(a),this.over&&this.updateDisplay()},registerCanvas:function(a){var b=d(a.canvas);this.canvas=a,this.$canvas=b,b.mouseenter(d.proxy(this.mouseenter,this)),b.mouseleave(d.proxy(this.mouseleave,this)),b.click(d.proxy(this.mouseclick,this))},reset:function(a){this.splist=[],this.tooltip&&a&&(this.tooltip.remove(),this.tooltip=c)},mouseclick:function(a){var b=d.Event("sparklineClick");b.originalEvent=a,b.sparklines=this.splist,this.$el.trigger(b)},mouseenter:function(b){d(a.body).unbind("mousemove.jqs"),d(a.body).bind("mousemove.jqs",d.proxy(this.mousemove,this)),this.over=!0,this.currentPageX=b.pageX,this.currentPageY=b.pageY,this.currentEl=b.target,!this.tooltip&&this.displayTooltips&&(this.tooltip=new v(this.options),this.tooltip.updatePosition(b.pageX,b.pageY)),this.updateDisplay()},mouseleave:function(){d(a.body).unbind("mousemove.jqs");var b=this.splist,c=b.length,e=!1,f,g;this.over=!1,this.currentEl=null,this.tooltip&&(this.tooltip.remove(),this.tooltip=null);for(g=0;g