├── .gitignore ├── MIT-LICENSE.txt ├── README.md ├── design └── redis-live.png ├── requirements.txt └── src ├── __init__.py ├── api ├── __init__.py ├── controller │ ├── BaseController.py │ ├── BaseStaticFileHandler.py │ ├── CommandsController.py │ ├── InfoController.py │ ├── MemoryController.py │ ├── ServerListController.py │ ├── TopCommandsController.py │ ├── TopKeysController.py │ └── __init__.py └── util │ ├── RDP.py │ ├── __init__.py │ ├── settings.py │ └── timeutils.py ├── dataprovider ├── __init__.py ├── dataprovider.py ├── redisprovider.py └── sqliteprovider.py ├── db ├── redislive.sqlite └── schema.sql ├── redis-live.conf.example ├── redis-live.py ├── redis-monitor.py ├── util ├── redis-fill-data.py └── redis-fill-data2.py └── www ├── images └── logo.png ├── index.html └── js ├── app.js ├── google ├── chart.js ├── chart.js~ ├── jsapi.css ├── jsapi.js └── visualization.js ├── libs ├── backbone │ └── backbone-min.js ├── bootstrap │ ├── bootstrap.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ ├── js │ │ ├── README.md │ │ ├── bootstrap-alert.js │ │ ├── bootstrap-button.js │ │ ├── bootstrap-carousel.js │ │ ├── bootstrap-collapse.js │ │ ├── bootstrap-dropdown.js │ │ ├── bootstrap-modal.js │ │ ├── bootstrap-popover.js │ │ ├── bootstrap-scrollspy.js │ │ ├── bootstrap-tab.js │ │ ├── bootstrap-tooltip.js │ │ ├── bootstrap-transition.js │ │ ├── bootstrap-typeahead.js │ │ └── tests │ │ │ ├── index.html │ │ │ ├── unit │ │ │ ├── bootstrap-alert.js │ │ │ ├── bootstrap-button.js │ │ │ ├── bootstrap-collapse.js │ │ │ ├── bootstrap-dropdown.js │ │ │ ├── bootstrap-modal.js │ │ │ ├── bootstrap-popover.js │ │ │ ├── bootstrap-scrollspy.js │ │ │ ├── bootstrap-tab.js │ │ │ ├── bootstrap-tooltip.js │ │ │ ├── bootstrap-transition.js │ │ │ └── bootstrap-typeahead.js │ │ │ └── vendor │ │ │ ├── jquery.js │ │ │ ├── qunit.css │ │ │ └── qunit.js │ └── less │ │ ├── accordion.less │ │ ├── alerts.less │ │ ├── badges.less │ │ ├── bootstrap.less │ │ ├── breadcrumbs.less │ │ ├── button-groups.less │ │ ├── buttons.less │ │ ├── carousel.less │ │ ├── close.less │ │ ├── code.less │ │ ├── component-animations.less │ │ ├── dropdowns.less │ │ ├── forms.less │ │ ├── grid.less │ │ ├── hero-unit.less │ │ ├── labels.less │ │ ├── layouts.less │ │ ├── mixins.less │ │ ├── modals.less │ │ ├── navbar.less │ │ ├── navs.less │ │ ├── pager.less │ │ ├── pagination.less │ │ ├── popovers.less │ │ ├── progress-bars.less │ │ ├── reset.less │ │ ├── responsive.less │ │ ├── scaffolding.less │ │ ├── sprites.less │ │ ├── tables.less │ │ ├── thumbnails.less │ │ ├── tooltip.less │ │ ├── type.less │ │ ├── utilities.less │ │ ├── variables.less │ │ ├── wells.less │ │ └── widget.less ├── handlebars │ └── handlebars-1.0.0.beta.6.js ├── jquery │ └── jquery-1.7.2.min.js ├── less │ └── less-1.3.0.min.js └── underscore │ └── underscore-min.js ├── models ├── commands-widget-model.js ├── info-widget-model.js ├── memory-widget-model.js ├── serverlist-model.js ├── top-commands-widget-model.js └── top-keys-widget-model.js └── views ├── base-widget-view.js ├── commands-widget-view.js ├── info-widget-view.js ├── memory-widget-view.js ├── serverlist-view.js ├── top-commands-widget-view.js └── top-keys-widget-view.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | .DS_Store 30 | 31 | *.sublime-workspace 32 | *.sublime-project 33 | src/redis-live.conf 34 | 35 | 36 | # Vim 37 | *.sw[po] 38 | venv 39 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Nitin Kumar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to 8 | do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RedisLive 2 | --------- 3 | original project and loading address: https://github.com/nkrode/RedisLive 4 | 5 | Visualize your redis instances, analyze query patterns and spikes. 6 | 7 | ![Redis Live](https://github.com/kumarnitin/RedisLive/blob/master/design/redis-live.png?raw=true "Redis Live") 8 | 9 | Setup Instructions 10 | ------------------ 11 | #### Installation 12 | 13 | Install Dependencies 14 | 15 | + [tornado](https://github.com/facebook/tornado) `pip install tornado` 16 | + [redis.py](https://github.com/andymccurdy/redis-py) `pip install redis` 17 | + [python-dateutil](http://labix.org/python-dateutil) `pip install python-dateutil` 18 | 19 | You'll also need argparse if you're running Python < 2.7: 20 | 21 | + [argparse](http://code.google.com/p/argparse/) `pip install argparse` 22 | 23 | 24 | Get RedisLive 25 | 26 | + Clone the repo `git clone https://github.com/kumarnitin/RedisLive.git` , or [download the latest release](https://github.com/kumarnitin/RedisLive/zipball/master) 27 | 28 | #### Configuration 29 | 30 | + edit redis-live.conf : 31 | + update the value of the key `RedisServers` to the redis instances you want to monitor. You can monitor multiple instances by appending more values to the RedisServers list. 32 | + update the value of the key `RedisStatsServer` to the redis instance you will use to store RedisLive data (this redis instance is different from the redis instances you are monitoring). 33 | + passwords can be added as an optional parameter for any redis instance 34 | 35 | if you don't have a spare redis instance to use to store Redis Live data, then you can configure to use sqlite by changing `"DataStoreType" : "sqlite"` 36 | 37 | #### Start RedisLive 38 | 39 | + start the monitoring script `./redis-monitor.py --duration=120` duration is in seconds (see caveat) 40 | + start the webserver `./redis-live.py` 41 | + RedisLive is now running @ `http://localhost:8888/index.html` 42 | 43 | 44 | #### Caveat on monitoring redis 45 | 46 | Currently the only hook into monitoring a redis instance is Redis [MONITOR](http://redis.io/commands/monitor) command, which streams back every command processed and reduces the throughput of the redis instance. It is recommended to run redis-monitor with --duration suitable for your redis deployment and scheduling it to run periodically as a cron job. 47 | 48 | 49 | Authors 50 | ------- 51 | 52 | **Nitin Kumar** 53 | 54 | + http://twitter.com/nkrode 55 | 56 | Contributors 57 | ------------ 58 | + [splee](https://github.com/splee) (Lee McFadden) 59 | + [bialecki](https://github.com/bialecki) (Andrew Bialecki) 60 | + [reustle](https://github.com/reustle) (Shane Reustle) 61 | + [markdube](https://github.com/markdube) (Mark Dube) 62 | + [skreuzer](https://github.com/skreuzer) (Steven Kreuzer) 63 | + [snikch](https://github.com/snikch) (Mal Curtis) 64 | + [quiver](https://github.com/quiver) (George) 65 | 66 | License 67 | ------- 68 | RedisLive is released under the MIT license: 69 | + www.opensource.org/licenses/MIT 70 | -------------------------------------------------------------------------------- /design/redis-live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/design/redis-live.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argparse==1.2.1 2 | python-dateutil==1.5 3 | redis 4 | tornado==2.1.1 5 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/__init__.py -------------------------------------------------------------------------------- /src/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/api/__init__.py -------------------------------------------------------------------------------- /src/api/controller/BaseController.py: -------------------------------------------------------------------------------- 1 | from dataprovider.dataprovider import RedisLiveDataProvider 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | 6 | 7 | class BaseController(tornado.web.RequestHandler): 8 | 9 | stats_provider = RedisLiveDataProvider.get_provider() 10 | 11 | def datetime_to_list(self, datetime): 12 | """Converts a datetime to a list. 13 | 14 | Args: 15 | datetime (datetime): The datetime to convert. 16 | """ 17 | parsed_date = dateutil.parser.parse(datetime) 18 | # don't return the last two fields, we don't want them. 19 | return tuple(parsed_date.timetuple())[:-2] 20 | 21 | # todo : fix this 22 | def average_data(self, data): 23 | """Averages data. 24 | 25 | TODO: More docstring here, once functionality is understood. 26 | """ 27 | average = [] 28 | 29 | deviation=1024*1024 30 | 31 | start = dateutil.parser.parse(data[0][0]) 32 | end = dateutil.parser.parse(data[-1][0]) 33 | difference = end - start 34 | weeks, days = divmod(difference.days, 7) 35 | minutes, seconds = divmod(difference.seconds, 60) 36 | hours, minutes = divmod(minutes, 60) 37 | 38 | # TODO: These if/elif/else branches chould probably be broken out into 39 | # individual functions to make it easier to follow what's going on. 40 | if difference.days > 0: 41 | current_max = 0 42 | current_current = 0 43 | current_d = 0 44 | 45 | for dt, max_memory, current_memory in data: 46 | d = dateutil.parser.parse(dt) 47 | if d.day != current_d: 48 | current_d = d.day 49 | average.append([dt, max_memory, current_memory]) 50 | current_max = max_memory 51 | current_current = current_memory 52 | else: 53 | if max_memory > current_max or \ 54 | current_memory > current_current: 55 | average.pop() 56 | average.append([dt, max_memory, current_memory]) 57 | current_max=max_memory 58 | current_current=current_memory 59 | elif hours > 0: 60 | current_max = 0 61 | current_current = 0 62 | current = -1 63 | keep_flag = False 64 | 65 | for dt, max_memory, current_memory in data: 66 | d = dateutil.parser.parse(dt) 67 | if d.hour != current: 68 | current = d.hour 69 | average.append([dt, max_memory, current_memory]) 70 | current_max=max_memory 71 | current_current=current_memory 72 | keep_flag=False 73 | elif abs(max_memory - current_max) > deviation or \ 74 | abs(current_memory - current_current) > deviation: 75 | #average.pop() 76 | average.append([dt, max_memory, current_memory]) 77 | current_max = max_memory 78 | current_current = current_memory 79 | keep_flag = True 80 | elif max_memory > current_max or \ 81 | current_memory > current_current: 82 | if keep_flag != True: 83 | average.pop() 84 | average.append([dt, max_memory, current_memory]) 85 | current_max = max_memory 86 | current_current = current_memory 87 | keep_flag = False 88 | else: 89 | current_max = 0 90 | current_current = 0 91 | current_m = -1 92 | keep_flag = False 93 | for dt, max_memory, current_memory in data: 94 | d = dateutil.parser.parse(dt) 95 | if d.minute != current_m: 96 | current_m = d.minute 97 | average.append([dt, max_memory, current_memory]) 98 | current_max = max_memory 99 | current_current = current_memory 100 | keep_flag = False 101 | elif abs(max_memory - current_max) > deviation or \ 102 | abs(current_memory - current_current) > deviation: 103 | #average.pop() 104 | average.append([dt, max_memory, current_memory]) 105 | current_max = max_memory 106 | current_current = current_memory 107 | keep_flag = True 108 | elif max_memory > current_max or \ 109 | current_memory > current_current: 110 | if keep_flag!=True: 111 | average.pop() 112 | average.append([dt,max_memory,current_memory]) 113 | current_max=max_memory 114 | current_current=current_memory 115 | keep_flag=False 116 | 117 | return average 118 | -------------------------------------------------------------------------------- /src/api/controller/BaseStaticFileHandler.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | 3 | class BaseStaticFileHandler(tornado.web.StaticFileHandler): 4 | def compute_etag(self): 5 | return None 6 | 7 | def get_cache_time(self, path, modified, mime_type): 8 | return None 9 | 10 | -------------------------------------------------------------------------------- /src/api/controller/CommandsController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | from datetime import datetime, timedelta 6 | 7 | 8 | class CommandsController(BaseController): 9 | 10 | def get(self): 11 | """Serves a GET request. 12 | """ 13 | return_data = dict(data=[], timestamp=datetime.now().isoformat()) 14 | 15 | server = self.get_argument("server") 16 | from_date = self.get_argument("from", None) 17 | to_date = self.get_argument("to", None) 18 | 19 | if not from_date or not to_date: 20 | end = datetime.now() 21 | delta = timedelta(seconds=120) 22 | start = end - delta 23 | else: 24 | start = dateutil.parser.parse(from_date) 25 | end = dateutil.parser.parse(to_date) 26 | 27 | difference = end - start 28 | # added to support python version < 2.7, otherwise timedelta has 29 | # total_seconds() 30 | difference_total_seconds = difference.days * 24 * 3600 31 | difference_total_seconds += difference.seconds 32 | difference_total_seconds += difference.microseconds / 1e6 33 | 34 | minutes = difference_total_seconds / 60 35 | hours = minutes / 60 36 | seconds = difference_total_seconds 37 | 38 | if hours > 120: 39 | group_by = "day" 40 | elif minutes > 120: 41 | group_by = "hour" 42 | elif seconds > 120: 43 | group_by = "minute" 44 | else: 45 | group_by = "second" 46 | 47 | combined_data = [] 48 | stats = self.stats_provider.get_command_stats(server, start, end, 49 | group_by) 50 | for data in stats: 51 | combined_data.append([data[1], data[0]]) 52 | 53 | for data in combined_data: 54 | return_data['data'].append([self.datetime_to_list(data[0]), data[1]]) 55 | 56 | self.write(return_data) 57 | -------------------------------------------------------------------------------- /src/api/controller/InfoController.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from BaseController import BaseController 3 | import tornado.ioloop 4 | import tornado.web 5 | import re 6 | 7 | 8 | class InfoController(BaseController): 9 | def get(self): 10 | """Serves a GET request. 11 | """ 12 | server = self.get_argument("server") 13 | redis_info = self.stats_provider.get_info(server) 14 | databases=[] 15 | 16 | for key in sorted(redis_info.keys()): 17 | if key.startswith("db"): 18 | database = redis_info[key] 19 | database['name']=key 20 | databases.append(database) 21 | 22 | total_keys=0 23 | for database in databases: 24 | total_keys+=database.get("keys") 25 | 26 | if(total_keys==0): 27 | databases=[{"name" : "db0", "keys" : "0", "expires" : "0"}] 28 | 29 | redis_info['databases'] = databases 30 | redis_info['total_keys']= self.shorten_number(total_keys) 31 | 32 | uptime_seconds = redis_info['uptime_in_seconds'] 33 | redis_info['uptime'] = self.shorten_time(uptime_seconds) 34 | 35 | commands_processed = redis_info['total_commands_processed'] 36 | commands_processed = self.shorten_number(commands_processed) 37 | redis_info['total_commands_processed_human'] = commands_processed 38 | 39 | self.write(redis_info) 40 | 41 | def shorten_time(self, seconds): 42 | """Takes an integer number of seconds and rounds it to a human readable 43 | format. 44 | 45 | Args: 46 | seconds (int): Number of seconds to convert. 47 | """ 48 | if seconds < 60: 49 | # less than 1 minute 50 | val = str(seconds) + " sec" 51 | elif seconds < 3600: 52 | # if the seconds is less than 1hr 53 | num = self.rounded_number(seconds, 60) 54 | if num == "60": 55 | val = '1h' 56 | else: 57 | val = num + "m" 58 | elif (seconds < 60*60*24): 59 | # if the number is less than 1 day 60 | num = self.rounded_number(seconds, 60 * 60) 61 | if num == "24": 62 | val = "1d" 63 | else: 64 | val = num + "h" 65 | else: 66 | num = self.rounded_number(seconds, 60*60*24) 67 | val = num + "d" 68 | 69 | return val 70 | 71 | def shorten_number(self, number): 72 | """Shortens a number to a human readable format. 73 | 74 | Args: 75 | number (int): Number to convert. 76 | """ 77 | if number < 1000: 78 | return number 79 | elif number >= 1000 and number < 1000000: 80 | num = self.rounded_number(number, 1000) 81 | val = "1M" if num == "1000" else num + "K" 82 | return val 83 | elif number >= 1000000 and number < 1000000000: 84 | num = self.rounded_number(number, 1000000) 85 | val = "1B" if num=="1000" else num + "M" 86 | return val 87 | elif number >= 1000000000 and number < 1000000000000: 88 | num = self.rounded_number(number, 1000000000) 89 | val = "1T" if num=="1000" else num + "B" 90 | return val 91 | else: 92 | num = self.rounded_number(number, 1000000000000) 93 | return num + "T" 94 | 95 | def rounded_number(self, number, denominator): 96 | """Rounds a number. 97 | 98 | Args: 99 | number (int|float): The number to round. 100 | denominator (int): The denominator. 101 | """ 102 | rounded = str(round(Decimal(number)/Decimal(denominator), 1)) 103 | replace_trailing_zero = re.compile('0$') 104 | no_trailing_zeros = replace_trailing_zero.sub('', rounded) 105 | replace_trailing_period = re.compile('\.$') 106 | final_number = replace_trailing_period.sub('', no_trailing_zeros) 107 | return final_number 108 | -------------------------------------------------------------------------------- /src/api/controller/MemoryController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import datetime 6 | 7 | 8 | class MemoryController(BaseController): 9 | 10 | def get(self): 11 | server = self.get_argument("server") 12 | from_date = self.get_argument("from", None) 13 | to_date = self.get_argument("to", None) 14 | 15 | return_data = dict(data=[], 16 | timestamp=datetime.datetime.now().isoformat()) 17 | 18 | if not from_date or not to_date: 19 | end = datetime.datetime.now() 20 | delta = datetime.timedelta(seconds=60) 21 | start = end - delta 22 | else: 23 | start = dateutil.parser.parse(from_date) 24 | end = dateutil.parser.parse(to_date) 25 | 26 | combined_data = [] 27 | # TODO: These variables aren't currently used; should they be removed? 28 | prev_max=0 29 | prev_current=0 30 | counter=0 31 | 32 | for data in self.stats_provider.get_memory_info(server, start, end): 33 | combined_data.append([data[0], data[1], data[2]]) 34 | 35 | for data in combined_data: 36 | d = [self.datetime_to_list(data[0]), data[1], data[2]] 37 | return_data['data'].append(d) 38 | 39 | self.write(return_data) 40 | 41 | -------------------------------------------------------------------------------- /src/api/controller/ServerListController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | from api.util import settings 3 | 4 | class ServerListController(BaseController): 5 | 6 | def get(self): 7 | servers = {"servers": self.read_server_config()} 8 | self.write(servers) 9 | 10 | def read_server_config(self): 11 | """Returns a list of servers with the 'id' field added. 12 | """ 13 | # TODO: Move this into the settings module so everything benefits. 14 | server_list = [] 15 | redis_servers = settings.get_redis_servers() 16 | 17 | for server in redis_servers: 18 | if 'password' not in server: 19 | server['password'] = None 20 | 21 | server_id = "%(server)s:%(port)s" % server 22 | s = dict(server=server['server'], port=server['port'], password=server['password'], id=server_id) 23 | server_list.append(s) 24 | 25 | return server_list 26 | -------------------------------------------------------------------------------- /src/api/controller/TopCommandsController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import datetime 6 | 7 | 8 | class TopCommandsController(BaseController): 9 | 10 | def get(self): 11 | return_data = dict(data=[], 12 | timestamp=datetime.datetime.now().isoformat()) 13 | 14 | server = self.get_argument("server") 15 | from_date = self.get_argument("from", None) 16 | to_date = self.get_argument("to", None) 17 | 18 | if not from_date or not to_date: 19 | end = datetime.datetime.now() 20 | delta = datetime.timedelta(seconds=120) 21 | start = end - delta 22 | else: 23 | start = dateutil.parser.parse(from_date) 24 | end = dateutil.parser.parse(to_date) 25 | 26 | for data in self.stats_provider.get_top_commands_stats(server, start, 27 | end): 28 | return_data['data'].append([data[0], data[1]]) 29 | 30 | self.write(return_data) 31 | -------------------------------------------------------------------------------- /src/api/controller/TopKeysController.py: -------------------------------------------------------------------------------- 1 | from BaseController import BaseController 2 | import tornado.ioloop 3 | import tornado.web 4 | import dateutil.parser 5 | import datetime 6 | 7 | 8 | class TopKeysController(BaseController): 9 | 10 | def get(self): 11 | return_data = dict(data=[], timestamp=datetime.datetime.now().isoformat()) 12 | 13 | server = self.get_argument("server") 14 | from_date = self.get_argument("from", None) 15 | to_date = self.get_argument("to", None) 16 | 17 | if not from_date or not to_date: 18 | end = datetime.datetime.now() 19 | delta = datetime.timedelta(seconds=120) 20 | start = end - delta 21 | else: 22 | start = dateutil.parser.parse(from_date) 23 | end = dateutil.parser.parse(to_date) 24 | 25 | for data in self.stats_provider.get_top_keys_stats(server, start, end): 26 | return_data['data'].append([data[0], data[1]]) 27 | 28 | self.write(return_data) 29 | -------------------------------------------------------------------------------- /src/api/controller/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/api/controller/__init__.py -------------------------------------------------------------------------------- /src/api/util/RDP.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Ramer-Douglas-Peucker algorithm roughly ported from the pseudo-code provided 3 | by http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm 4 | """ 5 | 6 | from math import sqrt 7 | 8 | def distance(a, b): 9 | return sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) 10 | 11 | def point_line_distance(point, start, end): 12 | if (start == end): 13 | return distance(point, start) 14 | else: 15 | n = abs( 16 | (end[0] - start[0]) * (start[1] - point[1]) - (start[0] - point[0]) * (end[1] - start[1]) 17 | ) 18 | d = sqrt( 19 | (end[0] - start[0]) ** 2 + (end[1] - start[1]) ** 2 20 | ) 21 | return n / d 22 | 23 | def rdp(points, epsilon): 24 | """ 25 | Reduces a series of points to a simplified version that loses detail, but 26 | maintains the general shape of the series. 27 | """ 28 | dmax = 0.0 29 | index = 0 30 | for i in range(1, len(points) - 1): 31 | d = point_line_distance(points[i], points[0], points[-1]) 32 | if d > dmax: 33 | index = i 34 | dmax = d 35 | if dmax >= epsilon: 36 | results = rdp(points[:index+1], epsilon)[:-1] + rdp(points[index:], epsilon) 37 | else: 38 | results = [points[0], points[-1]] 39 | return results -------------------------------------------------------------------------------- /src/api/util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/api/util/__init__.py -------------------------------------------------------------------------------- /src/api/util/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import os 3 | import json 4 | 5 | 6 | def get_settings(): 7 | """Parses the settings from redis-live.conf. 8 | """ 9 | # TODO: Consider YAML. Human writable, machine readable. 10 | with open(os.getenv("REDISLIVE_CONFIG", "redis-live.conf")) as config: 11 | return json.load(config) 12 | 13 | 14 | def get_redis_servers(): 15 | config = get_settings() 16 | return config["RedisServers"] 17 | 18 | 19 | def get_redis_stats_server(): 20 | config = get_settings() 21 | return config["RedisStatsServer"] 22 | 23 | 24 | def get_data_store_type(): 25 | config = get_settings() 26 | return config["DataStoreType"] 27 | 28 | 29 | def get_sqlite_stats_store(): 30 | config = get_settings() 31 | return config["SqliteStatsStore"] 32 | -------------------------------------------------------------------------------- /src/api/util/timeutils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | # Original fix for Py2.6: https://github.com/mozilla/mozdownload/issues/73 3 | def total_seconds(td): 4 | # Keep backward compatibility with Python 2.6 which doesn't have 5 | # this method 6 | if hasattr(td, 'total_seconds'): 7 | return td.total_seconds() 8 | else: 9 | return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 10 | 11 | def convert_to_epoch(timestamp): 12 | if (type(timestamp) is datetime.date): 13 | timestamp = datetime.datetime.fromordinal(timestamp.toordinal()) 14 | timestamp = timestamp.replace(tzinfo=None) 15 | diff = (timestamp - datetime.datetime(1970, 1, 1)) 16 | seconds = int(total_seconds(diff)) 17 | return seconds 18 | -------------------------------------------------------------------------------- /src/dataprovider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/dataprovider/__init__.py -------------------------------------------------------------------------------- /src/dataprovider/dataprovider.py: -------------------------------------------------------------------------------- 1 | from api.util import settings 2 | import sqliteprovider 3 | import redisprovider 4 | 5 | 6 | # TODO: Confirm there's not some implementation detail I've missed, then 7 | # ditch the classes here. 8 | class RedisLiveDataProvider(object): 9 | 10 | @staticmethod 11 | def get_provider(): 12 | """Returns a data provider based on the settings file. 13 | 14 | Valid providers are currently Redis and SQLite. 15 | """ 16 | data_store_type = settings.get_data_store_type() 17 | 18 | # FIXME: Should use a global variable for "redis" here. 19 | if data_store_type == "redis": 20 | return redisprovider.RedisStatsProvider() 21 | else: 22 | return sqliteprovider.RedisStatsProvider() 23 | -------------------------------------------------------------------------------- /src/dataprovider/sqliteprovider.py: -------------------------------------------------------------------------------- 1 | from api.util import settings 2 | import contextlib 3 | import sqlite3 4 | import json 5 | 6 | class RedisStatsProvider(object): 7 | """A Sqlite based persistance to store and fetch stats 8 | """ 9 | 10 | def __init__(self): 11 | stats = settings.get_sqlite_stats_store() 12 | self.location = stats.get('path', 'db/redislive.sqlite') 13 | self.conn = sqlite3.connect(self.location) 14 | self.retries = 10 15 | 16 | def save_memory_info(self, server, timestamp, used, peak): 17 | """Saves used and peak memory stats, 18 | 19 | Args: 20 | server (str): The server ID 21 | timestamp (datetime): The time of the info. 22 | used (int): Used memory value. 23 | peak (int): Peak memory value. 24 | """ 25 | query = "INSERT INTO memory VALUES (?, ?, ?, ?);" 26 | values = (timestamp.strftime('%Y-%m-%d %H:%M:%S'), used, peak, server) 27 | self._retry_query(query, values) 28 | 29 | def save_info_command(self, server, timestamp, info): 30 | """Save Redis info command dump 31 | 32 | Args: 33 | server (str): id of server 34 | timestamp (datetime): Timestamp. 35 | info (dict): The result of a Redis INFO command. 36 | """ 37 | query = "INSERT INTO info VALUES (?, ?, ?);" 38 | values = (timestamp.strftime('%Y-%m-%d %H:%M:%S'), json.dumps(info), 39 | server) 40 | self._retry_query(query, values) 41 | 42 | def save_monitor_command(self, server, timestamp, command, keyname, 43 | argument): 44 | """save information about every command 45 | 46 | Args: 47 | server (str): Server ID 48 | timestamp (datetime): Timestamp. 49 | command (str): The Redis command used. 50 | keyname (str): The key the command acted on. 51 | argument (str): The args sent to the command. 52 | """ 53 | # FIXME: why clear the argument here? 54 | argument = "" 55 | 56 | query = "INSERT INTO monitor " 57 | query += "(datetime, command, keyname, arguments, server) " 58 | query += "VALUES " 59 | query += "(?, ?, ?, ?, ?);" 60 | 61 | values = (timestamp.strftime('%Y-%m-%d %H:%M:%S'), command, keyname, 62 | argument, server) 63 | 64 | self._retry_query(query, values) 65 | 66 | def get_info(self, server): 67 | """Get info about the server 68 | 69 | Args: 70 | server (str): The server ID 71 | """ 72 | with contextlib.closing(self.conn.cursor()) as c: 73 | query = "SELECT info FROM info WHERE server=?" 74 | query += "ORDER BY datetime DESC LIMIT 1;" 75 | for r in c.execute(query, (server,)): 76 | return(json.loads(r[0])) 77 | 78 | def get_memory_info(self, server, from_date, to_date): 79 | """Get stats for Memory Consumption between a range of dates 80 | 81 | Args: 82 | server (str): The server ID 83 | from_date (datetime): Get memory info from this date onwards. 84 | to_date (datetime): Get memory info up to this date. 85 | """ 86 | time_fmt = '%Y-%m-%d %H:%M:%S' 87 | query = """SELECT strftime('%Y-%m-%d %H:%M:%S', datetime), max, current 88 | FROM memory 89 | WHERE datetime >= ? 90 | AND datetime <= ? 91 | AND server = ?;""" 92 | 93 | values = (from_date.strftime(time_fmt), to_date.strftime(time_fmt), 94 | server) 95 | 96 | with contextlib.closing(self.conn.cursor()) as c: 97 | return [[r[0], r[1], r[2]] for r in c.execute(query, values)] 98 | 99 | def get_command_stats(self, server, from_date, to_date, group_by): 100 | """Get total commands processed in the given time period 101 | 102 | Args: 103 | server (str): The server ID 104 | from_date (datetime): Get data from this date. 105 | to_date (datetime): Get data to this date. 106 | group_by (str): How to group the stats. 107 | """ 108 | time_fmt = '%Y-%m-%d %H:%M:%S' 109 | 110 | sql = """SELECT COUNT(*) AS total, strftime('%s', datetime) 111 | FROM monitor 112 | WHERE datetime >= ? 113 | AND datetime <= ? 114 | AND server = ? 115 | GROUP BY strftime('%s', datetime) 116 | ORDER BY datetime DESC;""" 117 | 118 | values = (from_date.strftime(time_fmt), to_date.strftime(time_fmt), 119 | server) 120 | 121 | if group_by == "day": 122 | query_time_fmt = '%Y-%m-%d' 123 | elif group_by == "hour": 124 | query_time_fmt = '%Y-%m-%d %H' 125 | elif group_by=="minute": 126 | query_time_fmt = '%Y-%m-%d %H:%M' 127 | else: 128 | query_time_fmt = '%Y-%m-%d %H:%M:%S' 129 | 130 | query = sql % (query_time_fmt, query_time_fmt) 131 | 132 | with contextlib.closing(self.conn.cursor()) as c: 133 | mem_data = [[r[0], r[1]] for r in c.execute(query, values)] 134 | return reversed(mem_data) 135 | 136 | def get_top_commands_stats(self, server, from_date, to_date): 137 | """Get top commands processed in the given time period 138 | 139 | Args: 140 | server (str): Server ID 141 | from_date (datetime): Get stats from this date. 142 | to_date (datetime): Get stats to this date. 143 | """ 144 | time_fmt = '%Y-%m-%d %H:%M:%S' 145 | query = """SELECT command, COUNT(*) AS total 146 | FROM monitor 147 | WHERE datetime >= ? 148 | AND datetime <= ? 149 | AND server = ? 150 | GROUP BY command 151 | ORDER BY total;""" 152 | values = (from_date.strftime(time_fmt), to_date.strftime(time_fmt), 153 | server) 154 | 155 | with contextlib.closing(self.conn.cursor()) as c: 156 | return [[r[0], r[1]] for r in c.execute(query, values)] 157 | 158 | def get_top_keys_stats(self, server, from_date, to_date): 159 | """Gets top comm processed 160 | 161 | Args: 162 | server (str): Server ID 163 | from_date (datetime): Get stats from this date. 164 | to_date (datetime): Get stats to this date. 165 | """ 166 | time_fmt = '%Y-%m-%d %H:%M:%S' 167 | query = """SELECT keyname, COUNT(*) AS total 168 | FROM monitor 169 | WHERE datetime >= ? 170 | AND datetime <= ? 171 | AND server = ? 172 | GROUP BY keyname ORDER BY total DESC 173 | LIMIT 10;""" 174 | values = (from_date.strftime(time_fmt), to_date.strftime(time_fmt), 175 | server) 176 | 177 | with contextlib.closing(self.conn.cursor()) as c: 178 | return [[r[0], r[1]] for r in c.execute(query, values)] 179 | 180 | def _retry_query(self, query, values=None): 181 | """Run a SQLite query until it sticks or until we reach the max number 182 | of retries. Single-threaded writes :( 183 | 184 | Args: 185 | query (str): The query to execute. 186 | 187 | Kwargs: 188 | values (tuple|dict): Used when the query is parameterized. 189 | """ 190 | with contextlib.closing(self.conn.cursor()) as cursor: 191 | completed = False 192 | counter = 0 193 | while counter < self.retries and not completed: 194 | counter += 1 195 | try: 196 | cursor.execute(query, values) 197 | self.conn.commit() 198 | completed = True 199 | except Exception: 200 | # FIXME: Catch specific exceptions here otherwise it's likely to 201 | # mask bugs/issues later. 202 | pass 203 | -------------------------------------------------------------------------------- /src/db/redislive.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/db/redislive.sqlite -------------------------------------------------------------------------------- /src/db/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE info(datetime text,info text, server text); 2 | 3 | CREATE TABLE keys(datetime text, expire number, persist number, server text); 4 | 5 | CREATE TABLE memory(datetime text,current real,max real, server text); 6 | 7 | CREATE TABLE "monitor" ("datetime" datetime,"command" text,"arguments" text,"server" text, keyname text); 8 | 9 | CREATE INDEX "monitor_dateTime_Index" ON "monitor" ("datetime" DESC); 10 | 11 | CREATE INDEX serverIndex on monitor(server); -------------------------------------------------------------------------------- /src/redis-live.conf.example: -------------------------------------------------------------------------------- 1 | { 2 | "RedisServers": 3 | [ 4 | { 5 | "server": "154.17.59.99", 6 | "port" : 6379 7 | }, 8 | 9 | { 10 | "server": "localhost", 11 | "port" : 6380, 12 | "password" : "some-password" 13 | } 14 | ], 15 | 16 | "DataStoreType" : "redis", 17 | 18 | "RedisStatsServer": 19 | { 20 | "server" : "ec2-184-72-166-144.compute-1.amazonaws.com", 21 | "port" : 6385 22 | }, 23 | 24 | "SqliteStatsStore" : 25 | { 26 | "path": "to your sql lite file" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/redis-live.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import tornado.ioloop 4 | import tornado.options 5 | from tornado.options import define, options 6 | import tornado.web 7 | 8 | from api.controller.BaseStaticFileHandler import BaseStaticFileHandler 9 | 10 | from api.controller.ServerListController import ServerListController 11 | from api.controller.InfoController import InfoController 12 | from api.controller.MemoryController import MemoryController 13 | from api.controller.CommandsController import CommandsController 14 | from api.controller.TopCommandsController import TopCommandsController 15 | from api.controller.TopKeysController import TopKeysController 16 | 17 | if __name__ == "__main__": 18 | define("port", default=8888, help="run on the given port", type=int) 19 | define("debug", default=0, help="debug mode", type=int) 20 | tornado.options.parse_command_line() 21 | 22 | # Bootup 23 | handlers = [ 24 | (r"/api/servers", ServerListController), 25 | (r"/api/info", InfoController), 26 | (r"/api/memory", MemoryController), 27 | (r"/api/commands", CommandsController), 28 | (r"/api/topcommands", TopCommandsController), 29 | (r"/api/topkeys", TopKeysController), 30 | (r"/(.*)", BaseStaticFileHandler, {"path": "www", "default_filename": "index.html"}) 31 | ] 32 | 33 | server_settings = {'debug': options.debug} 34 | application = tornado.web.Application(handlers, **server_settings) 35 | application.listen(options.port) 36 | tornado.ioloop.IOLoop.instance().start() 37 | -------------------------------------------------------------------------------- /src/util/redis-fill-data.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import redis 4 | from time import strftime 5 | import time 6 | import random 7 | 8 | def monitor(): 9 | redisHost = "127.0.0.1" 10 | redisPort = 6379 11 | redisClient = redis.StrictRedis(host=redisHost, port=redisPort, db=0) 12 | 13 | while True: 14 | 15 | x = random.randint(1, 100) 16 | y = random.randint(1, 20) 17 | 18 | if y==1: 19 | for z in xrange(1,x): 20 | redisClient.set("Key:" + `x`, x) 21 | elif y==2: 22 | for z in xrange(1,x): 23 | redisClient.get("Key:" + `x`) 24 | elif y==4: 25 | for z in xrange(1,x): 26 | redisClient.hset("HashKey:" + `x`, x, x) 27 | elif y==5: 28 | for z in xrange(1,(x/2)+2): 29 | redisClient.setex("Key:" + `x`, 1000, x) 30 | elif y==6: 31 | for z in xrange(1,x): 32 | redisClient.hexists("HashKey:" + `x`, y) 33 | elif y==7: 34 | for z in xrange(1,x): 35 | redisClient.setbit("BitSet:" + `x`, 1, 1) 36 | elif y==8: 37 | for z in xrange(1,x): 38 | redisClient.getbit("BitSet:" + `x`, 1) 39 | elif y==9: 40 | for z in xrange(1,x): 41 | redisClient.expire("Key:"+ `x`, 2000) 42 | elif y==11: 43 | redisClient.flushall() 44 | 45 | 46 | 47 | if __name__ == '__main__': 48 | monitor() -------------------------------------------------------------------------------- /src/util/redis-fill-data2.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import redis 4 | from time import strftime 5 | import time 6 | import random 7 | 8 | def monitor(): 9 | redisHost = "127.0.0.1" 10 | redisPort = 6381 11 | redisClient = redis.StrictRedis(host=redisHost, port=redisPort, db=0) 12 | 13 | while True: 14 | x=1 15 | redisClient.set("Key:" + `x`, x) 16 | redisClient.set("KeyYU:" + `x`, x) 17 | redisClient.set("Key:" + `x`, x) 18 | redisClient.set("KeyYU:" + `x`, x) 19 | 20 | if __name__ == '__main__': 21 | monitor() -------------------------------------------------------------------------------- /src/www/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/www/images/logo.png -------------------------------------------------------------------------------- /src/www/js/app.js: -------------------------------------------------------------------------------- 1 | /* Main App 2 | * ====================== */ 3 | 4 | var App = { 5 | 6 | init: function() { 7 | 8 | this.RegisterPartials() 9 | this.RegisterHelpers() 10 | 11 | var ServerDropDown = new ServerList({ 12 | el : $("#server-list") 13 | , model : new ServerListModel() 14 | }) 15 | 16 | var infoWidget = new InfoWidget({ 17 | el : $("#info-widget-placeholder") 18 | , model : new InfoWidgetModel() 19 | }) 20 | 21 | var memoryWidget = new MemoryWidget({ 22 | el : $("#memory-widget-placeholder") 23 | , model : new MemoryWidgetModel() 24 | }) 25 | 26 | var commandsWidget = new CommandsWidget({ 27 | el : $("#commands-widget-placeholder") 28 | , model : new CommandsWidgetModel() 29 | }) 30 | 31 | var topCommandsWidget = new TopCommandsWidget({ 32 | el : $("#top-commands-widget-placeholder") 33 | , model : new TopCommandsWidgetModel() 34 | }) 35 | 36 | var topKeysWidget = new TopKeysWidget({ 37 | el : $("#top-keys-widget-placeholder") 38 | , model : new TopKeysWidgetModel() 39 | }) 40 | } 41 | 42 | , RegisterPartials : function(){ 43 | 44 | Handlebars.registerPartial("date-dropdown", $("#date-dropdown-template").html()); 45 | 46 | } 47 | 48 | , RegisterHelpers : function(){ 49 | 50 | Handlebars.registerHelper('hash', function ( context, options ) { 51 | 52 | var ret = "" 53 | , counter = 0 54 | 55 | $.each(context, function ( key, value ) { 56 | 57 | if (typeof value != "object") { 58 | obj = { "key" : key, "value" : value , "index" : counter++ } 59 | ret = ret + options.fn(obj) 60 | } 61 | 62 | }) 63 | 64 | return ret 65 | }) 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/www/js/google/jsapi.css: -------------------------------------------------------------------------------- 1 | 2 | .google-visualization-toolbar{font-size:100%}.google-visualization-toolbar .google-visualization-toolbar-export-igoogle,.google-visualization-toolbar .google-visualization-toolbar-export-data,.google-visualization-toolbar .google-visualization-toolbar-html-code{margin-right:.1em}.google-visualization-toolbar-html-code-explanation{font-weight:bold}.google-visualization-toolbar-ok-button{padding:2px}.google-visualization-toolbar-triangle{position:absolute;right:0;top:0}.google-visualization-toolbar-caption-table{width:100%;padding:0;margin:0;border:0;border-collapse:collapse}.google-visualization-toolbar-small-dialog{width:500px}.google-visualization-toolbar-big-dialog{width:800px}.google-visualization-toolbar-small-dialog,.google-visualization-toolbar-big-dialog{position:absolute;background-color:#c1d9ff;border:1px solid #3a5774;padding:8px}.google-visualization-toolbar-small-dialog-bg,.google-visualization-toolbar-big-dialog-bg{background-color:#ddd;position:absolute;top:0;left:0}.google-visualization-toolbar-small-dialog-title,.google-visualization-toolbar-big-dialog-title{background-color:#e0edfe;color:#000;cursor:pointer;padding:8px;position:relative;font-size:12pt;font-weight:bold;vertical-align:middle}.google-visualization-toolbar-small-dialog-content,.google-visualization-toolbar-big-dialog-content{background-color:#fff;padding:4px;font-weight:normal;overflow:auto}.google-visualization-toolbar-small-dialog-title-close,.google-visualization-toolbar-big-dialog-title-close{background:transparent url(close_box.gif) no-repeat scroll center;height:15px;position:absolute;right:10px;top:8px;width:15px}.google-visualization-toolbar-small-dialog-content iframe,.google-visualization-toolbar-big-dialog-content iframe{width:500px;height:700px;border:1px solid black}.charts-inline-block{position:relative;display:-moz-inline-box;display:inline-block}* html .charts-inline-block,*:first-child+html .charts-inline-block{display:inline}.charts-menu{background:#fff;border-color:#ccc #666 #666 #ccc;border-style:solid;border-width:1px;cursor:default;font:normal 13px Arial,sans-serif;margin:0;outline:none;padding:4px 0;position:absolute;z-index:20000}.charts-menu-button{background:#ddd url(//ssl.gstatic.com/editor/button-bg.png) repeat-x top left;border:0;color:#000;cursor:pointer;list-style:none;margin:2px;outline:none;padding:0;text-decoration:none;vertical-align:middle}.charts-menu-button-outer-box,.charts-menu-button-inner-box{border-style:solid;border-color:#aaa;vertical-align:top}.charts-menu-button-outer-box{margin:0;border-width:1px 0;padding:0}.charts-menu-button-inner-box{margin:0 -1px;border-width:0 1px;padding:3px 4px}* html .charts-menu-button-inner-box{left:-1px}* html .charts-menu-button-rtl .charts-menu-button-outer-box{left:-1px;right:auto}* html .charts-menu-button-rtl .charts-menu-button-inner-box{right:auto}*:first-child+html .charts-menu-button-inner-box{left:-1px}*:first-child+html .charts-menu-button-rtl .charts-menu-button-inner-box{left:1px;right:auto}::root .charts-menu-button{line-height:0}::root .charts-menu-button-outer-box{line-height:0}::root .charts-menu-button-inner-box{line-height:0}::root .charts-menu-button-caption{line-height:normal}::root .charts-menu-button-dropdown{line-height:normal}.charts-menu-button-disabled{background-image:none!important;opacity:.3;-moz-opacity:.3;filter:alpha(opacity=30)}.charts-menu-button-disabled .charts-menu-button-outer-box,.charts-menu-button-disabled .charts-menu-button-inner-box,.charts-menu-button-disabled .charts-menu-button-caption,.charts-menu-button-disabled .charts-menu-button-dropdown{color:#333!important;border-color:#999!important}* html .charts-menu-button-disabled,*:first-child+html .charts-menu-button-disabled{margin:2px 1px!important;padding:0 1px!important}.charts-menu-button-hover .charts-menu-button-outer-box,.charts-menu-button-hover .charts-menu-button-inner-box{border-color:#9cf #69e #69e #7af!important}.charts-menu-button-active,.charts-menu-button-open{background-color:#bbb;background-position:bottom left}.charts-menu-button-focused .charts-menu-button-outer-box,.charts-menu-button-focused .charts-menu-button-inner-box{border-color:orange}.charts-menu-button-caption{padding:0 4px 0 0;vertical-align:top}.charts-menu-button-dropdown{height:15px;width:7px;background:url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -388px 0;vertical-align:top}.charts-menu-button-collapse-right,.charts-menu-button-collapse-right .charts-menu-button-outer-box,.charts-menu-button-collapse-right .charts-menu-button-inner-box{margin-right:0}.charts-menu-button-collapse-left,.charts-menu-button-collapse-left .charts-menu-button-outer-box{margin-left:0}.charts-menu-button-collapse-left .charts-menu-button-inner-box{margin-left:0;border-left:1px solid #fff}.charts-menu-button-collapse-left.charts-menu-button-checked .charts-menu-button-inner-box{border-left:1px solid #ddd}.charts-menuitem{color:#000;font:normal 13px Arial,sans-serif;list-style:none;margin:0;padding:4px 7em 4px 28px;white-space:nowrap}.charts-menuitem.charts-menuitem-rtl{padding-left:7em;padding-right:28px}.charts-menu-nocheckbox .charts-menuitem,.charts-menu-noicon .charts-menuitem{padding-left:12px}.charts-menu-noaccel .charts-menuitem{padding-right:20px}.charts-menuitem-content{color:#000;font:normal 13px Arial,sans-serif}.charts-menuitem-disabled .charts-menuitem-accel,.charts-menuitem-disabled .charts-menuitem-content{color:#ccc!important}.charts-menuitem-disabled .charts-menuitem-icon{opacity:.3;-moz-opacity:.3;filter:alpha(opacity=30)}.charts-menuitem-highlight,.charts-menuitem-hover{background-color:#d6e9f8;border-color:#d6e9f8;border-style:dotted;border-width:1px 0;padding-bottom:3px;padding-top:3px}.charts-menuitem-checkbox,.charts-menuitem-icon{background-repeat:no-repeat;height:16px;left:6px;position:absolute;right:auto;vertical-align:middle;width:16px}.charts-menuitem-rtl .charts-menuitem-checkbox,.charts-menuitem-rtl .charts-menuitem-icon{left:auto;right:6px}.charts-option-selected .charts-menuitem-checkbox,.charts-option-selected .charts-menuitem-icon{background:url(//ssl.gstatic.com/editor/editortoolbar.png) no-repeat -512px 0}.charts-menuitem-accel{color:#999;direction:ltr;left:auto;padding:0 6px;position:absolute;right:0;text-align:right}.charts-menuitem-rtl .charts-menuitem-accel{left:0;right:auto;text-align:left}.charts-menuitem-mnemonic-hint{text-decoration:underline}.charts-menuitem-mnemonic-separator{color:#999;font-size:12px;padding-left:4px} 3 | 4 | -------------------------------------------------------------------------------- /src/www/js/google/visualization.js: -------------------------------------------------------------------------------- 1 | if (window['google'] != undefined && window['google']['loader'] != undefined) { 2 | if (!window['google']['visualization']) { 3 | window['google']['visualization'] = {}; 4 | google.visualization.Version = '1.0'; 5 | google.visualization.JSHash = 'abbdd6ab2d343a51d49841bf93d022fb'; 6 | google.visualization.LoadArgs = 'file\75visualization\46v\0751\46packages\75corechart'; 7 | } 8 | // google.loader.writeLoadTag("css", google.loader.ServiceBase + "/api/visualization/1.0/abbdd6ab2d343a51d49841bf93d022fb/ui+en.css", false); 9 | // google.loader.writeLoadTag("script", google.loader.ServiceBase + "/api/visualization/1.0/abbdd6ab2d343a51d49841bf93d022fb/format+en,default+en,ui+en,corechart+en.I.js", false); 10 | } 11 | -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/www/js/libs/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkrode/RedisLive/46e1c755880358a3ddcd8cdc352c1beeeece789f/src/www/js/libs/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/README.md: -------------------------------------------------------------------------------- 1 | ## 2.0 BOOTSTRAP JS PHILOSOPHY 2 | These are the high-level design rules which guide the development of Bootstrap's plugin apis. 3 | 4 | --- 5 | 6 | ### DATA-ATTRIBUTE API 7 | 8 | We believe you should be able to use all plugins provided by Bootstrap purely through the markup API without writing a single line of javascript. 9 | 10 | We acknowledge that this isn't always the most performant and sometimes it may be desirable to turn this functionality off altogether. Therefore, as of 2.0 we provide the ability to disable the data attribute API by unbinding all events on the body namespaced with `'data-api'`. This looks like this: 11 | 12 | $('body').off('.data-api') 13 | 14 | To target a specific plugin, just include the plugins name as a namespace along with the data-api namespace like this: 15 | 16 | $('body').off('.alert.data-api') 17 | 18 | --- 19 | 20 | ### PROGRAMATIC API 21 | 22 | We also believe you should be able to use all plugins provided by Bootstrap purely through the JS API. 23 | 24 | All public APIs should be single, chainable methods, and return the collection acted upon. 25 | 26 | $(".btn.danger").button("toggle").addClass("fat") 27 | 28 | All methods should accept an optional options object, a string which targets a particular method, or null which initiates the default behavior: 29 | 30 | $("#myModal").modal() // initialized with defaults 31 | $("#myModal").modal({ keyboard: false }) // initialized with no keyboard 32 | $("#myModal").modal('show') // initializes and invokes show immediately afterqwe2 33 | 34 | --- 35 | 36 | ### OPTIONS 37 | 38 | Options should be sparse and add universal value. We should pick the right defaults. 39 | 40 | All plugins should have a default object which can be modified to affect all instances' default options. The defaults object should be available via `$.fn.plugin.defaults`. 41 | 42 | $.fn.modal.defaults = { … } 43 | 44 | An options definition should take the following form: 45 | 46 | *noun*: *adjective* - describes or modifies a quality of an instance 47 | 48 | examples: 49 | 50 | backdrop: true 51 | keyboard: false 52 | placement: 'top' 53 | 54 | --- 55 | 56 | ### EVENTS 57 | 58 | All events should have an infinitive and past participle form. The infinitive is fired just before an action takes place, the past participle on completion of the action. 59 | 60 | show | shown 61 | hide | hidden 62 | 63 | --- 64 | 65 | ### CONSTRUCTORS 66 | 67 | Each plugin should expose its raw constructor on a `Constructor` property -- accessed in the following way: 68 | 69 | 70 | $.fn.popover.Constructor 71 | 72 | --- 73 | 74 | ### DATA ACCESSOR 75 | 76 | Each plugin stores a copy of the invoked class on an object. This class instance can be accessed directly through jQuery's data API like this: 77 | 78 | $('[rel=popover]').data('popover') instanceof $.fn.popover.Constructor 79 | 80 | --- 81 | 82 | ### DATA ATTRIBUTES 83 | 84 | Data attributes should take the following form: 85 | 86 | - data-{{verb}}={{plugin}} - defines main interaction 87 | - data-target || href^=# - defined on "control" element (if element controls an element other than self) 88 | - data-{{noun}} - defines class instance options 89 | 90 | examples: 91 | 92 | // control other targets 93 | data-toggle="modal" data-target="#foo" 94 | data-toggle="collapse" data-target="#foo" data-parent="#bar" 95 | 96 | // defined on element they control 97 | data-spy="scroll" 98 | 99 | data-dismiss="modal" 100 | data-dismiss="alert" 101 | 102 | data-toggle="dropdown" 103 | 104 | data-toggle="button" 105 | data-toggle="buttons-checkbox" 106 | data-toggle="buttons-radio" -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-alert.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * bootstrap-alert.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#alerts 4 | * ========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* ALERT CLASS DEFINITION 26 | * ====================== */ 27 | 28 | var dismiss = '[data-dismiss="alert"]' 29 | , Alert = function ( el ) { 30 | $(el).on('click', dismiss, this.close) 31 | } 32 | 33 | Alert.prototype = { 34 | 35 | constructor: Alert 36 | 37 | , close: function ( e ) { 38 | var $this = $(this) 39 | , selector = $this.attr('data-target') 40 | , $parent 41 | 42 | if (!selector) { 43 | selector = $this.attr('href') 44 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 45 | } 46 | 47 | $parent = $(selector) 48 | $parent.trigger('close') 49 | 50 | e && e.preventDefault() 51 | 52 | $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) 53 | 54 | $parent 55 | .trigger('close') 56 | .removeClass('in') 57 | 58 | function removeElement() { 59 | $parent 60 | .trigger('closed') 61 | .remove() 62 | } 63 | 64 | $.support.transition && $parent.hasClass('fade') ? 65 | $parent.on($.support.transition.end, removeElement) : 66 | removeElement() 67 | } 68 | 69 | } 70 | 71 | 72 | /* ALERT PLUGIN DEFINITION 73 | * ======================= */ 74 | 75 | $.fn.alert = function ( option ) { 76 | return this.each(function () { 77 | var $this = $(this) 78 | , data = $this.data('alert') 79 | if (!data) $this.data('alert', (data = new Alert(this))) 80 | if (typeof option == 'string') data[option].call($this) 81 | }) 82 | } 83 | 84 | $.fn.alert.Constructor = Alert 85 | 86 | 87 | /* ALERT DATA-API 88 | * ============== */ 89 | 90 | $(function () { 91 | $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) 92 | }) 93 | 94 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-button.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-button.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#buttons 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | !function( $ ){ 21 | 22 | "use strict" 23 | 24 | /* BUTTON PUBLIC CLASS DEFINITION 25 | * ============================== */ 26 | 27 | var Button = function ( element, options ) { 28 | this.$element = $(element) 29 | this.options = $.extend({}, $.fn.button.defaults, options) 30 | } 31 | 32 | Button.prototype = { 33 | 34 | constructor: Button 35 | 36 | , setState: function ( state ) { 37 | var d = 'disabled' 38 | , $el = this.$element 39 | , data = $el.data() 40 | , val = $el.is('input') ? 'val' : 'html' 41 | 42 | state = state + 'Text' 43 | data.resetText || $el.data('resetText', $el[val]()) 44 | 45 | $el[val](data[state] || this.options[state]) 46 | 47 | // push to event loop to allow forms to submit 48 | setTimeout(function () { 49 | state == 'loadingText' ? 50 | $el.addClass(d).attr(d, d) : 51 | $el.removeClass(d).removeAttr(d) 52 | }, 0) 53 | } 54 | 55 | , toggle: function () { 56 | var $parent = this.$element.parent('[data-toggle="buttons-radio"]') 57 | 58 | $parent && $parent 59 | .find('.active') 60 | .removeClass('active') 61 | 62 | this.$element.toggleClass('active') 63 | } 64 | 65 | } 66 | 67 | 68 | /* BUTTON PLUGIN DEFINITION 69 | * ======================== */ 70 | 71 | $.fn.button = function ( option ) { 72 | return this.each(function () { 73 | var $this = $(this) 74 | , data = $this.data('button') 75 | , options = typeof option == 'object' && option 76 | if (!data) $this.data('button', (data = new Button(this, options))) 77 | if (option == 'toggle') data.toggle() 78 | else if (option) data.setState(option) 79 | }) 80 | } 81 | 82 | $.fn.button.defaults = { 83 | loadingText: 'loading...' 84 | } 85 | 86 | $.fn.button.Constructor = Button 87 | 88 | 89 | /* BUTTON DATA-API 90 | * =============== */ 91 | 92 | $(function () { 93 | $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { 94 | var $btn = $(e.target) 95 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 96 | $btn.button('toggle') 97 | }) 98 | }) 99 | 100 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-carousel.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * bootstrap-carousel.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#carousel 4 | * ========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* CAROUSEL CLASS DEFINITION 26 | * ========================= */ 27 | 28 | var Carousel = function (element, options) { 29 | this.$element = $(element) 30 | this.options = $.extend({}, $.fn.carousel.defaults, options) 31 | this.options.slide && this.slide(this.options.slide) 32 | this.options.pause == 'hover' && this.$element 33 | .on('mouseenter', $.proxy(this.pause, this)) 34 | .on('mouseleave', $.proxy(this.cycle, this)) 35 | } 36 | 37 | Carousel.prototype = { 38 | 39 | cycle: function () { 40 | this.interval = setInterval($.proxy(this.next, this), this.options.interval) 41 | return this 42 | } 43 | 44 | , to: function (pos) { 45 | var $active = this.$element.find('.active') 46 | , children = $active.parent().children() 47 | , activePos = children.index($active) 48 | , that = this 49 | 50 | if (pos > (children.length - 1) || pos < 0) return 51 | 52 | if (this.sliding) { 53 | return this.$element.one('slid', function () { 54 | that.to(pos) 55 | }) 56 | } 57 | 58 | if (activePos == pos) { 59 | return this.pause().cycle() 60 | } 61 | 62 | return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos])) 63 | } 64 | 65 | , pause: function () { 66 | clearInterval(this.interval) 67 | this.interval = null 68 | return this 69 | } 70 | 71 | , next: function () { 72 | if (this.sliding) return 73 | return this.slide('next') 74 | } 75 | 76 | , prev: function () { 77 | if (this.sliding) return 78 | return this.slide('prev') 79 | } 80 | 81 | , slide: function (type, next) { 82 | var $active = this.$element.find('.active') 83 | , $next = next || $active[type]() 84 | , isCycling = this.interval 85 | , direction = type == 'next' ? 'left' : 'right' 86 | , fallback = type == 'next' ? 'first' : 'last' 87 | , that = this 88 | 89 | this.sliding = true 90 | 91 | isCycling && this.pause() 92 | 93 | $next = $next.length ? $next : this.$element.find('.item')[fallback]() 94 | 95 | if ($next.hasClass('active')) return 96 | 97 | if (!$.support.transition && this.$element.hasClass('slide')) { 98 | this.$element.trigger('slide') 99 | $active.removeClass('active') 100 | $next.addClass('active') 101 | this.sliding = false 102 | this.$element.trigger('slid') 103 | } else { 104 | $next.addClass(type) 105 | $next[0].offsetWidth // force reflow 106 | $active.addClass(direction) 107 | $next.addClass(direction) 108 | this.$element.trigger('slide') 109 | this.$element.one($.support.transition.end, function () { 110 | $next.removeClass([type, direction].join(' ')).addClass('active') 111 | $active.removeClass(['active', direction].join(' ')) 112 | that.sliding = false 113 | setTimeout(function () { that.$element.trigger('slid') }, 0) 114 | }) 115 | } 116 | 117 | isCycling && this.cycle() 118 | 119 | return this 120 | } 121 | 122 | } 123 | 124 | 125 | /* CAROUSEL PLUGIN DEFINITION 126 | * ========================== */ 127 | 128 | $.fn.carousel = function ( option ) { 129 | return this.each(function () { 130 | var $this = $(this) 131 | , data = $this.data('carousel') 132 | , options = typeof option == 'object' && option 133 | if (!data) $this.data('carousel', (data = new Carousel(this, options))) 134 | if (typeof option == 'number') data.to(option) 135 | else if (typeof option == 'string' || (option = options.slide)) data[option]() 136 | else data.cycle() 137 | }) 138 | } 139 | 140 | $.fn.carousel.defaults = { 141 | interval: 5000 142 | , pause: 'hover' 143 | } 144 | 145 | $.fn.carousel.Constructor = Carousel 146 | 147 | 148 | /* CAROUSEL DATA-API 149 | * ================= */ 150 | 151 | $(function () { 152 | $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) { 153 | var $this = $(this), href 154 | , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 155 | , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data()) 156 | $target.carousel(options) 157 | e.preventDefault() 158 | }) 159 | }) 160 | 161 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-collapse.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * bootstrap-collapse.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#collapse 4 | * ============================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | !function( $ ){ 21 | 22 | "use strict" 23 | 24 | var Collapse = function ( element, options ) { 25 | this.$element = $(element) 26 | this.options = $.extend({}, $.fn.collapse.defaults, options) 27 | 28 | if (this.options["parent"]) { 29 | this.$parent = $(this.options["parent"]) 30 | } 31 | 32 | this.options.toggle && this.toggle() 33 | } 34 | 35 | Collapse.prototype = { 36 | 37 | constructor: Collapse 38 | 39 | , dimension: function () { 40 | var hasWidth = this.$element.hasClass('width') 41 | return hasWidth ? 'width' : 'height' 42 | } 43 | 44 | , show: function () { 45 | var dimension = this.dimension() 46 | , scroll = $.camelCase(['scroll', dimension].join('-')) 47 | , actives = this.$parent && this.$parent.find('.in') 48 | , hasData 49 | 50 | if (actives && actives.length) { 51 | hasData = actives.data('collapse') 52 | actives.collapse('hide') 53 | hasData || actives.data('collapse', null) 54 | } 55 | 56 | this.$element[dimension](0) 57 | this.transition('addClass', 'show', 'shown') 58 | this.$element[dimension](this.$element[0][scroll]) 59 | 60 | } 61 | 62 | , hide: function () { 63 | var dimension = this.dimension() 64 | this.reset(this.$element[dimension]()) 65 | this.transition('removeClass', 'hide', 'hidden') 66 | this.$element[dimension](0) 67 | } 68 | 69 | , reset: function ( size ) { 70 | var dimension = this.dimension() 71 | 72 | this.$element 73 | .removeClass('collapse') 74 | [dimension](size || 'auto') 75 | [0].offsetWidth 76 | 77 | this.$element[size ? 'addClass' : 'removeClass']('collapse') 78 | 79 | return this 80 | } 81 | 82 | , transition: function ( method, startEvent, completeEvent ) { 83 | var that = this 84 | , complete = function () { 85 | if (startEvent == 'show') that.reset() 86 | that.$element.trigger(completeEvent) 87 | } 88 | 89 | this.$element 90 | .trigger(startEvent) 91 | [method]('in') 92 | 93 | $.support.transition && this.$element.hasClass('collapse') ? 94 | this.$element.one($.support.transition.end, complete) : 95 | complete() 96 | } 97 | 98 | , toggle: function () { 99 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 100 | } 101 | 102 | } 103 | 104 | /* COLLAPSIBLE PLUGIN DEFINITION 105 | * ============================== */ 106 | 107 | $.fn.collapse = function ( option ) { 108 | return this.each(function () { 109 | var $this = $(this) 110 | , data = $this.data('collapse') 111 | , options = typeof option == 'object' && option 112 | if (!data) $this.data('collapse', (data = new Collapse(this, options))) 113 | if (typeof option == 'string') data[option]() 114 | }) 115 | } 116 | 117 | $.fn.collapse.defaults = { 118 | toggle: true 119 | } 120 | 121 | $.fn.collapse.Constructor = Collapse 122 | 123 | 124 | /* COLLAPSIBLE DATA-API 125 | * ==================== */ 126 | 127 | $(function () { 128 | $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function ( e ) { 129 | var $this = $(this), href 130 | , target = $this.attr('data-target') 131 | || e.preventDefault() 132 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 133 | , option = $(target).data('collapse') ? 'toggle' : $this.data() 134 | $(target).collapse(option) 135 | }) 136 | }) 137 | 138 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-dropdown.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-dropdown.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* DROPDOWN CLASS DEFINITION 26 | * ========================= */ 27 | 28 | var toggle = '[data-toggle="dropdown"]' 29 | , Dropdown = function ( element ) { 30 | var $el = $(element).on('click.dropdown.data-api', this.toggle) 31 | $('html').on('click.dropdown.data-api', function () { 32 | $el.parent().removeClass('open') 33 | }) 34 | } 35 | 36 | Dropdown.prototype = { 37 | 38 | constructor: Dropdown 39 | 40 | , toggle: function ( e ) { 41 | var $this = $(this) 42 | , selector = $this.attr('data-target') 43 | , $parent 44 | , isActive 45 | 46 | if (!selector) { 47 | selector = $this.attr('href') 48 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 49 | } 50 | 51 | $parent = $(selector) 52 | $parent.length || ($parent = $this.parent()) 53 | 54 | isActive = $parent.hasClass('open') 55 | 56 | clearMenus() 57 | !isActive && $parent.toggleClass('open') 58 | 59 | return false 60 | } 61 | 62 | } 63 | 64 | function clearMenus() { 65 | $(toggle).parent().removeClass('open') 66 | } 67 | 68 | 69 | /* DROPDOWN PLUGIN DEFINITION 70 | * ========================== */ 71 | 72 | $.fn.dropdown = function ( option ) { 73 | return this.each(function () { 74 | var $this = $(this) 75 | , data = $this.data('dropdown') 76 | if (!data) $this.data('dropdown', (data = new Dropdown(this))) 77 | if (typeof option == 'string') data[option].call($this) 78 | }) 79 | } 80 | 81 | $.fn.dropdown.Constructor = Dropdown 82 | 83 | 84 | /* APPLY TO STANDARD DROPDOWN ELEMENTS 85 | * =================================== */ 86 | 87 | $(function () { 88 | $('html').on('click.dropdown.data-api', clearMenus) 89 | $('body').on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) 90 | }) 91 | 92 | }( window.jQuery ); -------------------------------------------------------------------------------- /src/www/js/libs/bootstrap/js/bootstrap-modal.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-modal.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#modals 4 | * ========================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* MODAL CLASS DEFINITION 26 | * ====================== */ 27 | 28 | var Modal = function ( content, options ) { 29 | this.options = options 30 | this.$element = $(content) 31 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) 32 | } 33 | 34 | Modal.prototype = { 35 | 36 | constructor: Modal 37 | 38 | , toggle: function () { 39 | return this[!this.isShown ? 'show' : 'hide']() 40 | } 41 | 42 | , show: function () { 43 | var that = this 44 | 45 | if (this.isShown) return 46 | 47 | $('body').addClass('modal-open') 48 | 49 | this.isShown = true 50 | this.$element.trigger('show') 51 | 52 | escape.call(this) 53 | backdrop.call(this, function () { 54 | var transition = $.support.transition && that.$element.hasClass('fade') 55 | 56 | !that.$element.parent().length && that.$element.appendTo(document.body) //don't move modals dom position 57 | 58 | that.$element 59 | .show() 60 | 61 | if (transition) { 62 | that.$element[0].offsetWidth // force reflow 63 | } 64 | 65 | that.$element.addClass('in') 66 | 67 | transition ? 68 | that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) : 69 | that.$element.trigger('shown') 70 | 71 | }) 72 | } 73 | 74 | , hide: function ( e ) { 75 | e && e.preventDefault() 76 | 77 | if (!this.isShown) return 78 | 79 | var that = this 80 | this.isShown = false 81 | 82 | $('body').removeClass('modal-open') 83 | 84 | escape.call(this) 85 | 86 | this.$element 87 | .trigger('hide') 88 | .removeClass('in') 89 | 90 | $.support.transition && this.$element.hasClass('fade') ? 91 | hideWithTransition.call(this) : 92 | hideModal.call(this) 93 | } 94 | 95 | } 96 | 97 | 98 | /* MODAL PRIVATE METHODS 99 | * ===================== */ 100 | 101 | function hideWithTransition() { 102 | var that = this 103 | , timeout = setTimeout(function () { 104 | that.$element.off($.support.transition.end) 105 | hideModal.call(that) 106 | }, 500) 107 | 108 | this.$element.one($.support.transition.end, function () { 109 | clearTimeout(timeout) 110 | hideModal.call(that) 111 | }) 112 | } 113 | 114 | function hideModal( that ) { 115 | this.$element 116 | .hide() 117 | .trigger('hidden') 118 | 119 | backdrop.call(this) 120 | } 121 | 122 | function backdrop( callback ) { 123 | var that = this 124 | , animate = this.$element.hasClass('fade') ? 'fade' : '' 125 | 126 | if (this.isShown && this.options.backdrop) { 127 | var doAnimate = $.support.transition && animate 128 | 129 | this.$backdrop = $('