├── .gitignore ├── README.md ├── assets ├── doc_amount_line_chart.png ├── doc_amount_pie_chart.png ├── doc_amount_today_count.png ├── doc_amount_total_count.png ├── doc_datalist_list.png ├── doc_realtime_count.png ├── doc_realtime_line_chart.png ├── preview.png └── preview_thumbnail.png ├── config_default.yaml ├── load_config.js ├── middleware.js ├── package.json ├── public ├── favicon.ico ├── images │ ├── bg.jpg │ ├── col.gif │ ├── trend-down.gif │ └── trend-up.gif ├── img │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png ├── javascripts │ ├── lib │ │ ├── async.js │ │ ├── bootstrap.js │ │ ├── highcharts.js │ │ ├── highcharts_exporting.js │ │ ├── jquery.js │ │ ├── mustache.js │ │ └── underscore.js │ └── ranaly.js ├── kalendae │ ├── arrows.png │ ├── close.png │ ├── kalendae.css │ ├── kalendae.js │ └── kalendae.min.js └── stylesheets │ ├── bootstrap.min.css │ └── style.css ├── ranaly.js ├── template ├── default.mustache └── images.mustache └── views ├── error.jade ├── index.jade ├── layout.jade ├── login.jade ├── page.jade ├── page_layout.jade └── widgets ├── amount_line_chart.jade ├── amount_pie_chart.jade ├── amount_today_count.jade ├── amount_total_count.jade ├── custom_code.jade ├── datalist_list.jade ├── realtime_count.jade └── realtime_line_chart.jade /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | _* 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ranaly 2 | Ranaly是一个基于Redis的数据统计可视化工具。 3 | 4 | Ranaly的Node.js客户端库[node_ranaly](https://github.com/luin/node_ranaly)已经完成。 5 | 6 | ![效果图](./assets/preview.png) 7 | 8 | 特点如下: 9 | 10 | 1. 使用简单,容易维护 11 | 2. 基于Redis,性能优异 12 | 3. 支持模块级别的权限控制 13 | 4. 长连接实时数据统计 14 | 5. 多种图表可以选择,可以自定义模板、模块 15 | 16 | ## 快速入门 17 | 在项目中使用Ranaly统计数据十分简单,步骤如下。 18 | ### 1. 安装Node.js和Redis 19 | Ranaly使用Node.js开发,所以需要先安装[Node.js](http://nodejs.org/)。同样因为Ranaly的统计数据存储于Redis中,所以需要安装[Redis](http://redis.io/)。 20 | ### 2. 安装Ranaly 21 | 22 | git clone git://github.com/luin/ranaly.git 23 | cd ranaly 24 | npm install 25 | 26 | ### 3. 在已有项目中加入统计代码 27 | 如果你的项目使用Node.js开发,可以使用Ranaly的node客户端库,安装方法如下: 28 | 29 | npm install node_ranaly 30 | 31 | 如果希望统计项目注册用户的变化趋势,可以在用户注册成功后加上如下代码: 32 | 33 | var ranaly = require('node_ranaly').createClient(); 34 | var rUsers = new ranaly.Amount('Users'); 35 | rUsers.incr(); 36 | 37 | node_ranaly库会将名为“Users”的Amount类型的桶的值增1并和当前时间一起写入到Redis中。 38 | 39 | ### 4. 查看统计结果 40 | 建立配置文件,内容如下: 41 | 42 | app_name: Demo 43 | users: 44 | - username: admin 45 | password: admin123 46 | pages: 47 | - title: Overview 48 | widgets: 49 | - type: amount_line_chart 50 | bucket: Users 51 | 52 | 将文件保存,并进入Ranaly的目录执行: 53 | 54 | node ranaly /path/to/config_file 55 | 56 | 其中`/path/to/config_file`表示配置文件路径。此时就可以访问 http://127.0.0.1:3000 进入数据查看页面了,使用admin和admin123登录系统,能看到用户数量的折线图。 57 | 58 | ## 文档 59 | Ranaly由两个部分组成,分别是客户端库和数据展示部分,本页面项目是数据展示部分。在程序中通过客户端库在Redis中记录统计数据,而本页面项目的作用是将这些数据以图表的形式显示出来。 60 | 61 | ### 1. 客户端库 62 | Node.js:[node_ranaly](https://github.com/luin/node_ranaly) 63 | 64 | ### 2. 数据类型 65 | 为了适应不同场合的统计需要,Ranaly支持3种类型的数据统计: 66 | 67 | #### (1) Amount 68 | 当要记录某一个数据的在不用时间的数量变化时就需要使用Amount类型,如想记录用户数量的变化趋势等。 69 | 70 | #### (2) Realtime 71 | Realtime用来记录实时数据,如当前系统内存使用情况等。 72 | 73 | #### (3) DataList 74 | DataList用来记录数据列表,数据可以是数字、字符串甚至对象。如新注册的用户头像地址列表、新注册的用户资料列表等。DataList只保留数据列表中的前N条数据(N的数值可以指定)。 75 | 76 | ### 3. Ranaly配置文件 77 | 为了能够通过Ranaly来将Redis中记录的统计数据可视化,需要提供一份配置文件。配置文件的格式为YAML,样例如下: 78 | 79 | app_name: 应用名称 80 | port: Ranaly使用的端口号,默认是3000 81 | redis: 82 | host: Redis的主机地址,默认是127.0.0.1 83 | port: Redis的端口号,默认是6379 84 | key_prefix: 存储统计数据的键名前缀,需要和客户端库的配置一样 85 | users: 86 | - username: 用户名 87 | password: 密码 88 | role: 数字,表示用户的权限,默认是0 89 | pages: 90 | - title: 页面的标题 91 | widgets: 92 | - type: widget的类型 93 | bucket: widget对应的bucket 94 | title: widget的标题 95 | role: 数字,只对拥有大于或等于该role的用户可见 96 | 97 | ### 4. 桶(bucket) 98 | 为了区分不同的统计数据,需要为每类数据起个名字(桶),如统计用户名称的桶可以命名为“Users”,统计访问量的桶可以命名为“Page views”。不同数据类型的桶可以重名,桶的名称可以包含空格。 99 | 100 | ### 5. Widget类型 101 | 在配置文件中可以看到每一个项目是由若干个page组成的,每个page由若干个widget组成。widget分为不同种类,每一种widget只适用于一种数据类型,可以从其命名看出来。 102 | 103 | #### (1) amount_line_chart 104 | 该类型的widget用来显示折线图,只支持Amount。每个widget除了type、bucket、title和role四个参数外还支持其它不同的参数。amount_line_chart类型支持的参数如下: 105 | 106 | | 参数名 | 意义 | 取值 | 107 | |---------------|-----------|-----------------------------------------------------------------| 108 | | default_range | 默认显示的时间范围 | today(显示当天的数据),yesterday(昨天的数据),7days(最近7天的数据),30days(最近30天的数据) | 109 | | update_interval | 数据更新间隔,默认为20-40秒 | 数字,单位为秒 | 110 | 111 | 112 | ![doc_amount_line_chart](./assets/doc_amount_line_chart.png) 113 | 114 | amount_line_chart支持同时显示多个bucket的数据,在配置文件中以数组形式设置,如: 115 | 116 | type: amount_line_chart 117 | bucket: [Users, Page views, Groups] 118 | 119 | #### (2) amount_pie_chart 120 | 用来显示饼图。amount_pie_chart类型支持的其它参数如下: 121 | 122 | | 参数名 | 意义 | 取值 | 123 | |---------------|-----------|---------------------------------------------------------------------------| 124 | | default_range | 默认显示的时间范围 | today(显示当天的数据),yesterday(昨天的数据),7days(最近7天的数据),30days(最近30天的数据),all(所有数据) | 125 | | update_interval | 数据更新间隔,默认为20-40秒 | 数字,单位为秒 | 126 | 127 | 128 | amount_pie_chart支持同时比对多个bucket的数据。 129 | 130 | ![doc_amount_pie_chart](./assets/doc_amount_pie_chart.png) 131 | 132 | #### (3) amount_today_count 133 | 用来显示当天的数据,并根据昨天同时间的数据预测今天的全天的数据。支持的其它参数如下: 134 | 135 | | 参数名 | 意义 | 取值 | 136 | |---------------|-----------|---------------------------------------------------------------------------| 137 | | update_interval | 数据更新间隔,默认为20-40秒 | 数字,单位为秒 | 138 | 139 | 140 | ![doc_amount_today_count](./assets/doc_amount_today_count.png) 141 | 142 | amount_today_count支持同时显示多个bucket的数据。 143 | 144 | #### (4) amount_total_count 145 | 显示某一时间范围的数据总和,如最近30天注册的用户总数。支持的其它参数如下: 146 | 147 | | 参数名 | 意义 | 取值 | 148 | |---------------|-----------|---------------------------------------------------------------------------| 149 | | update_interval | 数据更新间隔,默认为20-40秒 | 数字,单位为秒 | 150 | 151 | 152 | ![doc_amount_total_count](./assets/doc_amount_total_count.png) 153 | 154 | amount_total_count支持同时显示多个bucket的数据。 155 | 156 | #### (5) realtime_line_chart 157 | 显示实时数据的折线图,更新频率是1秒。 158 | 159 | ![doc_realtime_line_chart](./assets/doc_realtime_line_chart.png) 160 | 161 | realtime_line_chart支持同时显示多个bucket的数据。 162 | 163 | #### (6) realtime_count 164 | 显示实时数据的数值,更新频率是实时。 165 | 166 | ![doc_realtime_count](./assets/doc_realtime_count.png) 167 | 168 | realtime_count支持同时显示多个bucket的数据。 169 | 170 | #### (7) datalist_list 171 | 显示DataList数据列表。由于DataList类型可以存储任何数据的列表,所以该类型的widget支持高度自定义。可以通过template参数指定显示DataList的模板,模板采用[Mustache](http://mustache.github.com/),渲染的数据格式是{"data": [*数据列表*]}。下面的实例中还会对此进行介绍。除此之外还支持count_per_page参数用来指定每页要显示的数据条数。 172 | 173 | ![doc_datalist_list](./assets/doc_datalist_list.png) 174 | 175 | #### (8) custom_code 176 | 该widget类型比较特殊,无需bucket参数,只需要content参数。作用是执行自定义的代码,包括HTML/CSS/JavaScript。如: 177 | 178 | - type: custom_code 179 | content: > 180 |

hi

181 | 182 | ## 综合实例 183 | 现在假设要统计Facebook网站的数据,以使用Ranaly的Node.js客户端为例。首先我们通过node_ranaly建立到Redis的连接(假设Redis运行在本机6380端口上): 184 | 185 | var ranaly = require('node_ranaly'); 186 | var ranalyClient = ranaly.createClient(6380, '127.0.0.1'); 187 | 188 | 首先要统计的是用户的数量,每当用户注册成功都调用如下代码令Users桶的数值加1: 189 | 190 | var rUsers = new ranalyClient.Amount('Users'); 191 | rUsers.incr(); 192 | 193 | 然后我们要统计网站的访问量,每当访问一个页面时,都执行: 194 | 195 | var rPageViews = new ranalyClient.Amount('Page views'); 196 | rPageViews.incr(); 197 | 198 | 现在来配置Ranaly可视化部分来显示我们的统计数据: 199 | 200 | app_name: Facebook 201 | redis: 202 | host: 127.0.0.1 203 | port: 6380 204 | users: 205 | - username: admin 206 | password: admin123 207 | pages: 208 | - title: Overview 209 | widgets: 210 | - type: amount_line_chart 211 | bucket: [Users, Page views] 212 | 213 | 在这个配置中,我们使用折线图来比对用户数量和访问量的关系。将该内容存为config.yaml,然后执行: 214 | 215 | $ node ./ranaly.js /path/to/config.yaml 216 | Ranaly server listening on port 3000 217 | 218 | 此时就可以通过http://127.0.0.1:3000来访问了。 219 | 220 | 在Facebook中,用户可以发布文字状态、照片和视频,若想统计这3种类型的状态的比例,可以在发布时执行: 221 | 222 | var bucket = ''; 223 | switch (status.type) { 224 | case 'text': 225 | bucket = 'Text'; 226 | break; 227 | case 'photo': 228 | bucket = 'Photo'; 229 | break; 230 | case 'video': 231 | bucket = 'Video'; 232 | break; 233 | } 234 | var rStatus = new ranalyClient.Amount(bucket); 235 | rStatus.incr(); 236 | 237 | 然后我们接着编辑config.yaml,在widgets中加入: 238 | 239 | - type: amount_pie_chart 240 | bucket: [Text, Photo, Video] 241 | 242 | 这时重新启动Ranaly服务器(每次修改config.yaml都得重启),可以看到显示3者比例的饼图,并且可以随意调整时间范围。 243 | 244 | 接下来显示服务器的资源,包括内存和CPU: 245 | 246 | var rMemory = new ranalyClient.Realtime('Memory'); 247 | var rCPU = new ranalyClient.Realtime('CPU'); 248 | setInterval(function () { 249 | rMemory.set(System.getUsedMemory()); 250 | rCPU.set(System.getUsedCPU()); 251 | }, 100); 252 | 253 | 嗯...好像是没有System.getUsedMemory()和System.getUsedCPU()这两个东西,不过只要知道他们都会返回个数字就好了。 254 | 255 | 同样我们接着编辑config.yaml,在widgets中加入: 256 | 257 | - type: realtime_count 258 | bucket: [Memory, CPU] 259 | 260 | 重启服务器,就可以看到空闲的内存和CPU资源了。每次Realtime类型的数据更新都会推送给Ranaly服务器,所以页面上显示的数值每100毫秒变一次。 261 | 262 | 现在我们还希望能在Ranaly页面中看到用户最新上传的头像,所以我们在头像上传成功后执行: 263 | 264 | var rAvatar = new ranalyClient.DataList('Avatar'); 265 | rAvatar.push(avatarURL, 100); 266 | 267 | 其中`avatarURL`是一个字符串,存储用户头像的URL地址。100表示只存储最新的100条记录,防止数据占用过多的内存(因为数据是存储在Redis中的)。 268 | 269 | 接着我们来配置config.yaml: 270 | 271 | - type: datalist_list 272 | bucket: [Avatar] 273 | 274 | 默认会以列表的形式展示数据,不是很好看,所以我们来自定义模板。Ranaly使用了BootStrap框架,所以可以在模板中自由使用BootStrap的风格。我们将数据修改成: 275 | 276 | - type: datalist_list 277 | bucket: [Avatar] 278 | template: > 279 | 288 | 289 | 对于展示图片,Ranaly提供了一个预置的模板,可以这样写: 290 | 291 | - type: datalist_list 292 | bucket: [Avatar] 293 | preset_template: images 294 | 295 | 接着我们想记录最近注册成功的用户的资料,则在用户注册成功后执行: 296 | 297 | var rUsers = new ranalyClient.DataList('Users'); 298 | rUsers.push({ 299 | id: user.id, 300 | name: user.name, 301 | age: user.age, 302 | description: user.description 303 | }, 100); 304 | 305 | 然后修改配置文件: 306 | 307 | - type: datalist_list 308 | bucket: [Users] 309 | template: 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | {{#data}} 321 | 322 | 323 | 324 | 325 | 326 | 327 | {{/data}} 328 | 329 |
IDnameagedescription
{{id}}{{name}}{{age}}{{description}}
330 | 331 | 就可以看到记录最新注册用户资料的表格了。 332 | 333 | 334 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/luin/ranaly/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 335 | 336 | -------------------------------------------------------------------------------- /assets/doc_amount_line_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_amount_line_chart.png -------------------------------------------------------------------------------- /assets/doc_amount_pie_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_amount_pie_chart.png -------------------------------------------------------------------------------- /assets/doc_amount_today_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_amount_today_count.png -------------------------------------------------------------------------------- /assets/doc_amount_total_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_amount_total_count.png -------------------------------------------------------------------------------- /assets/doc_datalist_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_datalist_list.png -------------------------------------------------------------------------------- /assets/doc_realtime_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_realtime_count.png -------------------------------------------------------------------------------- /assets/doc_realtime_line_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_realtime_line_chart.png -------------------------------------------------------------------------------- /assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/preview.png -------------------------------------------------------------------------------- /assets/preview_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/preview_thumbnail.png -------------------------------------------------------------------------------- /config_default.yaml: -------------------------------------------------------------------------------- 1 | app_name: My App 2 | port: 3000 3 | redis: 4 | host: 127.0.0.1 5 | port: 6379 6 | key_prefix: 'Ranaly:' 7 | users: 8 | - username: admin 9 | password: admin123 10 | role: 0 11 | pages: 12 | # Add your pages here. 13 | -------------------------------------------------------------------------------- /load_config.js: -------------------------------------------------------------------------------- 1 | var yaml = require('js-yaml'); 2 | var fs = require('fs'); 3 | 4 | if (process.argv[2] && fs.existsSync(process.argv[2])) { 5 | var configContent = fs.readFileSync(process.argv[2], 'utf8'); 6 | var config = yaml.load(configContent); 7 | } else { 8 | console.log('[WARNING] No config file specified, using the default config. In order to specify a config file use: node /path/to/ranaly /path/to/config.yaml'); 9 | var config = require('./config_default.yaml'); 10 | } 11 | 12 | var isArray = Array.isArray; 13 | var rOnlyLetterOrNumber = /^[a-zA-Z][a-zA-Z0-9 ]*$/; 14 | 15 | function generateID(prefix) { 16 | prefix = prefix || ''; 17 | return prefix + (Math.random() * 1000000000 | 0).toString(36); 18 | } 19 | 20 | function toString(value) { 21 | return value ? value.toString() : (value === 0 ? '0' : ''); 22 | } 23 | 24 | function initWidgets(wi) { 25 | var widgets = []; 26 | if (isArray(wi)) { 27 | wi.forEach(function (widget) { 28 | if (widget.type === 'custom_code') { 29 | if (widget.file) { 30 | try { 31 | widget.content = fs.readFileSync(widget.file, widget.encoding || 'utf8'); 32 | } catch (e) { 33 | console.log('[Error] Couldn\'t load the custom_code widget: ' + e.message); 34 | return; 35 | } 36 | } 37 | if (!widget.content) return; 38 | 39 | } else if (!widget.type || !widget.bucket) { 40 | return; 41 | } 42 | // Every widget should have an ID. 43 | widget.id = generateID('widget_'); 44 | // bucket should be an array 45 | if (typeof widget.bucket !== 'undefined' && !isArray(widget.bucket)) { 46 | widget.bucket = [widget.bucket]; 47 | } 48 | if (isArray(widget.bucket)) { 49 | widget.bucket = widget.bucket.map(function (b) { 50 | return b.toString(); 51 | }); 52 | } 53 | // widget should have a default role 54 | widget.role = parseInt(widget.role, 10) || 0; 55 | // bucket should have a title 56 | widget.subtitle = toString(widget.subtitle).replace(/["']/g, '') || ''; 57 | widget.y_axis_title = toString(widget.y_axis_title).replace(/["']/g, '') || ''; 58 | switch (widget.type) { 59 | case 'amount_line_chart': 60 | widget.title = toString(widget.title) || 'Line Chart'; 61 | widget.default_range = toString(widget.default_range) || 'today'; 62 | break; 63 | case 'amount_pie_chart': 64 | widget.title = toString(widget.title) || 'Pie Chart'; 65 | widget.default_range = toString(widget.default_range) || 'today'; 66 | break; 67 | case 'amount_total_count': 68 | widget.title = toString(widget.title) || 'Total Count'; 69 | widget.default_range = toString(widget.default_range) || 'all'; 70 | break; 71 | case 'amount_today_count': 72 | widget.title = toString(widget.title) || 'Today Count'; 73 | break; 74 | case 'realtime_line_chart': 75 | widget.title = toString(widget.title) || 'Realtime Chart'; 76 | break; 77 | case 'realtime_count': 78 | widget.title = toString(widget.title) || 'Realtime Count'; 79 | break; 80 | case 'datalist_list': 81 | widget.title = toString(widget.title) || 'Data List'; 82 | widget.count_per_page = parseInt(widget.count_per_page, 10) || '12'; 83 | if (widget.template) { 84 | widget.template = toString(widget.template); 85 | } else { 86 | if (!widget.preset_template) { 87 | widget.preset_template = 'default'; 88 | } 89 | var tplPath = __dirname + '/template/' + widget.preset_template + '.mustache'; 90 | if (fs.existsSync(tplPath)) { 91 | widget.template = fs.readFileSync(tplPath, 'utf8'); 92 | } else { 93 | console.log('[Error] Couldn\'t load datalist widget as the specified preset template isn\'t exists.'); 94 | return; 95 | } 96 | } 97 | break; 98 | } 99 | widgets.push(widget); 100 | 101 | }); 102 | } 103 | return widgets; 104 | } 105 | 106 | if (!config || typeof config !== 'object') config = {}; 107 | config.app_name = toString(config.app_name) || 'New App'; 108 | config.redis = config.redis || {}; 109 | config.port = config.port || 3000; 110 | config.secret = toString(config.secret) || generateID('cookieSecret'); 111 | 112 | // users 113 | var users = []; 114 | if (isArray(config.users)) { 115 | config.users.forEach(function (user) { 116 | if (!user.username) return; 117 | users.push({ 118 | username: user.username.toString(), 119 | password: toString(user.password), 120 | role: parseInt(user.role, 10) || 0 121 | }); 122 | }); 123 | } 124 | config.users = users; 125 | 126 | // pages 127 | var pages = []; 128 | var pageID = {}; 129 | if (isArray(config.pages)) { 130 | config.pages.forEach(function (page) { 131 | if (!page.title) return; 132 | page.title = page.title.toString(); 133 | // every page should have an id 134 | if (rOnlyLetterOrNumber.test(page.title)) { 135 | page.id = toString(page.id) || page.title.toLowerCase().replace(/ +/g, '-'); 136 | } 137 | if (!page.id || pageID[page.id]) { 138 | page.id = generateID('page_'); 139 | pageID[page.id] = true; 140 | } else { 141 | pageID[page.id] = true; 142 | } 143 | 144 | page.widgets = initWidgets(page.widgets); 145 | 146 | pages.push(page); 147 | }); 148 | } 149 | config.pages = pages; 150 | 151 | module.exports = config; 152 | -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | exports.flash = function (req, res, next) { 2 | var err = req.session.error; 3 | var msg = req.session.info; 4 | delete req.session.error; 5 | delete req.session.info; 6 | res.locals.message = ''; 7 | if (err) 8 | res.locals.message = '
Oh snap! ' + err + '
'; 9 | if (msg) 10 | res.locals.message = '
Well done! ' + msg + '
'; 11 | next(); 12 | }; 13 | 14 | exports.requireRole = function (req, res, next) { 15 | if (req.session.username) { 16 | res.locals.user = req.session; 17 | next(); 18 | } else { 19 | if (req.path !== '/') { 20 | req.session.error = 'You need to sign in before continuing.'; 21 | res.redirect('/login?next=' + encodeURIComponent(req.path)); 22 | } else { 23 | res.redirect('/login'); 24 | } 25 | } 26 | }; 27 | 28 | exports.generateMenu = function (config) { 29 | return function (req, res, next) { 30 | res.locals.menus = []; 31 | config.pages.forEach(function (page) { 32 | var pass = false; 33 | page.widgets.forEach(function (widget) { 34 | if (widget.role <= req.session.role) { 35 | pass = true; 36 | } 37 | }); 38 | if (pass) { 39 | res.locals.menus.push({ 40 | title: page.title, 41 | id: encodeURIComponent(page.id) 42 | }); 43 | } 44 | }); 45 | next(); 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ranaly", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "start": "node ranaly" 6 | }, 7 | "dependencies": { 8 | "express": "3.0.0rc2", 9 | "connect": "*", 10 | "jade": "0.35.0", 11 | "js-yaml": "2.0.2", 12 | "ranaly": "*", 13 | "redis": "*", 14 | "socket.io": "0.9.*", 15 | "session.socket.io": "0.1.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/favicon.ico -------------------------------------------------------------------------------- /public/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/images/bg.jpg -------------------------------------------------------------------------------- /public/images/col.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/images/col.gif -------------------------------------------------------------------------------- /public/images/trend-down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/images/trend-down.gif -------------------------------------------------------------------------------- /public/images/trend-up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/images/trend-up.gif -------------------------------------------------------------------------------- /public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/javascripts/lib/async.js: -------------------------------------------------------------------------------- 1 | (function(){function d(a){var c=!1;return function(){if(c)throw Error("Callback was already called.");c=!0,a.apply(b,arguments)}}var b,c,a={};b=this,null!=b&&(c=b.async),a.noConflict=function(){return b.async=c,a};var e=function(a,b){if(a.forEach)return a.forEach(b);for(var c=0;a.length>c;c+=1)b(a[c],c,a)},f=function(a,b){if(a.map)return a.map(b);var c=[];return e(a,function(a,d,e){c.push(b(a,d,e))}),c},g=function(a,b,c){return a.reduce?a.reduce(b,c):(e(a,function(a,d,e){c=b(c,a,d,e)}),c)},h=function(a){if(Object.keys)return Object.keys(a);var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c);return b};a.nextTick="undefined"!=typeof process&&process.nextTick?process.nextTick:"function"==typeof setImmediate?function(a){setImmediate(a)}:function(a){setTimeout(a,0)},a.forEach=function(a,b,c){if(c=c||function(){},!a.length)return c();var f=0;e(a,function(e){b(e,d(function(b){b?(c(b),c=function(){}):(f+=1,f>=a.length&&c(null))}))})},a.forEachSeries=function(b,c,d){if(d=d||function(){},!b.length)return d();var e=0,f=function(){var g=!0;c(b[e],function(c){c?(d(c),d=function(){}):(e+=1,e>=b.length?d(null):g?a.nextTick(f):f())}),g=!1};f()},a.forEachLimit=function(a,b,c,d){var e=i(b);e.apply(null,[a,c,d])};var i=function(a){return function(b,c,d){if(d=d||function(){},!b.length||0>=a)return d();var e=0,f=0,g=0;(function h(){if(e>=b.length)return d();for(;a>g&&b.length>f;)f+=1,g+=1,c(b[f-1],function(a){a?(d(a),d=function(){}):(e+=1,g-=1,e>=b.length?d():h())})})()}},j=function(b){return function(){var c=Array.prototype.slice.call(arguments);return b.apply(null,[a.forEach].concat(c))}},k=function(a,b){return function(){var c=Array.prototype.slice.call(arguments);return b.apply(null,[i(a)].concat(c))}},l=function(b){return function(){var c=Array.prototype.slice.call(arguments);return b.apply(null,[a.forEachSeries].concat(c))}},m=function(a,b,c,d){var e=[];b=f(b,function(a,b){return{index:b,value:a}}),a(b,function(a,b){c(a.value,function(c,d){e[a.index]=d,b(c)})},function(a){d(a,e)})};a.map=j(m),a.mapSeries=l(m),a.mapLimit=function(a,b,c,d){return n(b)(a,c,d)};var n=function(a){return k(a,m)};a.reduce=function(b,c,d,e){a.forEachSeries(b,function(a,b){d(c,a,function(a,d){c=d,b(a)})},function(a){e(a,c)})},a.inject=a.reduce,a.foldl=a.reduce,a.reduceRight=function(b,c,d,e){var g=f(b,function(a){return a}).reverse();a.reduce(g,c,d,e)},a.foldr=a.reduceRight;var o=function(a,b,c,d){var e=[];b=f(b,function(a,b){return{index:b,value:a}}),a(b,function(a,b){c(a.value,function(c){c&&e.push(a),b()})},function(){d(f(e.sort(function(a,b){return a.index-b.index}),function(a){return a.value}))})};a.filter=j(o),a.filterSeries=l(o),a.select=a.filter,a.selectSeries=a.filterSeries;var p=function(a,b,c,d){var e=[];b=f(b,function(a,b){return{index:b,value:a}}),a(b,function(a,b){c(a.value,function(c){c||e.push(a),b()})},function(){d(f(e.sort(function(a,b){return a.index-b.index}),function(a){return a.value}))})};a.reject=j(p),a.rejectSeries=l(p);var q=function(a,b,c,d){a(b,function(a,b){c(a,function(c){c?(d(a),d=function(){}):b()})},function(){d()})};a.detect=j(q),a.detectSeries=l(q),a.some=function(b,c,d){a.forEach(b,function(a,b){c(a,function(a){a&&(d(!0),d=function(){}),b()})},function(){d(!1)})},a.any=a.some,a.every=function(b,c,d){a.forEach(b,function(a,b){c(a,function(a){a||(d(!1),d=function(){}),b()})},function(){d(!0)})},a.all=a.every,a.sortBy=function(b,c,d){a.map(b,function(a,b){c(a,function(c,d){c?b(c):b(null,{value:a,criteria:d})})},function(a,b){if(a)return d(a);var c=function(a,b){var c=a.criteria,d=b.criteria;return d>c?-1:c>d?1:0};d(null,f(b.sort(c),function(a){return a.value}))})},a.auto=function(b,c){c=c||function(){};var d=h(b);if(!d.length)return c(null);var f={},i=[],j=function(a){i.unshift(a)},k=function(a){for(var b=0;i.length>b;b+=1)if(i[b]===a)return i.splice(b,1),void 0},l=function(){e(i.slice(0),function(a){a()})};j(function(){h(f).length===d.length&&(c(null,f),c=function(){})}),e(d,function(d){var e=b[d]instanceof Function?[b[d]]:b[d],h=function(b){if(b)c(b),c=function(){};else{var e=Array.prototype.slice.call(arguments,1);1>=e.length&&(e=e[0]),f[d]=e,a.nextTick(l)}},i=e.slice(0,Math.abs(e.length-1))||[],m=function(){return g(i,function(a,b){return a&&f.hasOwnProperty(b)},!0)&&!f.hasOwnProperty(d)};if(m())e[e.length-1](h,f);else{var n=function(){m()&&(k(n),e[e.length-1](h,f))};j(n)}})},a.waterfall=function(b,c){if(c=c||function(){},!b.length)return c();var d=function(b){return function(e){if(e)c.apply(null,arguments),c=function(){};else{var f=Array.prototype.slice.call(arguments,1),g=b.next();g?f.push(d(g)):f.push(c),a.nextTick(function(){b.apply(null,f)})}}};d(a.iterator(b))()};var r=function(a,b,c){if(c=c||function(){},b.constructor===Array)a.map(b,function(a,b){a&&a(function(a){var c=Array.prototype.slice.call(arguments,1);1>=c.length&&(c=c[0]),b.call(null,a,c)})},c);else{var d={};a.forEach(h(b),function(a,c){b[a](function(b){var e=Array.prototype.slice.call(arguments,1);1>=e.length&&(e=e[0]),d[a]=e,c(b)})},function(a){c(a,d)})}};a.parallel=function(b,c){r({map:a.map,forEach:a.forEach},b,c)},a.parallelLimit=function(a,b,c){r({map:n(b),forEach:i(b)},a,c)},a.series=function(b,c){if(c=c||function(){},b.constructor===Array)a.mapSeries(b,function(a,b){a&&a(function(a){var c=Array.prototype.slice.call(arguments,1);1>=c.length&&(c=c[0]),b.call(null,a,c)})},c);else{var d={};a.forEachSeries(h(b),function(a,c){b[a](function(b){var e=Array.prototype.slice.call(arguments,1);1>=e.length&&(e=e[0]),d[a]=e,c(b)})},function(a){c(a,d)})}},a.iterator=function(a){var b=function(c){var d=function(){return a.length&&a[c].apply(null,arguments),d.next()};return d.next=function(){return a.length-1>c?b(c+1):null},d};return b(0)},a.apply=function(a){var b=Array.prototype.slice.call(arguments,1);return function(){return a.apply(null,b.concat(Array.prototype.slice.call(arguments)))}};var s=function(a,b,c,d){var e=[];a(b,function(a,b){c(a,function(a,c){e=e.concat(c||[]),b(a)})},function(a){d(a,e)})};a.concat=j(s),a.concatSeries=l(s),a.whilst=function(b,c,d){if(b()){var e=!0;c(function(f){return f?d(f):(e?a.nextTick(function(){a.whilst(b,c,d)}):a.whilst(b,c,d),void 0)}),e=!1}else d()},a.doWhilst=function(b,c,d){var e=!0;b(function(f){return f?d(f):(c()?e?a.nextTick(function(){a.doWhilst(b,c,d)}):a.doWhilst(b,c,d):d(),void 0)}),e=!1},a.until=function(b,c,d){if(b())d();else{var e=!0;c(function(f){return f?d(f):(e?a.nextTick(function(){a.until(b,c,d)}):a.until(b,c,d),void 0)}),e=!1}},a.doUntil=function(b,c,d){var e=!0;b(function(f){return f?d(f):(c()?d():e?a.nextTick(function(){a.doUntil(b,c,d)}):a.doUntil(b,c,d),void 0)}),e=!1},a.queue=function(b,c){function f(b,d,f,g){d.constructor!==Array&&(d=[d]),e(d,function(d){var e={data:d,callback:"function"==typeof g?g:null};f?b.tasks.unshift(e):b.tasks.push(e),b.saturated&&b.tasks.length===c&&b.saturated(),a.nextTick(b.process)})}var g=0,h={tasks:[],concurrency:c,saturated:null,empty:null,drain:null,push:function(a,b){f(h,a,!1,b)},unshift:function(a,b){f(h,a,!0,b)},process:function(){if(h.concurrency>g&&h.tasks.length){var c=h.tasks.shift();h.empty&&0===h.tasks.length&&h.empty(),g+=1;var e=!0,f=function(){g-=1,c.callback&&c.callback.apply(c,arguments),h.drain&&0===h.tasks.length+g&&h.drain(),h.process()},i=d(function(){var b=arguments;e?a.nextTick(function(){f.apply(null,b)}):f.apply(null,arguments)});b(c.data,i),e=!1}},length:function(){return h.tasks.length},running:function(){return g}};return h},a.cargo=function(b,c){var d=!1,g=[],h={tasks:g,payload:c,saturated:null,empty:null,drain:null,push:function(b,d){b.constructor!==Array&&(b=[b]),e(b,function(a){g.push({data:a,callback:"function"==typeof d?d:null}),h.saturated&&g.length===c&&h.saturated()}),a.nextTick(h.process)},process:function i(){if(!d){if(0===g.length)return h.drain&&h.drain(),void 0;var a="number"==typeof c?g.splice(0,c):g.splice(0),j=f(a,function(a){return a.data});h.empty&&h.empty(),d=!0,b(j,function(){d=!1;var b=arguments;e(a,function(a){a.callback&&a.callback.apply(null,b)}),i()})}},length:function(){return g.length},running:function(){return d}};return h};var t=function(a){return function(b){var c=Array.prototype.slice.call(arguments,1);b.apply(null,c.concat([function(b){var c=Array.prototype.slice.call(arguments,1);"undefined"!=typeof console&&(b?console.error&&console.error(b):console[a]&&e(c,function(b){console[a](b)}))}]))}};a.log=t("log"),a.dir=t("dir"),a.memoize=function(a,b){var c={},d={};b=b||function(a){return a};var e=function(){var e=Array.prototype.slice.call(arguments),f=e.pop(),g=b.apply(null,e);g in c?f.apply(null,c[g]):g in d?d[g].push(f):(d[g]=[f],a.apply(null,e.concat([function(){c[g]=arguments;var a=d[g];delete d[g];for(var b=0,e=a.length;e>b;b++)a[b].apply(null,arguments)}])))};return e.memo=c,e.unmemoized=a,e},a.unmemoize=function(a){return function(){return(a.unmemoized||a).apply(null,arguments)}},a.times=function(b,c,d){for(var e=[],f=0;b>f;f++)e.push(f);return a.map(e,c,d)},a.timesSeries=function(b,c,d){for(var e=[],f=0;b>f;f++)e.push(f);return a.mapSeries(e,c,d)},"undefined"!=typeof define&&define.amd?define([],function(){return a}):"undefined"!=typeof module&&module.exports?module.exports=a:b.async=a})(); -------------------------------------------------------------------------------- /public/javascripts/lib/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @fat & @mdo 3 | * plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-alert.js 4 | * Copyright 2012 Twitter, Inc. 5 | * http://www.apache.org/licenses/LICENSE-2.0.txt 6 | */ 7 | !function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.focus().trigger("shown")}):b.$element.focus().trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('