├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README-dev.md ├── README.md ├── build_stat_browser.py ├── hexaparse.py ├── requirements-dev.txt ├── requirements.txt ├── stat_key_browser ├── __init__.py ├── browser_builder.py ├── categorizer.py ├── cluster_config.py ├── data │ ├── key_cats.hexa │ └── key_tags.hexa ├── key_collector.py ├── mapper.py ├── search_terms.py ├── tagger.py └── templates │ ├── app_template.html │ └── key_template.html ├── test1 ├── tests ├── functional │ └── test_functional.py └── unit │ ├── .coveragerc │ ├── test_browser_builder.py │ ├── test_categorizer.py │ ├── test_cluster_config.py │ ├── test_key_collector.py │ ├── test_mapper.py │ ├── test_search_terms.py │ └── test_tagger.py └── web_app ├── css ├── bootstrap-theme.css ├── bootstrap-theme.css.map ├── bootstrap-theme.min.css ├── bootstrap-theme.min.css.map ├── bootstrap.css ├── bootstrap.css.map ├── bootstrap.min.css ├── bootstrap.min.css.map ├── selectize.bootstrap3.css └── style.css ├── fonts ├── MaterialIcons-Regular.ttf ├── MaterialIcons-Regular.woff ├── MaterialIcons-Regular.woff2 ├── MtNoLFRo.ttf ├── MtThLf__.ttf ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 └── js ├── app.js ├── app_filter.js ├── app_filter_lib.js ├── app_lib.js ├── app_papi_link.js ├── app_tags.js ├── app_tooltips.js ├── bootstrap.js ├── bootstrap.min.js ├── jquery.js ├── jquery.typewatch.js └── npm.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | dist 4 | env 5 | .coverage 6 | .cache 7 | stat_key_browser/data/key_cats.json 8 | stat_key_browser/data/key_tags.json 9 | web_app/js/keys.js 10 | web_app/index.html 11 | isilon_stat_browser_*.zip 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | 8 | virtualenv: 9 | system_site_packages: false 10 | 11 | install: 12 | - pip install -r requirements-dev.txt 13 | - git clone https://github.com/Isilon/isilon_sdk_7_2_python.git 14 | - cd isilon_sdk_7_2_python 15 | - python setup.py install 16 | - cd .. 17 | 18 | script: 19 | - make travis-ci 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 EMC Corporation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | 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 THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON=`which python` 2 | PIP=`which pip` 3 | PYLINT=`which pylint` 4 | HEXAPARSE=./hexaparse.py 5 | 6 | DIST_DIR=dist 7 | BROWSER_VERS_STRING=`git describe --tags --exact-match --abbrev=0 || git rev-parse --short HEAD` 8 | 9 | 10 | clean: 11 | -rm -rf $(DIST_DIR) 12 | -rm -rf test_results 13 | -rm stat_key_browser/data/key_tags.json 14 | -rm stat_key_browser/data/key_cats.json 15 | -rm web_app/js/keys.js 16 | -rm web_app/index.html 17 | -rm isilon_stat_browser_v*.zip 18 | 19 | tags: 20 | $(HEXAPARSE) stat_key_browser/data/key_tags.hexa > stat_key_browser/data/key_tags.json 21 | $(HEXAPARSE) stat_key_browser/data/key_cats.hexa > stat_key_browser/data/key_cats.json 22 | 23 | lint: 24 | $(PYLINT) -E -f colorized -r n stat_key_browser bin/ tests/ 25 | 26 | unittests: lint 27 | $(PYTHON) -m pytest -v tests/unit/ *.py 28 | 29 | coverage: lint 30 | $(PYTHON) -m pytest -v --cov=stat_key_browser --cov-report term-missing --cov-config tests/unit/.coveragerc tests/unit/ *.py 31 | 32 | travis-ci: tags lint 33 | $(PYTHON) -m pytest -v tests/unit/ hexaparse.py 34 | 35 | check_cluster: 36 | if [ -z $$BUILD_BROWSER_ARGS ]; then echo BUILD_BROWSER_ARGS not set, builder will pause for input; fi 37 | 38 | functional_tests: check_cluster clean unittests tags 39 | $(PYTHON) -m pytest -v tests/functional/ 40 | 41 | dist: check_cluster clean unittests tags 42 | ./build_stat_browser.py -x $(BUILD_BROWSER_ARGS) 43 | mkdir -p $(DIST_DIR) 44 | cp build_stat_browser.py $(DIST_DIR) 45 | cp requirements.txt $(DIST_DIR) 46 | cp README.md $(DIST_DIR) 47 | cp -r stat_key_browser $(DIST_DIR) 48 | cp -r web_app $(DIST_DIR) 49 | zip -r isilon_stat_browser_$(BROWSER_VERS_STRING).zip dist/* 50 | -------------------------------------------------------------------------------- /README-dev.md: -------------------------------------------------------------------------------- 1 | [![Master Build Status](https://travis-ci.org/Isilon/isilon_stat_browser.svg?branch=master)](https://travis-ci.org/Isilon/isilon_stat_browser) 2 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/isilon/isilon_stat_browser.svg)](http://isitmaintained.com/project/isilon/isilon_stat_browser "Average time to resolve an issue") 3 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/isilon/isilon_stat_browser.svg)](http://isitmaintained.com/project/isilon/isilon_stat_browser "Percentage of issues still open") 4 | 5 | #Statistics Key Browser Development 6 | 7 | A single page web app that provides a browsable, searchable view of Isilon statistics keys. A Python script generates the html by querying an Isilon cluster for the list of statistics keys, then organizing and categorizing the keys before outputting the html app to web_app. 8 | 9 | ##Requirements 10 | Python: 2.7, 3.3, 3.4, 3.5 11 | 12 | Dependencies listed in requirements-dev.txt 13 | 14 | Isilon SDK Python language bindings 15 | 16 | [`https://github.com/isilon/`](https://github.com/isilon) 17 | 18 | ##Development getting started 19 | 20 | ###Clone this repo: 21 | 22 | `git clone ` 23 | 24 | `cd isilon_stat_browser` 25 | 26 | ### Install the dependencies: 27 | 28 | `pip install -r requirements-dev.txt` 29 | 30 | ### Run unit tests: 31 | 32 | `make unittests` 33 | 34 | ### Check unit test coverage: 35 | 36 | `make coverage` 37 | 38 | ### Run functional tests: 39 | The functional tests are only a stub currently. 40 | 41 | `make functional_tests` 42 | 43 | ###Run the page building tool 44 | First, convert the tagging data to JSON. This step creates stat\_key\_browser/data/key\_tags.json and stat\_key\_browser/data/key\_cats.json 45 | 46 | `make tags` 47 | 48 | Then run the browser builder: 49 | 50 | `./build_stat_browser.py -c ` 51 | 52 | ### Generate a distributable zip file: 53 | The build will pause to request cluster IP, username and password. 54 | 55 | `make dist` 56 | 57 | ### Generate a distributable zip file non-interactively: 58 | 59 | Supply the cluster IP, username and password when building via automation. 60 | 61 | `make dist BUILD_BROWSER_ARGS='-c -u -p '` 62 | 63 | # Release process 64 | 65 | **Note:** 66 | This is a temporary manual process to be used by repo owners to cut a release until automated build/release is implemented. 67 | 68 | 69 | Once the master branch is in a state ready for a release, tag the current commit 70 | with a version number. 71 | 72 | `git tag -a v0.0.1 -m 'version 0.0.1'` 73 | 74 | Push the tag to git 75 | 76 | `git push origin v0.0.1` 77 | 78 | Create the distribution for release 79 | 80 | `make dist BUILD_BROWSER_ARGS='-c -u -p '` 81 | 82 | This creates a .zip file in the top-level project directory. The file will be 83 | automatically named with the version specified in the tag. If no version number 84 | appears in the file name something has gone wrong. 85 | 86 | Go to [isilon\_stat\_browser/releases](../../releases) and draft a new release. 87 | Enter the tag into the tag version box and the tag should be recognized as an 88 | existing tag. 89 | 90 | Enter any needed release notes 91 | 92 | Attach the zipped release distribution to the release. 93 | 94 | Publish the release. 95 | 96 | # Files 97 | 98 | * `README-dev.md` 99 | 100 | The developer facing readme that you are reading now. 101 | 102 | * `README.md` 103 | 104 | The user-facing readme that gets packaged into the distributable zip. 105 | 106 | * `stat_key_browser/data/key_cats.hexa` 107 | 108 | A human written and readable file that defines which categories and subcategories are to be applied to lists of key names. Parsed into json by hexaparse.py during `make tags`. 109 | 110 | * `stat_key_browser/data/key_cats.json`: 111 | 112 | The automatically generated JSON that results when hexaparse.py parses key\_cats.hexa during the build process. This file is referenced by build\_stat\_browser.py to categorize stat keys received from PAPI. 113 | 114 | * `stat_key_browser/data/key_tags.hexa` 115 | 116 | A human written and readable file that defines which tags are to be applied to lists of key names. Parsed into json by hexaparse.py during the build process. 117 | 118 | * `stat_key_browser/data/key_tags.json`: 119 | 120 | The automatically generated JSON that results when hexaparse.py parsed key\_tags.hexa during the build process. This file is referenced by build\_stat\_browser.py to tag stat keys received from PAPI and is part of the distributable zip. 121 | 122 | * `keys.js` 123 | 124 | JSON formatted stat keys with tags and categories attached. This is created by build\_stat\_browser.py and read by the html app's JavaScript. 125 | 126 | * `stat_key_browser/templates/app_template.html` 127 | 128 | The main template for the app. Contains the main html plus categories. 129 | 130 | * `stat_key_browser/template/key_template.html` 131 | 132 | Contains a template representing a single key and all of its info, including its extra info. 133 | 134 | * `web_app/index.html` 135 | 136 | This is the file opened by the user to view the stat browser. This file is rendered by build_stat_browser.py from the templates, the definitions in key_tags.json and from the PAPI supplied list of keys. 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Master Build Status](https://travis-ci.org/Isilon/isilon_stat_browser.svg?branch=master)](https://travis-ci.org/Isilon/isilon_stat_browser) 2 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/isilon/isilon_stat_browser.svg)](http://isitmaintained.com/project/isilon/isilon_stat_browser "Average time to resolve an issue") 3 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/isilon/isilon_stat_browser.svg)](http://isitmaintained.com/project/isilon/isilon_stat_browser "Percentage of issues still open") 4 | 5 | # Statistics Key Browser 6 | 7 | This repository is part of the Isilon SDK, which is an evolving package of documents and files. This README describes how to use the statistics key browser (stat browser) to browse the statistics keys that an Isilon cluster exposes. The stat browser is a Python script-generated web browser that provides a searchable list of OneFS statistics keys, organized by functional categories. 8 | 9 | ## Requirements 10 | 11 | A compatible web browser: 12 | 13 | * Google Chrome 14 | * Firefox 15 | * Safari 16 | * Internet Explorer 11 17 | * Microsoft Edge 18 | 19 | ## Installation 20 | You can obtain the files for the stat browser by downloading a release zip file from this Github repository: 21 | 22 | [isilon\_stat\_browser/releases](../../releases) 23 | 24 | ## Development 25 | 26 | Developers can view the development readme for information about stat browser development. 27 | 28 | [isilon\_stat\_browser/README-dev.md](README-dev.md) 29 | 30 | 31 | ## View statistics keys with the stat browser 32 | Once the files and directories in the zip file are extracted, you will see a stat_key_browser directory. Open the web_app/index.html file in a web browser to browse the statistics keys available in the distribution package. 33 | 34 | ## Generate the stat browser 35 | 36 | You can also generate the stat browser from your OneFS cluster by running the build\_stat\_browser.py Python script. You may want to perform this step on your version of OneFS because available statistics keys can vary between OneFS versions, so generating the keys will provide the most accurate list for your implementation. During the generation process, your cluster will be queried for its list of statistc keys to generate the stat browser. 37 | 38 | ### Requirements 39 | Python: 2.7, 3.3, 3.4, 3.5 40 | 41 | Isilon SDK Python Language Bindings installed 42 | 43 | ### Generating the stat browser 44 | 45 | First unpack the zip file as described above and then install the requirements. 46 | 47 | `pip install -r requirements.txt` 48 | 49 | Next run the page builder: 50 | 51 | `./build_stat_browser.py --cluster ` 52 | 53 | For help on build\_stat\_browser.py usage, run: 54 | 55 | `./build_stat_browser.py --help` 56 | 57 | The script will prompt you for a username and password, and will create the browsable web\_app/index.html page and key\_cats.json Java script which contains the statistics keys categories. 58 | 59 | ## Install SDK Python language bindings 60 | 61 | You can use the SDK Python language bindings to automate the configuration, maintenance, and monitoring of your Isilon cluster. For information on how to install the Python language bindings and write Python scripts to access the OneFS API, refer to the following Github sites: 62 | 63 | [`https://github.com/Isilon/isilon_sdk_7_2_python`](https://github.com/Isilon/isilon_sdk_7_2_python) 64 | [`https://github.com/Isilon/isilon_sdk_8_0_python`](https://github.com/Isilon/isilon_sdk_8_0_python) 65 | 66 | 67 | Copyright (c) 2016 EMC Corporation 68 | 69 | Permission is hereby granted, free of charge, to any person obtaining a copy 70 | of this software and associated documentation files (the "Software"), to deal 71 | in the Software without restriction, including without limitation the rights 72 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 73 | copies of the Software, and to permit persons to whom the Software is 74 | furnished to do so, subject to the following conditions: 75 | 76 | The above copyright notice and this permission notice shall be included in all 77 | copies or substantial portions of the Software. 78 | 79 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 80 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 81 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 82 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 83 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 84 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 85 | SOFTWARE. 86 | -------------------------------------------------------------------------------- /build_stat_browser.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | """ 4 | Connects to PAPI to download list of current keys. 5 | 6 | Outputs an html app that provides an interface to browse stat keys. 7 | """ 8 | 9 | from builtins import input 10 | import getpass 11 | import logging 12 | import sys 13 | from optparse import OptionParser 14 | import urllib3 15 | from stat_key_browser.browser_builder import BrowserBuilder 16 | 17 | 18 | def get_parser(): 19 | """Return a parser to parse CLI args.""" 20 | parser = OptionParser() 21 | parser.add_option('-c', '--cluster', dest='cluster', type='string', 22 | help='Cluster IP or hostname') 23 | parser.add_option('-u', '--user', dest='username', type='string', 24 | help='Username to authenticate to PAPI or omit for prompt') 25 | parser.add_option('-p', '--password', dest='password', type='string', 26 | help='Password to authenticate to PAPI or omit for prompt') 27 | parser.add_option('-l', '--log', dest='logfile', type='string', 28 | help='Log to a file instead of STDERR') 29 | parser.add_option('-x', '--anon-ip', action='store_true', dest='anon_ip', 30 | default=False, help='Do not store cluster IP in output') 31 | parser.add_option('--debug', action='store_true', dest='debug', default=False) 32 | return parser 33 | 34 | 35 | def build_browser(): 36 | """Parse opts and run a BrowerBuilder.""" 37 | parser = get_parser() 38 | (opts, args) = parser.parse_args() 39 | 40 | if opts.cluster is None: 41 | opts.cluster = input('Cluster IP or hostname: ') 42 | 43 | if opts.debug: 44 | log_lvl = logging.DEBUG 45 | logging.getLogger("urllib3").setLevel(logging.INFO) 46 | else: 47 | log_lvl = logging.INFO 48 | logging.getLogger("urllib3").setLevel(logging.WARNING) 49 | if opts.logfile: 50 | logging.basicConfig(level=log_lvl, datefmt='%Y-%m-%dT%H:%M:%S', 51 | format='%(asctime)s [%(levelname)s] %(message)s', 52 | filename=opts.logfile) 53 | else: 54 | logging.basicConfig(level=log_lvl, datefmt='%Y-%m-%dT%H:%M:%S', 55 | format='%(asctime)s [%(levelname)s] %(message)s') 56 | 57 | if not opts.username: 58 | opts.username = input('Cluster username: ') 59 | if not opts.password: 60 | opts.password = getpass.getpass() 61 | store_ip = not opts.anon_ip 62 | bb = BrowserBuilder(opts.cluster, opts.username, opts.password, store_ip) 63 | bb.build_browser() 64 | 65 | 66 | if __name__ == '__main__': 67 | urllib3.disable_warnings() 68 | build_browser() 69 | -------------------------------------------------------------------------------- /hexaparse.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """ 3 | Parse a hexa key/lists-of-values doc into a list of dicts. 4 | 5 | Each list item is a dict. The contain lists referenced by keyname. 6 | 7 | -Input- 8 | :::::: 9 | :::keyA 10 | value1 11 | 12 | :::keyB 13 | value1 14 | value3 15 | value5 16 | 17 | :::::: 18 | :::key20 19 | valuesA 20 | 21 | -Outputs- 22 | [{'keyA': ['value1'], 'keyB': ['value1', 'value3', 'value5']}, {'key20': ['valuesA']}] 23 | 24 | """ 25 | 26 | import fileinput 27 | import json 28 | import re 29 | import logging 30 | import pytest 31 | 32 | 33 | def is_new_block(line): 34 | """Determine whether a string marks a new block.""" 35 | if re.match('::::::$', line): 36 | if len(line[3:]) > 0: 37 | return True 38 | else: 39 | return False 40 | 41 | 42 | def is_key(line): 43 | """Determine whether a string is a key.""" 44 | if re.match(':::[^:].*', line): 45 | if len(line[3:]) > 0: 46 | return True 47 | else: 48 | return False 49 | 50 | 51 | def is_comment(line): 52 | """Determine whether a string is a key.""" 53 | if re.match('#', line): 54 | return True 55 | else: 56 | return False 57 | 58 | 59 | def hexakey(line): 60 | """Parse a key from a hexakey.""" 61 | if not is_key(line): 62 | raise ValueError 63 | return line[3:].strip() 64 | 65 | 66 | def hexaparse(hexainput=None): 67 | """ 68 | Parse a hexa file. 69 | 70 | Parses a hexa formatted text file into dicts. If hexainput is none reads from 71 | a file name passed in sysargs, else from stdin. 72 | 73 | :param hexainput: A file-like object containing lines to be parsed 74 | :returns: An array of dicts 75 | """ 76 | logging.basicConfig(level=logging.INFO, datefmt='%Y-%m-%dT%H:%M:%S', 77 | format='%(asctime)s [%(levelname)s] %(message)s') 78 | logging.debug('Logging started') 79 | output = [] 80 | output_block = {} 81 | key = None 82 | values = [] 83 | if not hexainput: 84 | hexainput = fileinput.input() 85 | line_num = 1 86 | for line in hexainput: 87 | line = line.strip() 88 | logging.debug('Parsing line: ', line=line) 89 | if is_comment(line): 90 | continue 91 | elif is_key(line): 92 | if key: 93 | # Don't try to save values if this is the first key 94 | output_block[key] = values 95 | key = hexakey(line) 96 | values = [] 97 | elif is_new_block(line): 98 | if output_block != {}: 99 | output_block[key] = values 100 | output.append(output_block) 101 | output_block = {} 102 | values = [] 103 | key = None 104 | else: 105 | if line != '': 106 | if not key: 107 | raise ValueError('Key (:::key) must be set before adding value "{0}" on line {1}.'.format(line, line_num)) 108 | values.append(line) 109 | line_num += 1 110 | # Save the final keys, values 111 | if key: 112 | output_block[key] = values 113 | output.append(output_block) 114 | return output 115 | 116 | 117 | if __name__ == '__main__': 118 | print(json.dumps(hexaparse(), indent=2)) 119 | 120 | 121 | def test_is_new_block_1(): 122 | """Test proper block marker.""" 123 | assert is_new_block('::::::') 124 | 125 | 126 | def test_is_new_block_2(): 127 | """Test proper block marker with newline.""" 128 | assert is_new_block('::::::\n') 129 | 130 | 131 | def test_is_new_block_5(): 132 | """Test non block marker.""" 133 | assert not is_new_block(':::::') 134 | 135 | 136 | def test_is_new_block_6(): 137 | """Test non block marker with newline.""" 138 | assert not is_new_block(':::::\n') 139 | 140 | 141 | def test_is_new_block_7(): 142 | """Test non block marker.""" 143 | assert not is_new_block('::::') 144 | 145 | 146 | def test_is_new_block_8(): 147 | """Test non block marker.""" 148 | assert not is_new_block(' ::::::') 149 | 150 | 151 | def test_parse_01(): 152 | """Parse basic 1 key 1 val input.""" 153 | hexainput = [':::key1', 'value1'] 154 | expected = [{'key1': ['value1']}] 155 | result = hexaparse(hexainput) 156 | assert result == expected 157 | 158 | 159 | def test_parse_02(): 160 | """Parse basic 1 key 2 val input.""" 161 | hexainput = [':::key1', 'value1', 'value2'] 162 | expected = [{'key1': ['value1', 'value2']}] 163 | result = hexaparse(hexainput) 164 | assert result == expected 165 | 166 | 167 | def test_parse_03(): 168 | """Parse basic 2 key 2 vals input.""" 169 | hexainput = [':::key1', 'value1', 'value2', ':::key2', 'valueA', 'valueB'] 170 | expected = [{'key1': ['value1', 'value2'], 'key2': ['valueA', 'valueB']}] 171 | result = hexaparse(hexainput) 172 | assert result == expected 173 | 174 | 175 | def test_parse_04(): 176 | """Parse 1 block with sep.""" 177 | hexainput = [':::key1', 'value1', 'value2', ':::key2', 'valueA', 'valueB', 178 | '::::::', ] 179 | expected = [{'key1': ['value1', 'value2'], 'key2': ['valueA', 'valueB']}] 180 | result = hexaparse(hexainput) 181 | print ('expected: {0}'.format(expected)) 182 | print ('actual: {0}'.format(result)) 183 | assert result == expected 184 | 185 | 186 | def test_parse_05(): 187 | """Parse 2 blocks.""" 188 | hexainput = [':::key1', 'value1', 'value2', ':::key2', 'valueA', 'valueB', 189 | '::::::', ':::keyA', 'valueZ', 'valueX'] 190 | expected = [{'key1': ['value1', 'value2'], 'key2': ['valueA', 'valueB']}, 191 | {'keyA': ['valueZ', 'valueX']}] 192 | result = hexaparse(hexainput) 193 | print ('expected: {0}'.format(expected)) 194 | print ('actual: {0}'.format(result)) 195 | assert result == expected 196 | 197 | 198 | def test_parse_06(): 199 | """Parse key with no values.""" 200 | hexainput = [':::key1', 'value1', 'value2', ':::key2'] 201 | expected = [{'key1': ['value1', 'value2'], 'key2': []}] 202 | result = hexaparse(hexainput) 203 | assert result == expected 204 | 205 | 206 | def test_parse_07(): 207 | """Parse with leading block sep.""" 208 | hexainput = ['::::::', ':::key1', 'value1', 'value2'] 209 | expected = [{'key1': ['value1', 'value2']}] 210 | result = hexaparse(hexainput) 211 | assert result == expected 212 | 213 | 214 | def test_parse_08(): 215 | """Parse with surrounding block sep.""" 216 | hexainput = ['::::::', ':::key1', 'value1', 'value2', '::::::'] 217 | expected = [{'key1': ['value1', 'value2']}] 218 | result = hexaparse(hexainput) 219 | assert result == expected 220 | 221 | 222 | def test_parse_09(): 223 | """Parse with commas in keys and values.""" 224 | hexainput = ['::::::', ':::key:1', 'value:1', 'value:2', '::::::', ':::key::2', 'value::2'] 225 | expected = [{'key:1': ['value:1', 'value:2'], 'key::2': ['value::2']}] 226 | result = hexaparse(hexainput) 227 | assert result == expected 228 | 229 | def test_parse_10(): 230 | """Parse with commas in keys and values.""" 231 | hexainput = ['::::::', '# a comment', ':::key:1', 'value:1', '#another comment', 'value:2', '::::::', ':::key::2', 'value::2'] 232 | expected = [{'key:1': ['value:1', 'value:2'], 'key::2': ['value::2']}] 233 | result = hexaparse(hexainput) 234 | assert result == expected 235 | 236 | 237 | def test_is_key_1(): 238 | """Test valid key.""" 239 | assert is_key(':::key') 240 | 241 | 242 | def test_is_key_2(): 243 | """Test non-key.""" 244 | assert not is_key('::key') 245 | 246 | 247 | def test_is_key_3(): 248 | """Test non-key.""" 249 | assert not is_key(':key') 250 | 251 | 252 | def test_is_key_4(): 253 | """Test non-key.""" 254 | assert not is_key(' :::key') 255 | 256 | 257 | def test_is_key_7(): 258 | """Test non-key.""" 259 | assert not is_key(':::') 260 | 261 | 262 | def test_hexakey_1(): 263 | """Test valid key.""" 264 | assert hexakey(':::key') == 'key' 265 | 266 | 267 | def test_hexakey_2(): 268 | """Test valid key.""" 269 | assert hexakey(':::key ') == 'key' 270 | 271 | 272 | def test_hexakey_3(): 273 | """Test non-key.""" 274 | with pytest.raises(ValueError): 275 | hexakey(':') 276 | 277 | 278 | def test_hexakey_4(): 279 | """Test non-key.""" 280 | with pytest.raises(ValueError): 281 | hexakey(': ') 282 | 283 | 284 | def test_hexakey_comment_0(): 285 | """Test comment.""" 286 | assert is_comment('# a comment') 287 | 288 | def test_hexakey_comment_1(): 289 | """Test comment.""" 290 | assert is_comment('#a comment') 291 | 292 | def test_hexakey_comment_3(): 293 | """Test comment.""" 294 | assert is_comment('#') 295 | 296 | def test_hexakey_comment_4(): 297 | """Test non-comment.""" 298 | assert not is_comment(' #not a comment') 299 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | future>=0.15.2 2 | pytest>=2.9.1 3 | pytest-cov>=2.2.1 4 | mock>=2.0.0 5 | jinja2>=2.8 6 | pylint>=1.5.5 7 | urllib3 >= 1.15 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future>=0.15.2 2 | jinja2>=2.8 3 | urllib3 >= 1.15 4 | -------------------------------------------------------------------------------- /stat_key_browser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/stat_key_browser/__init__.py -------------------------------------------------------------------------------- /stat_key_browser/browser_builder.py: -------------------------------------------------------------------------------- 1 | """Manage the creation of the key browser.""" 2 | 3 | import json 4 | from jinja2 import Environment, PackageLoader 5 | import logging 6 | import os 7 | import stat_key_browser.cluster_config as cluster_config 8 | import stat_key_browser.search_terms as search_terms 9 | from stat_key_browser.tagger import Tagger 10 | from stat_key_browser.categorizer import Categorizer 11 | import stat_key_browser.key_collector as key_collector 12 | import stat_key_browser.mapper as mapper 13 | from stat_key_browser.cluster_config import ApiException 14 | 15 | OUTPUT_DIR = 'web_app' 16 | JS_DIR = 'js' 17 | HTML_FNAME = 'index.html' 18 | JSKEYS_FNAME = 'keys.js' 19 | 20 | UNKNOWN_RELEASE = 'unknown' 21 | 22 | class BrowserBuilder(object): 23 | """ 24 | Generate an html file representing the key browser app. 25 | 26 | Inputs: List of keys from cluster via PAPI, key tag definitions via file. 27 | Outputs: an .html file + .js key data file 28 | """ 29 | 30 | def __init__(self, cluster_ip, username, password, store_ip): 31 | """ 32 | Instantiate a BrowserBuilder. 33 | 34 | :param cluster_ip: IP to make PAPI queries against 35 | :param username: PAPI username 36 | :param password: PAPI password 37 | :param store_ip: If false cluster IP is not displayed in output 38 | """ 39 | self.cluster_ip = cluster_ip 40 | self.username = username 41 | self.password = password 42 | self.store_ip = store_ip 43 | 44 | def _get_cluster_info(self): 45 | info = {} 46 | try: 47 | info['release'] = cluster_config.get_release(self.cluster_ip, 48 | self.username, 49 | self.password) 50 | except ApiException as exp: 51 | logging.warning('Unable to determine OneFS version via SDK') 52 | info['release'] = UNKNOWN_RELEASE 53 | if self.store_ip: 54 | info['host'] = self.cluster_ip 55 | else: 56 | info['host'] = None 57 | return info 58 | 59 | def _get_categories(self, key_dict): 60 | categorizer = Categorizer() 61 | categories = categorizer.categorize(key_dict) 62 | return categories 63 | 64 | def _get_mappings(self, key_dict): 65 | mappings = {} 66 | mappings['keys'] = mapper.key_ids(key_dict) 67 | mappings['categories'] = mapper.category_ids(key_dict) 68 | return mappings 69 | 70 | def _get_tags(self, key_dict): 71 | tagger = Tagger() 72 | tags = tagger.tag_list() 73 | return tags 74 | 75 | def _get_cluster(self): 76 | cluster = self._get_cluster_info() 77 | return cluster 78 | 79 | def _get_dataset(self, key_dict): 80 | data = {} 81 | data['categories'] = self._get_categories(key_dict) 82 | data['mappings'] = self._get_mappings(key_dict) 83 | data['tags'] = self._get_tags(key_dict) 84 | data['cluster'] = self._get_cluster() 85 | data['keys'] = key_dict 86 | return data 87 | 88 | def _transform_key_dict(self, key_dict): 89 | tagger = Tagger() 90 | key_dict = tagger.tag_keys(key_dict) 91 | search_terms.add_to_dict(key_dict) 92 | return key_dict 93 | 94 | def _write_key_data_json(self, output_path): 95 | """ 96 | Write the parsed keys to keys.js for use by js app. 97 | 98 | :returns: The key_dict written to file. 99 | """ 100 | key_dict = key_collector.get_tagged_squashed_dict(self.cluster_ip, 101 | self.username, 102 | self.password) 103 | key_dict = self._transform_key_dict(key_dict) 104 | data = self._get_dataset(key_dict) 105 | 106 | with open(output_path, 'w') as keyfile: 107 | keyfile.write('var keyDict = ' + json.dumps(data, indent=2)) 108 | return data 109 | 110 | def _render_app(self, key_data, output_path): 111 | jinloader = PackageLoader('stat_key_browser', 'templates') 112 | jinenviro = Environment(loader=jinloader) 113 | jintemplate = jinenviro.get_template('app_template.html') 114 | # pylint: disable=maybe-no-member 115 | app_html = jintemplate.render(categories=key_data['categories'], 116 | keys=key_data['keys'], 117 | cat_mappings=key_data['mappings']['categories'], 118 | key_mappings=key_data['mappings']['keys'], 119 | tags=key_data['tags'], 120 | cluster=key_data['cluster']) 121 | logging.info('Writing html to {0}'.format(output_path)) 122 | with open(output_path, 'w') as htmlfile: 123 | htmlfile.write(app_html) 124 | return app_html 125 | 126 | def build_browser(self): 127 | """Kick off the html app generation.""" 128 | html_path = os.path.join(OUTPUT_DIR, HTML_FNAME) 129 | jskeys_path = os.path.join(OUTPUT_DIR, JS_DIR, JSKEYS_FNAME) 130 | key_data = self._write_key_data_json(jskeys_path) 131 | html = self._render_app(key_data, html_path) 132 | -------------------------------------------------------------------------------- /stat_key_browser/categorizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide access to the category definitions and utilities. 3 | 4 | Reads and parses into a dict the json category def file. Provides access to this dict. 5 | Takes a key_dict and applies category attributes per the tag definitions. 6 | """ 7 | import logging 8 | import json 9 | import os 10 | import re 11 | import sys 12 | import stat_key_browser 13 | 14 | CAT_TAG_DEFS_FILENAME = 'key_cats.json' 15 | DEFAULT_CATEGORY = 'Uncategorized' 16 | DEFAULT_CAT_DESC = 'Statistics that have not been assigned a category.' 17 | 18 | 19 | 20 | class Categorizer(object): 21 | """Categorizer object.""" 22 | 23 | def __init__(self, defs=None): 24 | """Instantiate a Categorizer.""" 25 | self.default_category = DEFAULT_CATEGORY 26 | if defs is None: 27 | def_path = self._get_defs_path() 28 | try: 29 | with open(def_path, 'r') as def_file: 30 | defs = json.load(def_file) 31 | except IOError as err: 32 | logging.error('Unable to open {0}: {1}'.format(def_path, err)) 33 | logging.error("Try running 'make tags' to create the categories file") 34 | sys.exit(1) 35 | self.validate_defs(defs) 36 | self.cat_defs = self._flatten_uni_attrs(defs) 37 | 38 | def _get_defs_path(self): 39 | """Return path to category definitions file.""" 40 | basedir = stat_key_browser.__path__[0] 41 | defs_path = os.path.join(basedir, 'data', CAT_TAG_DEFS_FILENAME) 42 | return defs_path 43 | 44 | def _flatten_uni_attrs(self, defins): 45 | """ 46 | Flatten the super, sub, subsub, description lists into strings. 47 | """ 48 | for defin in defins: 49 | defin['super'] = defin['super'][0] 50 | if 'sub' in defin: 51 | defin['sub'] = defin['sub'][0] 52 | if 'subsub' in defin: 53 | defin['subsub'] = defin['subsub'][0] 54 | if 'category description' in defin: 55 | defin['category description'] = defin['category description'][0] 56 | return defins 57 | 58 | def validate_defs(self, defins): 59 | """ 60 | Check a cat definitions list for proper structure. 61 | 62 | Each defnitition: 63 | - must only have attrs in the valid attrs list 64 | - must have one super attr 65 | - may have zero or one sub attr 66 | - should have a category_description attr 67 | """ 68 | errors = [] 69 | valid_attrs = ['super', 'sub', 'subsub', 'keys', 're-keys', 'category description'] 70 | for defin in defins: 71 | for key in defin: 72 | if key not in valid_attrs: 73 | raise ValueError('Key "{0}" is not a valid category definition attr in {1}'.format(key, defin)) 74 | if 'super' not in defin: 75 | raise ValueError ('Attr "super" must be defined when attr "sub" is defined in {0}'.format(defin)) 76 | if len(defin['super']) != 1: 77 | raise ValueError('{0} is illegal. Only 1 "super" category is ' 78 | 'allowed in definition {1}'.format(defin['super'], defin)) 79 | if 'sub' in defin: 80 | if len(defin['sub']) != 1: 81 | raise ValueError('{0} is illegal. Only 1 "sub" category is ' 82 | 'allowed in definition {1}'.format(defin['sub'], defin)) 83 | if 'subsub' in defin: 84 | if len(defin['subsub']) != 1: 85 | raise ValueError('{0} is illegal. Only 1 "subsub" category is ' 86 | 'allowed in definition {1}'.format(defin['subsub'], defin)) 87 | 88 | def get_categories_list(self): 89 | """Return a list of category names.""" 90 | cats = [] 91 | for defin in self.cat_defs: 92 | if defin['super'] not in cats: 93 | cats.append(defin['super']) 94 | if 'sub' in defin: 95 | if defin['sub'] not in cats: 96 | cats.append(defin['sub']) 97 | return cats 98 | 99 | def _insert_keys(self, key_dict, cat_dict): 100 | """Insert keys into cat_dict according to key and re-key attrs.""" 101 | for (key, data) in key_dict.items(): 102 | supercat = data['super'] 103 | subcat = None if not 'sub' in data else data['sub'] 104 | subsubcat = None if not 'subsub' in data else data['subsub'] 105 | if subcat is None: 106 | cat_dict[supercat]['keys'].append(key) 107 | elif subsubcat is None: 108 | cat_dict[supercat]['categories'][subcat]['keys'].append(key) 109 | else: 110 | cat_dict[supercat]['categories'][subcat]['categories'][subsubcat]['keys'].append(key) 111 | return cat_dict 112 | 113 | def categorize(self, key_dict): 114 | """ 115 | Add super and sub category tags to key_dict and return a cat_dict. 116 | 117 | See design/data_formats/cat_dict.json 118 | """ 119 | # Need to apply super and sub attrs to key_dict 120 | key_dict = self._apply_categories(key_dict) 121 | cat_dict = {} 122 | for defin in self.cat_defs: 123 | supercat = defin['super'] 124 | cat_dict.setdefault(supercat, {}) 125 | cat_dict[supercat].setdefault('categories', {}) 126 | cat_dict[supercat].setdefault('keys', []) 127 | if 'category description' in defin and 'sub' not in defin: 128 | cat_dict[supercat]['description'] = defin['category description'] 129 | if 'sub' in defin: 130 | subcat = defin['sub'] 131 | cat_dict[supercat]['categories'].setdefault(subcat, {}) 132 | cat_dict[supercat]['categories'][subcat].setdefault('categories', {}) 133 | cat_dict[supercat]['categories'][subcat].setdefault('keys', []) 134 | if 'category description' in defin and 'subsub' not in defin: 135 | cat_dict[supercat]['categories'][subcat]['description'] = \ 136 | defin['category description'] 137 | if 'subsub' in defin: 138 | subsubcat = defin['subsub'] 139 | basesub = cat_dict[supercat]['categories'][subcat] 140 | basesub['categories'].setdefault(subsubcat, {}) 141 | basesub['categories'][subsubcat].setdefault('categories', {}) 142 | basesub['categories'][subsubcat].setdefault('keys', []) 143 | if 'category description' in defin: 144 | basesub['categories'][subsubcat]['description'] = \ 145 | defin['category description'] 146 | # Special setup for default (uncategorized) 147 | cat_dict[DEFAULT_CATEGORY] = {} 148 | cat_dict[DEFAULT_CATEGORY]['categories'] = {} 149 | cat_dict[DEFAULT_CATEGORY]['keys'] = [] 150 | cat_dict[DEFAULT_CATEGORY]['description'] = DEFAULT_CAT_DESC 151 | cat_dict_n_keys = self._insert_keys(key_dict, cat_dict) 152 | return cat_dict_n_keys 153 | 154 | def _match_key_to_cat(self, key): 155 | # If no match is found, these serve as defaults 156 | supercat = DEFAULT_CATEGORY 157 | subcat = None 158 | subsubcat = None 159 | 160 | for defin in self.cat_defs: 161 | # Check for exact matches 162 | if 'keys' in defin: 163 | if key in defin['keys']: 164 | supercat = defin['super'] 165 | if 'sub' in defin: 166 | subcat = defin['sub'] 167 | if 'subsub' in defin: 168 | subsubcat = defin['subsub'] 169 | # Use the first match we find 170 | return (supercat, subcat, subsubcat) 171 | # Check for regex matches 172 | if 're-keys' in defin: 173 | for re_key in defin['re-keys']: 174 | if re.search(re_key, key): 175 | supercat = defin['super'] 176 | if 'sub' in defin: 177 | subcat = defin['sub'] 178 | if 'subsub' in defin: 179 | subsubcat = defin['subsub'] 180 | return (supercat, subcat, subsubcat) 181 | return (supercat, subcat, subsubcat) 182 | 183 | def _apply_categories(self, key_dict): 184 | """Apply super and sub category attributes to keys in a key_dict.""" 185 | for (key, data) in key_dict.items(): 186 | (supercat, subcat, subsubcat) = self._match_key_to_cat(key) 187 | data['super'] = supercat 188 | data['sub'] = subcat 189 | data['subsub'] = subsubcat 190 | return key_dict 191 | -------------------------------------------------------------------------------- /stat_key_browser/cluster_config.py: -------------------------------------------------------------------------------- 1 | import sys 2 | try: 3 | import isi_sdk_7_2 as isi_sdk 4 | except ImportError as err: 5 | try: 6 | # Try the original SDK library name 7 | import isi_sdk 8 | except ImportError as err: 9 | print('Unable to import isi_sdk_7_2. Please install the Isilon SDK.') 10 | print('See https://github.com/isilon') 11 | print(err) 12 | sys.exit(1) 13 | 14 | 15 | class ApiException(Exception): 16 | def __init__(self, msg): 17 | self.msg = msg 18 | 19 | def __str__(self): 20 | return repr(self.msg) 21 | 22 | 23 | def _get_cluster_version(cluster_ip, username, password): 24 | isi_sdk.configuration.username = username 25 | isi_sdk.configuration.password = password 26 | isi_sdk.configuration.verify_ssl = False 27 | host = 'https://' + cluster_ip + ':8080' 28 | papi = isi_sdk.ApiClient(host) 29 | cluster_api = isi_sdk.ClusterApi(papi) 30 | response = cluster_api.get_cluster_config().to_dict() 31 | if 'onefs_version' not in response: 32 | raise ApiException('Could not find onefs_version in API response: {0}' 33 | .format(response)) 34 | release = response['onefs_version']['release'] 35 | return release 36 | 37 | 38 | def get_release(cluster_ip, username, password): 39 | version = _get_cluster_version(cluster_ip, username, password) 40 | return version 41 | -------------------------------------------------------------------------------- /stat_key_browser/data/key_cats.hexa: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # This file contains the key tagging inputs that get parsed into key_tags.json 3 | # 4 | # Stanzas are separated by 6 colons-> ::::: 5 | # Attr keys are declared with 3 colons-> :::keyname 6 | # Following a key attr declaration is a list of attrs to be assigned 7 | # The keys and attrs will be assigned to the stat keys listed under the key named keys 8 | # Regex stat key name matching is also permitted. 9 | # 10 | # 11 | # -- Exact stat key name matching -- 12 | # :::keys 13 | # node.sensors.key.name 14 | # 15 | # -- Regex stat key name matching 16 | # :::re-keys 17 | # node\.sensors.tem.* 18 | # 19 | # Entries should be ordered from most to least specific. The first entry that 20 | # matches a particular key will assign the category. So placing more general 21 | # matches later in the file ensures that keys are assigned to the most specific 22 | # match possible. 23 | 24 | 25 | ############################################################################### 26 | # ____ 27 | # / ___| ___ _ __ ___ ___ _ __ ___ 28 | # \___ \ / _ \ '_ \/ __|/ _ \| '__/ __| 29 | # ___) | __/ | | \__ \ (_) | | \__ \ 30 | # |____/ \___|_| |_|___/\___/|_| |___/ 31 | # 32 | 33 | :::::: 34 | :::super 35 | Sensors 36 | :::category description 37 | Stats for sensors related to power, temperature, and altitude. 38 | 39 | :::::: 40 | :::super 41 | Sensors 42 | :::sub 43 | Power 44 | :::re-keys 45 | ^node.sensor.power.* 46 | :::category description 47 | Power sensor information. 48 | 49 | :::::: 50 | :::super 51 | Sensors 52 | :::sub 53 | Miscellaneous 54 | :::re-keys 55 | ^node.sensor.misc.* 56 | :::category description 57 | Miscellaneous sensor information. 58 | 59 | :::::: 60 | :::super 61 | Sensors 62 | :::sub 63 | Altitude 64 | :::re-keys 65 | ^node.sensor.altitude.* 66 | :::category description 67 | Altitude sensor information (if node so equipped). 68 | 69 | :::::: 70 | :::super 71 | Sensors 72 | :::sub 73 | Current (Amperage) 74 | :::re-keys 75 | ^node.sensor.curr.* 76 | :::category description 77 | Sensors that display current (amperage) information about various components. 78 | 79 | :::::: 80 | :::super 81 | Sensors 82 | :::sub 83 | Fans 84 | :::re-keys 85 | ^node.sensor.fan.* 86 | :::category description 87 | Fan speed sensors. 88 | 89 | :::::: 90 | :::super 91 | Sensors 92 | :::sub 93 | Temperature 94 | :::re-keys 95 | ^node.sensor.temp.* 96 | :::category description 97 | Temperature sensors for various components. 98 | 99 | :::::: 100 | :::super 101 | Sensors 102 | :::sub 103 | Voltage 104 | :::re-keys 105 | ^node.sensor.volt.* 106 | :::category description 107 | Voltage sensor for various components. 108 | 109 | ############################################################################### 110 | # ____ _ 111 | # / ___|__ _ ___| |__ ___ 112 | # | | / _` |/ __| '_ \ / _ \ 113 | # | |__| (_| | (__| | | | __/ 114 | # \____\__,_|\___|_| |_|\___| 115 | # 116 | 117 | :::::: 118 | :::super 119 | Cache 120 | :::category description 121 | Statistics related to L1, L2, and L3 Caches. 122 | 123 | :::::: 124 | :::super 125 | Cache 126 | :::sub 127 | L1 Cache 128 | :::category description 129 | L1 cache stats 130 | :::re-keys 131 | ^node.ifs.cache.l1.* 132 | 133 | :::::: 134 | :::super 135 | Cache 136 | :::sub 137 | L2 Cache 138 | :::category description 139 | L2 cache stats 140 | :::re-keys 141 | ^node.ifs.cache.l2.* 142 | 143 | :::::: 144 | :::super 145 | Cache 146 | :::sub 147 | L3 Cache 148 | :::re-keys 149 | ^node.ifs.cache.l3.* 150 | :::category description 151 | L3 cache stats 152 | 153 | :::::: 154 | :::super 155 | Cache 156 | :::sub 157 | Other 158 | :::re-keys 159 | ^node.ifs.cache.* 160 | :::category description 161 | Miscelaneous Other Cache Stats 162 | 163 | 164 | ############################################################################### 165 | # ____ _ _ _ 166 | # / ___| (_) ___ _ __ | |_ 167 | # | | | | |/ _ \ '_ \| __| 168 | # | |___| | | __/ | | | |_ 169 | # \____|_|_|\___|_| |_|\__| 170 | # 171 | 172 | 173 | :::::: 174 | :::super 175 | Client 176 | :::category description 177 | Client statistics showing per-client protocol activity. 178 | 179 | :::::: 180 | :::super 181 | Client 182 | :::sub 183 | Top-N Clients 184 | :::re-keys 185 | node.clientstats.proto.* 186 | :::category description 187 | Top-N individual client statistics per protocol and node. 188 | 189 | :::::: 190 | :::super 191 | Client 192 | :::sub 193 | Active 194 | :::re-keys 195 | node.clientstats.active.* 196 | :::category description 197 | Active client counts per protocol and node. 198 | 199 | :::::: 200 | :::super 201 | Client 202 | :::sub 203 | Connected 204 | :::re-keys 205 | ^node.clientstats.connected.* 206 | :::category description 207 | Connected client counts per protocol and node. 208 | 209 | :::::: 210 | :::super 211 | Client 212 | :::sub 213 | Disk IO 214 | :::re-keys 215 | ^node.clientstats.io 216 | :::category description 217 | Per client per disk IO Statistics. 218 | 219 | ############################################################################### 220 | # ____ ____ _ _ 221 | # / ___| _ \| | | | 222 | # | | | |_) | | | | 223 | # | |___| __/| |_| | 224 | # \____|_| \___/ 225 | # 226 | 227 | :::::: 228 | :::super 229 | CPU 230 | :::category description 231 | CPU utilization statistics. 232 | 233 | :::::: 234 | :::super 235 | CPU 236 | :::sub 237 | Node 238 | :::category description 239 | CPU statistics by node. 240 | :::re-keys 241 | ^node.cpu 242 | 243 | :::::: 244 | :::super 245 | CPU 246 | :::sub 247 | Cluster 248 | :::category description 249 | CPU statistics averaged across all nodes in the cluster. 250 | :::re-keys 251 | ^cluster.cpu 252 | 253 | ############################################################################### 254 | # ____ _ 255 | # | _ \ ___ __| |_ _ _ __ ___ 256 | # | | | |/ _ \/ _` | | | | '_ \ / _ \ 257 | # | |_| | __/ (_| | |_| | |_) | __/ 258 | # |____/ \___|\__,_|\__,_| .__/ \___| 259 | # |_| 260 | # 261 | 262 | :::::: 263 | :::super 264 | Deduplication 265 | :::keys 266 | cluster.dedupe.estimated.deduplicated.bytes 267 | cluster.dedupe.estimated.saved.bytes 268 | cluster.dedupe.logical.deduplicated.bytes 269 | cluster.dedupe.logical.saved.bytes 270 | :::re-keys 271 | cluster.dedupe 272 | :::category description 273 | Statistics related to the file deduplication process. 274 | 275 | ############################################################################### 276 | # ____ _ _ 277 | # | _ \(_)___| | __ 278 | # | | | | / __| |/ / 279 | # | |_| | \__ \ < 280 | # |____/|_|___/_|\_\ 281 | # 282 | 283 | :::::: 284 | :::super 285 | Disk 286 | :::category description 287 | Disk status and activity statistics 288 | 289 | :::::: 290 | :::super 291 | Disk 292 | :::sub 293 | Cluster 294 | :::re-keys 295 | ^cluster.disk\..* 296 | :::category description 297 | Disk statistics averaged across all disks in the cluster. 298 | 299 | :::::: 300 | :::super 301 | Disk 302 | :::sub 303 | Node 304 | :::re-keys 305 | ^node.disk\..* 306 | :::category description 307 | Disk statistics per node and per disk. 308 | 309 | 310 | 311 | ############################################################################### 312 | # _ _ _ _ 313 | # | \ | | ___| |___ _____ _ __| | __ 314 | # | \| |/ _ \ __\ \ /\ / / _ \| '__| |/ / 315 | # | |\ | __/ |_ \ V V / (_) | | | < 316 | # |_| \_|\___|\__| \_/\_/ \___/|_| |_|\_\ 317 | # 318 | 319 | 320 | :::::: 321 | :::super 322 | Network 323 | :::category description 324 | Network statistics for both internal (back-end) and external (front-end) network. 325 | 326 | :::::: 327 | :::super 328 | Network 329 | :::sub 330 | Internal 331 | :::category description 332 | Internal (back-end) network statistics. 333 | 334 | :::::: 335 | :::super 336 | Network 337 | :::sub 338 | Internal 339 | :::subsub 340 | Node 341 | :::re-keys 342 | ^node.net.iface. 343 | ^node.net.int. 344 | :::category description 345 | Internal NIC counters by Node 346 | 347 | :::::: 348 | :::super 349 | Network 350 | :::sub 351 | Internal 352 | :::subsub 353 | Cluster 354 | :::re-keys 355 | ^cluster.net.int\..* 356 | :::category description 357 | Internal NIC counters by Cluster 358 | 359 | 360 | :::::: 361 | :::super 362 | Network 363 | :::sub 364 | External 365 | :::category description 366 | External (front-end) network statistics. 367 | 368 | :::::: 369 | :::super 370 | Network 371 | :::sub 372 | External 373 | :::subsub 374 | Node 375 | :::re-keys 376 | ^node.net.ext\..* 377 | :::category description 378 | External NIC counters by Node 379 | 380 | :::::: 381 | :::super 382 | Network 383 | :::sub 384 | External 385 | :::subsub 386 | Cluster 387 | :::re-keys 388 | ^cluster.net.ext\..* 389 | :::category description 390 | External NIC counters by Cluster 391 | 392 | 393 | 394 | ############################################################################### 395 | # _____ _ _ ____ _ 396 | # | ___(_) | ___ / ___| _ _ ___| |_ ___ _ __ ___ 397 | # | |_ | | |/ _ \ \___ \| | | / __| __/ _ \ '_ ` _ \ 398 | # | _| | | | __/ ___) | |_| \__ \ || __/ | | | | | 399 | # |_| |_|_|\___| |____/ \__, |___/\__\___|_| |_| |_| 400 | # |___/ 401 | 402 | :::::: 403 | :::super 404 | File System 405 | :::category description 406 | OneFS File System Statistics. 407 | 408 | :::::: 409 | :::super 410 | File System 411 | :::sub 412 | Cluster 413 | :::category description 414 | File system statistics averaged across all nodes in the cluster. 415 | 416 | :::::: 417 | :::super 418 | File System 419 | :::sub 420 | Cluster 421 | :::subsub 422 | Throughput 423 | :::re-keys 424 | ^ifs.bytes.in.* 425 | ^ifs.bytes.out.* 426 | :::category description 427 | File system throughput by cluster. 428 | 429 | 430 | :::::: 431 | :::super 432 | File System 433 | :::sub 434 | Cluster 435 | :::subsub 436 | Capacity 437 | :::re-keys 438 | ^ifs..*bytes..* 439 | :::category description 440 | Cluster capacity. 441 | 442 | :::::: 443 | :::super 444 | File System 445 | :::sub 446 | Cluster 447 | :::subsub 448 | Other 449 | :::re-keys 450 | ^ifs..* 451 | :::category description 452 | Other miscelaneous cluster file system stats. 453 | 454 | :::::: 455 | :::super 456 | File System 457 | :::sub 458 | Node 459 | :::category description 460 | File system statistics for each node in the cluster. 461 | 462 | :::::: 463 | :::super 464 | File System 465 | :::sub 466 | Node 467 | :::subsub 468 | Throughput 469 | :::re-keys 470 | ^node.ifs.bytes.in.* 471 | ^node.ifs.bytes.out.* 472 | :::category description 473 | File system throughput by node. 474 | 475 | :::::: 476 | :::super 477 | File System 478 | :::sub 479 | Node 480 | :::subsub 481 | Capacity 482 | :::re-keys 483 | ^node.ifs..*bytes..* 484 | :::category description 485 | File system capacity by node. 486 | 487 | 488 | :::::: 489 | :::super 490 | File System 491 | :::sub 492 | Node 493 | :::subsub 494 | File Heat 495 | :::re-keys 496 | ^node.ifs.heat..* 497 | :::category description 498 | File system event rates by file. 499 | 500 | 501 | :::::: 502 | :::super 503 | File System 504 | :::sub 505 | Node 506 | :::subsub 507 | Other 508 | :::re-keys 509 | ^node.ifs..* 510 | :::category description 511 | Other miscelaneous node file system stats 512 | 513 | 514 | ############################################################################### 515 | # _ _ _____ _ 516 | # | | ___ | |__ | ____|_ __ __ _(_)_ __ ___ 517 | # _ | |/ _ \| '_ \ | _| | '_ \ / _` | | '_ \ / _ \ 518 | # | |_| | (_) | |_) | | |___| | | | (_| | | | | | __/ 519 | # \___/ \___/|_.__/ |_____|_| |_|\__, |_|_| |_|\___| 520 | # |___/ 521 | 522 | :::::: 523 | :::super 524 | Job Engine 525 | :::keys 526 | node.je.num_workers 527 | :::category description 528 | Job engine statistics. 529 | 530 | 531 | ############################################################################### 532 | # ____ _ _ 533 | # | _ \ _ __ ___ | |_ ___ ___ ___ | |___ 534 | # | |_) | '__/ _ \| __/ _ \ / __/ _ \| / __| 535 | # | __/| | | (_) | || (_) | (_| (_) | \__ \ 536 | # |_| |_| \___/ \__\___/ \___\___/|_|___/ 537 | # 538 | 539 | :::::: 540 | :::super 541 | Protocols 542 | :::category description 543 | Protocol performance statistics. 544 | 545 | :::::: 546 | :::super 547 | Protocols 548 | :::sub 549 | Node 550 | :::re-keys 551 | ^node.protostats\..* 552 | :::category description 553 | Per protocol performance by Node. 554 | 555 | :::::: 556 | :::super 557 | Protocols 558 | :::sub 559 | Cluster 560 | :::re-keys 561 | ^cluster.protostats\..* 562 | :::category description 563 | Per protocol performance by Cluster. 564 | 565 | 566 | ################################################################################ 567 | # _ _ _ _ 568 | # / \ _ _ __| (_) |_ 569 | # / _ \| | | |/ _` | | __| 570 | # / ___ \ |_| | (_| | | |_ 571 | # /_/ \_\__,_|\__,_|_|\__| 572 | # 573 | # 574 | 575 | :::::: 576 | :::super 577 | Audit 578 | :::re-keys 579 | ^node.audit. 580 | :::category description 581 | Statistics related to the auditing system 582 | 583 | ################################################################################ 584 | # ____ _ 585 | # / ___| _ _ ___| |_ ___ _ __ ___ 586 | # \___ \| | | / __| __/ _ \ '_ ` _ \ 587 | # ___) | |_| \__ \ || __/ | | | | | 588 | # |____/ \__, |___/\__\___|_| |_| |_| 589 | # |___/ 590 | 591 | :::::: 592 | :::super 593 | System 594 | :::category description 595 | OneFS and System level statistics 596 | 597 | :::::: 598 | :::super 599 | System 600 | :::sub 601 | Node Info and Status 602 | :::re-keys 603 | ^cluster.node. 604 | ^node.diskless 605 | ^node.open.files 606 | ^node.uptime 607 | ^node.boottime 608 | ^node.thread.stats 609 | ^node.process.count 610 | ^node.health 611 | 612 | :::category description 613 | Cluster node status, counts, and info statistics 614 | 615 | :::::: 616 | :::super 617 | System 618 | :::sub 619 | System Volumes 620 | :::re-keys 621 | ^node.sysfs 622 | :::category description 623 | Statistics related to the root OS filesystem volumes on each node. 624 | 625 | :::::: 626 | :::super 627 | System 628 | :::sub 629 | Load 630 | :::re-keys 631 | ^node.load 632 | :::category description 633 | System load averages 634 | 635 | :::::: 636 | :::super 637 | System 638 | :::sub 639 | Memory and Internal Cache Usage 640 | :::re-keys 641 | ^node.memory 642 | ^node.malloc 643 | ^node.dfm 644 | ^node.ifm 645 | ^node.mds 646 | ^node.uma 647 | ^node.stf.cache 648 | ^node.rp.stats 649 | :::category description 650 | System memory usage and allocation statistics 651 | 652 | :::::: 653 | :::super 654 | System 655 | :::sub 656 | NFS Operational 657 | :::re-keys 658 | ^node.nfs 659 | :::category description 660 | Low level NFS protocol operational statistics. 661 | 662 | :::::: 663 | :::super 664 | System 665 | :::sub 666 | NVRAM 667 | :::re-keys 668 | ^node.nvram 669 | :::category description 670 | Statistics on the NVRAM hardware and system 671 | 672 | :::::: 673 | :::super 674 | System 675 | :::sub 676 | Cluster Info and Status 677 | :::re-keys 678 | ^cluster.group 679 | ^cluster.alert.info 680 | ^cluster.health 681 | 682 | :::category description 683 | Cluster-wide health and alerts 684 | 685 | :::::: 686 | :::super 687 | System 688 | :::sub 689 | TCP/IP 690 | :::re-keys 691 | ^node.tcp 692 | ^node.ip.stats 693 | :::category description 694 | TCP and IP statistics 695 | 696 | -------------------------------------------------------------------------------- /stat_key_browser/data/key_tags.hexa: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # This file defines tags applied to keys, as well as extra attributes and 3 | # values to be displayed in a key's data block. 4 | # 5 | # Stanzas are separated by 6 colons-> ::::: 6 | # Attr keys are declared with 3 colons-> :::keyname 7 | # Following a key attr declaration is a list of attrs to be assigned 8 | # The keys and attrs will be assigned to the stat keys listed under the key named keys 9 | # Regex stat key name matching is also permitted. 10 | # 11 | # The tags and attributes in a stanza will be assigned to all the keys 12 | # that match the key selectors 'keys' and 're-keys'. 'tags' is a list 13 | # of tags assigned to the stanza's keys. Any other attributes in the 14 | # stanza are arbitrary attibutes that will appear as an additional 15 | # data field in the generated web app. 16 | # 17 | # -- Exact stat key name matching -- 18 | # :::keys 19 | # node.sensors.key.name 20 | # 21 | # -- Regex stat key name matching -- 22 | # :::re-keys 23 | # node\.sensors.tem.* 24 | # 25 | # -- Add additional data tags to keys -- 26 | # :::re-keys 27 | # ^test\. 28 | # :::Extra Info 29 | # Key used for testing only. 30 | # 31 | # 32 | 33 | 34 | 35 | :::::: 36 | :::tags 37 | InsightIQ 38 | cache 39 | :::keys 40 | node.ifs.cache 41 | 42 | 43 | :::::: 44 | :::tags 45 | InsightIQ 46 | cache 47 | :::keys 48 | node.ifs.cache.l3.data.prefetch.hit 49 | node.ifs.cache.l3.data.prefetch.start 50 | node.ifs.cache.l3.data.read.hit 51 | node.ifs.cache.l3.data.read.miss 52 | node.ifs.cache.l3.data.read.start 53 | node.ifs.cache.l3.data.read.wait 54 | node.ifs.cache.l3.meta.prefetch.hit 55 | node.ifs.cache.l3.meta.prefetch.start 56 | node.ifs.cache.l3.meta.read.hit 57 | node.ifs.cache.l3.meta.read.miss 58 | node.ifs.cache.l3.meta.read.start 59 | node.ifs.cache.l3.meta.read.wait 60 | 61 | 62 | :::::: 63 | :::tags 64 | InsightIQ 65 | client 66 | :::keys 67 | node.clientstats.proto.ftp 68 | node.clientstats.proto.hdfs 69 | node.clientstats.proto.http 70 | node.clientstats.proto.nfs3 71 | node.clientstats.proto.nfs4 72 | node.clientstats.proto.nlm 73 | node.clientstats.proto.papi 74 | node.clientstats.proto.siq 75 | node.clientstats.proto.smb1 76 | node.clientstats.proto.smb2 77 | 78 | :::::: 79 | :::tags 80 | client_active 81 | InsightIQ 82 | :::keys 83 | node.clientstats.active.ftp 84 | node.clientstats.active.hdfs 85 | node.clientstats.active.http 86 | node.clientstats.active.nfs3 87 | node.clientstats.active.nfs4 88 | node.clientstats.active.nlm 89 | node.clientstats.active.papi 90 | node.clientstats.active.siq 91 | node.clientstats.active.smb1 92 | node.clientstats.active.smb2 93 | 94 | :::::: 95 | :::tags 96 | client_connected 97 | InsightIQ 98 | :::keys 99 | node.clientstats.connected.ftp 100 | node.clientstats.connected.hdfs 101 | node.clientstats.connected.http 102 | node.clientstats.connected.nfs 103 | node.clientstats.connected.nlm 104 | node.clientstats.connected.papi 105 | node.clientstats.connected.siq 106 | node.clientstats.connected.smb 107 | 108 | :::::: 109 | :::tags 110 | cpu 111 | InsightIQ 112 | :::keys 113 | node.cpu.idle.avg 114 | node.cpu.idle.N 115 | 116 | :::::: 117 | :::tags 118 | dedupe 119 | InsightIQ 120 | :::keys 121 | cluster.dedupe.estimated.deduplicated.bytes 122 | cluster.dedupe.estimated.saved.bytes 123 | cluster.dedupe.logical.deduplicated.bytes 124 | cluster.dedupe.logical.saved.bytes 125 | 126 | 127 | :::::: 128 | :::tags 129 | disk 130 | InsightIQ 131 | :::keys 132 | node.disk.access.latency.all 133 | node.disk.access.slow.all 134 | node.disk.busy.all 135 | node.disk.bytes.in.rate.all 136 | node.disk.bytes.out.rate.all 137 | node.disk.iosched.latency.all 138 | node.disk.iosched.queue.all 139 | node.disk.xfer.size.in.all 140 | node.disk.xfer.size.out.all 141 | node.disk.xfers.in.rate.all 142 | node.disk.xfers.out.rate.all 143 | 144 | :::::: 145 | :::tags 146 | disk_ifs 147 | InsightIQ 148 | :::keys 149 | node.disk.ifs.bytes.total.all 150 | node.disk.ifs.bytes.used.all 151 | 152 | 153 | :::::: 154 | :::tags 155 | ext_interface 156 | InsightIQ 157 | :::keys 158 | node.net.iface.bytes.in.rate.N 159 | node.net.iface.bytes.out.rate.N 160 | node.net.iface.errors.in.rate.N 161 | node.net.iface.errors.out.rate.N 162 | node.net.iface.packets.in.rate.N 163 | node.net.iface.packets.out.rate.N 164 | 165 | 166 | :::::: 167 | :::tags 168 | ext_network 169 | InsightIQ 170 | :::keys 171 | node.net.ext.bytes.in.rate 172 | node.net.ext.bytes.out.rate 173 | node.net.ext.errors.in.rate 174 | node.net.ext.errors.out.rate 175 | node.net.ext.packets.in.rate 176 | node.net.ext.packets.out.rate 177 | 178 | 179 | :::::: 180 | :::tags 181 | ifs_cluster 182 | InsightIQ 183 | :::keys 184 | ifs.bytes.avail 185 | ifs.bytes.free 186 | ifs.bytes.total 187 | ifs.ssd.bytes.avail 188 | ifs.ssd.bytes.free 189 | ifs.ssd.bytes.total 190 | 191 | 192 | :::::: 193 | :::tags 194 | ifs_file_heat 195 | InsightIQ 196 | :::keys 197 | node.ifs.heat.blocked 198 | node.ifs.heat.contended 199 | node.ifs.heat.deadlocked 200 | node.ifs.heat.getattr 201 | node.ifs.heat.link 202 | node.ifs.heat.lock 203 | node.ifs.heat.lookup 204 | node.ifs.heat.read 205 | node.ifs.heat.rename 206 | node.ifs.heat.setattr 207 | node.ifs.heat.unlink 208 | node.ifs.heat.write 209 | 210 | :::::: 211 | :::tags 212 | ifs_heat 213 | InsightIQ 214 | :::keys 215 | node.ifs.heat.blocked.total 216 | node.ifs.heat.contended.total 217 | node.ifs.heat.deadlocked.total 218 | node.ifs.heat.getattr.total 219 | node.ifs.heat.link.total 220 | node.ifs.heat.lock.total 221 | node.ifs.heat.lookup.total 222 | node.ifs.heat.read.total 223 | node.ifs.heat.rename.total 224 | node.ifs.heat.setattr.total 225 | node.ifs.heat.unlink.total 226 | node.ifs.heat.write.total 227 | 228 | 229 | :::::: 230 | :::tags 231 | ifs_throughput 232 | InsightIQ 233 | :::keys 234 | node.ifs.bytes.in.rate 235 | node.ifs.bytes.out.rate 236 | 237 | 238 | :::::: 239 | :::tags 240 | job 241 | InsightIQ 242 | :::keys 243 | node.je.num_workers 244 | 245 | 246 | 247 | :::::: 248 | :::tags 249 | protocol 250 | InsightIQ 251 | :::keys 252 | node.protostats.ftp 253 | node.protostats.hdfs 254 | node.protostats.http 255 | node.protostats.nfs3 256 | node.protostats.nfs4 257 | node.protostats.nlm 258 | node.protostats.papi 259 | node.protostats.siq 260 | node.protostats.smb1 261 | node.protostats.smb2 262 | -------------------------------------------------------------------------------- /stat_key_browser/key_collector.py: -------------------------------------------------------------------------------- 1 | """ 2 | Query the cluster for keys and parse. 3 | 4 | Queries the cluster via PAPI for the list of stat keys. Conversts the key_list 5 | to a key_dict, squashes indexed keys. 6 | """ 7 | 8 | import re 9 | import logging 10 | import sys 11 | try: 12 | import isi_sdk_7_2 as isi_sdk 13 | except ImportError as err: 14 | try: 15 | # Try the original SDK library name 16 | import isi_sdk 17 | except ImportError as err: 18 | print('Unable to import isi_sdk_7_2. Please install the Isilon SDK.') 19 | print('See https://github.com/isilon') 20 | print(err) 21 | sys.exit(1) 22 | 23 | PRIMARY = 0 24 | 25 | 26 | def _basename(key): 27 | """ 28 | Return the base name of a key name. 29 | 30 | a.b.c.1 -> a.b.c 31 | a.b.c -> a.b 32 | """ 33 | return '.'.join(key.split('.')[:-1]) 34 | 35 | 36 | def _get_indexed_key_names(key_dict): 37 | indexed_key_names = [k for k in key_dict if re.match(r'^.*\.[0-9]+$', k)] 38 | return indexed_key_names 39 | 40 | 41 | def _squash_description(desc): 42 | if re.search('[0-9]+$', desc): 43 | return re.sub('[0-9]+$', 'N', desc) 44 | if re.search('index [0-9]+', desc): 45 | return re.sub('index [0-9]+', 'index N', desc) 46 | if re.search('number [0-9]+', desc): 47 | return re.sub('number [0-9]+', 'number N', desc) 48 | # If we made it here, we don't know how to squash it so just return the original 49 | logging.warning('Did not know how to squash description: {0}'.format(desc)) 50 | return desc 51 | 52 | 53 | def _squash_keys(key_dict): 54 | """Squash indexed keys to .N.""" 55 | indexed_key_names = _get_indexed_key_names(key_dict) 56 | indexed_key_bases = {_basename(k): key_dict.pop(k) for k in indexed_key_names} 57 | squashed_keys = {k + '.N': indexed_key_bases[k] for k in indexed_key_bases} 58 | for k in squashed_keys: 59 | key_dict[k] = squashed_keys[k] 60 | if 'key' in key_dict[k]: 61 | # If it has a key name inside squash it too 62 | key_dict[k]['key'] = k 63 | if 'description' in key_dict[k]: 64 | key_dict[k]['description'] = _squash_description(key_dict[k]['description']) 65 | return key_dict 66 | 67 | 68 | def deabbreviate(key_dict): 69 | """De-abbreviate key values.""" 70 | deabbreviations = {'aggregation_type': 71 | {'avg': 'average', 72 | 'max': 'maximum', 73 | 'min': 'minimum'}} 74 | for key in key_dict.values(): 75 | for (field, mappings) in deabbreviations.items(): 76 | if field in key: 77 | if key[field] in mappings: 78 | key[field] = mappings[key[field]] 79 | 80 | 81 | 82 | def _key_list_to_dict(key_list): 83 | """Convert list of keys to dict keyed by stat key name.""" 84 | key_dict = {} 85 | for key in key_list: 86 | key_name = key['key'] 87 | key_dict[key_name] = key 88 | return key_dict 89 | 90 | 91 | def _get_key_list(cluster_ip, username, password): 92 | """Query the cluster for keys and return as a list of keys.""" 93 | isi_sdk.configuration.username = username 94 | isi_sdk.configuration.password = password 95 | isi_sdk.configuration.verify_ssl = False 96 | host = 'https://' + cluster_ip + ':8080' 97 | papi = isi_sdk.ApiClient(host) 98 | statistics = isi_sdk.StatisticsApi(papi) 99 | logging.debug('Querying cluster for stat key list') 100 | key_list = statistics.get_statistics_keys().to_dict()['keys'] 101 | return key_list 102 | 103 | 104 | def _get_key_dict(cluster_ip, username, password): 105 | """ 106 | Query the cluster for keys and return as a dict. 107 | 108 | Each stat key name is a key, and the value is a dict containing the key's 109 | info. 110 | """ 111 | key_list = _get_key_list(cluster_ip, username, password) 112 | key_dict = _key_list_to_dict(key_list) 113 | return key_dict 114 | 115 | 116 | def get_tagged_squashed_dict(cluster_ip, username, password): 117 | """ 118 | Query the cluster for keys and returns squashed keys as dict.. 119 | 120 | Each stat key name is a key, and the value is a dict containing the key's 121 | info. Indexed keys are squashed to .N. 122 | """ 123 | key_dict = _get_key_dict(cluster_ip, username, password) 124 | squashed_dict = _squash_keys(key_dict) 125 | deabbreviate(key_dict) 126 | 127 | return squashed_dict 128 | -------------------------------------------------------------------------------- /stat_key_browser/mapper.py: -------------------------------------------------------------------------------- 1 | """Generate key/category to id mappings.""" 2 | 3 | import hashlib 4 | 5 | 6 | def _convert_to_id(name): 7 | """Convert a string to a suitable form for html attributes and ids.""" 8 | hasher = hashlib.md5() 9 | hasher.update(name.encode('utf-8')) 10 | return hasher.hexdigest() 11 | 12 | 13 | def cat_join(*args): 14 | """Join super, sub, subsub cats into consolidated category name.""" 15 | return '-'.join(args) 16 | 17 | 18 | def cat_id(category): 19 | """Return a category ID for a category name.""" 20 | return 'cat_' + _convert_to_id(category) 21 | 22 | 23 | def key_ids(key_dict, prefix='key_'): 24 | """Iterate through key_dict and map key names to key IDs.""" 25 | mapping = {} 26 | for key_name in key_dict: 27 | mapping[key_name] = prefix + _convert_to_id(key_name) 28 | return mapping 29 | 30 | 31 | def category_ids(key_dict): 32 | """Iterate through key_dict and map category names to category IDs.""" 33 | mapping = {} 34 | for (key, value) in key_dict.items(): 35 | if 'super' in value and value['super'] is not None: 36 | supercat = cat_join(value['super']) 37 | mapping.setdefault(supercat, cat_id(supercat)) 38 | if 'sub' in value and value['sub'] is not None: 39 | subcat = cat_join(value['super'], value['sub']) 40 | mapping.setdefault(subcat, cat_id(subcat)) 41 | if 'subsub' in value and value['subsub'] is not None: 42 | subsubcat = cat_join(value['super'], value['sub'], value['subsub']) 43 | mapping.setdefault(subsubcat, cat_id(subsubcat)) 44 | return mapping 45 | -------------------------------------------------------------------------------- /stat_key_browser/search_terms.py: -------------------------------------------------------------------------------- 1 | KEY = 'key' 2 | DESCRIPTION = 'description' 3 | TAGS = 'tags' 4 | XTRA_ATTRS = 'xtra_attrs' 5 | SEARCH_TERMS = 'search_terms' 6 | 7 | 8 | def list_search_terms(key): 9 | '''Given a stat key, returns a list of search terms.''' 10 | terms = [] 11 | if KEY in key: 12 | terms += key[KEY].lower().split('.') 13 | terms.append(key[KEY].lower()) 14 | if DESCRIPTION in key: 15 | terms += key[DESCRIPTION].lower().split(' ') 16 | if TAGS in key: 17 | terms += [tag.lower() for tag in key[TAGS]] 18 | if XTRA_ATTRS in key: 19 | for attr in key[XTRA_ATTRS].values(): 20 | terms += attr.lower().split(' ') 21 | # dedupe the terms 22 | terms = [x for x in set(terms)] 23 | return terms 24 | 25 | def add_to_dict(key_dict): 26 | '''Add search terms attr to keys in dict.''' 27 | for key in key_dict.values(): 28 | search_terms = list_search_terms(key) 29 | key[SEARCH_TERMS] = search_terms 30 | -------------------------------------------------------------------------------- /stat_key_browser/tagger.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide access to the tag definitions and utilities. 3 | 4 | Reads and parses into a dict the json tag def file. Provides access to this dict. 5 | Takes a key_dict and applies tags per the tag definations. 6 | """ 7 | import logging 8 | import json 9 | import os 10 | import re 11 | import sys 12 | import stat_key_browser 13 | 14 | KEY_TAG_DEFS_FILENAME = 'key_tags.json' 15 | EXTRA_ATTRS = 'xtra_attrs' 16 | 17 | def dedupe_list(l): 18 | s = set(l) 19 | deduped_l = [x for x in s] 20 | return deduped_l 21 | 22 | 23 | class Tagger(object): 24 | def __init__(self, defs=None): 25 | if defs is None: 26 | def_path = self.get_defs_path() 27 | try: 28 | with open(def_path, 'r') as def_file: 29 | defs = json.load(def_file) 30 | except IOError as err: 31 | logging.error('Unable to open {0}: {1}'.format(def_path, err)) 32 | logging.error("Try running 'make tags' to create the tag file") 33 | sys.exit(1) 34 | self.tag_defs = defs 35 | 36 | def _add_tags(self, key, tags): 37 | key.setdefault('tags', []) 38 | key['tags'] += tags 39 | 40 | def _dedupe_tag_lists(self, key_dict): 41 | for data in key_dict.values(): 42 | if 'tags' in data: 43 | data['tags'] = dedupe_list(data['tags']) 44 | return key_dict 45 | 46 | def _pop_keys(self, dictionary, *args): 47 | di = dictionary.copy() 48 | for key in args: 49 | try: 50 | di.pop(key) 51 | except KeyError: 52 | pass 53 | return di 54 | 55 | def _get_extra_attrs(self, defin): 56 | arb_attrs = self._pop_keys(defin.copy(), 'keys', 're-keys', 'tags') 57 | for (extra_attr, val) in arb_attrs.items(): 58 | if len(val) != 1: 59 | msg = 'Extra attibute must have a single value. {0} has value {1}' 60 | raise ValueError(msg.format(extra_attr, val)) 61 | return arb_attrs 62 | 63 | def _add_extra_attrs(self, key, extra_attrs): 64 | """Add extra attrs to a key.""" 65 | for (attr_name, val) in extra_attrs.items(): 66 | key.setdefault(EXTRA_ATTRS, {}) 67 | key[EXTRA_ATTRS][attr_name] = '\n'.join(val) 68 | 69 | def get_defs_path(self): 70 | """Return path to tag definitions file.""" 71 | basedir = stat_key_browser.__path__[0] 72 | defs_path = os.path.join(basedir, 'data', KEY_TAG_DEFS_FILENAME) 73 | logging.debug('Expect key tag definitions at ', path=defs_path) 74 | return defs_path 75 | 76 | def tag_list(self): 77 | """Return a list of all the tags that appear in the definations.""" 78 | tags = [] 79 | for defin in self.tag_defs: 80 | tags += defin['tags'] 81 | tags = dedupe_list(tags) 82 | tags.sort() 83 | return tags 84 | 85 | def tag_keys(self, key_dict): 86 | """Apply tags to keys in key_dict.""" 87 | for defin in self.tag_defs: 88 | extra_attrs = self._get_extra_attrs(defin) 89 | for key in defin.get('keys', []): 90 | self._add_tags(key_dict[key], defin['tags']) 91 | self._add_extra_attrs(key_dict[key], extra_attrs) 92 | if 're-keys' in defin: 93 | for (key, data) in key_dict.items(): 94 | for re_key in defin['re-keys']: 95 | if re.search(re_key, key): 96 | self._add_tags(data, defin['tags']) 97 | self._add_extra_attrs(key_dict[key], extra_attrs) 98 | # Fix multiply matching keys that have duplicated tags. 99 | key_dict = self._dedupe_tag_lists(key_dict) 100 | return key_dict 101 | -------------------------------------------------------------------------------- /stat_key_browser/templates/app_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | EMC | Isilon OneFS Statistics 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |

EMC | Isilon OneFS Statistics

28 |
29 | 30 |
31 |
32 |
33 |
34 |
35 | Search 36 | 38 |
39 |
40 | 44 | 49 |
50 | 51 |
52 |
53 |
54 |
55 | 56 |
57 |
58 |

Categories

59 |
60 |
61 | 62 |
63 |
64 |
65 |
66 |
67 |
68 | No matching keys found. 69 |
70 |
71 |
72 |
73 | {% for category, data in categories.items() %} 74 | 75 |
76 |
77 |
78 |
79 | {{ category }} 82 |
83 |
84 |
85 |
86 | {{ data.description }} 87 |
88 |
89 |
90 |
91 | 92 | {% for subcategory, data in categories[category]['categories'].items() %} 93 |
94 |
95 |
96 |
97 | {{ subcategory }} 100 |
101 |
102 |
103 |
104 | {{ data.description }} 105 |
106 |
107 |
108 |
109 | 110 | {% for subsubcategory, subdata in categories[category]['categories'][subcategory]['categories'].items() %} 111 |
112 |
113 |
114 |
115 | {{ subsubcategory }} 118 |
119 |
120 |
121 |
122 | {{ subdata.description }} 123 |
124 |
125 |
126 |
127 | 128 | 129 | {% for key in categories[category]['categories'][subcategory]['categories'][subsubcategory]['keys'] %} 130 | {% include 'key_template.html' %} 131 | {% endfor %} 132 | 133 |
134 |
135 |
136 |
137 | {% endfor %} 138 | 139 | 140 | {% for key in categories[category]['categories'][subcategory]['keys'] %} 141 | {% include 'key_template.html' %} 142 | {% endfor %} 143 | 144 |
145 |
146 |
147 |
148 | {% endfor %} 149 | 150 | 151 | {% for key in categories[category]['keys'] %} 152 | {% include 'key_template.html' %} 153 | {% endfor %} 154 | 155 |
156 |
157 |
158 |
159 | 160 | {% endfor %} 161 |
162 |
163 |
164 |
165 | 166 |
167 |
168 |
169 | Key list retreived from OneFS {{ cluster['release'] }}
170 | {% if cluster['host'] != None %} 171 | at {{ cluster['host'] }} 172 | {% endif %} 173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 | 182 |
183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /stat_key_browser/templates/key_template.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {{ key }} 6 | {% for tag in keys[key]['tags'] %} 7 | 8 | {% endfor %} 9 |
10 |
11 |
12 |
13 | 14 | 15 | {% if keys[key]['xtra_attrs'] %} 16 | {% for extra_attr, text in keys[key]['xtra_attrs'].items() %} 17 | 18 | {% endfor %} 19 | {% endif%} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | 35 | 36 | 37 | 38 | 56 | 57 | 58 | 59 | 60 | 61 |
Description{{ keys[key]['description'] }}
{{ extra_attr }}{{ text }}
Type{{ keys[key]['type'] }}
Units{{ keys[key]['units'] }}
Interval Detail 39 | {% if keys[key]['policies'] %} 40 | 41 | 42 | 43 | {% for policy in keys[key]['policies'] %} 44 | 45 | 46 | 47 | 48 | 49 | {% endfor %} 50 | 51 |
IntervalPersistentRetention
{{ policy['interval'] }} seconds{{ policy['persistent'] }}{{ policy['retention'] }} seconds
52 | {% else %} 53 | No interval policies defined 54 | {% endif %} 55 |
Downsampling Type{{ keys[key]['aggregation_type'] }}
Policy Cache Time{{ keys[key]['policy_cache_time'] }} seconds
Default Cache Time{{ keys[key]['default_cache_time'] }} seconds
Scope{{ keys[key]['scope'] }}
62 | more detail... 63 |
64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /test1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/test1 -------------------------------------------------------------------------------- /tests/functional/test_functional.py: -------------------------------------------------------------------------------- 1 | class TestBrowserBuilderFunctional(object): 2 | def test_001_stub(self): 3 | pass 4 | -------------------------------------------------------------------------------- /tests/unit/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = tests/* 3 | stat_key_browser/swagger_client/* 4 | -------------------------------------------------------------------------------- /tests/unit/test_browser_builder.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | import stat_key_browser.browser_builder as browser_builder 3 | from stat_key_browser.browser_builder import BrowserBuilder 4 | import stat_key_browser.cluster_config as cluster_config 5 | 6 | 7 | class TestBrowserBuilder(object): 8 | 9 | @patch('stat_key_browser.cluster_config.get_release') 10 | def test_bb_01_cluster_info_false(self, get_release): 11 | get_release.return_value('5.5') 12 | bb = BrowserBuilder('1.2.3.4', 'u', 'p', False) 13 | results = bb._get_cluster_info() 14 | assert results['host'] is None 15 | 16 | @patch('stat_key_browser.cluster_config.get_release') 17 | def test_bb_01_cluster_info_true(self, get_release): 18 | get_release.return_value('5.5') 19 | bb = BrowserBuilder('1.2.3.4', 'u', 'p', True) 20 | results = bb._get_cluster_info() 21 | assert results['host'] == '1.2.3.4' 22 | 23 | @patch('stat_key_browser.cluster_config.get_release') 24 | def test_bb_02_cluster_info_bug_169014(self, get_release): 25 | get_release.side_effect = cluster_config.ApiException('test') 26 | ip = '1.2.3.4' 27 | bb = BrowserBuilder(ip, 'u', 'p', True) 28 | results = bb._get_cluster_info() 29 | assert results['release'] == browser_builder.UNKNOWN_RELEASE 30 | assert results['host'] == ip 31 | -------------------------------------------------------------------------------- /tests/unit/test_categorizer.py: -------------------------------------------------------------------------------- 1 | from stat_key_browser.categorizer import Categorizer 2 | import pytest 3 | 4 | class TestCategorizer(object): 5 | 6 | @pytest.fixture 7 | def cat(self): 8 | """Return a new Categorizer.""" 9 | 10 | # Has 3 definitions, 4 categories 11 | cat_defs_3 = [ 12 | { 13 | "category description": [ 14 | "Power sensor information." 15 | ], 16 | "super": [ 17 | "Sensors" 18 | ], 19 | "sub": [ 20 | "Power" 21 | ], 22 | "re-keys": [ 23 | "^node.sensor.power" 24 | ] 25 | }, 26 | { 27 | "category description": [ 28 | "Walrus sensor information." 29 | ], 30 | "super": [ 31 | "Walruses" 32 | ], 33 | "sub": [ 34 | "Tusks" 35 | ], 36 | "keys": [ 37 | "^node.sensor.walrus.tusk" 38 | ] 39 | }, 40 | { 41 | "category description": [ 42 | "Altitude sensor information (if node so equipped)." 43 | ], 44 | "super": [ 45 | "Sensors" 46 | ], 47 | "sub": [ 48 | "Power" 49 | ], 50 | "subsub": [ 51 | "A" 52 | ], 53 | "re-keys": [ 54 | "^node.sensor.altitude" 55 | ], 56 | "keys": [ 57 | "node.sensor.power.A" 58 | ] 59 | }, 60 | { 61 | "keys": [ 62 | "node.protostats.smb1", 63 | "node.protostats.smb2" 64 | ], 65 | "category description": [ 66 | "Per protocol performance" 67 | ], 68 | "super": [ 69 | "Protocols" 70 | ] 71 | } 72 | ] 73 | return cat_defs_3 74 | 75 | @pytest.fixture 76 | def key_dict(self): 77 | """Return a key_dict.""" 78 | key_dict_3= { 79 | "node.sensor.power.A": {}, 80 | "node.sensor.power.B": {}, 81 | "node.sensor.altitude.A": {}, 82 | "node.sensor.altitude.B": {}, 83 | "node.protostats.smb1": {}, 84 | "node.protostats.smb2": {}, 85 | "node.protostats.smb3": {}, 86 | "node.sensor.walrus.tusk": {} 87 | } 88 | return key_dict_3 89 | 90 | 91 | def assert_is_string(self, subject): 92 | assert isinstance(subject, (str, unicode)) 93 | 94 | 95 | def test_categorizer_init_00(self, cat): 96 | categorizer = Categorizer(cat) 97 | assert len(categorizer.cat_defs) == len(cat) 98 | for defin in categorizer.cat_defs: 99 | assert defin['super'] is not None 100 | if 'sub' in defin: 101 | assert defin['sub'] is not None 102 | 103 | def test_validate_defs_00(self): 104 | Categorizer([{'super': ['a']}]) 105 | 106 | def test_validate_defs_01(self): 107 | with pytest.raises(ValueError): 108 | Categorizer([{'badkey': None, 'super': ['a']}]) 109 | 110 | def test_validate_defs_02(self): 111 | with pytest.raises(ValueError): 112 | Categorizer([{'super': ['a', 'b']}]) 113 | 114 | def test_validate_defs_03(self): 115 | with pytest.raises(ValueError): 116 | Categorizer([{'sub': ['a']}]) 117 | 118 | def test_validate_defs_04(self): 119 | with pytest.raises(ValueError): 120 | Categorizer([{'super': 'a', 'sub': ['a', 'b']}]) 121 | 122 | def test_defs_path(self, cat): 123 | categorizer = Categorizer(cat) 124 | results = categorizer._get_defs_path() 125 | assert(results is not None) 126 | 127 | 128 | def test_categories_list_00(self, cat): 129 | categorizer = Categorizer(cat) 130 | expected = ['Power', 'Sensors', 'Protocols', 'Walruses', 'Tusks'] 131 | results = categorizer.get_categories_list() 132 | assert len(results) == len(expected) 133 | for cat in results: 134 | assert cat in expected 135 | for exp in expected: 136 | assert exp in results 137 | 138 | def test_categories_dict(self, cat, key_dict): 139 | categorizer = Categorizer(cat) 140 | expected_supers = ['Sensors', 'Protocols', 'Walruses', categorizer.default_category] 141 | expected_subs = ['Power', 'Altitude', 'Tusks'] 142 | expected_subsubs = ['A'] 143 | results = categorizer.categorize(key_dict) 144 | for res_super in results.values(): 145 | # Each super category should have categories and keys 146 | assert 'categories' in res_super 147 | assert 'keys' in res_super 148 | for res_sub in res_super['categories'].values(): 149 | # Each sub should have categories and keys 150 | assert 'categories' in res_sub 151 | assert 'keys' in res_sub 152 | # Sub cats should be as expected 153 | for sub_cat in res_sub['categories']: 154 | assert sub_cat in expected_subsubs 155 | for subsub_cat in res_sub['categories'].values(): 156 | assert 'keys' in subsub_cat 157 | assert subsub_cat['keys'] is not None 158 | for subsub_cat in res_sub['categories']: 159 | assert subsub_cat in expected_subsubs 160 | for super_cat in results: 161 | assert super_cat in expected_supers 162 | for expected_super in expected_supers: 163 | assert expected_super in results 164 | assert 'Sensors' in results 165 | assert 'Power' in results['Sensors']['categories'] 166 | assert 'A' in results['Sensors']['categories']['Power']['categories'] 167 | -------------------------------------------------------------------------------- /tests/unit/test_cluster_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from mock import patch 3 | import pytest 4 | import stat_key_browser.cluster_config as cluster_config 5 | 6 | class TestClusterConfig(object): 7 | 8 | @patch('stat_key_browser.cluster_config._get_cluster_version') 9 | def test_0_get_cluster_release(self, get_cluster_version): 10 | get_cluster_version.return_value = '2.0' 11 | results = cluster_config.get_release('4.3.2.1', 'u', 'p') 12 | assert results == '2.0' 13 | 14 | @patch('stat_key_browser.cluster_config._get_cluster_version') 15 | def test_0_get_cluster_release(self, get_cluster_version): 16 | get_cluster_version.return_value = '1.0' 17 | results = cluster_config.get_release('4.3.2.1', 'u', 'p') 18 | assert results == '1.0' 19 | 20 | 21 | @patch('isi_sdk.ClusterApi.get_cluster_config') 22 | def test_1_get_cluster_version(self, cluster_vers): 23 | canned = {'onefs_version': {'release': '1.0'}} 24 | response = MockClusterVersionResponse(canned) 25 | cluster_vers.return_value = response 26 | results = cluster_config._get_cluster_version('4.3.2.1', 'u', 'p') 27 | assert results == '1.0' 28 | 29 | @patch('isi_sdk.ClusterApi.get_cluster_config') 30 | def test_3_get_cluster_versions_bug_169014(self, cluster_vers): 31 | response = MockClusterVersionResponse({}) 32 | cluster_vers.return_value = response 33 | with pytest.raises(cluster_config.ApiException): 34 | cluster_config._get_cluster_version('4.3.2.1', 'u', 'p') 35 | 36 | 37 | 38 | class MockClusterVersionResponse(object): 39 | def __init__(self, response): 40 | self.response = response 41 | 42 | def to_dict(self): 43 | return self.response 44 | -------------------------------------------------------------------------------- /tests/unit/test_key_collector.py: -------------------------------------------------------------------------------- 1 | from mock import patch 2 | import isi_sdk 3 | import unittest 4 | import stat_key_browser.key_collector as key_collector 5 | 6 | 7 | class TestKeyCollector(unittest.TestCase): 8 | 9 | def test_key_list_to_dict(self): 10 | as_list = [{'key': 'keyname', 'data': 'stuff'}] 11 | as_dict = key_collector._key_list_to_dict(as_list) 12 | assert(as_dict['keyname'] == {'key': 'keyname', 'data': 'stuff'}) 13 | 14 | def test_squash_keys_1(self): 15 | keys = {'a.a': {}, 'a.b': {}, 'a.1': {}, 'a.2': {}, 'c.1.a': {}} 16 | expected = {'a.a': {}, 'a.b': {}, 'a.N': {}, 'c.1.a': {}} 17 | results = key_collector._squash_keys(keys) 18 | assert(results == expected) 19 | 20 | def test_squash_keys_2(self): 21 | keys = {'a.a': {}, 'a.b': {}, 'a.1': {}, 'a.2': {}, 'c.1.a': {}, 22 | 'a.4': {}, 'a.9999999': {}} 23 | expected = {'a.a': {}, 'a.b': {}, 'a.N': {}, 'c.1.a': {}} 24 | results = key_collector._squash_keys(keys) 25 | assert(results == expected) 26 | 27 | def test_squash_keys_desc_1(self): 28 | keys = {'a.a': {}, 'a.1': {'key': 'a.1', 'description': 'index 1'}, 'a.2': {'key': 'a.2', 'description': 'index 2'}} 29 | expected = {'a.a': {}, 'a.N': {'key': 'a.N', 'description': 'index N'}} 30 | results = key_collector._squash_keys(keys) 31 | assert(results == expected) 32 | 33 | def test_get_indexed_key_names(self): 34 | keys = {'a.a': {}, 'a.b': {}, 'a.1': {}, 'a.2': {}, 'c.1.a': {}} 35 | expected = ['a.1', 'a.2'] 36 | results = key_collector._get_indexed_key_names(keys) 37 | assert(results.sort() == expected.sort()) 38 | 39 | def test_basename_1(self): 40 | assert('a.b.c' == key_collector._basename('a.b.c.1')) 41 | 42 | def test_basename_2(self): 43 | assert('a.b' == key_collector._basename('a.b.c')) 44 | 45 | def test_basename_3(self): 46 | assert('a' == key_collector._basename('a.b')) 47 | 48 | def test_basename_4(self): 49 | assert('' == key_collector._basename('a')) 50 | 51 | def test_basename_5(self): 52 | assert('a!,___' == key_collector._basename('a!,___.!!$#')) 53 | 54 | def test_squash_description_1(self): 55 | desc = 'Input transfers per second for disk with index 10' 56 | expected = 'Input transfers per second for disk with index N' 57 | results = key_collector._squash_description(desc) 58 | assert(results == expected) 59 | 60 | def test_squash_description_2(self): 61 | desc = 'Input errors per second for interface 15' 62 | expected = 'Input errors per second for interface N' 63 | results = key_collector._squash_description(desc) 64 | assert(results == expected) 65 | 66 | def test_squash_description_3(self): 67 | desc = 'no: of read accesses in bin 4 44' 68 | expected = 'no: of read accesses in bin 4 N' 69 | results = key_collector._squash_description(desc) 70 | assert(results == expected) 71 | 72 | def test_squash_description_4(self): 73 | desc = 'Reading in Celsius of temperature sensor number 25' 74 | expected = 'Reading in Celsius of temperature sensor number N' 75 | results = key_collector._squash_description(desc) 76 | assert(results == expected) 77 | 78 | def test_squash_description_5(self): 79 | desc = 'missing stat' 80 | expected = 'missing stat' 81 | results = key_collector._squash_description(desc) 82 | assert(results == expected) 83 | 84 | def test_squash_description_6(self): 85 | desc = 'True if interface number 10 is internal' 86 | expected = 'True if interface number N is internal' 87 | results = key_collector._squash_description(desc) 88 | assert(results == expected) 89 | 90 | def test_squash_description_7(self): 91 | desc = 'Status of battery with index 1. 0 = Unknown, 1 = Unable to communicate with battery charger, 2 = Charger unable to operate, 3 = Battery pack disconnected, 4 = Charge paused due to temperature error, 5 = Charge C/10 paused due to temperature error, 6 = Good, 7 = Battery unable to accept charge, 8 = Battery charging normally, 9 = Good, 10 = Good, 11 = Battery charge circuit shutdown, 12 = Good, 13 = Battery tray removed, 14 = Battery failed, 15 = Battery overcharged' 92 | expected = 'Status of battery with index N. 0 = Unknown, 1 = Unable to communicate with battery charger, 2 = Charger unable to operate, 3 = Battery pack disconnected, 4 = Charge paused due to temperature error, 5 = Charge C/10 paused due to temperature error, 6 = Good, 7 = Battery unable to accept charge, 8 = Battery charging normally, 9 = Good, 10 = Good, 11 = Battery charge circuit shutdown, 12 = Good, 13 = Battery tray removed, 14 = Battery failed, 15 = Battery overcharged' 93 | results = key_collector._squash_description(desc) 94 | assert(results == expected) 95 | 96 | def test_deabbreviate_1(self): 97 | key_dict = {'a': {'aggregation_type': 'avg'}} 98 | key_collector.deabbreviate(key_dict) 99 | self.assertEqual('average', key_dict['a']['aggregation_type']) 100 | 101 | def test_deabbreviate_2(self): 102 | key_dict = {'a': {'aggregation_type': 'max'}} 103 | key_collector.deabbreviate(key_dict) 104 | self.assertEqual('maximum', key_dict['a']['aggregation_type']) 105 | 106 | def test_deabbreviate_3(self): 107 | key_dict = {'a': {'key': 'max'}} 108 | key_collector.deabbreviate(key_dict) 109 | self.assertEqual('max', key_dict['a']['key']) 110 | 111 | @patch('stat_key_browser.key_collector._get_key_list') 112 | def test_get_key_dict_1(self, mock_method): 113 | mock_method.return_value = [{'key': 'a.a'}] 114 | result = key_collector._get_key_dict('testing123', 'user1', 'pass1') 115 | assert 'a.a' in result 116 | mock_method.assert_called_with('testing123', 'user1', 'pass1') 117 | 118 | @patch.object(isi_sdk, 'ApiClient') 119 | def test_get_key_list_1(self, mock_method): 120 | key_collector._get_key_list('testing123', 'user', 'pass') 121 | mock_method.assert_called_with('https://testing123:8080') 122 | 123 | @patch('stat_key_browser.key_collector._get_key_dict') 124 | def test_get_tagged_squashed(self, mock_method): 125 | mock_method.return_value = {'a.a': {}, 'a.b': {}} 126 | results = key_collector.get_tagged_squashed_dict('testing123', 'user1', 127 | 'pass1') 128 | assert 'a.a' in results 129 | assert 'a.b' in results 130 | mock_method.assert_called_with('testing123', 'user1', 'pass1') 131 | -------------------------------------------------------------------------------- /tests/unit/test_mapper.py: -------------------------------------------------------------------------------- 1 | import stat_key_browser.mapper as mapper 2 | 3 | class TestKeyMapper(object): 4 | 5 | def test_key_ids_00(self): 6 | key_dict = {'a': 1, 'b': 2, 'c': 3} 7 | results = mapper.key_ids(key_dict) 8 | assert(len(results) == len(key_dict)) 9 | for key in key_dict: 10 | assert(key in results) 11 | assert(results['a'] != results['b']) 12 | assert(results['a'] != results['c']) 13 | assert(results['b'] != results['c']) 14 | 15 | def test_key_ids_01(self): 16 | key_dict = {'a b.c': 1} 17 | results = mapper.key_ids(key_dict) 18 | assert(' ' not in results['a b.c']) 19 | assert('.' not in results['a b.c']) 20 | 21 | def test_cat_ids_00(self): 22 | key_dict = {'a': {'super': 'cat1', 'sub': 'cat2', 'subsub': 'cat3'}} 23 | results = mapper.category_ids(key_dict) 24 | assert('cat1' in results) 25 | assert('cat1-cat2' in results) 26 | assert('cat1-cat2-cat3' in results) 27 | assert('a' not in results) 28 | assert(results['cat1'] is not None) 29 | assert(results['cat1-cat2'] != results['cat1']) 30 | assert(results['cat1-cat2-cat3'] != results['cat1']) 31 | assert(results['cat1-cat2-cat3'] != results['cat1-cat2']) 32 | 33 | def test_cat_ids_01(self): 34 | key_dict = {'a': {'super': 'cat1'}} 35 | results = mapper.category_ids(key_dict) 36 | assert('cat1' in results) 37 | -------------------------------------------------------------------------------- /tests/unit/test_search_terms.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from stat_key_browser.search_terms import list_search_terms, add_to_dict 3 | from stat_key_browser.search_terms import DESCRIPTION, TAGS, KEY 4 | from stat_key_browser.search_terms import XTRA_ATTRS, SEARCH_TERMS 5 | 6 | class TestSearchTerms(unittest.TestCase): 7 | 8 | def setUp(self, *args, **kawgs): 9 | self.keyname = {KEY: 'one.space here.CAPITAL'} 10 | self.key_desc = {DESCRIPTION: 'A b C'} 11 | 12 | self.tags = ['ap', 'ba', 'ch', 'da'] 13 | self.key_tags = {TAGS: self.tags} 14 | 15 | tags = ['aa' for x in range(6)] 16 | self.key_tags_dup = {TAGS: tags} 17 | 18 | xtra_attrs = {'attr1': 'value1', 'attr2': 'value2'} 19 | self.key_xtra_attrs = {XTRA_ATTRS: xtra_attrs} 20 | print('key_xtra_attrs: {0}'.format(self.key_xtra_attrs)) 21 | 22 | def test_search_terms_01_empty(self): 23 | results = list_search_terms({}) 24 | self.assertEqual(results, []) 25 | 26 | def test_search_terms_02_keyname(self): 27 | results = list_search_terms(self.keyname) 28 | self.assertEqual(len(results), 4) 29 | self.assertIn('one', results) 30 | self.assertIn('space here', results) 31 | self.assertIn('capital', results) 32 | self.assertIn('one.space here.capital', results) 33 | 34 | 35 | def test_search_terms_04_description(self): 36 | data = self.key_desc 37 | results = list_search_terms(data) 38 | self.assertEqual(len(results), 3) 39 | self.assertIn('a', results) 40 | self.assertIn('b', results) 41 | self.assertIn('c', results) 42 | 43 | def test_search_terms_05_tags(self): 44 | data = self.key_tags 45 | results = list_search_terms(data) 46 | self.assertEqual(len(results), len(self.tags)) 47 | for tag in self.tags: 48 | self.assertIn(tag, results) 49 | 50 | def test_search_terms_06_tags_dup(self): 51 | data = self.key_tags_dup 52 | results = list_search_terms(data) 53 | self.assertEqual(len(results), 1) 54 | self.assertIn('aa', results) 55 | 56 | def test_search_terms_10_xtra_attr(self): 57 | data = self.key_xtra_attrs 58 | results = list_search_terms(data) 59 | self.assertEqual(len(results), 2) 60 | self.assertIn('value1', results) 61 | self.assertIn('value2', results) 62 | 63 | def test_search_terms_20_multi(self): 64 | expected = ['a', 'b', 'c', 'ap', 'ba', 'ch', 'da', 65 | 'value1', 'value2'] 66 | data = {} 67 | data.update(self.key_desc) 68 | data.update(self.key_tags) 69 | data.update(self.key_xtra_attrs) 70 | results = list_search_terms(data) 71 | self.assertEqual(len(results), len(expected), 72 | 'expected {0} but got {1}'.format(expected, results)) 73 | for expect in expected: 74 | self.assertIn(expect, results) 75 | 76 | def test_search_terms_30_add_attr(self): 77 | key_dict = {'key1': self.key_desc} 78 | add_to_dict(key_dict) 79 | self.assertTrue(len(key_dict), 1) 80 | result_key = list(key_dict.values())[0] 81 | self.assertIn(SEARCH_TERMS, result_key) 82 | for term in ['a', 'b', 'c']: 83 | self.assertIn(term, result_key[SEARCH_TERMS]) 84 | -------------------------------------------------------------------------------- /tests/unit/test_tagger.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import stat_key_browser.tagger as tagger 3 | from stat_key_browser.tagger import Tagger 4 | 5 | class TestTagger(unittest.TestCase): 6 | 7 | 8 | def test_dedupe_list_00(self): 9 | l = [1, 1] 10 | result = tagger.dedupe_list(l) 11 | assert result == [1] 12 | 13 | def test_dedupe_list_01(self): 14 | l = [1] 15 | result = tagger.dedupe_list(l) 16 | assert result == [1] 17 | 18 | def test_dedupe_list_02(self): 19 | l = [1, 2, 1, 2] 20 | result = tagger.dedupe_list(l) 21 | assert 1 in result 22 | assert 2 in result 23 | assert len(result) == 2 24 | 25 | def test_dedupe_list_03(self): 26 | l = [1, 2, 3] 27 | result = tagger.dedupe_list(l) 28 | assert 1 in result 29 | assert 2 in result 30 | assert 3 in result 31 | assert len(result) == 3 32 | 33 | def test_dedupe_tag_lists_00(self): 34 | tg = Tagger([]) 35 | result = tg._dedupe_tag_lists({'a': {'tags': ['a', 'a', 'b', 'c']}}) 36 | rtl = result['a']['tags'] 37 | assert 'a' in rtl 38 | assert 'b' in rtl 39 | assert 'c' in rtl 40 | assert len(rtl) == 3 41 | 42 | def test_tag_list_00(self): 43 | tg = Tagger([{'tags': ['a', 'b', 'c']}, {'tags': ['d', 'e']}]) 44 | result = tg.tag_list() 45 | assert 'a' in result 46 | assert 'b' in result 47 | assert 'c' in result 48 | assert 'd' in result 49 | assert 'e' in result 50 | assert len(result) == 5 51 | 52 | def test_tag_keys_00(self): 53 | tg = Tagger([{'keys': ['g', 'h'], 'tags': ['a', 'b']}]) 54 | result = tg.tag_keys({'g': {}, 'h': {}, 'i': {}}) 55 | assert 'tags' in result['g'] 56 | assert 'tags' in result['h'] 57 | assert 'tags' not in result['i'] 58 | assert 'a' in result['g']['tags'] 59 | assert 'b' in result['g']['tags'] 60 | assert 'a' in result['h']['tags'] 61 | assert 'b' in result['h']['tags'] 62 | 63 | def test_tag_keys_re_00(self): 64 | tg = Tagger([{'re-keys': ['g', 'h'], 'tags': ['a', 'b']}]) 65 | result = tg.tag_keys({'ogo': {}, 'oho': {}, 'i': {}}) 66 | assert 'tags' in result['ogo'] 67 | assert 'tags' in result['oho'] 68 | assert 'tags' not in result['i'] 69 | assert 'a' in result['ogo']['tags'] 70 | assert 'b' in result['ogo']['tags'] 71 | assert 'a' in result['oho']['tags'] 72 | assert 'b' in result['oho']['tags'] 73 | 74 | def test_pop_keys_00(self): 75 | tg = Tagger([{'re-keys': ['g', 'h'], 'tags': ['a', 'b']}]) 76 | data = {'a': 1, 'b': 2} 77 | result = tg._pop_keys(data, 'b', 'c') 78 | assert 'a' in result 79 | assert len(result) == 1 80 | 81 | def test_get_arb_attrs_00(self): 82 | tg = Tagger([{'re-keys': ['g', 'h'], 'tags': ['a', 'b']}]) 83 | 84 | defin = {'re-keys': ['g', 'h'], 'tags': ['a', 'b']} 85 | result = tg._get_extra_attrs(defin) 86 | assert len(result) == 0 87 | assert result == {} 88 | 89 | def test_get_arb_attrs_01(self): 90 | tg = Tagger([{'re-keys': ['g', 'h'], 'tags': ['a', 'b']}]) 91 | 92 | defin = {'re-keys': ['g', 'h'], 'tags': ['a', 'b'], 'arb': ['arbarb']} 93 | result = tg._get_extra_attrs(defin) 94 | assert result == {'arb': 'arbarb'} 95 | 96 | def test_get_arb_attrs_01(self): 97 | tg = Tagger([{'re-keys': ['g', 'h'], 'tags': ['a', 'b']}]) 98 | 99 | defin = {'re-keys': ['g', 'h'], 'tags': ['a', 'b'], 'arb': ['arbarb', 'arbTWO']} 100 | self.assertRaises(ValueError, tg._get_extra_attrs, defin) 101 | -------------------------------------------------------------------------------- /web_app/css/bootstrap-theme.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | .btn-default, 7 | .btn-primary, 8 | .btn-success, 9 | .btn-info, 10 | .btn-warning, 11 | .btn-danger { 12 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); 13 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 14 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); 15 | } 16 | .btn-default:active, 17 | .btn-primary:active, 18 | .btn-success:active, 19 | .btn-info:active, 20 | .btn-warning:active, 21 | .btn-danger:active, 22 | .btn-default.active, 23 | .btn-primary.active, 24 | .btn-success.active, 25 | .btn-info.active, 26 | .btn-warning.active, 27 | .btn-danger.active { 28 | -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 29 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); 30 | } 31 | .btn-default.disabled, 32 | .btn-primary.disabled, 33 | .btn-success.disabled, 34 | .btn-info.disabled, 35 | .btn-warning.disabled, 36 | .btn-danger.disabled, 37 | .btn-default[disabled], 38 | .btn-primary[disabled], 39 | .btn-success[disabled], 40 | .btn-info[disabled], 41 | .btn-warning[disabled], 42 | .btn-danger[disabled], 43 | fieldset[disabled] .btn-default, 44 | fieldset[disabled] .btn-primary, 45 | fieldset[disabled] .btn-success, 46 | fieldset[disabled] .btn-info, 47 | fieldset[disabled] .btn-warning, 48 | fieldset[disabled] .btn-danger { 49 | -webkit-box-shadow: none; 50 | box-shadow: none; 51 | } 52 | .btn-default .badge, 53 | .btn-primary .badge, 54 | .btn-success .badge, 55 | .btn-info .badge, 56 | .btn-warning .badge, 57 | .btn-danger .badge { 58 | text-shadow: none; 59 | } 60 | .btn:active, 61 | .btn.active { 62 | background-image: none; 63 | } 64 | .btn-default { 65 | text-shadow: 0 1px 0 #fff; 66 | background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); 67 | background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); 68 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); 69 | background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); 70 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); 71 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 72 | background-repeat: repeat-x; 73 | border-color: #dbdbdb; 74 | border-color: #ccc; 75 | } 76 | .btn-default:hover, 77 | .btn-default:focus { 78 | background-color: #e0e0e0; 79 | background-position: 0 -15px; 80 | } 81 | .btn-default:active, 82 | .btn-default.active { 83 | background-color: #e0e0e0; 84 | border-color: #dbdbdb; 85 | } 86 | .btn-default.disabled, 87 | .btn-default[disabled], 88 | fieldset[disabled] .btn-default, 89 | .btn-default.disabled:hover, 90 | .btn-default[disabled]:hover, 91 | fieldset[disabled] .btn-default:hover, 92 | .btn-default.disabled:focus, 93 | .btn-default[disabled]:focus, 94 | fieldset[disabled] .btn-default:focus, 95 | .btn-default.disabled.focus, 96 | .btn-default[disabled].focus, 97 | fieldset[disabled] .btn-default.focus, 98 | .btn-default.disabled:active, 99 | .btn-default[disabled]:active, 100 | fieldset[disabled] .btn-default:active, 101 | .btn-default.disabled.active, 102 | .btn-default[disabled].active, 103 | fieldset[disabled] .btn-default.active { 104 | background-color: #e0e0e0; 105 | background-image: none; 106 | } 107 | .btn-primary { 108 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); 109 | background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); 110 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); 111 | background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); 112 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); 113 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 114 | background-repeat: repeat-x; 115 | border-color: #245580; 116 | } 117 | .btn-primary:hover, 118 | .btn-primary:focus { 119 | background-color: #265a88; 120 | background-position: 0 -15px; 121 | } 122 | .btn-primary:active, 123 | .btn-primary.active { 124 | background-color: #265a88; 125 | border-color: #245580; 126 | } 127 | .btn-primary.disabled, 128 | .btn-primary[disabled], 129 | fieldset[disabled] .btn-primary, 130 | .btn-primary.disabled:hover, 131 | .btn-primary[disabled]:hover, 132 | fieldset[disabled] .btn-primary:hover, 133 | .btn-primary.disabled:focus, 134 | .btn-primary[disabled]:focus, 135 | fieldset[disabled] .btn-primary:focus, 136 | .btn-primary.disabled.focus, 137 | .btn-primary[disabled].focus, 138 | fieldset[disabled] .btn-primary.focus, 139 | .btn-primary.disabled:active, 140 | .btn-primary[disabled]:active, 141 | fieldset[disabled] .btn-primary:active, 142 | .btn-primary.disabled.active, 143 | .btn-primary[disabled].active, 144 | fieldset[disabled] .btn-primary.active { 145 | background-color: #265a88; 146 | background-image: none; 147 | } 148 | .btn-success { 149 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); 150 | background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); 151 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); 152 | background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); 153 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); 154 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 155 | background-repeat: repeat-x; 156 | border-color: #3e8f3e; 157 | } 158 | .btn-success:hover, 159 | .btn-success:focus { 160 | background-color: #419641; 161 | background-position: 0 -15px; 162 | } 163 | .btn-success:active, 164 | .btn-success.active { 165 | background-color: #419641; 166 | border-color: #3e8f3e; 167 | } 168 | .btn-success.disabled, 169 | .btn-success[disabled], 170 | fieldset[disabled] .btn-success, 171 | .btn-success.disabled:hover, 172 | .btn-success[disabled]:hover, 173 | fieldset[disabled] .btn-success:hover, 174 | .btn-success.disabled:focus, 175 | .btn-success[disabled]:focus, 176 | fieldset[disabled] .btn-success:focus, 177 | .btn-success.disabled.focus, 178 | .btn-success[disabled].focus, 179 | fieldset[disabled] .btn-success.focus, 180 | .btn-success.disabled:active, 181 | .btn-success[disabled]:active, 182 | fieldset[disabled] .btn-success:active, 183 | .btn-success.disabled.active, 184 | .btn-success[disabled].active, 185 | fieldset[disabled] .btn-success.active { 186 | background-color: #419641; 187 | background-image: none; 188 | } 189 | .btn-info { 190 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 191 | background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); 192 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); 193 | background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); 195 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 196 | background-repeat: repeat-x; 197 | border-color: #28a4c9; 198 | } 199 | .btn-info:hover, 200 | .btn-info:focus { 201 | background-color: #2aabd2; 202 | background-position: 0 -15px; 203 | } 204 | .btn-info:active, 205 | .btn-info.active { 206 | background-color: #2aabd2; 207 | border-color: #28a4c9; 208 | } 209 | .btn-info.disabled, 210 | .btn-info[disabled], 211 | fieldset[disabled] .btn-info, 212 | .btn-info.disabled:hover, 213 | .btn-info[disabled]:hover, 214 | fieldset[disabled] .btn-info:hover, 215 | .btn-info.disabled:focus, 216 | .btn-info[disabled]:focus, 217 | fieldset[disabled] .btn-info:focus, 218 | .btn-info.disabled.focus, 219 | .btn-info[disabled].focus, 220 | fieldset[disabled] .btn-info.focus, 221 | .btn-info.disabled:active, 222 | .btn-info[disabled]:active, 223 | fieldset[disabled] .btn-info:active, 224 | .btn-info.disabled.active, 225 | .btn-info[disabled].active, 226 | fieldset[disabled] .btn-info.active { 227 | background-color: #2aabd2; 228 | background-image: none; 229 | } 230 | .btn-warning { 231 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 232 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); 233 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); 234 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); 235 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); 236 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 237 | background-repeat: repeat-x; 238 | border-color: #e38d13; 239 | } 240 | .btn-warning:hover, 241 | .btn-warning:focus { 242 | background-color: #eb9316; 243 | background-position: 0 -15px; 244 | } 245 | .btn-warning:active, 246 | .btn-warning.active { 247 | background-color: #eb9316; 248 | border-color: #e38d13; 249 | } 250 | .btn-warning.disabled, 251 | .btn-warning[disabled], 252 | fieldset[disabled] .btn-warning, 253 | .btn-warning.disabled:hover, 254 | .btn-warning[disabled]:hover, 255 | fieldset[disabled] .btn-warning:hover, 256 | .btn-warning.disabled:focus, 257 | .btn-warning[disabled]:focus, 258 | fieldset[disabled] .btn-warning:focus, 259 | .btn-warning.disabled.focus, 260 | .btn-warning[disabled].focus, 261 | fieldset[disabled] .btn-warning.focus, 262 | .btn-warning.disabled:active, 263 | .btn-warning[disabled]:active, 264 | fieldset[disabled] .btn-warning:active, 265 | .btn-warning.disabled.active, 266 | .btn-warning[disabled].active, 267 | fieldset[disabled] .btn-warning.active { 268 | background-color: #eb9316; 269 | background-image: none; 270 | } 271 | .btn-danger { 272 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 273 | background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); 274 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); 275 | background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); 277 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 278 | background-repeat: repeat-x; 279 | border-color: #b92c28; 280 | } 281 | .btn-danger:hover, 282 | .btn-danger:focus { 283 | background-color: #c12e2a; 284 | background-position: 0 -15px; 285 | } 286 | .btn-danger:active, 287 | .btn-danger.active { 288 | background-color: #c12e2a; 289 | border-color: #b92c28; 290 | } 291 | .btn-danger.disabled, 292 | .btn-danger[disabled], 293 | fieldset[disabled] .btn-danger, 294 | .btn-danger.disabled:hover, 295 | .btn-danger[disabled]:hover, 296 | fieldset[disabled] .btn-danger:hover, 297 | .btn-danger.disabled:focus, 298 | .btn-danger[disabled]:focus, 299 | fieldset[disabled] .btn-danger:focus, 300 | .btn-danger.disabled.focus, 301 | .btn-danger[disabled].focus, 302 | fieldset[disabled] .btn-danger.focus, 303 | .btn-danger.disabled:active, 304 | .btn-danger[disabled]:active, 305 | fieldset[disabled] .btn-danger:active, 306 | .btn-danger.disabled.active, 307 | .btn-danger[disabled].active, 308 | fieldset[disabled] .btn-danger.active { 309 | background-color: #c12e2a; 310 | background-image: none; 311 | } 312 | .thumbnail, 313 | .img-thumbnail { 314 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 315 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 316 | } 317 | .dropdown-menu > li > a:hover, 318 | .dropdown-menu > li > a:focus { 319 | background-color: #e8e8e8; 320 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 321 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 322 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 323 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 325 | background-repeat: repeat-x; 326 | } 327 | .dropdown-menu > .active > a, 328 | .dropdown-menu > .active > a:hover, 329 | .dropdown-menu > .active > a:focus { 330 | background-color: #2e6da4; 331 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 332 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 333 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 334 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 335 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 336 | background-repeat: repeat-x; 337 | } 338 | .navbar-default { 339 | background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); 340 | background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); 341 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); 342 | background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); 344 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 345 | background-repeat: repeat-x; 346 | border-radius: 4px; 347 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 348 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); 349 | } 350 | .navbar-default .navbar-nav > .open > a, 351 | .navbar-default .navbar-nav > .active > a { 352 | background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 353 | background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); 354 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); 355 | background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); 356 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); 357 | background-repeat: repeat-x; 358 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 359 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); 360 | } 361 | .navbar-brand, 362 | .navbar-nav > li > a { 363 | text-shadow: 0 1px 0 rgba(255, 255, 255, .25); 364 | } 365 | .navbar-inverse { 366 | background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); 367 | background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); 368 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); 369 | background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); 370 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); 371 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 372 | background-repeat: repeat-x; 373 | border-radius: 4px; 374 | } 375 | .navbar-inverse .navbar-nav > .open > a, 376 | .navbar-inverse .navbar-nav > .active > a { 377 | background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); 378 | background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); 379 | background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); 380 | background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); 381 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); 382 | background-repeat: repeat-x; 383 | -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 384 | box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); 385 | } 386 | .navbar-inverse .navbar-brand, 387 | .navbar-inverse .navbar-nav > li > a { 388 | text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); 389 | } 390 | .navbar-static-top, 391 | .navbar-fixed-top, 392 | .navbar-fixed-bottom { 393 | border-radius: 0; 394 | } 395 | @media (max-width: 767px) { 396 | .navbar .navbar-nav .open .dropdown-menu > .active > a, 397 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, 398 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { 399 | color: #fff; 400 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 401 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 402 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 403 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 404 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 405 | background-repeat: repeat-x; 406 | } 407 | } 408 | .alert { 409 | text-shadow: 0 1px 0 rgba(255, 255, 255, .2); 410 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 411 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); 412 | } 413 | .alert-success { 414 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 415 | background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); 416 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); 417 | background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); 418 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); 419 | background-repeat: repeat-x; 420 | border-color: #b2dba1; 421 | } 422 | .alert-info { 423 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 424 | background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); 425 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); 426 | background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); 427 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); 428 | background-repeat: repeat-x; 429 | border-color: #9acfea; 430 | } 431 | .alert-warning { 432 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 433 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); 434 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); 435 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); 436 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); 437 | background-repeat: repeat-x; 438 | border-color: #f5e79e; 439 | } 440 | .alert-danger { 441 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 442 | background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); 443 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); 444 | background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); 445 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); 446 | background-repeat: repeat-x; 447 | border-color: #dca7a7; 448 | } 449 | .progress { 450 | background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 451 | background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); 452 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); 453 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 454 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); 455 | background-repeat: repeat-x; 456 | } 457 | .progress-bar { 458 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); 459 | background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); 460 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); 461 | background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); 462 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); 463 | background-repeat: repeat-x; 464 | } 465 | .progress-bar-success { 466 | background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); 467 | background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); 468 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); 469 | background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); 470 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); 471 | background-repeat: repeat-x; 472 | } 473 | .progress-bar-info { 474 | background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 475 | background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); 476 | background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); 477 | background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); 478 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); 479 | background-repeat: repeat-x; 480 | } 481 | .progress-bar-warning { 482 | background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 483 | background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); 484 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); 485 | background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); 486 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); 487 | background-repeat: repeat-x; 488 | } 489 | .progress-bar-danger { 490 | background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); 491 | background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); 492 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); 493 | background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); 494 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); 495 | background-repeat: repeat-x; 496 | } 497 | .progress-bar-striped { 498 | background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 499 | background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 500 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 501 | } 502 | .list-group { 503 | border-radius: 4px; 504 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 505 | box-shadow: 0 1px 2px rgba(0, 0, 0, .075); 506 | } 507 | .list-group-item.active, 508 | .list-group-item.active:hover, 509 | .list-group-item.active:focus { 510 | text-shadow: 0 -1px 0 #286090; 511 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); 512 | background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); 513 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); 514 | background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); 516 | background-repeat: repeat-x; 517 | border-color: #2b669a; 518 | } 519 | .list-group-item.active .badge, 520 | .list-group-item.active:hover .badge, 521 | .list-group-item.active:focus .badge { 522 | text-shadow: none; 523 | } 524 | .panel { 525 | -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 526 | box-shadow: 0 1px 2px rgba(0, 0, 0, .05); 527 | } 528 | .panel-default > .panel-heading { 529 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 530 | background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); 531 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); 532 | background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); 533 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); 534 | background-repeat: repeat-x; 535 | } 536 | .panel-primary > .panel-heading { 537 | background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 538 | background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); 539 | background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); 540 | background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); 541 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); 542 | background-repeat: repeat-x; 543 | } 544 | .panel-success > .panel-heading { 545 | background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 546 | background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); 547 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); 548 | background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); 549 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); 550 | background-repeat: repeat-x; 551 | } 552 | .panel-info > .panel-heading { 553 | background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 554 | background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); 555 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); 556 | background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); 557 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); 558 | background-repeat: repeat-x; 559 | } 560 | .panel-warning > .panel-heading { 561 | background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 562 | background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); 563 | background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); 564 | background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); 565 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); 566 | background-repeat: repeat-x; 567 | } 568 | .panel-danger > .panel-heading { 569 | background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 570 | background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); 571 | background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); 572 | background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); 573 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); 574 | background-repeat: repeat-x; 575 | } 576 | .well { 577 | background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 578 | background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); 579 | background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); 580 | background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); 581 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); 582 | background-repeat: repeat-x; 583 | border-color: #dcdcdc; 584 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 585 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); 586 | } 587 | /*# sourceMappingURL=bootstrap-theme.css.map */ 588 | -------------------------------------------------------------------------------- /web_app/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.6 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} 6 | /*# sourceMappingURL=bootstrap-theme.min.css.map */ -------------------------------------------------------------------------------- /web_app/css/bootstrap-theme.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":";;;;AAmBA,YAAA,aAAA,UAAA,aAAA,aAAA,aAME,YAAA,EAAA,KAAA,EAAA,eC2CA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBDvCR,mBAAA,mBAAA,oBAAA,oBAAA,iBAAA,iBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBAAA,oBCsCA,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBDlCR,qBAAA,sBAAA,sBAAA,uBAAA,mBAAA,oBAAA,sBAAA,uBAAA,sBAAA,uBAAA,sBAAA,uBAAA,+BAAA,gCAAA,6BAAA,gCAAA,gCAAA,gCCiCA,mBAAA,KACQ,WAAA,KDlDV,mBAAA,oBAAA,iBAAA,oBAAA,oBAAA,oBAuBI,YAAA,KAyCF,YAAA,YAEE,iBAAA,KAKJ,aErEI,YAAA,EAAA,IAAA,EAAA,KACA,iBAAA,iDACA,iBAAA,4CAAA,iBAAA,qEAEA,iBAAA,+CCnBF,OAAA,+GH4CA,OAAA,0DACA,kBAAA,SAuC2C,aAAA,QAA2B,aAAA,KArCtE,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAgBN,aEtEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAiBN,aEvEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAkBN,UExEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,gBAAA,gBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,iBAAA,iBAEE,iBAAA,QACA,aAAA,QAMA,mBAAA,0BAAA,yBAAA,0BAAA,yBAAA,yBAAA,oBAAA,2BAAA,0BAAA,2BAAA,0BAAA,0BAAA,6BAAA,oCAAA,mCAAA,oCAAA,mCAAA,mCAME,iBAAA,QACA,iBAAA,KAmBN,aEzEI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,mBAAA,mBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,oBAAA,oBAEE,iBAAA,QACA,aAAA,QAMA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,uBAAA,8BAAA,6BAAA,8BAAA,6BAAA,6BAAA,gCAAA,uCAAA,sCAAA,uCAAA,sCAAA,sCAME,iBAAA,QACA,iBAAA,KAoBN,YE1EI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDAEA,OAAA,+GCnBF,OAAA,0DH4CA,kBAAA,SACA,aAAA,QAEA,kBAAA,kBAEE,iBAAA,QACA,oBAAA,EAAA,MAGF,mBAAA,mBAEE,iBAAA,QACA,aAAA,QAMA,qBAAA,4BAAA,2BAAA,4BAAA,2BAAA,2BAAA,sBAAA,6BAAA,4BAAA,6BAAA,4BAAA,4BAAA,+BAAA,sCAAA,qCAAA,sCAAA,qCAAA,qCAME,iBAAA,QACA,iBAAA,KA2BN,eAAA,WClCE,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBD2CV,0BAAA,0BE3FI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GF0FF,kBAAA,SAEF,yBAAA,+BAAA,+BEhGI,iBAAA,QACA,iBAAA,oDACA,iBAAA,+CAAA,iBAAA,wEACA,iBAAA,kDACA,OAAA,+GFgGF,kBAAA,SASF,gBE7GI,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SH+HA,cAAA,ICjEA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,iBD6DV,sCAAA,oCE7GI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,iBD0EV,cAAA,iBAEE,YAAA,EAAA,IAAA,EAAA,sBAIF,gBEhII,iBAAA,iDACA,iBAAA,4CACA,iBAAA,qEAAA,iBAAA,+CACA,OAAA,+GACA,OAAA,0DCnBF,kBAAA,SHkJA,cAAA,IAHF,sCAAA,oCEhII,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SD2CF,mBAAA,MAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBDgFV,8BAAA,iCAYI,YAAA,EAAA,KAAA,EAAA,gBAKJ,qBAAA,kBAAA,mBAGE,cAAA,EAqBF,yBAfI,mDAAA,yDAAA,yDAGE,MAAA,KE7JF,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,UFqKJ,OACE,YAAA,EAAA,IAAA,EAAA,qBC3HA,mBAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,MAAA,EAAA,IAAA,EAAA,sBAAA,EAAA,IAAA,IAAA,gBDsIV,eEtLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAKF,YEvLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAMF,eExLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAOF,cEzLI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF8KF,aAAA,QAeF,UEjMI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFuMJ,cE3MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFwMJ,sBE5MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyMJ,mBE7MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0MJ,sBE9MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2MJ,qBE/MI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF+MJ,sBElLI,iBAAA,yKACA,iBAAA,oKACA,iBAAA,iKFyLJ,YACE,cAAA,IC9KA,mBAAA,EAAA,IAAA,IAAA,iBACQ,WAAA,EAAA,IAAA,IAAA,iBDgLV,wBAAA,8BAAA,8BAGE,YAAA,EAAA,KAAA,EAAA,QEnOE,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFiOF,aAAA,QALF,+BAAA,qCAAA,qCAQI,YAAA,KAUJ,OCnME,mBAAA,EAAA,IAAA,IAAA,gBACQ,WAAA,EAAA,IAAA,IAAA,gBD4MV,8BE5PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFyPJ,8BE7PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF0PJ,8BE9PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF2PJ,2BE/PI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF4PJ,8BEhQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SF6PJ,6BEjQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFoQJ,MExQI,iBAAA,oDACA,iBAAA,+CACA,iBAAA,wEAAA,iBAAA,kDACA,OAAA,+GACA,kBAAA,SFsQF,aAAA,QC3NA,mBAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA,qBACQ,WAAA,MAAA,EAAA,IAAA,IAAA,gBAAA,EAAA,IAAA,EAAA"} -------------------------------------------------------------------------------- /web_app/css/selectize.bootstrap3.css: -------------------------------------------------------------------------------- 1 | /** 2 | * selectize.bootstrap3.css (v0.12.1) - Bootstrap 3 Theme 3 | * Copyright (c) 2013–2015 Brian Reavis & contributors 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy of the License at: 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under 10 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language 12 | * governing permissions and limitations under the License. 13 | * 14 | * @author Brian Reavis 15 | */ 16 | .selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder { 17 | visibility: visible !important; 18 | background: #f2f2f2 !important; 19 | background: rgba(0, 0, 0, 0.06) !important; 20 | border: 0 none !important; 21 | -webkit-box-shadow: inset 0 0 12px 4px #ffffff; 22 | box-shadow: inset 0 0 12px 4px #ffffff; 23 | } 24 | .selectize-control.plugin-drag_drop .ui-sortable-placeholder::after { 25 | content: '!'; 26 | visibility: hidden; 27 | } 28 | .selectize-control.plugin-drag_drop .ui-sortable-helper { 29 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 30 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 31 | } 32 | .selectize-dropdown-header { 33 | position: relative; 34 | padding: 3px 12px; 35 | border-bottom: 1px solid #d0d0d0; 36 | background: #f8f8f8; 37 | -webkit-border-radius: 4px 4px 0 0; 38 | -moz-border-radius: 4px 4px 0 0; 39 | border-radius: 4px 4px 0 0; 40 | } 41 | .selectize-dropdown-header-close { 42 | position: absolute; 43 | right: 12px; 44 | top: 50%; 45 | color: #333333; 46 | opacity: 0.4; 47 | margin-top: -12px; 48 | line-height: 20px; 49 | font-size: 20px !important; 50 | } 51 | .selectize-dropdown-header-close:hover { 52 | color: #000000; 53 | } 54 | .selectize-dropdown.plugin-optgroup_columns .optgroup { 55 | border-right: 1px solid #f2f2f2; 56 | border-top: 0 none; 57 | float: left; 58 | -webkit-box-sizing: border-box; 59 | -moz-box-sizing: border-box; 60 | box-sizing: border-box; 61 | } 62 | .selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { 63 | border-right: 0 none; 64 | } 65 | .selectize-dropdown.plugin-optgroup_columns .optgroup:before { 66 | display: none; 67 | } 68 | .selectize-dropdown.plugin-optgroup_columns .optgroup-header { 69 | border-top: 0 none; 70 | } 71 | .selectize-control.plugin-remove_button [data-value] { 72 | position: relative; 73 | padding-right: 24px !important; 74 | } 75 | .selectize-control.plugin-remove_button [data-value] .remove { 76 | z-index: 1; 77 | /* fixes ie bug (see #392) */ 78 | position: absolute; 79 | top: 0; 80 | right: 0; 81 | bottom: 0; 82 | width: 17px; 83 | text-align: center; 84 | font-weight: bold; 85 | font-size: 12px; 86 | color: inherit; 87 | text-decoration: none; 88 | vertical-align: middle; 89 | display: inline-block; 90 | padding: 1px 0 0 0; 91 | border-left: 1px solid rgba(0, 0, 0, 0); 92 | -webkit-border-radius: 0 2px 2px 0; 93 | -moz-border-radius: 0 2px 2px 0; 94 | border-radius: 0 2px 2px 0; 95 | -webkit-box-sizing: border-box; 96 | -moz-box-sizing: border-box; 97 | box-sizing: border-box; 98 | } 99 | .selectize-control.plugin-remove_button [data-value] .remove:hover { 100 | background: rgba(0, 0, 0, 0.05); 101 | } 102 | .selectize-control.plugin-remove_button [data-value].active .remove { 103 | border-left-color: rgba(0, 0, 0, 0); 104 | } 105 | .selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { 106 | background: none; 107 | } 108 | .selectize-control.plugin-remove_button .disabled [data-value] .remove { 109 | border-left-color: rgba(77, 77, 77, 0); 110 | } 111 | .selectize-control { 112 | position: relative; 113 | } 114 | .selectize-dropdown, 115 | .selectize-input, 116 | .selectize-input input { 117 | color: #333333; 118 | font-family: inherit; 119 | font-size: inherit; 120 | line-height: 20px; 121 | -webkit-font-smoothing: inherit; 122 | } 123 | .selectize-input, 124 | .selectize-control.single .selectize-input.input-active { 125 | background: #ffffff; 126 | cursor: text; 127 | display: inline-block; 128 | } 129 | .selectize-input { 130 | border: 1px solid #cccccc; 131 | padding: 6px 12px; 132 | display: inline-block; 133 | width: 100%; 134 | overflow: hidden; 135 | position: relative; 136 | z-index: 1; 137 | -webkit-box-sizing: border-box; 138 | -moz-box-sizing: border-box; 139 | box-sizing: border-box; 140 | -webkit-box-shadow: none; 141 | box-shadow: none; 142 | -webkit-border-radius: 4px; 143 | -moz-border-radius: 4px; 144 | border-radius: 4px; 145 | } 146 | .selectize-control.multi .selectize-input.has-items { 147 | padding: 5px 12px 2px; 148 | } 149 | .selectize-input.full { 150 | background-color: #ffffff; 151 | } 152 | .selectize-input.disabled, 153 | .selectize-input.disabled * { 154 | cursor: default !important; 155 | } 156 | .selectize-input.focus { 157 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); 158 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); 159 | } 160 | .selectize-input.dropdown-active { 161 | -webkit-border-radius: 4px 4px 0 0; 162 | -moz-border-radius: 4px 4px 0 0; 163 | border-radius: 4px 4px 0 0; 164 | } 165 | .selectize-input > * { 166 | vertical-align: baseline; 167 | display: -moz-inline-stack; 168 | display: inline-block; 169 | zoom: 1; 170 | *display: inline; 171 | } 172 | .selectize-control.multi .selectize-input > div { 173 | cursor: pointer; 174 | margin: 0 3px 3px 0; 175 | padding: 1px 3px; 176 | background: #efefef; 177 | color: #333333; 178 | border: 0 solid rgba(0, 0, 0, 0); 179 | } 180 | .selectize-control.multi .selectize-input > div.active { 181 | background: #428bca; 182 | color: #ffffff; 183 | border: 0 solid rgba(0, 0, 0, 0); 184 | } 185 | .selectize-control.multi .selectize-input.disabled > div, 186 | .selectize-control.multi .selectize-input.disabled > div.active { 187 | color: #808080; 188 | background: #ffffff; 189 | border: 0 solid rgba(77, 77, 77, 0); 190 | } 191 | .selectize-input > input { 192 | display: inline-block !important; 193 | padding: 0 !important; 194 | min-height: 0 !important; 195 | max-height: none !important; 196 | max-width: 100% !important; 197 | margin: 0 !important; 198 | text-indent: 0 !important; 199 | border: 0 none !important; 200 | background: none !important; 201 | line-height: inherit !important; 202 | -webkit-user-select: auto !important; 203 | -webkit-box-shadow: none !important; 204 | box-shadow: none !important; 205 | } 206 | .selectize-input > input::-ms-clear { 207 | display: none; 208 | } 209 | .selectize-input > input:focus { 210 | outline: none !important; 211 | } 212 | .selectize-input::after { 213 | content: ' '; 214 | display: block; 215 | clear: left; 216 | } 217 | .selectize-input.dropdown-active::before { 218 | content: ' '; 219 | display: block; 220 | position: absolute; 221 | background: #ffffff; 222 | height: 1px; 223 | bottom: 0; 224 | left: 0; 225 | right: 0; 226 | } 227 | .selectize-dropdown { 228 | position: absolute; 229 | z-index: 10; 230 | border: 1px solid #d0d0d0; 231 | background: #ffffff; 232 | margin: -1px 0 0 0; 233 | border-top: 0 none; 234 | -webkit-box-sizing: border-box; 235 | -moz-box-sizing: border-box; 236 | box-sizing: border-box; 237 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 238 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 239 | -webkit-border-radius: 0 0 4px 4px; 240 | -moz-border-radius: 0 0 4px 4px; 241 | border-radius: 0 0 4px 4px; 242 | } 243 | .selectize-dropdown [data-selectable] { 244 | cursor: pointer; 245 | overflow: hidden; 246 | } 247 | .selectize-dropdown [data-selectable] .highlight { 248 | background: rgba(255, 237, 40, 0.4); 249 | -webkit-border-radius: 1px; 250 | -moz-border-radius: 1px; 251 | border-radius: 1px; 252 | } 253 | .selectize-dropdown [data-selectable], 254 | .selectize-dropdown .optgroup-header { 255 | padding: 3px 12px; 256 | } 257 | .selectize-dropdown .optgroup:first-child .optgroup-header { 258 | border-top: 0 none; 259 | } 260 | .selectize-dropdown .optgroup-header { 261 | color: #777777; 262 | background: #ffffff; 263 | cursor: default; 264 | } 265 | .selectize-dropdown .active { 266 | background-color: #f5f5f5; 267 | color: #262626; 268 | } 269 | .selectize-dropdown .active.create { 270 | color: #262626; 271 | } 272 | .selectize-dropdown .create { 273 | color: rgba(51, 51, 51, 0.5); 274 | } 275 | .selectize-dropdown-content { 276 | overflow-y: auto; 277 | overflow-x: hidden; 278 | max-height: 200px; 279 | } 280 | .selectize-control.single .selectize-input, 281 | .selectize-control.single .selectize-input input { 282 | cursor: pointer; 283 | } 284 | .selectize-control.single .selectize-input.input-active, 285 | .selectize-control.single .selectize-input.input-active input { 286 | cursor: text; 287 | } 288 | .selectize-control.single .selectize-input:after { 289 | content: ' '; 290 | display: block; 291 | position: absolute; 292 | top: 50%; 293 | right: 17px; 294 | margin-top: -3px; 295 | width: 0; 296 | height: 0; 297 | border-style: solid; 298 | border-width: 5px 5px 0 5px; 299 | border-color: #333333 transparent transparent transparent; 300 | } 301 | .selectize-control.single .selectize-input.dropdown-active:after { 302 | margin-top: -4px; 303 | border-width: 0 5px 5px 5px; 304 | border-color: transparent transparent #333333 transparent; 305 | } 306 | .selectize-control.rtl.single .selectize-input:after { 307 | left: 17px; 308 | right: auto; 309 | } 310 | .selectize-control.rtl .selectize-input > input { 311 | margin: 0 4px 0 -2px !important; 312 | } 313 | .selectize-control .selectize-input.disabled { 314 | opacity: 0.5; 315 | background-color: #ffffff; 316 | } 317 | .selectize-dropdown, 318 | .selectize-dropdown.form-control { 319 | height: auto; 320 | padding: 0; 321 | margin: 2px 0 0 0; 322 | z-index: 1000; 323 | background: #ffffff; 324 | border: 1px solid #cccccc; 325 | border: 1px solid rgba(0, 0, 0, 0.15); 326 | -webkit-border-radius: 4px; 327 | -moz-border-radius: 4px; 328 | border-radius: 4px; 329 | -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 330 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 331 | } 332 | .selectize-dropdown .optgroup-header { 333 | font-size: 12px; 334 | line-height: 1.42857143; 335 | } 336 | .selectize-dropdown .optgroup:first-child:before { 337 | display: none; 338 | } 339 | .selectize-dropdown .optgroup:before { 340 | content: ' '; 341 | display: block; 342 | height: 1px; 343 | margin: 9px 0; 344 | overflow: hidden; 345 | background-color: #e5e5e5; 346 | margin-left: -12px; 347 | margin-right: -12px; 348 | } 349 | .selectize-dropdown-content { 350 | padding: 5px 0; 351 | } 352 | .selectize-dropdown-header { 353 | padding: 6px 12px; 354 | } 355 | .selectize-input { 356 | min-height: 34px; 357 | } 358 | .selectize-input.dropdown-active { 359 | -webkit-border-radius: 4px; 360 | -moz-border-radius: 4px; 361 | border-radius: 4px; 362 | } 363 | .selectize-input.dropdown-active::before { 364 | display: none; 365 | } 366 | .selectize-input.focus { 367 | border-color: #66afe9; 368 | outline: 0; 369 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); 370 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); 371 | } 372 | .has-error .selectize-input { 373 | border-color: #a94442; 374 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 375 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 376 | } 377 | .has-error .selectize-input:focus { 378 | border-color: #843534; 379 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; 380 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; 381 | } 382 | .selectize-control.multi .selectize-input.has-items { 383 | padding-left: 9px; 384 | padding-right: 9px; 385 | } 386 | .selectize-control.multi .selectize-input > div { 387 | -webkit-border-radius: 3px; 388 | -moz-border-radius: 3px; 389 | border-radius: 3px; 390 | } 391 | .form-control.selectize-control { 392 | padding: 0; 393 | height: auto; 394 | border: none; 395 | background: none; 396 | -webkit-box-shadow: none; 397 | box-shadow: none; 398 | -webkit-border-radius: 0; 399 | -moz-border-radius: 0; 400 | border-radius: 0; 401 | } 402 | -------------------------------------------------------------------------------- /web_app/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Material Icons'), 6 | local('MaterialIcons-Regular'), 7 | url(../fonts/MaterialIcons-Regular.woff2) format('woff2'), 8 | url(../fonts/MaterialIcons-Regular.woff) format('woff'), 9 | url(../fonts/MaterialIcons-Regular.ttf) format('truetype'); 10 | } 11 | 12 | @font-face { 13 | font-family: 'Meta-ThinLF'; 14 | font-style: normal; 15 | font-weight: 400; 16 | src: url(../fonts/MtThLf__.ttf); 17 | } 18 | 19 | @font-face { 20 | font-family: 'Meta-NormalLF'; 21 | font-style: normal; 22 | font-weight: 400; 23 | src: url(../fonts/MtNoLFRo.ttf); 24 | } 25 | 26 | 27 | .material-icons { 28 | font-family: 'Material Icons'; 29 | font-weight: normal; 30 | font-style: normal; 31 | font-size: 24px; /* Preferred icon size */ 32 | display: inline-block; 33 | line-height: 1; 34 | text-transform: none; 35 | letter-spacing: normal; 36 | word-wrap: normal; 37 | white-space: nowrap; 38 | direction: ltr; 39 | 40 | /* Support for all WebKit browsers. */ 41 | -webkit-font-smoothing: antialiased; 42 | /* Support for Safari and Chrome. */ 43 | text-rendering: optimizeLegibility; 44 | 45 | /* Support for Firefox. */ 46 | -moz-osx-font-smoothing: grayscale; 47 | 48 | /* Support for IE. */ 49 | font-feature-settings: 'liga'; 50 | } 51 | 52 | /* Rules for sizing the icon. */ 53 | .material-icons.md-18 { font-size: 18px; } 54 | .material-icons.md-24 { font-size: 24px; } 55 | .material-icons.md-36 { font-size: 36px; } 56 | .material-icons.md-48 { font-size: 48px; } 57 | 58 | /* Rules for using icons as black on a light background. */ 59 | .material-icons.md-dark { color: rgba(0, 0, 0, 0.54); } 60 | .material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); } 61 | 62 | /* Rules for using icons as white on a dark background. */ 63 | .material-icons.md-light { color: rgba(255, 255, 255, 1); } 64 | .material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); } 65 | 66 | 67 | .no-transition { 68 | -webkit-transition: height 0.01s; 69 | -moz-transition: height 0.01s; 70 | -ms-transition: height 0.01s; 71 | -o-transition: height 0.01s; 72 | transition: height 0.01s; 73 | } 74 | 75 | html { 76 | background-color: #BABCBE; 77 | } 78 | 79 | 80 | h1 { 81 | font-family: 'Meta-NormalLF'; 82 | background-color: #2C95DD; 83 | color: #FFFFFF; 84 | padding: 15px; 85 | } 86 | 87 | h2 { 88 | font-family: 'Meta-NormalLF'; 89 | } 90 | 91 | input { 92 | font-weight: bolder; 93 | } 94 | 95 | .category_row { 96 | margin: 5px; 97 | padding: 10px; 98 | border: 3px solid #FFFFFF; 99 | background-color: #F0F0F0; 100 | } 101 | 102 | .category { 103 | font-family: 'Meta-NormalLF'; 104 | font-size: 1.8em; 105 | margin: 10px; 106 | vertical-align: middle; 107 | } 108 | 109 | .category_description { 110 | font-family: 'Meta-NormalLF'; 111 | font-size: 1.1em; 112 | } 113 | 114 | .key { 115 | font-family: 'Meta-NormalLF'; 116 | font-size: 1.5em; 117 | margin: 15px; 118 | } 119 | 120 | .subcontents { 121 | display: none; 122 | } 123 | 124 | .table { 125 | font-family: 'Meta-NormalLF'; 126 | } 127 | 128 | .subcontents_toggle { 129 | height: 48px; 130 | width: 48px; 131 | color: #2C95DD; 132 | } 133 | 134 | .icon { 135 | vertical-align: middle; 136 | text-align: center; 137 | margin: 0px; 138 | padding: 0px; 139 | } 140 | 141 | .filter-control{ 142 | 143 | } 144 | 145 | .btn-tag { 146 | margin-bottom: 5px; 147 | } 148 | 149 | .clickable { 150 | cursor: pointer; 151 | } 152 | -------------------------------------------------------------------------------- /web_app/fonts/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /web_app/fonts/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /web_app/fonts/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /web_app/fonts/MtNoLFRo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/MtNoLFRo.ttf -------------------------------------------------------------------------------- /web_app/fonts/MtThLf__.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/MtThLf__.ttf -------------------------------------------------------------------------------- /web_app/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /web_app/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /web_app/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /web_app/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /web_app/js/app.js: -------------------------------------------------------------------------------- 1 | var show_icon = 'add_circle_outline' 2 | var hide_icon = 'remove_circle_outline' 3 | var clear_icon = 'clear' 4 | 5 | var g_categories; 6 | 7 | $( document ).ready(function() { 8 | // Collapse contens and update the toggle icons appropriately 9 | $('.key_extra_detail').hide() 10 | $('.subcontents').hide(); 11 | $('.subcontents_toggle').html(show_icon); 12 | 13 | // Handle category show/hide 14 | $('.subcontents_toggle').click(toggle_handler); 15 | $('.category_head').click(toggle_handler); 16 | 17 | // Handle key extra detail show/hide 18 | $('.key_detail_toggle').click(toggle_key_detail); 19 | $('span.key-name').click(toggle_key_detail); 20 | 21 | // Handle filter tag buttons 22 | $('.btn-tag').click(key_filter_button); 23 | 24 | // Initialize drop-downs 25 | $('.dropdown-menu').dropdown() 26 | 27 | sort_items('.category_container', '.category_main', '.sort-key', true) 28 | sort_items('.key_container', '.key_main', '.sort-key', false) 29 | 30 | // Handle the filter box 31 | var options = { 32 | callback: filter, 33 | wait: 750, 34 | captureLength: 2 35 | } 36 | $('#text_filter').typeWatch(options) 37 | }); 38 | -------------------------------------------------------------------------------- /web_app/js/app_filter.js: -------------------------------------------------------------------------------- 1 | function filter() { 2 | // Called when text in filter box changes 3 | var text = $("#text_filter").val().trim(); 4 | if (text.length < 2) { 5 | // Dont filter for 2 characters, it's of little value and makes the UI lag 6 | $('.subcontents').hide(); 7 | $('.key_main').show() 8 | $('.category_main').show() 9 | no_results_warn(false) 10 | update_toggle_icons() 11 | return 12 | } 13 | var keys = keyDict['keys'] 14 | // Retreive a list of keys that match the query 15 | var deduped_key_matches = list_matching_keys(text, keys) 16 | 17 | // Hide everything, then display only matching keys and their containing cats 18 | $('.subcontents').hide(); 19 | hide_keys() 20 | hide_cats() 21 | no_results_warn(false) 22 | var category_map = keyDict['mappings']['categories'] 23 | var key_map = keyDict['mappings']['keys'] 24 | for (i in deduped_key_matches) { 25 | var key = deduped_key_matches[i] 26 | var supercat = keys[key]['super'] 27 | var subcat = supercat + '-' + keys[key]['sub'] 28 | var subsubcat = subcat + '-' + keys[key]['subsub'] 29 | show_cat(supercat) 30 | show_cat(subcat) 31 | show_cat(subsubcat) 32 | 33 | var supercat_subcontents = $('#' + category_map[supercat]).find('div.subcontents').first() 34 | $(supercat_subcontents).show() 35 | var subcat_subcontents = $('#' + category_map[supercat + '-' + subcat]).find('div.subcontents').first() 36 | $(subcat_subcontents).show() 37 | var subsubcat_subcontents = $('#' + category_map[supercat + '-' + subcat + '-' + subsubcat]).find('div.subcontents').first() 38 | $(subsubcat_subcontents).show() 39 | var key_id = '#' + key_map[key] 40 | $(key_id).show() 41 | } 42 | if (deduped_key_matches.length == 0) { 43 | no_results_warn(true) 44 | } 45 | update_toggle_icons() 46 | } 47 | 48 | function reset_search() { 49 | $('#text_filter').val('') 50 | filter() 51 | } 52 | -------------------------------------------------------------------------------- /web_app/js/app_filter_lib.js: -------------------------------------------------------------------------------- 1 | function hide_keys() { 2 | // Need to select keys by ID -> find class for adequate JQuery selector perf 3 | var cat_mappings = keyDict['mappings']['categories'] 4 | for (category in cat_mappings) { 5 | var category_id = '#' + cat_mappings[category]; 6 | $(category_id).find('.key_main').hide() 7 | } 8 | } 9 | 10 | function hide_cats() { 11 | // Hide all category divs 12 | $('.category_main').hide() 13 | } 14 | 15 | function show_cat(category) { 16 | var cat_map = keyDict['mappings']['categories'] 17 | var cat_id_selector = '#' + cat_map[category] 18 | $(cat_id_selector).show() 19 | var el_cat_subcontents = $(cat_id_selector).find('div.subcontents').first() 20 | $(el_cat_subcontents).show() 21 | } 22 | 23 | function no_results_warn(visible) { 24 | if (visible) { 25 | $('#no_results_warn').show() 26 | } else { 27 | $('#no_results_warn').hide() 28 | } 29 | } 30 | 31 | function cat_selector(category_name) { 32 | var cat_map = keyDict['mappings']['categories'] 33 | var cat_id_selector = '#' + cat_map[category_name] 34 | return cat_id_selector 35 | } 36 | 37 | 38 | function parse_search_terms(text) { 39 | // Splits search query, normalizes case, drops emtpy search terms 40 | var raw_terms = text.toLowerCase().split(' ') 41 | var terms = [] 42 | for (i in raw_terms) { 43 | if (raw_terms[i] != "" && raw_terms[i].length > 1) { 44 | terms.push(raw_terms[i]) 45 | } 46 | } 47 | return terms 48 | } 49 | 50 | 51 | function match_key(key, search_terms) { 52 | // Checks whether a particular key matches all search terms (AND) 53 | var key_terms = key['search_terms'] 54 | for (var i in search_terms) { 55 | var search_term_matched = false 56 | for (var j in key_terms) { 57 | if (key_terms[j].search(search_terms[i]) != -1) { 58 | search_term_matched = true 59 | break 60 | } 61 | } 62 | if (!search_term_matched) { 63 | return false 64 | } 65 | } 66 | return true 67 | } 68 | 69 | function parse_subsearches(search_terms) { 70 | // Splits the search on OR's 71 | var subsearches = [] 72 | var subsearch = [] 73 | while (search_terms.length > 0) { 74 | var term = search_terms.pop() 75 | if (term == 'or') { 76 | if (subsearch.length > 0) { 77 | subsearches.push(subsearch) 78 | var subsearch = [] 79 | } 80 | } 81 | else if (term != 'and') { 82 | subsearch.push(term) 83 | } 84 | } 85 | if (subsearch.length > 0) { 86 | subsearches.push(subsearch) 87 | } 88 | return subsearches 89 | } 90 | 91 | 92 | function list_matching_keys(text, keys) { 93 | // Main logic for determing which keys match a search 94 | // Splits the search on 'OR' to get subsearches 95 | // Subsearches are implicitly 'AND' 96 | var key_matches = [] 97 | var search_terms = parse_search_terms(text) 98 | var subsearches = parse_subsearches(search_terms) 99 | for (i in subsearches) { 100 | var subsearch = subsearches[i] 101 | for (var key in keys) { 102 | if (match_key(keys[key], subsearch)){ 103 | key_matches.push(key); 104 | } 105 | } 106 | } 107 | // Dedupe the list since key may be matched in multiple ways 108 | return dedupe_array(key_matches); 109 | } 110 | -------------------------------------------------------------------------------- /web_app/js/app_lib.js: -------------------------------------------------------------------------------- 1 | function dedupe_array(a) { 2 | // Returns an array containing hte unique items in the input array a. 3 | var hashes = {}; 4 | var deduped = []; 5 | for (var i=0; i< a.length; i++) { 6 | if (!hashes[a[i]]) { 7 | hashes[a] = true 8 | deduped.push(a[i]) 9 | } 10 | } 11 | return deduped 12 | } 13 | 14 | 15 | function sort_items(container_class, items_class, sort_attr, prepend) { 16 | /* 17 | Sorts items within a container according to an iteem attribute. Assumes the items 18 | are contained within a div within the container as is the case with Bootstrap: 19 | < div .row .container_class> 20 |
21 |
22 |
23 | :param container_class: Class of the container holding the items to be sorted. 24 | :param items_class: Class of the items to be sorted 25 | :param sort_attr: Attr of the item for sort to be keyed on 26 | :param prepend: If true, items are prepended to the inner container. Else, append. 27 | */ 28 | $(container_class).each(function(i, elem) { 29 | var items = $(elem).children('div').children(items_class) 30 | items.sort(function(a, b) { 31 | var cat_a = $(a).find(sort_attr).first().text() 32 | var cat_b = $(b).find(sort_attr).first().text() 33 | if (cat_a.toUpperCase() > cat_b.toUpperCase()) { 34 | return 1 35 | } 36 | else { 37 | return -1 38 | } 39 | }); 40 | if (prepend) { 41 | items.detach().prependTo($(elem).children('div')) 42 | } 43 | else { 44 | items.detach().appendTo($(elem).children('div')) 45 | 46 | } 47 | }); 48 | } 49 | 50 | 51 | function update_toggle_icons() { 52 | // Needs to be called anytime categories are hidden/shown other than 53 | // actual clicks on the toggle buttons 54 | var subcontents = $('div').find('.subcontents').each(function(index) { 55 | if ($(this).css('display') == 'none') { 56 | $(this).parent().find('.subcontents_toggle').first().html(show_icon); 57 | } 58 | else { 59 | $(this).parent().find('.subcontents_toggle').first().html(hide_icon); 60 | } 61 | }); 62 | } 63 | 64 | 65 | function toggle_handler() { 66 | // Find the subcontents div to be toggled 67 | var subcontents = $(this).parentsUntil('.category_main').last().children('.category_subcontents').first(); 68 | subcontents.fadeToggle(function () { 69 | update_toggle_icons(); 70 | }); 71 | return false 72 | } 73 | 74 | 75 | function toggle_key_detail() { 76 | var key_detail = $(this).parentsUntil('.key_main').find('.key-data').find('.key_extra_detail'); 77 | var key_not_extra = $(key_detail).parent().children('.key_not_extra') 78 | var key_toggle = $(this).parentsUntil('.key_main').find('.key_detail_toggle') 79 | if (key_detail.css('display') == 'none') { 80 | $(key_toggle).text('less detail...'); 81 | } 82 | else { 83 | $(key_toggle).text('more detail...'); 84 | } 85 | key_detail.toggle(); 86 | key_not_extra.toggle() 87 | 88 | } 89 | 90 | 91 | function key_filter_button() { 92 | var tag = $(this).text(); 93 | var search_string = $('#text_filter').val() 94 | var search_terms = search_string.split(' ') 95 | // Don't add a tag multiple times 96 | if (search_terms.indexOf(tag) == -1) { 97 | $('#text_filter').val((search_string + ' ' + tag).trim()) 98 | filter() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /web_app/js/app_papi_link.js: -------------------------------------------------------------------------------- 1 | var cluster_ip = null 2 | var clicked_link = null 3 | 4 | $(document).ready(function () { 5 | $('.papi_demo').click(papi_link_clicked) 6 | $('#modal_cluster_save').click(save_cluster_ip) 7 | $('#modal_cluster').on('shown.bs.modal', function () {$('#text_cluster').focus()}) 8 | cluster_ip = keyDict['cluster']['host'] 9 | if (cluster_ip != null) { 10 | // Populate PAPI links if IP is known 11 | update_papi_links(keyDict['cluster']['host']) 12 | } 13 | }); 14 | 15 | function papi_link_clicked() { 16 | if (cluster_ip == null) { 17 | clicked_link = $(this) 18 | $('#modal_cluster').modal() 19 | return false 20 | } 21 | } 22 | 23 | function set_cluster_ip() { 24 | clicked_link = null 25 | $('#modal_cluster').modal() 26 | } 27 | 28 | function save_cluster_ip() { 29 | cluster_ip = $('#text_cluster').val() 30 | $('#modal_cluster').modal('hide') 31 | update_papi_links(cluster_ip) 32 | $('#button_cluster_ip').show() 33 | if (clicked_link != null) { 34 | window.open($(clicked_link).attr('href'), '_blank') 35 | } 36 | } 37 | 38 | function update_papi_links() { 39 | $('.papi_demo_current').each(update_papi_link, ['current']) 40 | $('.papi_demo_history').each(update_papi_link, ['history']) 41 | 42 | } 43 | 44 | function update_papi_link(endpoint) { 45 | key = $(this).parentsUntil('.key_main').find('span.key-name').text() 46 | link = papi_stat_link(key, endpoint, 1) 47 | $(this).attr('href', link) 48 | $(this).text(link) 49 | } 50 | 51 | 52 | function antisquash_key(key) { 53 | // If a key ends in .N, convert the N to 1 54 | if (key.endsWith('.N')) { 55 | key = key.replace(/\.N$/, '.1') 56 | } 57 | return key 58 | } 59 | 60 | function papi_stat_link(key, endpoint, papi_vers) { 61 | key = antisquash_key(key) 62 | server = 'https://' + cluster_ip + ':8080' 63 | uri = '/platform/' + papi_vers + '/statistics/' 64 | uri += endpoint + '?key=' + key 65 | link = server + encodeURI(uri) 66 | return link 67 | } 68 | -------------------------------------------------------------------------------- /web_app/js/app_tags.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Isilon/isilon_stat_browser/c0368c079c73f44b6b1a49a4605920efe48a17c4/web_app/js/app_tags.js -------------------------------------------------------------------------------- /web_app/js/app_tooltips.js: -------------------------------------------------------------------------------- 1 | var tooltips = { 2 | 'key_current_link': 'Opens a new window/tab to the statistics current URI for this key.', 3 | 'key_history_link': 'Opens a new window/tab to the statistics history URI for this key.', 4 | 'key_type': 'Data type of key values.', 5 | 'key_units': 'Units of key values.', 6 | 'key_interval_detail': 'The resolution and retention period for historic data.', 7 | 'key_aggregation_type': 'Type of aggregation used in down-sampling.', 8 | 'key_policy_cache_time': 'Configured time in seconds system will used cached values.', 9 | 'key_default_cache_time': 'Default time in seconds system will used cached values.', 10 | 'key_scope': 'Node or cluster.' 11 | } 12 | 13 | 14 | $( document ).ready(function() { 15 | for (var field in tooltips) { 16 | field_el = $('.' + field).children('td') 17 | field_el.attr('data-toggle', 'tooltip') 18 | field_el.attr('title', tooltips[field]) 19 | field_el.tooltip({container: 'body'}) 20 | } 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /web_app/js/jquery.typewatch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TypeWatch 2.2.2 3 | * 4 | * Examples/Docs: github.com/dennyferra/TypeWatch 5 | * 6 | * Copyright(c) 2014 7 | * Denny Ferrassoli - dennyferra.com 8 | * Charles Christolini 9 | * 10 | * Dual licensed under the MIT and GPL licenses: 11 | * http://www.opensource.org/licenses/mit-license.php 12 | * http://www.gnu.org/licenses/gpl.html 13 | */ 14 | 15 | !function(root, factory) { 16 | if (typeof define === 'function' && define.amd) { 17 | define(['jquery'], factory); 18 | } else if (typeof exports === 'object') { 19 | factory(require('jquery')); 20 | } else { 21 | factory(root.jQuery); 22 | } 23 | }(this, function($) { 24 | 'use strict'; 25 | $.fn.typeWatch = function(o) { 26 | // The default input types that are supported 27 | var _supportedInputTypes = 28 | ['TEXT', 'TEXTAREA', 'PASSWORD', 'TEL', 'SEARCH', 'URL', 'EMAIL', 'DATETIME', 'DATE', 'MONTH', 'WEEK', 'TIME', 'DATETIME-LOCAL', 'NUMBER', 'RANGE']; 29 | 30 | // Options 31 | var options = $.extend({ 32 | wait: 750, 33 | callback: function() { }, 34 | highlight: true, 35 | captureLength: 2, 36 | inputTypes: _supportedInputTypes 37 | }, o); 38 | 39 | function checkElement(timer, override) { 40 | var value = $(timer.el).val(); 41 | 42 | // Fire if text >= options.captureLength AND text != saved text OR if override AND text >= options.captureLength 43 | if ( ( value.length >= options.captureLength && value.toUpperCase() != timer.text ) 44 | || ( override && value.length >= options.captureLength ) 45 | || ( value.length == 0 && timer.text ) ) 46 | { 47 | timer.text = value.toUpperCase(); 48 | timer.cb.call(timer.el, value); 49 | } 50 | }; 51 | 52 | function watchElement(elem) { 53 | var elementType = elem.type.toUpperCase(); 54 | if ($.inArray(elementType, options.inputTypes) >= 0) { 55 | 56 | // Allocate timer element 57 | var timer = { 58 | timer: null, 59 | text: $(elem).val().toUpperCase(), 60 | cb: options.callback, 61 | el: elem, 62 | wait: options.wait 63 | }; 64 | 65 | // Set focus action (highlight) 66 | if (options.highlight) { 67 | $(elem).click( 68 | function() { 69 | this.select(); 70 | }); 71 | } 72 | 73 | // Key watcher / clear and reset the timer 74 | var startWatch = function(evt) { 75 | var timerWait = timer.wait; 76 | var overrideBool = false; 77 | var evtElementType = this.type.toUpperCase(); 78 | 79 | // If enter key is pressed and not a TEXTAREA and matched inputTypes 80 | if (typeof evt.keyCode != 'undefined' && evt.keyCode == 13 && evtElementType != 'TEXTAREA' && $.inArray(evtElementType, options.inputTypes) >= 0) { 81 | timerWait = 1; 82 | overrideBool = true; 83 | } 84 | 85 | var timerCallbackFx = function() { 86 | checkElement(timer, overrideBool) 87 | } 88 | 89 | // Clear timer 90 | clearTimeout(timer.timer); 91 | timer.timer = setTimeout(timerCallbackFx, timerWait); 92 | }; 93 | 94 | $(elem).on('keydown paste cut input change', startWatch); 95 | } 96 | }; 97 | 98 | // Watch Each Element 99 | return this.each(function() { 100 | watchElement(this); 101 | }); 102 | 103 | }; 104 | }); 105 | -------------------------------------------------------------------------------- /web_app/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') --------------------------------------------------------------------------------