diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index 7a15de5f4c..0000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "rd_ui/app/bower_components" -} diff --git a/.dockerignore b/.dockerignore index 508222d445..87087e5526 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ -rd_ui/.tmp/ -rd_ui/node_modules/ +client/.tmp/ +client/node_modules/ .git/ .vagrant/ diff --git a/.env.example b/.env.example deleted file mode 100644 index 182ef2e3f4..0000000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -REDASH_STATIC_ASSETS_PATH="../rd_ui/app/" -REDASH_LOG_LEVEL="INFO" -REDASH_REDIS_URL=redis://localhost:6379/1 -REDASH_DATABASE_URL="postgresql://redash" -REDASH_COOKIE_SECRET=veryverysecret diff --git a/.gitignore b/.gitignore index e9308d88e6..58529596d6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .idea *.pyc .coverage -rd_ui/dist +client/dist .DS_Store celerybeat-schedule* .#* @@ -26,5 +26,4 @@ docker-compose.yml node_modules .tmp .sass-cache -rd_ui/app/bower_components npm-debug.log diff --git a/Dockerfile b/Dockerfile index 465086d4af..4824c50398 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,8 +23,6 @@ RUN chown -R redash /opt/redash/current # Setting working directory WORKDIR /opt/redash/current -ENV REDASH_STATIC_ASSETS_PATH="../rd_ui/dist/" - # Install project specific dependencies RUN pip install -r requirements_all_ds.txt && \ pip install -r requirements.txt @@ -32,7 +30,7 @@ RUN pip install -r requirements_all_ds.txt && \ RUN curl https://deb.nodesource.com/setup_4.x | bash - && \ apt-get install -y nodejs && \ sudo -u redash -H make deps && \ - rm -rf node_modules rd_ui/node_modules /home/redash/.npm /home/redash/.cache && \ + rm -rf node_modules client/node_modules /home/redash/.npm /home/redash/.cache && \ apt-get purge -y nodejs && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/Makefile b/Makefile index f4e15a9b66..3396f885af 100644 --- a/Makefile +++ b/Makefile @@ -6,17 +6,15 @@ BASE_VERSION=$(shell python ./manage.py version | cut -d + -f 1) FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(VERSION).tar.gz deps: - if [ -d "./rd_ui/app" ]; then npm install; fi - if [ -d "./rd_ui/app" ]; then npm run bower install; fi - if [ -d "./rd_ui/app" ]; then npm run build; fi + if [ -d "./client/app" ]; then cd client && npm install; fi + if [ -d "./client/app" ]; then cd client && npm run build; fi pack: sed -ri "s/^__version__ = '([0-9.]*)'/__version__ = '$(FULL_VERSION)'/" redash/__init__.py - tar -zcv -f $(FILENAME) --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" * + tar -zcv -f $(FILENAME) --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="client/node_modules" --exclude="client/app" * upload: python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME) test: nosetests --with-coverage --cover-package=redash tests/ - #grunt test diff --git a/bower.json b/bower.json deleted file mode 100644 index 56408f0371..0000000000 --- a/bower.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "redash", - "version": "0.11.0", - "dependencies": { - "angular": "1.2.18", - "angular-resource": "1.2.18", - "angular-route": "1.2.18", - "angular-growl": "0.4.0", - "json3": "3.2.4", - "jquery": "1.9.1", - "bootstrap": "3.3.6", - "es5-shim": "2.0.8", - "angular-moment": "0.10.3", - "moment": "~2.8.0", - "underscore": "1.5.1", - "pivottable": "2.0.2", - "cornelius": "https://github.com/restorando/cornelius.git", - "gridster": "0.2.0", - "mousetrap": "~1.4.6", - "jquery-ui": "~1.10.4", - "underscore.string": "~2.3.3", - "marked": "~0.3.2", - "pace": "~0.5.1", - "font-awesome": "~4.2.0", - "mustache": "~1.0.0", - "canvg": "gabelerner/canvg", - "angular-ui-bootstrap-bower": "~0.12.1", - "leaflet": "~0.7.3", - "angular-base64-upload": "~0.1.11", - "angular-ui-select": "~0.13.2", - "angular-bootstrap-show-errors": "~2.3.0", - "angular-sanitize": "1.2.18", - "d3": "3.5.6", - "angular-ui-sortable": "~0.13.4", - "angular-resizable": "^1.2.0", - "material-design-iconic-font": "^2.2.0", - "plotly.js": "~1.16.0", - "angular-ui-ace": "bower", - "angular-vs-repeat": "^1.1.7", - "leaflet.markercluster": "^0.5.0" - }, - "devDependencies": { - "angular-mocks": "1.2.18", - "angular-scenario": "1.2.18" - }, - "resolutions": { - "angular": "1.2.18" - } -} diff --git a/circle.yml b/circle.yml index 720b6a23da..af16de59ed 100644 --- a/circle.yml +++ b/circle.yml @@ -13,9 +13,10 @@ dependencies: - pip install -r requirements.txt - pip install pymongo==3.2.1 - if [ "$CIRCLE_BRANCH" = "master" ]; then make deps; fi + - if [ "$CIRCLE_BRANCH" = "webpack" ]; then make deps; fi cache_directories: - node_modules/ - - rd_ui/app/bower_components/ + - client/node_modules/ test: override: - nosetests --with-xunit --xunit-file=$CIRCLE_TEST_REPORTS/junit.xml --with-coverage --cover-package=redash tests/ @@ -24,8 +25,9 @@ deployment: branch: master commands: - make pack - - make upload - - echo "rd_ui/app" >> .dockerignore + # Skipping uploads for now, until master is stable. + # - make upload + - echo "client/app" >> .dockerignore - docker pull redash/redash:latest - docker build -t redash/redash:$(./manage.py version | sed -e "s/\+/./") . - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS diff --git a/client/.babelrc b/client/.babelrc new file mode 100644 index 0000000000..05581748b9 --- /dev/null +++ b/client/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "stage-2"] +} diff --git a/client/.eslintignore b/client/.eslintignore new file mode 100644 index 0000000000..34af3774f3 --- /dev/null +++ b/client/.eslintignore @@ -0,0 +1,2 @@ +build/*.js +config/*.js diff --git a/client/.eslintrc.js b/client/.eslintrc.js new file mode 100644 index 0000000000..868568a84c --- /dev/null +++ b/client/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + root: true, + extends: 'airbnb-base', + env: { + "browser": true, + "node": true + }, + rules: { + // allow debugger during development + 'no-param-reassign': 0, + 'no-mixed-operators': 0, + 'no-underscore-dangle': 0, + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + } +} diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000000..1521c8b765 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1 @@ +dist diff --git a/rd_ui/app/styles/redash.css b/client/app/assets/css/redash.css similarity index 97% rename from rd_ui/app/styles/redash.css rename to client/app/assets/css/redash.css index 8a0af319a8..a615cd1690 100644 --- a/rd_ui/app/styles/redash.css +++ b/client/app/assets/css/redash.css @@ -160,16 +160,13 @@ a.navbar-brand img { /* Gridster */ -.gridster ul { - list-style-type: none; -} - li.widget { /*background-color:grey;*/ border-width: 1px; border-style: solid; border-color: grey; opacity: 0.7; + cursor: move; } li.widget:hover { @@ -188,6 +185,12 @@ li.widget:hover { background: rgba(0, 0, 0, 0.5) !important; } +.gridster li .heading { + border: #ddd; + background-color: #f5f5f5; + padding: 5px; +} + /* Editor */ .ace_editor { @@ -477,21 +480,6 @@ div.table-name:hover { display: none !important; } -/* Smart Table */ - -.smart-table { - margin-bottom: 0px; -} - -.smart-table .pagination { - margin-bottom: 5px; - margin-top: 10px; -} - -.smart-table .smart-table-header-row .header-content { - cursor: pointer; -} - .voffset { margin-top: 2px; } @@ -679,3 +667,7 @@ div.table-name:hover { stroke-opacity: .2; } +/*Dashboard list view */ +.m-2{ + margin:2px; +} diff --git a/rd_ui/app/styles/superflat_redash.css b/client/app/assets/css/superflat_redash.css similarity index 93% rename from rd_ui/app/styles/superflat_redash.css rename to client/app/assets/css/superflat_redash.css index d0bcb29c2d..ba9ed52a7f 100644 --- a/rd_ui/app/styles/superflat_redash.css +++ b/client/app/assets/css/superflat_redash.css @@ -254,809 +254,6 @@ th { border: 1px solid #ddd !important; } } -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.glyphicon-asterisk:before { - content: "\2a"; -} -.glyphicon-plus:before { - content: "\2b"; -} -.glyphicon-euro:before, -.glyphicon-eur:before { - content: "\20ac"; -} -.glyphicon-minus:before { - content: "\2212"; -} -.glyphicon-cloud:before { - content: "\2601"; -} -.glyphicon-envelope:before { - content: "\2709"; -} -.glyphicon-pencil:before { - content: "\270f"; -} -.glyphicon-glass:before { - content: "\e001"; -} -.glyphicon-music:before { - content: "\e002"; -} -.glyphicon-search:before { - content: "\e003"; -} -.glyphicon-heart:before { - content: "\e005"; -} -.glyphicon-star:before { - content: "\e006"; -} -.glyphicon-star-empty:before { - content: "\e007"; -} -.glyphicon-user:before { - content: "\e008"; -} -.glyphicon-film:before { - content: "\e009"; -} -.glyphicon-th-large:before { - content: "\e010"; -} -.glyphicon-th:before { - content: "\e011"; -} -.glyphicon-th-list:before { - content: "\e012"; -} -.glyphicon-ok:before { - content: "\e013"; -} -.glyphicon-remove:before { - content: "\e014"; -} -.glyphicon-zoom-in:before { - content: "\e015"; -} -.glyphicon-zoom-out:before { - content: "\e016"; -} -.glyphicon-off:before { - content: "\e017"; -} -.glyphicon-signal:before { - content: "\e018"; -} -.glyphicon-cog:before { - content: "\e019"; -} -.glyphicon-trash:before { - content: "\e020"; -} -.glyphicon-home:before { - content: "\e021"; -} -.glyphicon-file:before { - content: "\e022"; -} -.glyphicon-time:before { - content: "\e023"; -} -.glyphicon-road:before { - content: "\e024"; -} -.glyphicon-download-alt:before { - content: "\e025"; -} -.glyphicon-download:before { - content: "\e026"; -} -.glyphicon-upload:before { - content: "\e027"; -} -.glyphicon-inbox:before { - content: "\e028"; -} -.glyphicon-play-circle:before { - content: "\e029"; -} -.glyphicon-repeat:before { - content: "\e030"; -} -.glyphicon-refresh:before { - content: "\e031"; -} -.glyphicon-list-alt:before { - content: "\e032"; -} -.glyphicon-lock:before { - content: "\e033"; -} -.glyphicon-flag:before { - content: "\e034"; -} -.glyphicon-headphones:before { - content: "\e035"; -} -.glyphicon-volume-off:before { - content: "\e036"; -} -.glyphicon-volume-down:before { - content: "\e037"; -} -.glyphicon-volume-up:before { - content: "\e038"; -} -.glyphicon-qrcode:before { - content: "\e039"; -} -.glyphicon-barcode:before { - content: "\e040"; -} -.glyphicon-tag:before { - content: "\e041"; -} -.glyphicon-tags:before { - content: "\e042"; -} -.glyphicon-book:before { - content: "\e043"; -} -.glyphicon-bookmark:before { - content: "\e044"; -} -.glyphicon-print:before { - content: "\e045"; -} -.glyphicon-camera:before { - content: "\e046"; -} -.glyphicon-font:before { - content: "\e047"; -} -.glyphicon-bold:before { - content: "\e048"; -} -.glyphicon-italic:before { - content: "\e049"; -} -.glyphicon-text-height:before { - content: "\e050"; -} -.glyphicon-text-width:before { - content: "\e051"; -} -.glyphicon-align-left:before { - content: "\e052"; -} -.glyphicon-align-center:before { - content: "\e053"; -} -.glyphicon-align-right:before { - content: "\e054"; -} -.glyphicon-align-justify:before { - content: "\e055"; -} -.glyphicon-list:before { - content: "\e056"; -} -.glyphicon-indent-left:before { - content: "\e057"; -} -.glyphicon-indent-right:before { - content: "\e058"; -} -.glyphicon-facetime-video:before { - content: "\e059"; -} -.glyphicon-picture:before { - content: "\e060"; -} -.glyphicon-map-marker:before { - content: "\e062"; -} -.glyphicon-adjust:before { - content: "\e063"; -} -.glyphicon-tint:before { - content: "\e064"; -} -.glyphicon-edit:before { - content: "\e065"; -} -.glyphicon-share:before { - content: "\e066"; -} -.glyphicon-check:before { - content: "\e067"; -} -.glyphicon-move:before { - content: "\e068"; -} -.glyphicon-step-backward:before { - content: "\e069"; -} -.glyphicon-fast-backward:before { - content: "\e070"; -} -.glyphicon-backward:before { - content: "\e071"; -} -.glyphicon-play:before { - content: "\e072"; -} -.glyphicon-pause:before { - content: "\e073"; -} -.glyphicon-stop:before { - content: "\e074"; -} -.glyphicon-forward:before { - content: "\e075"; -} -.glyphicon-fast-forward:before { - content: "\e076"; -} -.glyphicon-step-forward:before { - content: "\e077"; -} -.glyphicon-eject:before { - content: "\e078"; -} -.glyphicon-chevron-left:before { - content: "\e079"; -} -.glyphicon-chevron-right:before { - content: "\e080"; -} -.glyphicon-plus-sign:before { - content: "\e081"; -} -.glyphicon-minus-sign:before { - content: "\e082"; -} -.glyphicon-remove-sign:before { - content: "\e083"; -} -.glyphicon-ok-sign:before { - content: "\e084"; -} -.glyphicon-question-sign:before { - content: "\e085"; -} -.glyphicon-info-sign:before { - content: "\e086"; -} -.glyphicon-screenshot:before { - content: "\e087"; -} -.glyphicon-remove-circle:before { - content: "\e088"; -} -.glyphicon-ok-circle:before { - content: "\e089"; -} -.glyphicon-ban-circle:before { - content: "\e090"; -} -.glyphicon-arrow-left:before { - content: "\e091"; -} -.glyphicon-arrow-right:before { - content: "\e092"; -} -.glyphicon-arrow-up:before { - content: "\e093"; -} -.glyphicon-arrow-down:before { - content: "\e094"; -} -.glyphicon-share-alt:before { - content: "\e095"; -} -.glyphicon-resize-full:before { - content: "\e096"; -} -.glyphicon-resize-small:before { - content: "\e097"; -} -.glyphicon-exclamation-sign:before { - content: "\e101"; -} -.glyphicon-gift:before { - content: "\e102"; -} -.glyphicon-leaf:before { - content: "\e103"; -} -.glyphicon-fire:before { - content: "\e104"; -} -.glyphicon-eye-open:before { - content: "\e105"; -} -.glyphicon-eye-close:before { - content: "\e106"; -} -.glyphicon-warning-sign:before { - content: "\e107"; -} -.glyphicon-plane:before { - content: "\e108"; -} -.glyphicon-calendar:before { - content: "\e109"; -} -.glyphicon-random:before { - content: "\e110"; -} -.glyphicon-comment:before { - content: "\e111"; -} -.glyphicon-magnet:before { - content: "\e112"; -} -.glyphicon-chevron-up:before { - content: "\e113"; -} -.glyphicon-chevron-down:before { - content: "\e114"; -} -.glyphicon-retweet:before { - content: "\e115"; -} -.glyphicon-shopping-cart:before { - content: "\e116"; -} -.glyphicon-folder-close:before { - content: "\e117"; -} -.glyphicon-folder-open:before { - content: "\e118"; -} -.glyphicon-resize-vertical:before { - content: "\e119"; -} -.glyphicon-resize-horizontal:before { - content: "\e120"; -} -.glyphicon-hdd:before { - content: "\e121"; -} -.glyphicon-bullhorn:before { - content: "\e122"; -} -.glyphicon-bell:before { - content: "\e123"; -} -.glyphicon-certificate:before { - content: "\e124"; -} -.glyphicon-thumbs-up:before { - content: "\e125"; -} -.glyphicon-thumbs-down:before { - content: "\e126"; -} -.glyphicon-hand-right:before { - content: "\e127"; -} -.glyphicon-hand-left:before { - content: "\e128"; -} -.glyphicon-hand-up:before { - content: "\e129"; -} -.glyphicon-hand-down:before { - content: "\e130"; -} -.glyphicon-circle-arrow-right:before { - content: "\e131"; -} -.glyphicon-circle-arrow-left:before { - content: "\e132"; -} -.glyphicon-circle-arrow-up:before { - content: "\e133"; -} -.glyphicon-circle-arrow-down:before { - content: "\e134"; -} -.glyphicon-globe:before { - content: "\e135"; -} -.glyphicon-wrench:before { - content: "\e136"; -} -.glyphicon-tasks:before { - content: "\e137"; -} -.glyphicon-filter:before { - content: "\e138"; -} -.glyphicon-briefcase:before { - content: "\e139"; -} -.glyphicon-fullscreen:before { - content: "\e140"; -} -.glyphicon-dashboard:before { - content: "\e141"; -} -.glyphicon-paperclip:before { - content: "\e142"; -} -.glyphicon-heart-empty:before { - content: "\e143"; -} -.glyphicon-link:before { - content: "\e144"; -} -.glyphicon-phone:before { - content: "\e145"; -} -.glyphicon-pushpin:before { - content: "\e146"; -} -.glyphicon-usd:before { - content: "\e148"; -} -.glyphicon-gbp:before { - content: "\e149"; -} -.glyphicon-sort:before { - content: "\e150"; -} -.glyphicon-sort-by-alphabet:before { - content: "\e151"; -} -.glyphicon-sort-by-alphabet-alt:before { - content: "\e152"; -} -.glyphicon-sort-by-order:before { - content: "\e153"; -} -.glyphicon-sort-by-order-alt:before { - content: "\e154"; -} -.glyphicon-sort-by-attributes:before { - content: "\e155"; -} -.glyphicon-sort-by-attributes-alt:before { - content: "\e156"; -} -.glyphicon-unchecked:before { - content: "\e157"; -} -.glyphicon-expand:before { - content: "\e158"; -} -.glyphicon-collapse-down:before { - content: "\e159"; -} -.glyphicon-collapse-up:before { - content: "\e160"; -} -.glyphicon-log-in:before { - content: "\e161"; -} -.glyphicon-flash:before { - content: "\e162"; -} -.glyphicon-log-out:before { - content: "\e163"; -} -.glyphicon-new-window:before { - content: "\e164"; -} -.glyphicon-record:before { - content: "\e165"; -} -.glyphicon-save:before { - content: "\e166"; -} -.glyphicon-open:before { - content: "\e167"; -} -.glyphicon-saved:before { - content: "\e168"; -} -.glyphicon-import:before { - content: "\e169"; -} -.glyphicon-export:before { - content: "\e170"; -} -.glyphicon-send:before { - content: "\e171"; -} -.glyphicon-floppy-disk:before { - content: "\e172"; -} -.glyphicon-floppy-saved:before { - content: "\e173"; -} -.glyphicon-floppy-remove:before { - content: "\e174"; -} -.glyphicon-floppy-save:before { - content: "\e175"; -} -.glyphicon-floppy-open:before { - content: "\e176"; -} -.glyphicon-credit-card:before { - content: "\e177"; -} -.glyphicon-transfer:before { - content: "\e178"; -} -.glyphicon-cutlery:before { - content: "\e179"; -} -.glyphicon-header:before { - content: "\e180"; -} -.glyphicon-compressed:before { - content: "\e181"; -} -.glyphicon-earphone:before { - content: "\e182"; -} -.glyphicon-phone-alt:before { - content: "\e183"; -} -.glyphicon-tower:before { - content: "\e184"; -} -.glyphicon-stats:before { - content: "\e185"; -} -.glyphicon-sd-video:before { - content: "\e186"; -} -.glyphicon-hd-video:before { - content: "\e187"; -} -.glyphicon-subtitles:before { - content: "\e188"; -} -.glyphicon-sound-stereo:before { - content: "\e189"; -} -.glyphicon-sound-dolby:before { - content: "\e190"; -} -.glyphicon-sound-5-1:before { - content: "\e191"; -} -.glyphicon-sound-6-1:before { - content: "\e192"; -} -.glyphicon-sound-7-1:before { - content: "\e193"; -} -.glyphicon-copyright-mark:before { - content: "\e194"; -} -.glyphicon-registration-mark:before { - content: "\e195"; -} -.glyphicon-cloud-download:before { - content: "\e197"; -} -.glyphicon-cloud-upload:before { - content: "\e198"; -} -.glyphicon-tree-conifer:before { - content: "\e199"; -} -.glyphicon-tree-deciduous:before { - content: "\e200"; -} -.glyphicon-cd:before { - content: "\e201"; -} -.glyphicon-save-file:before { - content: "\e202"; -} -.glyphicon-open-file:before { - content: "\e203"; -} -.glyphicon-level-up:before { - content: "\e204"; -} -.glyphicon-copy:before { - content: "\e205"; -} -.glyphicon-paste:before { - content: "\e206"; -} -.glyphicon-alert:before { - content: "\e209"; -} -.glyphicon-equalizer:before { - content: "\e210"; -} -.glyphicon-king:before { - content: "\e211"; -} -.glyphicon-queen:before { - content: "\e212"; -} -.glyphicon-pawn:before { - content: "\e213"; -} -.glyphicon-bishop:before { - content: "\e214"; -} -.glyphicon-knight:before { - content: "\e215"; -} -.glyphicon-baby-formula:before { - content: "\e216"; -} -.glyphicon-tent:before { - content: "\26fa"; -} -.glyphicon-blackboard:before { - content: "\e218"; -} -.glyphicon-bed:before { - content: "\e219"; -} -.glyphicon-apple:before { - content: "\f8ff"; -} -.glyphicon-erase:before { - content: "\e221"; -} -.glyphicon-hourglass:before { - content: "\231b"; -} -.glyphicon-lamp:before { - content: "\e223"; -} -.glyphicon-duplicate:before { - content: "\e224"; -} -.glyphicon-piggy-bank:before { - content: "\e225"; -} -.glyphicon-scissors:before { - content: "\e226"; -} -.glyphicon-bitcoin:before { - content: "\e227"; -} -.glyphicon-btc:before { - content: "\e227"; -} -.glyphicon-xbt:before { - content: "\e227"; -} -.glyphicon-yen:before { - content: "\00a5"; -} -.glyphicon-jpy:before { - content: "\00a5"; -} -.glyphicon-ruble:before { - content: "\20bd"; -} -.glyphicon-rub:before { - content: "\20bd"; -} -.glyphicon-scale:before { - content: "\e230"; -} -.glyphicon-ice-lolly:before { - content: "\e231"; -} -.glyphicon-ice-lolly-tasted:before { - content: "\e232"; -} -.glyphicon-education:before { - content: "\e233"; -} -.glyphicon-option-horizontal:before { - content: "\e234"; -} -.glyphicon-option-vertical:before { - content: "\e235"; -} -.glyphicon-menu-hamburger:before { - content: "\e236"; -} -.glyphicon-modal-window:before { - content: "\e237"; -} -.glyphicon-oil:before { - content: "\e238"; -} -.glyphicon-grain:before { - content: "\e239"; -} -.glyphicon-sunglasses:before { - content: "\e240"; -} -.glyphicon-text-size:before { - content: "\e241"; -} -.glyphicon-text-color:before { - content: "\e242"; -} -.glyphicon-text-background:before { - content: "\e243"; -} -.glyphicon-object-align-top:before { - content: "\e244"; -} -.glyphicon-object-align-bottom:before { - content: "\e245"; -} -.glyphicon-object-align-horizontal:before { - content: "\e246"; -} -.glyphicon-object-align-left:before { - content: "\e247"; -} -.glyphicon-object-align-vertical:before { - content: "\e248"; -} -.glyphicon-object-align-right:before { - content: "\e249"; -} -.glyphicon-triangle-right:before { - content: "\e250"; -} -.glyphicon-triangle-left:before { - content: "\e251"; -} -.glyphicon-triangle-bottom:before { - content: "\e252"; -} -.glyphicon-triangle-top:before { - content: "\e253"; -} -.glyphicon-console:before { - content: "\e254"; -} -.glyphicon-superscript:before { - content: "\e255"; -} -.glyphicon-subscript:before { - content: "\e256"; -} -.glyphicon-menu-left:before { - content: "\e257"; -} -.glyphicon-menu-right:before { - content: "\e258"; -} -.glyphicon-menu-down:before { - content: "\e259"; -} -.glyphicon-menu-up:before { - content: "\e260"; -} * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; diff --git a/rd_ui/app/fonts/roboto/Roboto-Bold-webfont.eot b/client/app/assets/fonts/roboto/Roboto-Bold-webfont.eot similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Bold-webfont.eot rename to client/app/assets/fonts/roboto/Roboto-Bold-webfont.eot diff --git a/rd_ui/app/fonts/roboto/Roboto-Bold-webfont.svg b/client/app/assets/fonts/roboto/Roboto-Bold-webfont.svg similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Bold-webfont.svg rename to client/app/assets/fonts/roboto/Roboto-Bold-webfont.svg diff --git a/rd_ui/app/fonts/roboto/Roboto-Bold-webfont.ttf b/client/app/assets/fonts/roboto/Roboto-Bold-webfont.ttf similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Bold-webfont.ttf rename to client/app/assets/fonts/roboto/Roboto-Bold-webfont.ttf diff --git a/rd_ui/app/fonts/roboto/Roboto-Bold-webfont.woff b/client/app/assets/fonts/roboto/Roboto-Bold-webfont.woff similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Bold-webfont.woff rename to client/app/assets/fonts/roboto/Roboto-Bold-webfont.woff diff --git a/rd_ui/app/fonts/roboto/Roboto-Light-webfont.eot b/client/app/assets/fonts/roboto/Roboto-Light-webfont.eot similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Light-webfont.eot rename to client/app/assets/fonts/roboto/Roboto-Light-webfont.eot diff --git a/rd_ui/app/fonts/roboto/Roboto-Light-webfont.svg b/client/app/assets/fonts/roboto/Roboto-Light-webfont.svg similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Light-webfont.svg rename to client/app/assets/fonts/roboto/Roboto-Light-webfont.svg diff --git a/rd_ui/app/fonts/roboto/Roboto-Light-webfont.ttf b/client/app/assets/fonts/roboto/Roboto-Light-webfont.ttf similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Light-webfont.ttf rename to client/app/assets/fonts/roboto/Roboto-Light-webfont.ttf diff --git a/rd_ui/app/fonts/roboto/Roboto-Light-webfont.woff b/client/app/assets/fonts/roboto/Roboto-Light-webfont.woff similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Light-webfont.woff rename to client/app/assets/fonts/roboto/Roboto-Light-webfont.woff diff --git a/rd_ui/app/fonts/roboto/Roboto-Medium-webfont.eot b/client/app/assets/fonts/roboto/Roboto-Medium-webfont.eot similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Medium-webfont.eot rename to client/app/assets/fonts/roboto/Roboto-Medium-webfont.eot diff --git a/rd_ui/app/fonts/roboto/Roboto-Medium-webfont.svg b/client/app/assets/fonts/roboto/Roboto-Medium-webfont.svg similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Medium-webfont.svg rename to client/app/assets/fonts/roboto/Roboto-Medium-webfont.svg diff --git a/rd_ui/app/fonts/roboto/Roboto-Medium-webfont.ttf b/client/app/assets/fonts/roboto/Roboto-Medium-webfont.ttf similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Medium-webfont.ttf rename to client/app/assets/fonts/roboto/Roboto-Medium-webfont.ttf diff --git a/rd_ui/app/fonts/roboto/Roboto-Medium-webfont.woff b/client/app/assets/fonts/roboto/Roboto-Medium-webfont.woff similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Medium-webfont.woff rename to client/app/assets/fonts/roboto/Roboto-Medium-webfont.woff diff --git a/rd_ui/app/fonts/roboto/Roboto-Regular-webfont.eot b/client/app/assets/fonts/roboto/Roboto-Regular-webfont.eot similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Regular-webfont.eot rename to client/app/assets/fonts/roboto/Roboto-Regular-webfont.eot diff --git a/rd_ui/app/fonts/roboto/Roboto-Regular-webfont.svg b/client/app/assets/fonts/roboto/Roboto-Regular-webfont.svg similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Regular-webfont.svg rename to client/app/assets/fonts/roboto/Roboto-Regular-webfont.svg diff --git a/rd_ui/app/fonts/roboto/Roboto-Regular-webfont.ttf b/client/app/assets/fonts/roboto/Roboto-Regular-webfont.ttf similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Regular-webfont.ttf rename to client/app/assets/fonts/roboto/Roboto-Regular-webfont.ttf diff --git a/rd_ui/app/fonts/roboto/Roboto-Regular-webfont.woff b/client/app/assets/fonts/roboto/Roboto-Regular-webfont.woff similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Regular-webfont.woff rename to client/app/assets/fonts/roboto/Roboto-Regular-webfont.woff diff --git a/rd_ui/app/fonts/roboto/Roboto-Thin-webfont.eot b/client/app/assets/fonts/roboto/Roboto-Thin-webfont.eot similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Thin-webfont.eot rename to client/app/assets/fonts/roboto/Roboto-Thin-webfont.eot diff --git a/rd_ui/app/fonts/roboto/Roboto-Thin-webfont.svg b/client/app/assets/fonts/roboto/Roboto-Thin-webfont.svg similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Thin-webfont.svg rename to client/app/assets/fonts/roboto/Roboto-Thin-webfont.svg diff --git a/rd_ui/app/fonts/roboto/Roboto-Thin-webfont.ttf b/client/app/assets/fonts/roboto/Roboto-Thin-webfont.ttf similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Thin-webfont.ttf rename to client/app/assets/fonts/roboto/Roboto-Thin-webfont.ttf diff --git a/rd_ui/app/fonts/roboto/Roboto-Thin-webfont.woff b/client/app/assets/fonts/roboto/Roboto-Thin-webfont.woff similarity index 100% rename from rd_ui/app/fonts/roboto/Roboto-Thin-webfont.woff rename to client/app/assets/fonts/roboto/Roboto-Thin-webfont.woff diff --git a/rd_ui/app/images/favicon-16x16.png b/client/app/assets/images/favicon-16x16.png similarity index 100% rename from rd_ui/app/images/favicon-16x16.png rename to client/app/assets/images/favicon-16x16.png diff --git a/rd_ui/app/images/favicon-32x32.png b/client/app/assets/images/favicon-32x32.png similarity index 100% rename from rd_ui/app/images/favicon-32x32.png rename to client/app/assets/images/favicon-32x32.png diff --git a/rd_ui/app/images/favicon-96x96.png b/client/app/assets/images/favicon-96x96.png similarity index 100% rename from rd_ui/app/images/favicon-96x96.png rename to client/app/assets/images/favicon-96x96.png diff --git a/rd_ui/app/images/redash_icon_small.png b/client/app/assets/images/redash_icon_small.png similarity index 100% rename from rd_ui/app/images/redash_icon_small.png rename to client/app/assets/images/redash_icon_small.png diff --git a/rd_ui/app/views/app_header.html b/client/app/components/app-header/app-header.html similarity index 58% rename from rd_ui/app/views/app_header.html rename to client/app/components/app-header/app-header.html index 75c335295d..0c51f97a4f 100644 --- a/rd_ui/app/views/app_header.html +++ b/client/app/components/app-header/app-header.html @@ -7,19 +7,19 @@ - + diff --git a/client/app/pages/admin/tasks/cancel-query-button/index.js b/client/app/pages/admin/tasks/cancel-query-button/index.js new file mode 100644 index 0000000000..4e57a4503d --- /dev/null +++ b/client/app/pages/admin/tasks/cancel-query-button/index.js @@ -0,0 +1,32 @@ +function cancelQueryButton() { + return { + restrict: 'E', + scope: { + queryId: '=', + taskId: '=', + }, + transclude: true, + template: '', + replace: true, + controller($scope, $http, currentUser, Events) { + $scope.inProgress = false; + + $scope.cancelExecution = () => { + $http.delete(`api/jobs/${$scope.taskId}`).success(() => { + }); + + let queryId = $scope.queryId; + if ($scope.queryId === 'adhoc') { + queryId = null; + } + + Events.record('cancel_execute', 'query', queryId, { admin: true }); + $scope.inProgress = true; + }; + }, + }; +} + +export default function (ngModule) { + ngModule.directive('cancelQueryButton', cancelQueryButton); +} diff --git a/client/app/pages/admin/tasks/index.js b/client/app/pages/admin/tasks/index.js new file mode 100644 index 0000000000..2fef8b68c1 --- /dev/null +++ b/client/app/pages/admin/tasks/index.js @@ -0,0 +1,65 @@ +import moment from 'moment'; +import template from './tasks.html'; +import registerCancelQueryButton from './cancel-query-button'; + +function TasksCtrl($scope, $location, $http, $timeout, NgTableParams, currentUser, Events) { + Events.record('view', 'page', 'admin/tasks'); + $scope.autoUpdate = true; + + $scope.selectedTab = 'in_progress'; + + $scope.tasks = { + pending: [], + in_progress: [], + done: [], + }; + + this.tableParams = new NgTableParams({ count: 50 }, {}); + + $scope.setTab = (tab) => { + $scope.selectedTab = tab; + this.tableParams.settings({ + dataset: $scope.tasks[tab], + }); + }; + + $scope.setTab($location.hash() || 'in_progress'); + + const refresh = () => { + if ($scope.autoUpdate) { + $scope.refresh_time = moment().add(1, 'minutes'); + $http.get('/api/admin/queries/tasks').success((data) => { + $scope.tasks = data; + this.tableParams.settings({ + dataset: $scope.tasks[$scope.selectedTab], + }); + }); + } + + const timer = $timeout(refresh, 5 * 1000); + + $scope.$on('$destroy', () => { + if (timer) { + $timeout.cancel(timer); + } + }); + }; + + refresh(); +} + +export default function (ngModule) { + ngModule.component('tasksPage', { + template, + controller: TasksCtrl, + }); + + registerCancelQueryButton(ngModule); + + return { + '/admin/queries/tasks': { + template: '', + title: 'Running Queries', + }, + }; +} diff --git a/client/app/pages/admin/tasks/tasks.html b/client/app/pages/admin/tasks/tasks.html new file mode 100644 index 0000000000..42b9c12a80 --- /dev/null +++ b/client/app/pages/admin/tasks/tasks.html @@ -0,0 +1,42 @@ + + + +
+
+ + + + + + + + + + + + + + + + + +
{{row.data_source_id}}{{row.username}}{{row.state}} {{row.query_id}}{{row.query_hash}}{{row.run_time | durationHumanize}}{{row.created_at | dateTime }}{{row.started_at | dateTime }}{{row.updated_at | dateTime }} + +
+ + + + + +
+
diff --git a/rd_ui/app/views/alerts/alert_subscriptions.html b/client/app/pages/alert/alert-subscriptions/alert-subscriptions.html similarity index 100% rename from rd_ui/app/views/alerts/alert_subscriptions.html rename to client/app/pages/alert/alert-subscriptions/alert-subscriptions.html diff --git a/client/app/pages/alert/alert-subscriptions/index.js b/client/app/pages/alert/alert-subscriptions/index.js new file mode 100644 index 0000000000..5952c31309 --- /dev/null +++ b/client/app/pages/alert/alert-subscriptions/index.js @@ -0,0 +1,100 @@ +import { contains, without, compact } from 'underscore'; +import template from './alert-subscriptions.html'; + +function controller($scope, $q, $sce, currentUser, AlertSubscription, Destination, toastr) { + $scope.newSubscription = {}; + $scope.subscribers = []; + $scope.destinations = []; + $scope.currentUser = currentUser; + + $q.all([Destination.query().$promise, + AlertSubscription.query({ alertId: $scope.alertId }).$promise]).then((responses) => { + const destinations = responses[0]; + const subscribers = responses[1]; + + const subscribedDestinations = + compact(subscribers.map(s => s.destination && s.destination.id)); + + const subscribedUsers = + compact(subscribers.map(s => !s.destination && s.user.id)); + + $scope.destinations = destinations.filter(d => !contains(subscribedDestinations, d.id)); + + if (!contains(subscribedUsers, currentUser.id)) { + $scope.destinations.unshift({ user: { name: currentUser.name } }); + } + + $scope.newSubscription.destination = $scope.destinations[0]; + $scope.subscribers = subscribers; + }); + + $scope.destinationsDisplay = (d) => { + if (!d) { + return ''; + } + + let destination = d; + if (d.destination) { + destination = destination.destination; + } else if (destination.user) { + destination = { + name: `${d.user.name} (Email)`, + icon: 'fa-envelope', + type: 'user', + }; + } + + return $sce.trustAsHtml(` ${destination.name}`); + }; + + $scope.saveSubscriber = () => { + const sub = new AlertSubscription({ alert_id: $scope.alertId }); + if ($scope.newSubscription.destination.id) { + sub.destination_id = $scope.newSubscription.destination.id; + } + + sub.$save(() => { + toastr.success('Subscribed.'); + $scope.subscribers.push(sub); + $scope.destinations = without($scope.destinations, $scope.newSubscription.destination); + if ($scope.destinations.length > 0) { + $scope.newSubscription.destination = $scope.destinations[0]; + } else { + $scope.newSubscription.destination = undefined; + } + }, () => { + toastr.error('Failed saving subscription.'); + }); + }; + + $scope.unsubscribe = (subscriber) => { + const destination = subscriber.destination; + const user = subscriber.user; + + subscriber.$delete(() => { + toastr.success('Unsubscribed'); + $scope.subscribers = without($scope.subscribers, subscriber); + if (destination) { + $scope.destinations.push(destination); + } else if (user.id === currentUser.id) { + $scope.destinations.push({ user: { name: currentUser.name } }); + } + + if ($scope.destinations.length === 1) { + $scope.newSubscription.destination = $scope.destinations[0]; + } + }, () => { + toastr.error('Failed unsubscribing.'); + }); + }; +} + +export default () => ({ + restrict: 'E', + replace: true, + scope: { + alertId: '=', + }, + template, + controller, +}); diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html new file mode 100644 index 0000000000..c7d117be64 --- /dev/null +++ b/client/app/pages/alert/alert.html @@ -0,0 +1,70 @@ + + + +
+ + +
+
+
+
+
+ + + {{$select.selected.name}} + +
+
+
+
+ +
+ + +
+ +
+
+ +
+ +
+ +
+

{{$ctrl.queryResult.getData()[0][$ctrl.alert.options.column]}}

+
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js new file mode 100644 index 0000000000..a765f330ff --- /dev/null +++ b/client/app/pages/alert/index.js @@ -0,0 +1,95 @@ +import { template as templateBuilder } from 'underscore'; +import template from './alert.html'; +import alertSubscriptions from './alert-subscriptions'; + +function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Events, Alert) { + this.alertId = $routeParams.alertId; + + if (this.alertId === 'new') { + Events.record('view', 'page', 'alerts/new'); + } else { + Events.record('view', 'alert', this.alertId); + } + + this.trustAsHtml = html => $sce.trustAsHtml(html); + + this.onQuerySelected = (item) => { + this.selectedQuery = item; + item.getQueryResultPromise().then((result) => { + this.queryResult = result; + this.alert.options.column = this.alert.options.column || result.getColumnNames()[0]; + }); + }; + + if (this.alertId === 'new') { + this.alert = new Alert({ options: {} }); + this.canEdit = true; + } else { + this.alert = Alert.get({ id: this.alertId }, (alert) => { + this.onQuerySelected(new Query(alert.query)); + }); + this.canEdit = currentUser.canEdit(this.alert); + } + + this.ops = ['greater than', 'less than', 'equals']; + this.selectedQuery = null; + + this.getDefaultName = () => { + if (!this.alert.query) { + return undefined; + } + return templateBuilder('<%= query.name %>: <%= options.column %> <%= options.op %> <%= options.value %>', this.alert); + }; + + this.searchQueries = (term) => { + if (!term || term.length < 3) { + return; + } + + Query.search({ q: term }, (results) => { + this.queries = results; + }); + }; + + this.saveChanges = () => { + if (this.alert.name === undefined || this.alert.name === '') { + this.alert.name = this.getDefaultName(); + } + if (this.alert.rearm === '' || this.alert.rearm === 0) { + this.alert.rearm = null; + } + this.alert.$save((alert) => { + toastr.success('Saved.'); + if (this.alertId === 'new') { + $location.path(`/alerts/${alert.id}`).replace(); + } + }, () => { + toastr.error('Failed saving alert.'); + }); + }; + + this.delete = () => { + this.alert.$delete(() => { + $location.path('/alerts'); + toastr.success('Alert deleted.'); + }, () => { + toastr.error('Failed deleting alert.'); + }); + }; +} + +export default function (ngModule) { + ngModule.component('alertPage', { + template, + controller: AlertCtrl, + }); + + ngModule.directive('alertSubscriptions', alertSubscriptions); + + return { + '/alerts/:alertId': { + template: '', + title: 'Alerts', + }, + }; +} diff --git a/client/app/pages/alerts-list/alerts-list.html b/client/app/pages/alerts-list/alerts-list.html new file mode 100644 index 0000000000..bc83e10eb5 --- /dev/null +++ b/client/app/pages/alerts-list/alerts-list.html @@ -0,0 +1,18 @@ +
+ + New Alert + + +
+
+ + + + + + + +
{{alert.name}}{{alert.user.name}}{{alert.state | uppercase}} since
+
+
+
diff --git a/client/app/pages/alerts-list/index.js b/client/app/pages/alerts-list/index.js new file mode 100644 index 0000000000..93af831a91 --- /dev/null +++ b/client/app/pages/alerts-list/index.js @@ -0,0 +1,38 @@ +import template from './alerts-list.html'; + +class AlertsListCtrl { + constructor(NgTableParams, currentUser, Events, Alert) { + Events.record('view', 'page', 'alerts'); + this.tableParams = new NgTableParams({ count: 50 }, {}); + + Alert.query((alerts) => { + const stateClass = { + ok: 'label label-success', + triggered: 'label label-danger', + unknown: 'label label-warning', + }; + + alerts.forEach((alert) => { + alert.class = stateClass[alert.state]; + }); + + this.tableParams.settings({ + dataset: alerts, + }); + }); + } +} + +export default function (ngModule) { + ngModule.component('alertsListPage', { + template, + controller: AlertsListCtrl, + }); + + return { + '/alerts': { + template: '', + title: 'Alerts', + }, + }; +} diff --git a/client/app/pages/dashboards/add-widget-dialog.html b/client/app/pages/dashboards/add-widget-dialog.html new file mode 100644 index 0000000000..62c43efeb1 --- /dev/null +++ b/client/app/pages/dashboards/add-widget-dialog.html @@ -0,0 +1,51 @@ + + + + diff --git a/client/app/pages/dashboards/add-widget-dialog.js b/client/app/pages/dashboards/add-widget-dialog.js new file mode 100644 index 0000000000..9fab031a16 --- /dev/null +++ b/client/app/pages/dashboards/add-widget-dialog.js @@ -0,0 +1,98 @@ +import template from './add-widget-dialog.html'; + +const AddWidgetDialog = { + template, + bindings: { + resolve: '<', + close: '&', + dismiss: '&', + }, + controller($sce, toastr, Query, Widget) { + this.dashboard = this.resolve.dashboard; + this.saveInProgress = false; + this.widgetSize = 1; + this.selectedVis = null; + this.query = {}; + this.selected_query = undefined; + this.text = ''; + this.widgetSizes = [{ + name: 'Regular', + value: 1, + }, { + name: 'Double', + value: 2, + }]; + + this.type = 'visualization'; + + this.trustAsHtml = html => $sce.trustAsHtml(html); + this.isVisualization = () => this.type === 'visualization'; + this.isTextBox = () => this.type === 'textbox'; + + this.setType = (type) => { + this.type = type; + if (type === 'textbox') { + this.widgetSizes.push({ name: 'Hidden', value: 0 }); + } else if (this.widgetSizes.length > 2) { + this.widgetSizes.pop(); + } + }; + + this.onQuerySelect = () => { + if (!this.query.selected) { + return; + } + + Query.get({ id: this.query.selected.id }, (query) => { + if (query) { + this.selected_query = query; + if (query.visualizations.length) { + this.selectedVis = query.visualizations[0]; + } + } + }); + }; + + this.searchQueries = (term) => { + if (!term || term.length < 3) { + return; + } + + Query.search({ q: term }, (results) => { + this.queries = results; + }); + }; + + this.saveWidget = () => { + this.saveInProgress = true; + const widget = new Widget({ + visualization_id: this.selectedVis && this.selectedVis.id, + dashboard_id: this.dashboard.id, + options: {}, + width: this.widgetSize, + text: this.text, + }); + + widget.$save().then((response) => { + // update dashboard layout + this.dashboard.layout = response.layout; + this.dashboard.version = response.version; + const newWidget = new Widget(response.widget); + if (response.new_row) { + this.dashboard.widgets.push([newWidget]); + } else { + this.dashboard.widgets[this.dashboard.widgets.length - 1].push(newWidget); + } + this.close(); + }).catch(() => { + toastr.error('Widget can not be added'); + }).finally(() => { + this.saveInProgress = false; + }); + }; + }, +}; + +export default function (ngModule) { + ngModule.component('addWidgetDialog', AddWidgetDialog); +} diff --git a/client/app/pages/dashboards/dashboard-list.html b/client/app/pages/dashboards/dashboard-list.html new file mode 100644 index 0000000000..28b116fa70 --- /dev/null +++ b/client/app/pages/dashboards/dashboard-list.html @@ -0,0 +1,29 @@ +
+ +
+ +
+

Tags

+ + {{ tag }} + +
+
+
+
+ + + + + + +
+ + {{ dashboard.untagged_name }} + + {{ dashboard.created_at | dateTime }}
+
+
+
\ No newline at end of file diff --git a/client/app/pages/dashboards/dashboard-list.js b/client/app/pages/dashboards/dashboard-list.js new file mode 100644 index 0000000000..179502a969 --- /dev/null +++ b/client/app/pages/dashboards/dashboard-list.js @@ -0,0 +1,80 @@ +import _ from 'underscore'; +import template from './dashboard-list.html'; + +function DashboardListCtrl($scope, Dashboard, $location, currentUser, clientConfig, NgTableParams) { + const self = this; + + this.logoUrl = clientConfig.logoUrl; + const page = parseInt($location.search().page || 1, 10); + const count = 25; + + this.defaultOptions = {}; + this.dashboards = Dashboard.query({}); // shared promise + + this.selectedTags = []; // in scope because it needs to be accessed inside a table refresh + this.searchText = ''; + + this.tagIsSelected = tag => this.selectedTags.indexOf(tag) > -1; + + this.toggleTag = (tag) => { + if (this.tagIsSelected(tag)) { + this.selectedTags = this.selectedTags.filter(e => e !== tag); + } else { + this.selectedTags.push(tag); + } + this.tableParams.reload(); + }; + + this.allTags = []; + this.dashboards.$promise.then((data) => { + const out = data.map(dashboard => dashboard.name.match(/(^\w+):|(#\w+)/ig)); + this.allTags = _.unique(_.flatten(out)).filter(e => e); + }); + + this.tableParams = new NgTableParams({ page, count }, { + getData(params) { + const options = params.url(); + $location.search('page', options.page); + + return self.dashboards.$promise.then((data) => { + params.total(data.count); + return data.map((dashboard) => { + dashboard.tags = dashboard.name.match(/(^\w+):|(#\w+)/ig); + dashboard.untagged_name = dashboard.name.replace(/(\w+):|(#\w+)/ig, '').trim(); + return dashboard; + }).filter((value) => { + if (self.selectedTags.length) { + const valueTags = new Set(value.tags); + const tagMatch = self.selectedTags; + const filteredMatch = tagMatch.filter(x => valueTags.has(x)); + if (tagMatch.length !== filteredMatch.length) { + return false; + } + } + if (self.searchText && self.searchText.length) { + if (!value.untagged_name.toLowerCase().includes(self.searchText)) { + return false; + } + } + return true; + }); + }); + }, + }); +} + +export default function (ngModule) { + ngModule.component('pageDashboardList', { + template, + controller: DashboardListCtrl, + }); + + const route = { + template: '', + reloadOnSearch: false, + }; + + return { + '/dashboards': route, + }; +} diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html new file mode 100644 index 0000000000..74598a8f17 --- /dev/null +++ b/client/app/pages/dashboards/dashboard.html @@ -0,0 +1,61 @@ +
+ + +
+ + + +
+ + +
+ +
+ +
+ This dashboard is archived and won't appear in the dashboards list or search results. +
+ +
+ This dashboard is a draft. +
+ +
+ +
+ +
+ +
+
diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js new file mode 100644 index 0000000000..6c7a673b55 --- /dev/null +++ b/client/app/pages/dashboards/dashboard.js @@ -0,0 +1,236 @@ +import * as _ from 'underscore'; +import template from './dashboard.html'; +import shareDashboardTemplate from './share-dashboard.html'; + +function DashboardCtrl($routeParams, $location, $timeout, $q, $uibModal, + Title, AlertDialog, Dashboard, currentUser, clientConfig, Events) { + this.refreshEnabled = false; + this.isFullscreen = false; + this.refreshRate = null; + this.showPermissionsControl = clientConfig.showPermissionsControl; + this.currentUser = currentUser; + this.refreshRates = [ + { name: '10 seconds', rate: 10 }, + { name: '30 seconds', rate: 30 }, + { name: '1 minute', rate: 60 }, + { name: '5 minutes', rate: 60 * 5 }, + { name: '10 minutes', rate: 60 * 10 }, + { name: '30 minutes', rate: 60 * 30 }, + { name: '1 hour', rate: 60 * 60 }, + ]; + + this.setRefreshRate = (rate) => { + this.refreshRate = rate; + if (rate !== null) { + this.loadDashboard(true); + this.autoRefresh(); + } + }; + + const renderDashboard = (dashboard, force) => { + Title.set(dashboard.name); + const promises = []; + + this.dashboard.widgets.forEach(row => + row.forEach((widget) => { + if (widget.visualization) { + const maxAge = force ? 0 : undefined; + const queryResult = widget.getQuery().getQueryResult(maxAge); + if (!_.isUndefined(queryResult)) { + promises.push(queryResult.toPromise()); + } + } + }) + ); + + $q.all(promises).then((queryResults) => { + const filters = {}; + queryResults.forEach((queryResult) => { + const queryFilters = queryResult.getFilters(); + queryFilters.forEach((queryFilter) => { + const hasQueryStringValue = _.has($location.search(), queryFilter.name); + + if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) { + // If dashboard filters not enabled, or no query string value given, + // skip filters linking. + return; + } + + if (!_.has(filters, queryFilter.name)) { + const filter = _.extend({}, queryFilter); + filters[filter.name] = filter; + filters[filter.name].originFilters = []; + if (hasQueryStringValue) { + filter.current = $location.search()[filter.name]; + } + + // $scope.$watch(() => filter.current, (value) => { + // _.each(filter.originFilters, (originFilter) => { + // originFilter.current = value; + // }); + // }); + } + + // TODO: merge values. + filters[queryFilter.name].originFilters.push(queryFilter); + }); + }); + + this.filters = _.values(filters); + }); + }; + + this.loadDashboard = _.throttle((force) => { + this.dashboard = Dashboard.get({ slug: $routeParams.dashboardSlug }, (dashboard) => { + Events.record('view', 'dashboard', dashboard.id); + renderDashboard(dashboard, force); + }, () => { + // error... + // try again. we wrap loadDashboard with throttle so it doesn't happen too often. + // we might want to consider exponential backoff and also move this as a general + // solution in $http/$resource for all AJAX calls. + this.loadDashboard(); + }); + }, 1000); + + this.loadDashboard(); + + this.autoRefresh = () => { + $timeout(() => { + this.loadDashboard(true); + }, this.refreshRate.rate * 1000 + ).then(() => this.autoRefresh()); + }; + + this.archiveDashboard = () => { + const archive = () => { + Events.record('archive', 'dashboard', this.dashboard.id); + this.dashboard.$delete(() => { + // TODO: + // this.$parent.reloadDashboards(); + }); + }; + + const title = 'Archive Dashboard'; + const message = `Are you sure you want to archive the "${this.dashboard.name}" dashboard?`; + const confirm = { class: 'btn-warning', title: 'Archive' }; + + AlertDialog.open(title, message, confirm).then(archive); + }; + + this.showManagePermissionsModal = () => { + $uibModal.open({ + component: 'permissionsEditor', + resolve: { + aclUrl: { url: `api/dashboards/${this.dashboard.id}/acl` }, + }, + }); + }; + + this.editDashboard = () => { + $uibModal.open({ + component: 'editDashboardDialog', + resolve: { + dashboard: () => this.dashboard, + }, + }).result.then((dashboard) => { this.dashboard = dashboard; }); + }; + + this.addWidget = () => { + $uibModal.open({ + component: 'addWidgetDialog', + resolve: { + dashboard: () => this.dashboard, + }, + }); + }; + + this.toggleFullscreen = () => { + this.isFullscreen = !this.isFullscreen; + document.querySelector('body').classList.toggle('headless'); + + if (this.isFullscreen) { + $location.search('fullscreen', true); + } else { + $location.search('fullscreen', null); + } + }; + + this.togglePublished = () => { + Events.record(currentUser, 'toggle_published', 'dashboard', this.dashboard.id); + this.dashboard.is_draft = !this.dashboard.is_draft; + this.saveInProgress = true; + Dashboard.save({ + slug: this.dashboard.id, + name: this.dashboard.name, + layout: JSON.stringify(this.dashboard.layout), + is_draft: this.dashboard.is_draft, + }, (dashboard) => { + this.saveInProgress = false; + this.dashboard.version = dashboard.version; + }); + }; + + if (_.has($location.search(), 'fullscreen')) { + this.toggleFullscreen(); + } + + this.openShareForm = () => { + $uibModal.open({ + component: 'shareDashboard', + resolve: { + dashboard: this.dashboard, + }, + }); + }; +} + +const ShareDashboardComponent = { + template: shareDashboardTemplate, + bindings: { + resolve: '<', + close: '&', + dismiss: '&', + }, + controller($http) { + this.dashboard = this.resolve.dashboard; + + this.toggleSharing = () => { + const url = `api/dashboards/${this.dashboard.id}/share`; + + if (!this.dashboard.publicAccessEnabled) { + // disable + $http.delete(url).success(() => { + this.dashboard.publicAccessEnabled = false; + delete this.dashboard.public_url; + }).error(() => { + this.dashboard.publicAccessEnabled = true; + // TODO: show message + }); + } else { + $http.post(url).success((data) => { + this.dashboard.publicAccessEnabled = true; + this.dashboard.public_url = data.public_url; + }).error(() => { + this.dashboard.publicAccessEnabled = false; + // TODO: show message + }); + } + }; + }, +}; + +export default function (ngModule) { + ngModule.component('shareDashboard', ShareDashboardComponent); + ngModule.component('dashboardPage', { + template, + controller: DashboardCtrl, + }); + + return { + '/dashboard/:dashboardSlug': { + template: '', + reloadOnSearch: false, + }, + }; +} diff --git a/client/app/pages/dashboards/edit-dashboard-dialog.html b/client/app/pages/dashboards/edit-dashboard-dialog.html new file mode 100644 index 0000000000..cf09d1ac81 --- /dev/null +++ b/client/app/pages/dashboards/edit-dashboard-dialog.html @@ -0,0 +1,21 @@ + + + diff --git a/client/app/pages/dashboards/edit-dashboard-dialog.js b/client/app/pages/dashboards/edit-dashboard-dialog.js new file mode 100644 index 0000000000..3d6bc47707 --- /dev/null +++ b/client/app/pages/dashboards/edit-dashboard-dialog.js @@ -0,0 +1,98 @@ +import { sortBy } from 'underscore'; +import template from './edit-dashboard-dialog.html'; + +const EditDashboardDialog = { + bindings: { + resolve: '<', + close: '&', + dismiss: '&', + }, + template, + controller($location, $http, toastr, Events, currentUser, Dashboard) { + this.dashboard = this.resolve.dashboard; + this.gridsterOptions = { + margins: [5, 5], + rowHeight: 100, + colWidth: 260, + columns: 2, + mobileModeEnabled: false, + swapping: true, + minRows: 1, + draggable: { + enabled: true, + }, + resizable: { + enabled: false, + }, + }; + + this.items = []; + + if (this.dashboard.widgets) { + this.dashboard.widgets.forEach((row, rowIndex) => { + row.forEach((widget, colIndex) => { + this.items.push({ + id: widget.id, + col: colIndex, + row: rowIndex, + sizeY: 1, + sizeX: widget.width, + name: widget.getName(), // visualization.query.name + }); + }); + }); + } + + this.saveDashboard = () => { + this.saveInProgress = true; + + if (this.dashboard.id) { + const layout = []; + const sortedItems = sortBy(this.items, item => item.row * 10 + item.col); + + sortedItems.forEach((item) => { + layout[item.row] = layout[item.row] || []; + if (item.col > 0 && layout[item.row][item.col - 1] === undefined) { + layout[item.row][item.col - 1] = item.id; + } else { + layout[item.row][item.col] = item.id; + } + }); + + const request = { + slug: this.dashboard.id, + name: this.dashboard.name, + version: this.dashboard.version, + layout: JSON.stringify(layout), + }; + + Dashboard.save(request, (dashboard) => { + this.dashboard = dashboard; + this.saveInProgress = false; + this.close({ $value: this.dashboard }); + }, (error) => { + this.saveInProgress = false; + if (error.status === 403) { + toastr.error('Unable to save dashboard: Permission denied.'); + } else if (error.status === 409) { + toastr.error('It seems like the dashboard has been modified by another user. ' + + 'Please copy/backup your changes and reload this page.', { autoDismiss: false }); + } + }); + Events.record('edit', 'dashboard', this.dashboard.id); + } else { + $http.post('api/dashboards', { + name: this.dashboard.name, + }).success((response) => { + this.close(); + $location.path(`/dashboard/${response.slug}`).replace(); + }); + Events.record('create', 'dashboard'); + } + }; + }, +}; + +export default function (ngModule) { + ngModule.component('editDashboardDialog', EditDashboardDialog); +} diff --git a/client/app/pages/dashboards/edit-text-box.html b/client/app/pages/dashboards/edit-text-box.html new file mode 100644 index 0000000000..3c947135df --- /dev/null +++ b/client/app/pages/dashboards/edit-text-box.html @@ -0,0 +1,18 @@ + + + + diff --git a/client/app/pages/dashboards/index.js b/client/app/pages/dashboards/index.js new file mode 100644 index 0000000000..8395019961 --- /dev/null +++ b/client/app/pages/dashboards/index.js @@ -0,0 +1,14 @@ +import dashboardPage from './dashboard'; +import dashboardList from './dashboard-list'; +import widgetComponent from './widget'; +import addWidgetDialog from './add-widget-dialog'; +import registerEditDashboardDialog from './edit-dashboard-dialog'; +import publicDashboardPage from './public-dashboard-page'; + +export default function (ngModule) { + addWidgetDialog(ngModule); + widgetComponent(ngModule); + publicDashboardPage(ngModule); + registerEditDashboardDialog(ngModule); + return Object.assign({}, dashboardPage(ngModule), dashboardList(ngModule)); +} diff --git a/client/app/pages/dashboards/public-dashboard-page.html b/client/app/pages/dashboards/public-dashboard-page.html new file mode 100644 index 0000000000..281d87ed5f --- /dev/null +++ b/client/app/pages/dashboards/public-dashboard-page.html @@ -0,0 +1,21 @@ + + + +
+ + + +
+ +
+ +
+ +
+
diff --git a/client/app/pages/dashboards/public-dashboard-page.js b/client/app/pages/dashboards/public-dashboard-page.js new file mode 100644 index 0000000000..a4a190ff4e --- /dev/null +++ b/client/app/pages/dashboards/public-dashboard-page.js @@ -0,0 +1,51 @@ +import template from './public-dashboard-page.html'; +import logoUrl from '../../assets/images/redash_icon_small.png'; + +const PublicDashboardPage = { + template, + bindings: { + dashboard: '<', + }, + controller($routeParams, Widget) { + // embed in params == headless + this.logoUrl = logoUrl; + this.headless = $routeParams.embed; + if (this.headless) { + document.querySelector('body').classList.add('headless'); + } + this.public = true; + this.dashboard.widgets = this.dashboard.widgets.map(row => + row.map(widget => + new Widget(widget) + ) + ); + }, +}; + +export default function (ngModule) { + ngModule.component('publicDashboardPage', PublicDashboardPage); + + function loadPublicDashboard($http, $route) { + const token = $route.current.params.token; + return $http.get(`/api/dashboards/public/${token}`).then(response => + response.data + ); + } + + function session($http, $route, Auth) { + const token = $route.current.params.token; + Auth.setApiKey(token); + return Auth.loadConfig(); + } + + ngModule.config(($routeProvider) => { + $routeProvider.when('/public/dashboards/:token', { + template: '', + reloadOnSearch: false, + resolve: { + dashboard: loadPublicDashboard, + session, + }, + }); + }); +} diff --git a/client/app/pages/dashboards/share-dashboard.html b/client/app/pages/dashboards/share-dashboard.html new file mode 100644 index 0000000000..14400de462 --- /dev/null +++ b/client/app/pages/dashboards/share-dashboard.html @@ -0,0 +1,17 @@ + + diff --git a/client/app/pages/dashboards/widget.html b/client/app/pages/dashboards/widget.html new file mode 100644 index 0000000000..3109c4dc6a --- /dev/null +++ b/client/app/pages/dashboards/widget.html @@ -0,0 +1,77 @@ +
+
+
+
+

+ {{$ctrl.query.name}} + + +

+

+ {{$ctrl.query.name}} + +

+
+
+ +
+ + + +
+
+
Error running query: {{$ctrl.queryResult.getError()}}
+
+
+ +
+
+ +
+
+ +
+ Updated: + + Updated: {{$ctrl.queryResult.getUpdatedAt() | dateTime}} + + +
+
+ +
+
+
+

+

+ This widget requires access to a data source you don't have access to. +

+
+
+
+ +
+
+ +

+
+
+
diff --git a/client/app/pages/dashboards/widget.js b/client/app/pages/dashboards/widget.js new file mode 100644 index 0000000000..a6c714ad30 --- /dev/null +++ b/client/app/pages/dashboards/widget.js @@ -0,0 +1,93 @@ +import template from './widget.html'; +import editTextBoxTemplate from './edit-text-box.html'; + +const EditTextBoxComponent = { + template: editTextBoxTemplate, + bindings: { + resolve: '<', + close: '&', + dismiss: '&', + }, + controller(toastr) { + this.saveInProgress = false; + this.widget = this.resolve.widget; + this.saveWidget = () => { + this.saveInProgress = true; + this.widget.$save().then(() => { + this.close(); + }).catch(() => { + toastr.error('Widget can not be updated'); + }).finally(() => { + this.saveInProgress = false; + }); + }; + }, +}; + +function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser) { + this.canViewQuery = currentUser.hasPermission('view_query'); + + this.editTextBox = () => { + $uibModal.open({ + component: 'editTextBox', + resolve: { + widget: this.widget, + }, + }); + }; + + this.deleteWidget = () => { + if (!$window.confirm(`Are you sure you want to remove "${this.widget.getName()}" from the dashboard?`)) { + return; + } + + Events.record('delete', 'widget', this.widget.id); + + this.widget.$delete((response) => { + this.dashboard.widgets = + this.dashboard.widgets.map(row => row.filter(widget => widget.id !== undefined)); + + this.dashboard.widgets = this.dashboard.widgets.filter(row => row.length > 0); + + this.dashboard.layout = response.layout; + this.dashboard.version = response.version; + }); + }; + + Events.record('view', 'widget', this.widget.id); + + this.reload = (force) => { + let maxAge = $location.search().maxAge; + if (force) { + maxAge = 0; + } + this.queryResult = this.query.getQueryResult(maxAge); + }; + + if (this.widget.visualization) { + Events.record('view', 'query', this.widget.visualization.query.id); + Events.record('view', 'visualization', this.widget.visualization.id); + + this.query = this.widget.getQuery(); + this.reload(false); + + this.type = 'visualization'; + } else if (this.widget.restricted) { + this.type = 'restricted'; + } else { + this.type = 'textbox'; + } +} + +export default function (ngModule) { + ngModule.component('editTextBox', EditTextBoxComponent); + ngModule.component('dashboardWidget', { + template, + controller: DashboardWidgetCtrl, + bindings: { + widget: '<', + public: '<', + dashboard: '<', + }, + }); +} diff --git a/client/app/pages/data-sources/index.js b/client/app/pages/data-sources/index.js new file mode 100644 index 0000000000..5988411af5 --- /dev/null +++ b/client/app/pages/data-sources/index.js @@ -0,0 +1,6 @@ +import registerList from './list'; +import registerShow from './show'; + +export default function (ngModule) { + return Object.assign({}, registerList(ngModule), registerShow(ngModule)); +} diff --git a/rd_ui/app/views/data_sources/list.html b/client/app/pages/data-sources/list.html similarity index 100% rename from rd_ui/app/views/data_sources/list.html rename to client/app/pages/data-sources/list.html diff --git a/client/app/pages/data-sources/list.js b/client/app/pages/data-sources/list.js new file mode 100644 index 0000000000..580e6cb29d --- /dev/null +++ b/client/app/pages/data-sources/list.js @@ -0,0 +1,19 @@ +import template from './list.html'; + +function DataSourcesCtrl($scope, $location, currentUser, Events, DataSource) { + Events.record('view', 'page', 'admin/data_sources'); + + $scope.dataSources = DataSource.query(); +} + +export default function (ngModule) { + ngModule.controller('DataSourcesCtrl', DataSourcesCtrl); + + return { + '/data_sources': { + template, + controller: 'DataSourcesCtrl', + title: 'Data Sources', + }, + }; +} diff --git a/rd_ui/app/views/data_sources/edit.html b/client/app/pages/data-sources/show.html similarity index 100% rename from rd_ui/app/views/data_sources/edit.html rename to client/app/pages/data-sources/show.html diff --git a/client/app/pages/data-sources/show.js b/client/app/pages/data-sources/show.js new file mode 100644 index 0000000000..1b0d4e9478 --- /dev/null +++ b/client/app/pages/data-sources/show.js @@ -0,0 +1,69 @@ +import debug from 'debug'; +import template from './show.html'; + +const logger = debug('redash:http'); + +function DataSourceCtrl($scope, $routeParams, $http, $location, toastr, + currentUser, Events, DataSource) { + Events.record('view', 'page', 'admin/data_source'); + + $scope.dataSourceId = $routeParams.dataSourceId; + + if ($scope.dataSourceId === 'new') { + $scope.dataSource = new DataSource({ options: {} }); + } else { + $scope.dataSource = DataSource.get({ id: $routeParams.dataSourceId }); + } + + $scope.$watch('dataSource.id', (id) => { + if (id !== $scope.dataSourceId && id !== undefined) { + $location.path(`/data_sources/${id}`).replace(); + } + }); + + function deleteDataSource() { + Events.record('delete', 'datasource', $scope.dataSource.id); + + $scope.dataSource.$delete(() => { + toastr.success('Data source deleted successfully.'); + $location.path('/data_sources/'); + }, (httpResponse) => { + logger('Failed to delete data source: ', httpResponse.status, httpResponse.statusText, httpResponse.data); + toastr.error('Failed to delete data source.'); + }); + } + + function testConnection(callback) { + Events.record('test', 'datasource', $scope.dataSource.id); + + DataSource.test({ id: $scope.dataSource.id }, (httpResponse) => { + if (httpResponse.ok) { + toastr.success('Success'); + } else { + toastr.error(httpResponse.message, 'Connection Test Failed:', { timeOut: 10000 }); + } + callback(); + }, (httpResponse) => { + logger('Failed to test data source: ', httpResponse.status, httpResponse.statusText, httpResponse); + toastr.error('Unknown error occurred while performing connection test. Please try again later.', 'Connection Test Failed:', { timeOut: 10000 }); + callback(); + }); + } + + $scope.actions = [ + { name: 'Delete', class: 'btn-danger', callback: deleteDataSource }, + { name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true }, + ]; +} + +export default function (ngModule) { + ngModule.controller('DataSourceCtrl', DataSourceCtrl); + + return { + '/data_sources/:dataSourceId': { + template, + controller: 'DataSourceCtrl', + title: 'Datasources', + }, + }; +} diff --git a/client/app/pages/destinations/index.js b/client/app/pages/destinations/index.js new file mode 100644 index 0000000000..5988411af5 --- /dev/null +++ b/client/app/pages/destinations/index.js @@ -0,0 +1,6 @@ +import registerList from './list'; +import registerShow from './show'; + +export default function (ngModule) { + return Object.assign({}, registerList(ngModule), registerShow(ngModule)); +} diff --git a/rd_ui/app/views/destinations/list.html b/client/app/pages/destinations/list.html similarity index 100% rename from rd_ui/app/views/destinations/list.html rename to client/app/pages/destinations/list.html diff --git a/client/app/pages/destinations/list.js b/client/app/pages/destinations/list.js new file mode 100644 index 0000000000..94dbc46dea --- /dev/null +++ b/client/app/pages/destinations/list.js @@ -0,0 +1,19 @@ +import template from './list.html'; + +function DestinationsCtrl($scope, $location, toastr, currentUser, Events, Destination) { + Events.record('view', 'page', 'admin/destinations'); + + $scope.destinations = Destination.query(); +} + +export default function (ngModule) { + ngModule.controller('DestinationsCtrl', DestinationsCtrl); + + return { + '/destinations': { + template, + controller: 'DestinationsCtrl', + title: 'Destinations', + }, + }; +} diff --git a/rd_ui/app/views/destinations/edit.html b/client/app/pages/destinations/show.html similarity index 100% rename from rd_ui/app/views/destinations/edit.html rename to client/app/pages/destinations/show.html diff --git a/client/app/pages/destinations/show.js b/client/app/pages/destinations/show.js new file mode 100644 index 0000000000..a33a37ab6d --- /dev/null +++ b/client/app/pages/destinations/show.js @@ -0,0 +1,48 @@ +import debug from 'debug'; + +import template from './show.html'; + +const logger = debug('redash:http'); + +function DestinationCtrl($scope, $routeParams, $http, $location, toastr, + currentUser, Events, Destination) { + Events.record('view', 'page', 'admin/destination'); + + $scope.destinationId = $routeParams.destinationId; + + if ($scope.destinationId === 'new') { + $scope.destination = new Destination({ options: {} }); + } else { + $scope.destination = Destination.get({ id: $routeParams.destinationId }); + } + + $scope.$watch('destination.id', (id) => { + if (id !== $scope.destinationId && id !== undefined) { + $location.path(`/destinations/${id}`).replace(); + } + }); + + $scope.delete = () => { + Events.record('delete', 'destination', $scope.destination.id); + + $scope.destination.$delete(() => { + toastr.success('Destination deleted successfully.'); + $location.path('/destinations/'); + }, (httpResponse) => { + logger('Failed to delete destination: ', httpResponse.status, httpResponse.statusText, httpResponse.data); + toastr.error('Failed to delete destination.'); + }); + }; +} + +export default function (ngModule) { + ngModule.controller('DestinationCtrl', DestinationCtrl); + + return { + '/destinations/:destinationId': { + template, + controller: 'DestinationCtrl', + title: 'Destinations', + }, + }; +} diff --git a/rd_ui/app/views/groups/show_data_sources.html b/client/app/pages/groups/data-sources.html similarity index 88% rename from rd_ui/app/views/groups/show_data_sources.html rename to client/app/pages/groups/data-sources.html index f8a5b6f6d6..df7f4fc989 100644 --- a/rd_ui/app/views/groups/show_data_sources.html +++ b/client/app/pages/groups/data-sources.html @@ -30,16 +30,16 @@ {{dataSource.name}} -
- - -
- - +
@@ -35,6 +34,10 @@
+ +
+ No members. +
diff --git a/client/app/pages/groups/show.js b/client/app/pages/groups/show.js new file mode 100644 index 0000000000..c27bdf9cbf --- /dev/null +++ b/client/app/pages/groups/show.js @@ -0,0 +1,58 @@ +import { contains } from 'underscore'; +import template from './show.html'; + +function GroupCtrl($scope, $routeParams, $http, $location, toastr, + currentUser, Events, Group, User) { + Events.record('view', 'group', $scope.groupId); + + $scope.currentUser = currentUser; + $scope.group = Group.get({ id: $routeParams.groupId }); + $scope.members = Group.members({ id: $routeParams.groupId }); + $scope.newMember = {}; + + $scope.findUser = (search) => { + if (search === '') { + return; + } + + if ($scope.foundUsers === undefined) { + User.query((users) => { + const existingIds = $scope.members.map(m => m.id); + users.forEach((user) => { user.alreadyMember = contains(existingIds, user.id); }); + $scope.foundUsers = users; + }); + } + }; + + $scope.addMember = (user) => { + // Clear selection, to clear up the input control. + $scope.newMember.selected = undefined; + + $http.post(`api/groups/${$routeParams.groupId}/members`, { user_id: user.id }).success(() => { + $scope.members.unshift(user); + user.alreadyMember = true; + }); + }; + + $scope.removeMember = (member) => { + $http.delete(`api/groups/${$routeParams.groupId}/members/${member.id}`).success(() => { + $scope.members = $scope.members.filter(m => m !== member); + + if ($scope.foundUsers) { + $scope.foundUsers.forEach((user) => { + if (user.id === member.id) { user.alreadyMember = false; } + }); + } + }); + }; +} + +export default function (ngModule) { + ngModule.controller('GroupCtrl', GroupCtrl); + return { + '/groups/:groupId': { + template, + controller: 'GroupCtrl', + }, + }; +} diff --git a/rd_ui/app/views/index.html b/client/app/pages/home/home.html similarity index 66% rename from rd_ui/app/views/index.html rename to client/app/pages/home/home.html index 27985cbd22..5c82c140cb 100644 --- a/rd_ui/app/views/index.html +++ b/client/app/pages/home/home.html @@ -1,10 +1,8 @@
-
- New Query - New Alert
@@ -16,7 +14,7 @@

Recent Dashboards

@@ -26,7 +24,7 @@

Recent Queries

{{query.name}} + ng-repeat="query in $ctrl.recentQueries">{{query.name}}
diff --git a/client/app/pages/home/index.js b/client/app/pages/home/index.js new file mode 100644 index 0000000000..ff0788e256 --- /dev/null +++ b/client/app/pages/home/index.js @@ -0,0 +1,35 @@ +import template from './home.html'; + +function HomeCtrl($scope, $uibModal, currentUser, Events, Dashboard, Query) { + Events.record('view', 'page', 'personal_homepage'); + + // todo: maybe this should come from some serivce as we have this logic elsewhere. + this.canCreateQuery = currentUser.hasPermission('create_query'); + this.canCreateDashboard = currentUser.hasPermission('create_dashboard'); + + this.recentQueries = Query.recent(); + this.recentDashboards = Dashboard.recent(); + + this.newDashboard = () => { + $uibModal.open({ + component: 'editDashboardDialog', + resolve: { + dashboard: () => ({ name: null, layout: null }), + }, + }); + }; +} + +export default function (ngModule) { + ngModule.component('homePage', { + template, + controller: HomeCtrl, + }); + + return { + '/': { + template: '', + title: 'Redash', + }, + }; +} diff --git a/client/app/pages/index.js b/client/app/pages/index.js new file mode 100644 index 0000000000..4e5a718eca --- /dev/null +++ b/client/app/pages/index.js @@ -0,0 +1,12 @@ +export { default as home } from './home'; +export { default as queriesList } from './queries-list'; +export { default as alertsList } from './alerts-list'; +export { default as alert } from './alert'; +export { default as admin } from './admin'; +export { default as dashboards } from './dashboards'; +export { default as querySnippets } from './query-snippets'; +export { default as users } from './users'; +export { default as groups } from './groups'; +export { default as destinations } from './destinations'; +export { default as dataSources } from './data-sources'; +export { default as queries } from './queries'; diff --git a/client/app/pages/queries-list/index.js b/client/app/pages/queries-list/index.js new file mode 100644 index 0000000000..83fb18c18f --- /dev/null +++ b/client/app/pages/queries-list/index.js @@ -0,0 +1,75 @@ +import moment from 'moment'; +import template from './queries-list.html'; + +class QueriesListCtrl { + constructor($scope, $location, NgTableParams, Title, Query) { + const page = parseInt($location.search().page || 1, 10); + const count = 25; + + this.defaultOptions = {}; + + const self = this; + + this.tableParams = new NgTableParams({ page, count }, { + getData(params) { + const options = params.url(); + + $location.search('page', options.page); + + const request = Object.assign({}, self.defaultOptions, + { page: options.page, page_size: options.count }); + + return self.resource(request).$promise.then((data) => { + params.total(data.count); + return data.results.map((query) => { + query.created_at = moment(query.created_at); + query.retrieved_at = moment(query.retrieved_at); + return query; + }); + }); + }, + }); + + switch ($location.path()) { + case '/queries': + Title.set('Queries'); + this.resource = Query.query; + break; + case '/queries/drafts': + Title.set('Draft Queries'); + this.resource = Query.myQueries; + this.defaultOptions.drafts = true; + break; + case '/queries/my': + Title.set('My Queries'); + this.resource = Query.myQueries; + break; + default: + break; + } + + this.tabs = [ + { name: 'My Queries', path: 'queries/my' }, + { path: 'queries', name: 'All Queries', isActive: path => path === '/queries' }, + { path: 'queries/drafts', name: 'Drafts' }, + ]; + } +} + +export default function (ngModule) { + ngModule.component('pageQueriesList', { + template, + controller: QueriesListCtrl, + }); + + const route = { + template: '', + reloadOnSearch: false, + }; + + return { + '/queries': route, + '/queries/my': route, + '/queries/drafts': route, + }; +} diff --git a/client/app/pages/queries-list/queries-list.html b/client/app/pages/queries-list/queries-list.html new file mode 100644 index 0000000000..f5473255d1 --- /dev/null +++ b/client/app/pages/queries-list/queries-list.html @@ -0,0 +1,17 @@ +
+ + + +
+ + + + + + + + + +
{{query.name}}{{query.user.name}}{{query.created_at | dateTime}}{{query.runtime | durationHumanize}}{{query.retrieved_at | dateTime}}{{query.schedule | scheduleHumanize}}
+
+
diff --git a/client/app/pages/queries/alert-unsaved-changes.js b/client/app/pages/queries/alert-unsaved-changes.js new file mode 100644 index 0000000000..cd4624bc43 --- /dev/null +++ b/client/app/pages/queries/alert-unsaved-changes.js @@ -0,0 +1,37 @@ +function alertUnsavedChanges($window) { + return { + restrict: 'E', + replace: true, + scope: { + isDirty: '=', + }, + link($scope) { + const unloadMessage = 'You will lose your changes if you leave'; + const confirmMessage = `${unloadMessage}\n\nAre you sure you want to leave this page?`; + // store original handler (if any) + const _onbeforeunload = $window.onbeforeunload; + + $window.onbeforeunload = function onbeforeunload() { + return $scope.isDirty ? unloadMessage : null; + }; + + $scope.$on('$locationChangeStart', (event, next, current) => { + if (next.split('?')[0] === current.split('?')[0] || next.split('#')[0] === current.split('#')[0]) { + return; + } + + if ($scope.isDirty && !$window.confirm(confirmMessage)) { + event.preventDefault(); + } + }); + + $scope.$on('$destroy', () => { + $window.onbeforeunload = _onbeforeunload; + }); + }, + }; +} + +export default function (ngModule) { + ngModule.directive('alertUnsavedChanges', alertUnsavedChanges); +} diff --git a/rd_ui/app/views/dialogs/embed_code.html b/client/app/pages/queries/embed-code-dialog.html similarity index 83% rename from rd_ui/app/views/dialogs/embed_code.html rename to client/app/pages/queries/embed-code-dialog.html index 3bcf08bf8b..2ca2560ff1 100644 --- a/rd_ui/app/views/dialogs/embed_code.html +++ b/client/app/pages/queries/embed-code-dialog.html @@ -5,7 +5,7 @@ @@ -157,13 +148,13 @@

ng-disabled="queryExecuting || !canExecuteQuery()" title="Refresh Dataset" ng-if="!sourceMode"> -
+
-
- diff --git a/client/app/visualizations/sunburst/index.js b/client/app/visualizations/sunburst/index.js new file mode 100644 index 0000000000..291eb3af7c --- /dev/null +++ b/client/app/visualizations/sunburst/index.js @@ -0,0 +1,55 @@ +import jQuery from 'jquery'; +import Sunburst from './sunburst'; +import editorTemplate from './sunburst-sequence-editor.html'; + +function sunburstSequenceRenderer() { + return { + restrict: 'E', + link(scope, element) { + let sunburst = new Sunburst(scope, element); + + function resize() { + sunburst.remove(); + sunburst = new Sunburst(scope, element); + } + + jQuery(window).on('resize', resize); + scope.$watch('visualization.options.height', (oldValue, newValue) => { + if (oldValue !== newValue) { + resize(); + } + }); + }, + }; +} + +function sunburstSequenceEditor() { + return { + restrict: 'E', + template: editorTemplate, + }; +} + +export default function (ngModule) { + ngModule.directive('sunburstSequenceRenderer', sunburstSequenceRenderer); + ngModule.directive('sunburstSequenceEditor', sunburstSequenceEditor); + + ngModule.config((VisualizationProvider) => { + const renderTemplate = + ''; + + const editTemplate = ''; + const defaultOptions = { + height: 300, + }; + + VisualizationProvider.registerVisualization({ + type: 'SUNBURST_SEQUENCE', + name: 'Sunburst Sequence', + renderTemplate, + editorTemplate: editTemplate, + defaultOptions, + }); + } + ); +} diff --git a/rd_ui/app/views/visualizations/sunburst_sequence_editor.html b/client/app/visualizations/sunburst/sunburst-sequence-editor.html similarity index 100% rename from rd_ui/app/views/visualizations/sunburst_sequence_editor.html rename to client/app/visualizations/sunburst/sunburst-sequence-editor.html diff --git a/client/app/visualizations/sunburst/sunburst.js b/client/app/visualizations/sunburst/sunburst.js new file mode 100644 index 0000000000..fbfc1c3429 --- /dev/null +++ b/client/app/visualizations/sunburst/sunburst.js @@ -0,0 +1,407 @@ +import d3 from 'd3'; +import _ from 'underscore'; +import angular from 'angular'; + +const exitNode = '<<>>'; +const colors = d3.scale.category10(); + +// helper function colorMap - color gray if "end" is detected +function colorMap(d) { + return colors(d.name); +} + + +// Return array of ancestors of nodes, highest first, but excluding the root. +function getAncestors(node) { + const path = []; + let current = node; + + while (current.parent) { + path.unshift(current); + current = current.parent; + } + return path; +} + +// The following is based on @chrisrzhou's example from: http://bl.ocks.org/chrisrzhou/d5bdd8546f64ca0e4366. +export default function Sunburst(scope, element) { + this.element = element; + this.watches = []; + + // svg dimensions + const width = element[0].parentElement.clientWidth; + const height = scope.visualization.options.height; + const radius = Math.min(width, height) / 2; + + // Breadcrumb dimensions: width, height, spacing, width of tip/tail. + const b = { + w: width / 6, + h: 30, + s: 3, + t: 10, + }; + + // margins + const margin = { + top: radius, + bottom: 50, + left: radius, + right: 0, + }; + + /** + * Drawing variables: + * + * e.g. colors, totalSize, partitions, arcs + */ + // Mapping of nodes to colorscale. + + // Total size of all nodes, to be used later when data is loaded + let totalSize = 0; + + // create d3.layout.partition + const partition = d3.layout.partition() + .size([2 * Math.PI, radius * radius]) + .value(d => + d.size + ); + + // create arcs for drawing D3 paths + const arc = d3.svg.arc() + .startAngle(d => + d.x + ) + .endAngle(d => + d.x + d.dx + ) + .innerRadius(d => + Math.sqrt(d.y) + ) + .outerRadius(d => + Math.sqrt(d.y + d.dy) + ); + + + /** + * Define and initialize D3 select references and div-containers + * + * e.g. vis, breadcrumbs, lastCrumb, summary, sunburst, legend + */ + // create main vis selection + const vis = d3.select(element[0]) + .append('div').classed('vis-container', true) + .style('position', 'relative') + .style('margin-top', '5px') + .style('height', `${height + 2 * b.h}px`); + + // create and position breadcrumbs container and svg + const breadcrumbs = vis + .append('div').classed('breadcrumbs-container', true) + .append('svg') + .attr('width', width) + .attr('height', b.h) + .attr('fill', 'white') + .attr('font-weight', 600); + + const marginLeft = (width - radius * 2) / 2; + + // create and position SVG + const sunburst = vis + .append('div').classed('sunburst-container', true) + .style('z-index', '2') + // .style("margin-left", marginLeft + "px") + .style('left', `${marginLeft}px`) + .style('position', 'absolute') + .append('svg') + .attr('width', width) + .attr('height', height) + .append('g') + .attr('transform', `translate(${margin.left},${margin.top})`); + + // create last breadcrumb element + const lastCrumb = breadcrumbs.append('text').classed('lastCrumb', true); + + // create and position summary container + const summary = vis + .append('div').classed('summary-container', true) + .style('position', 'absolute') + .style('top', `${b.h + radius * 0.80}px`) + .style('left', `${marginLeft + radius / 2}px`) + .style('width', `${radius}px`) + .style('height', `${radius}px`) + .style('text-align', 'center') + .style('font-size', '11px') + .style('color', '#666') + .style('z-index', '1'); + + // Generate a string representation for drawing a breadcrumb polygon. + function breadcrumbPoints(d, i) { + const points = []; + points.push('0,0'); + points.push(`${b.w},0`); + points.push(`${b.w + b.t},${b.h / 2}`); + points.push(`${b.w},${b.h}`); + points.push(`0,${b.h}`); + + if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex. + points.push(`${b.t},${b.h / 2}`); + } + return points.join(' '); + } + + // Update the breadcrumb breadcrumbs to show the current sequence and percentage. + function updateBreadcrumbs(ancestors, percentageString) { + // Data join, where primary key = name + depth. + const g = breadcrumbs.selectAll('g') + .data(ancestors, d => + d.name + d.depth + ); + + // Add breadcrumb and label for entering nodes. + const breadcrumb = g.enter().append('g'); + + breadcrumb + .append('polygon').classed('breadcrumbs-shape', true) + .attr('points', breadcrumbPoints) + .attr('fill', colorMap); + + breadcrumb + .append('text').classed('breadcrumbs-text', true) + .attr('x', (b.w + b.t) / 2) + .attr('y', b.h / 2) + .attr('dy', '0.35em') + .attr('font-size', '10px') + .attr('text-anchor', 'middle') + .text(d => + d.name + ); + + // Set position for entering and updating nodes. + g.attr('transform', (d, i) => + `translate(${i * (b.w + b.s)}, 0)` + ); + + // Remove exiting nodes. + g.exit().remove(); + + // Update percentage at the lastCrumb. + lastCrumb + .attr('x', (ancestors.length + 0.5) * (b.w + b.s)) + .attr('y', b.h / 2) + .attr('dy', '0.35em') + .attr('text-anchor', 'middle') + .attr('fill', 'black') + .attr('font-weight', 600) + .text(percentageString); + } + + // helper function mouseover to handle mouseover events/animations and calculation + // of ancestor nodes etc + function mouseover(d) { + // build percentage string + const percentage = (100 * d.value / totalSize).toPrecision(3); + let percentageString = `${percentage}%`; + if (percentage < 1) { + percentageString = '< 1.0%'; + } + + // update breadcrumbs (get all ancestors) + const ancestors = getAncestors(d); + updateBreadcrumbs(ancestors, percentageString); + + // update sunburst (Fade all the segments and highlight only ancestors of current segment) + sunburst.selectAll('path') + .attr('opacity', 0.3); + sunburst.selectAll('path') + .filter(node => + (ancestors.indexOf(node) >= 0) + ) + .attr('opacity', 1); + + // update summary + summary.html( + `Stage: ${d.depth}
` + + `${percentageString}
${ + d.value} of ${totalSize}
` + ); + + // display summary and breadcrumbs if hidden + summary.style('visibility', ''); + breadcrumbs.style('visibility', ''); + } + + + // helper function click to handle mouseleave events/animations + function click() { + // Deactivate all segments then retransition each segment to full opacity. + sunburst.selectAll('path').on('mouseover', null); + sunburst.selectAll('path') + .transition() + .duration(1000) + .attr('opacity', 1) + .each('end', function endClick() { + d3.select(this).on('mouseover', mouseover); + }); + + // hide summary and breadcrumbs if visible + breadcrumbs.style('visibility', 'hidden'); + summary.style('visibility', 'hidden'); + } + + // helper function to draw the sunburst and breadcrumbs + function drawSunburst(json) { + // Build only nodes of a threshold "visible" sizes to improve efficiency + const nodes = partition.nodes(json) + .filter(d => + (d.dx > 0.005) && d.name !== exitNode // 0.005 radians = 0.29 degrees + ); + + // this section is required to update the colors.domain() every time the data updates + const uniqueNames = (function uniqueNames(a) { + const output = []; + a.forEach((d) => { + if (output.indexOf(d.name) === -1) output.push(d.name); + }); + return output; + }(nodes)); + colors.domain(uniqueNames); // update domain colors + + // create path based on nodes + const path = sunburst.data([json]).selectAll('path') + .data(nodes).enter() + .append('path') + .classed('nodePath', true) + .attr('display', d => (d.depth ? null : 'none')) + .attr('d', arc) + .attr('fill', colorMap) + .attr('opacity', 1) + .attr('stroke', 'white') + .on('mouseover', mouseover); + + + // // trigger mouse click over sunburst to reset visualization summary + vis.on('click', click); + + // Update totalSize of the tree = value of root node from partition. + totalSize = path.node().__data__.value; + } + + // visualize json tree structure + function createVisualization(json) { + drawSunburst(json); // draw sunburst + } + + function removeVisualization() { + sunburst.selectAll('.nodePath').remove(); + // legend.selectAll("g").remove(); + } + + function buildNodes(raw) { + let values; + + if (_.has(raw[0], 'sequence') && _.has(raw[0], 'stage') && _.has(raw[0], 'node') && _.has(raw[0], 'value')) { + const grouped = _.groupBy(raw, 'sequence'); + + values = _.map(grouped, (value) => { + const sorted = _.sortBy(value, 'stage'); + return { + size: value[0].value, + sequence: value[0].sequence, + nodes: _.pluck(sorted, 'node'), + }; + }); + } else { + const keys = _.sortBy(_.without(_.keys(raw[0]), 'value'), _.identity); + + values = _.map(raw, (row, sequence) => + ({ + size: row.value, + sequence, + nodes: _.compact(_.map(keys, key => row[key])), + }) + ); + } + + return values; + } + + function buildHierarchy(csv) { + const data = buildNodes(csv); + + // build tree + const root = { + name: 'root', + children: [], + }; + + data.forEach((d) => { + const nodes = d.nodes; + const size = parseInt(d.size, 10); + + // build graph, nodes, and child nodes + let currentNode = root; + for (let j = 0; j < nodes.length; j += 1) { + let children = currentNode.children; + const nodeName = nodes[j]; + const isLeaf = j + 1 === nodes.length; + + + if (!children) { + currentNode.children = children = []; + children.push({ + name: exitNode, + size: currentNode.size, + }); + } + + let childNode = _.find(children, child => child.name === nodeName); + + if (isLeaf && childNode) { + childNode.children.push({ + name: exitNode, + size, + }); + } else if (isLeaf) { + children.push({ + name: nodeName, + size, + }); + } else { + if (!childNode) { + childNode = { + name: nodeName, + children: [], + }; + children.push(childNode); + } + + currentNode = childNode; + } + } + }); + + return root; + } + + function render(data) { + const json = buildHierarchy(data); // build json tree + removeVisualization(); // remove existing visualization if any + createVisualization(json); // visualize json tree + } + + function refreshData() { + const queryData = scope.queryResult.getData(); + if (queryData) { + render(queryData); + } + } + + refreshData(); + this.watches.push(scope.$watch('visualization.options', refreshData, true)); + this.watches.push(scope.$watch('queryResult && queryResult.getData()', refreshData)); +} + +Sunburst.prototype.remove = function remove() { + this.watches.forEach((unregister) => { unregister(); }); + angular.element(this.element[0]).empty('.vis-container'); +}; diff --git a/client/app/visualizations/table/index.js b/client/app/visualizations/table/index.js new file mode 100644 index 0000000000..f136a556e7 --- /dev/null +++ b/client/app/visualizations/table/index.js @@ -0,0 +1,98 @@ +import moment from 'moment'; +import { each, isString, object, pluck } from 'underscore'; +import { getColumnCleanName } from '../../services/query-result'; +import template from './table.html'; + +function formatValue($filter, clientConfig, value, type) { + let formattedValue = value; + switch (type) { + case 'integer': + formattedValue = $filter('number')(value, 0); + break; + case 'float': + formattedValue = $filter('number')(value, 2); + break; + case 'boolean': + if (value !== undefined) { + formattedValue = String(value); + } + break; + case 'date': + if (value && moment.isMoment(value)) { + formattedValue = value.format(clientConfig.dateFormat); + } + break; + case 'datetime': + if (value && moment.isMoment(value)) { + formattedValue = value.format(clientConfig.dateTimeFormat); + } + break; + default: + if (isString(value)) { + formattedValue = $filter('linkify')(value); + } + break; + } + + return formattedValue; +} + +function GridRenderer(clientConfig) { + return { + restrict: 'E', + scope: { + queryResult: '=', + itemsPerPage: '=', + }, + template, + replace: false, + controller($scope, $filter) { + $scope.gridColumns = []; + $scope.gridRows = []; + + $scope.$watch('queryResult && queryResult.getData()', (queryResult) => { + if (!queryResult) { + return; + } + + if ($scope.queryResult.getData() == null) { + $scope.gridColumns = []; + $scope.filters = []; + } else { + $scope.filters = $scope.queryResult.getFilters(); + + const columns = $scope.queryResult.getColumns(); + const columnsMap = object(pluck(columns, 'name'), pluck(columns, 'type')); + + const prepareGridData = (data) => { + const gridData = data.map((row) => { + const newRow = {}; + each(row, (val, key) => { + const formattedValue = formatValue($filter, clientConfig, val, columnsMap[key]); + newRow[getColumnCleanName(key)] = formattedValue; + }); + return newRow; + }); + + return gridData; + }; + + $scope.gridRows = prepareGridData($scope.queryResult.getData()); + $scope.gridColumns = $scope.queryResult.getColumnCleanNames(); + } + }); + }, + }; +} + +export default function (ngModule) { + ngModule.config((VisualizationProvider) => { + VisualizationProvider.registerVisualization({ + type: 'TABLE', + name: 'Table', + renderTemplate: '', + skipTypes: true, + }); + }); + ngModule.directive('gridRenderer', GridRenderer); +} diff --git a/client/app/visualizations/table/table.html b/client/app/visualizations/table/table.html new file mode 100644 index 0000000000..5557179ae0 --- /dev/null +++ b/client/app/visualizations/table/table.html @@ -0,0 +1 @@ + diff --git a/client/app/visualizations/word-cloud/index.js b/client/app/visualizations/word-cloud/index.js new file mode 100644 index 0000000000..7a95e3169a --- /dev/null +++ b/client/app/visualizations/word-cloud/index.js @@ -0,0 +1,109 @@ +import d3 from 'd3'; +import angular from 'angular'; +import cloud from 'd3-cloud'; +import { each } from 'underscore'; + +import editorTemplate from './word-cloud-editor.html'; + +function findWordFrequencies(data, columnName) { + const wordsHash = {}; + + data.forEach((row) => { + const wordsList = row[columnName].toString().split(' '); + wordsList.forEach((d) => { + if (d in wordsHash) { + wordsHash[d] += 1; + } else { + wordsHash[d] = 1; + } + }); + }); + + return wordsHash; +} + + +function wordCloudRenderer() { + return { + restrict: 'E', + link($scope, elem) { + function reloadCloud() { + if (!angular.isDefined($scope.queryResult)) return; + + const data = $scope.queryResult.getData(); + let wordsHash = {}; + + if ($scope.visualization.options.column) { + wordsHash = findWordFrequencies(data, $scope.visualization.options.column); + } + + const wordList = []; + each(wordsHash, (v, key) => { + wordList.push({ text: key, size: 10 + Math.pow(v, 2) }); + }); + + const fill = d3.scale.category20(); + const layout = cloud() + .size([500, 500]) + .words(wordList) + .padding(5) + .rotate(() => Math.floor(Math.random() * 2) * 90) + .font('Impact') + .fontSize(d => d.size); + + function draw(words) { + d3.select(elem[0].parentNode) + .select('svg') + .remove(); + + d3.select(elem[0].parentNode) + .append('svg') + .attr('width', layout.size()[0]) + .attr('height', layout.size()[1]) + .append('g') + .attr('transform', `translate(${layout.size()[0] / 2},${layout.size()[1] / 2})`) + .selectAll('text') + .data(words) + .enter() + .append('text') + .style('font-size', d => `${d.size}px`) + .style('font-family', 'Impact') + .style('fill', (d, i) => fill(i)) + .attr('text-anchor', 'middle') + .attr('transform', d => + `translate(${[d.x, d.y]})rotate(${d.rotate})` + ) + .text(d => d.text); + } + + layout.on('end', draw); + + layout.start(); + } + + $scope.$watch('queryResult && queryResult.getData()', reloadCloud); + $scope.$watch('visualization.options.column', reloadCloud); + }, + }; +} + +function wordCloudEditor() { + return { + restrict: 'E', + template: editorTemplate, + }; +} + +export default function (ngModule) { + ngModule.directive('wordCloudEditor', wordCloudEditor); + ngModule.directive('wordCloudRenderer', wordCloudRenderer); + + ngModule.config((VisualizationProvider) => { + VisualizationProvider.registerVisualization({ + type: 'WORD_CLOUD', + name: 'Word Cloud', + renderTemplate: '', + editorTemplate: '', + }); + }); +} diff --git a/rd_ui/app/views/visualizations/word_cloud_editor.html b/client/app/visualizations/word-cloud/word-cloud-editor.html similarity index 100% rename from rd_ui/app/views/visualizations/word_cloud_editor.html rename to client/app/visualizations/word-cloud/word-cloud-editor.html diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000000..75aed3746c --- /dev/null +++ b/client/package.json @@ -0,0 +1,80 @@ +{ + "name": "redash-frontend", + "version": "1.0.0", + "description": "The frontend part of Redash.", + "main": "index.js", + "scripts": { + "test": "NODE_ENV=test karma start", + "start": "webpack-dev-server --content-base app", + "build": "NODE_ENV=production node node_modules/.bin/webpack", + "watch": "webpack --watch --progress --colors -d" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/getredash/redash.git" + }, + "author": "Redash Contributors", + "license": "BSD-2-Clause", + "bugs": { + "url": "https://github.com/getredash/redash/issues" + }, + "homepage": "https://redash.io/", + "dependencies": { + "angular": "^1.5.8", + "angular-base64-upload": "^0.1.19", + "angular-gridster": "^0.13.14", + "angular-messages": "^1.5.8", + "angular-moment": "^1.0.0", + "angular-resizable": "^1.2.0", + "angular-resource": "^1.5.8", + "angular-route": "^1.5.8", + "angular-sanitize": "^1.5.8", + "angular-toastr": "^2.1.1", + "angular-ui-ace": "^0.2.3", + "angular-ui-bootstrap": "^2.2.0", + "angular-vs-repeat": "^1.1.7", + "babel-core": "^6.18.0", + "babel-loader": "^6.2.7", + "babel-preset-es2015": "^6.18.0", + "babel-preset-stage-2": "^6.18.0", + "brace": "^0.9.0", + "cornelius": "git+https://github.com/restorando/cornelius.git", + "css-loader": "^0.25.0", + "d3": "^3.5.17", + "d3-cloud": "^1.2.1", + "debug": "^2.2.0", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.9.0", + "font-awesome": "^4.7.0", + "html-webpack-plugin": "^2.24.0", + "jquery": "^3.1.1", + "jquery-ui": "^1.12.1", + "leaflet": "^1.0.2", + "leaflet.markercluster": "^1.0.0", + "marked": "^0.3.6", + "material-design-iconic-font": "^2.2.0", + "moment": "^2.15.2", + "mousetrap": "^1.6.0", + "mustache": "^2.2.1", + "ng-annotate": "^1.2.1", + "ng-annotate-loader": "^0.2.0", + "ng-table": "^2.1.0", + "pace-progress": "git+https://github.com/getredash/pace.git", + "pivottable": "^2.3.0", + "plotly.js": "^1.16.0", + "raw-loader": "^0.5.1", + "ui-select": "^0.19.6", + "underscore": "^1.8.3", + "underscore.string": "^3.3.4", + "url-loader": "^0.5.7", + "webpack": "^1.13.3", + "webpack-dev-server": "^1.16.2" + }, + "devDependencies": { + "eslint": "^3.9.0", + "eslint-config-airbnb-base": "^9.0.0", + "eslint-loader": "^1.6.0", + "eslint-plugin-import": "^2.0.1", + "webpack-build-notifier": "^0.1.13" + } +} diff --git a/client/webpack.config.js b/client/webpack.config.js new file mode 100644 index 0000000000..06696cd0a4 --- /dev/null +++ b/client/webpack.config.js @@ -0,0 +1,110 @@ +/* eslint-disable */ + +var webpack = require('webpack'); +var HtmlWebpackPlugin = require('html-webpack-plugin'); +var ExtractTextPlugin = require("extract-text-webpack-plugin"); +var WebpackBuildNotifierPlugin = require('webpack-build-notifier'); +var path = require('path'); + + +var config = { + entry: { + app: './app/index.js' + }, + output: { + // path: process.env.NODE_ENV === 'production' ? './dist' : './dev', + path: './dist', + filename: '[name].js', + }, + + plugins: [ + new WebpackBuildNotifierPlugin({title: 'Redash'}), + new webpack.DefinePlugin({ + ON_TEST: process.env.NODE_ENV === 'test' + }), + new webpack.optimize.DedupePlugin(), + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: function (module, count) { + // any required modules inside node_modules are extracted to vendor + return ( + module.resource && + /\.js$/.test(module.resource) && + module.resource.indexOf( + path.join(__dirname, './node_modules') + ) === 0 + ) + } + }), + // extract webpack runtime and module manifest to its own file in order to + // prevent vendor hash from being updated whenever app bundle is updated + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest', + chunks: ['vendor'] + }), + new HtmlWebpackPlugin({ + // template: __dirname + '/app/' + 'index.html' + template: './app/index.html' + }), + new ExtractTextPlugin('styles.css') + ], + + module: { + loaders: [ + {test: /\.js$/, loader: 'ng-annotate!babel!eslint', exclude: /node_modules/}, + {test: /\.html$/, loader: 'raw', exclude: [/node_modules/,/index\.html/]}, + // {test: /\.css$/, loader: 'style!css', exclude: /node_modules/}, + {test: /\.css$/, loader: ExtractTextPlugin.extract("css-loader") }, + {test: /\.styl$/, loader: 'style!css!stylus', exclude: /node_modules/}, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url', + query: { + limit: 10000, + name: 'img/[name].[hash:7].[ext]' + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url', + query: { + limit: 10000, + name: 'fonts/[name].[hash:7].[ext]' + } + } + + ] + }, + // devtool: 'eval-source-map', + devtool: 'cheap-eval-source-map', + devServer: { + inline: true, + historyApiFallback: true, + proxy: { + '/login': { + target: 'http://localhost:5000/', + secure: false + }, + '/status.json': { + target: 'http://localhost:5000/', + secure: false + }, + '/api/admin': { + target: 'http://localhost:5000/', + secure: false + }, + '/api': { + target: 'http://localhost:5000', + secure: false + } + } + } +}; + +if (process.env.NODE_ENV === 'production') { + config.output.path = __dirname + '/dist'; + config.plugins.push(new webpack.optimize.UglifyJsPlugin()); + config.devtool = 'source-map'; +} + +module.exports = config; diff --git a/docker-compose-example.yml b/docker-compose-example.yml index 9c1ae8baba..d8bd5e9a99 100644 --- a/docker-compose-example.yml +++ b/docker-compose-example.yml @@ -6,7 +6,6 @@ redash: - redis - postgres environment: - REDASH_STATIC_ASSETS_PATH: "../rd_ui/dist/" REDASH_LOG_LEVEL: "INFO" REDASH_REDIS_URL: "redis://redis:6379/0" REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres" diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 173c4d4e1e..0000000000 --- a/gulpfile.js +++ /dev/null @@ -1,141 +0,0 @@ -// Generated on 2016-02-09 using generator-angular 0.15.1 -'use strict'; - -var gulp = require('gulp'); -var $ = require('gulp-load-plugins')(); -var lazypipe = require('lazypipe'); -var rimraf = require('rimraf'); -var wiredep = require('wiredep').stream; -var runSequence = require('run-sequence'); -var map = require('lodash.map'); - -var yeoman = { - app: 'rd_ui/app', - dist: 'rd_ui/dist' -}; - -function applyAppPath(p) { - if (typeof p === 'string') { - return yeoman.app + p; - } else { - return map(p, function (path) { - return applyAppPath(path); - }); - } -} - -var paths = { - scripts: [yeoman.app + '/scripts/**/*.js'], - styles: [yeoman.app + '/styles/**/*.css'], - views: { - main: applyAppPath(['/index.html', '/vendor_scripts.html', '/login.html', '/embed.html', '/public.html', '/app_layout.html', '/signed_out_layout.html']), - files: [yeoman.app + '/views/**/*.html'] - } -}; - -//////////////////////// -// Reusable pipelines // -//////////////////////// - -var lintScripts = lazypipe() - .pipe($.jshint, '.jshintrc') - .pipe($.jshint.reporter, 'jshint-stylish'); - -var styles = lazypipe() - .pipe($.autoprefixer, 'last 1 version') - .pipe(gulp.dest, '.tmp/styles'); - -/////////// -// Tasks // -/////////// - -gulp.task('styles', function () { - return gulp.src(paths.styles) - .pipe(styles()); -}); - -gulp.task('lint:scripts', function () { - return gulp.src(paths.scripts) - .pipe(lintScripts()); -}); - -gulp.task('clean:tmp', function (cb) { - rimraf('./.tmp', cb); -}); - -// inject bower components -gulp.task('bower', function () { - return gulp.src(paths.views.main) - .pipe(wiredep({ - directory: yeoman.app + '/bower_components', - ignorePath: '..' - })) - .pipe(gulp.dest(yeoman.app + '/views')); -}); - -/////////// -// Build // -/////////// - -gulp.task('clean:dist', function (cb) { - rimraf('./dist', cb); -}); - -gulp.task('client:build', ['html', 'styles'], function () { - var jsFilter = $.filter('**/*.js'); - var cssFilter = $.filter('**/*.css'); - - return gulp.src(paths.views.main) - .pipe($.useref({searchPath: [yeoman.app, '.tmp']})) - .pipe(jsFilter) - .pipe($.ngAnnotate()) - .pipe($.uglify()) - .pipe(jsFilter.restore()) - .pipe($.print()) - .pipe(cssFilter) - .pipe($.minifyCss({cache: true})) - .pipe(cssFilter.restore()) - .pipe(new $.revAll({dontRenameFile: ['.html'], dontUpdateReference: ['vendor_scripts.html', 'app_layout.html', 'signed_out_layout.html']}).revision()) - .pipe(gulp.dest(yeoman.dist)); -}); - -gulp.task('html', function () { - return gulp.src(yeoman.app + '/views/**/*') - .pipe(gulp.dest(yeoman.dist + '/views')); -}); - -gulp.task('images', function () { - return gulp.src(applyAppPath(['/images/**/*'])) - .pipe($.cache($.imagemin({ - optimizationLevel: 5, - progressive: true, - interlaced: true - }))) - .pipe(gulp.dest(yeoman.dist + '/images')); -}); - -gulp.task('leaflet', function () { - return gulp.src(applyAppPath(['/bower_components/leaflet/dist/images/**/*'])) - .pipe($.cache($.imagemin({ - optimizationLevel: 5, - progressive: true, - interlaced: true - }))) - .pipe(gulp.dest(yeoman.dist + '/styles/images')); -}); - -gulp.task('copy:extras', function () { - return gulp.src(applyAppPath(['/*/.*', '/google_login.png', '/favicon.ico', '/robots.txt']), { dot: true }) - .pipe(gulp.dest(yeoman.dist)); -}); - -gulp.task('copy:fonts', function () { - return gulp.src(applyAppPath(['/fonts/**/*', '/bower_components/font-awesome/fonts/*', '/bower_components/material-design-iconic-font/dist/fonts/*'])) - .pipe(gulp.dest(yeoman.dist + '/fonts')); -}); - -gulp.task('build', ['clean:dist'], function () { - runSequence(['images', 'leaflet', 'copy:extras', 'copy:fonts', 'client:build']); -}); - -gulp.task('default', ['build']); diff --git a/package.json b/package.json deleted file mode 100644 index 72e2f9e3f9..0000000000 --- a/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "redash", - "devDependencies": { - "gulp": "^3.9.0", - "gulp-connect": "^2.2.0", - "gulp-autoprefixer": "2.3.1", - "gulp-cache": "^0.2.10", - "rimraf": "^2.4.0", - "gulp-filter": "^2.0.2", - "gulp-imagemin": "^2.3.0", - "gulp-jshint": "^1.11.1", - "gulp-karma": "0.0.4", - "gulp-load-plugins": "^0.10.0", - "gulp-plumber": "^1.0.1", - "gulp-minify-css": "^1.2.0", - "gulp-uglify": "^1.2.0", - "gulp-useref": "^3.0.0", - "gulp-util": "^3.0.6", - "gulp-watch": "^4.2.4", - "run-sequence": "^1.1.1", - "wiredep": "^2.2.2", - "lazypipe": "^0.2.4", - "gulp-ng-annotate": "^1.0.0", - "open": "0.0.5", - "jshint-stylish": "^1.0.0", - "gulp-print": "^2.0.1", - "gulp-rev-all": "^0.8.22", - "bower": "~1.7.1", - "gulp-cli": "~1.2.0", - "lodash.map": "^4.4.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "scripts": { - "test": "echo 'No tests.'", - "build": "gulp build", - "bower": "bower", - "heroku-postbuild": "npm install --dev && npm run bower install && npm run build && npm prune --production" - }, - "dependencies": { - } -} diff --git a/rd_ui/.editorconfig b/rd_ui/.editorconfig deleted file mode 100644 index c2cdfb8ada..0000000000 --- a/rd_ui/.editorconfig +++ /dev/null @@ -1,21 +0,0 @@ -# EditorConfig helps developers define and maintain consistent -# coding styles between different editors and IDEs -# editorconfig.org - -root = true - - -[*] - -# Change these settings to your own preference -indent_style = space -indent_size = 2 - -# We recommend you to keep these unchanged -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -trim_trailing_whitespace = false diff --git a/rd_ui/.jshintrc b/rd_ui/.jshintrc deleted file mode 100644 index 51f09a80f5..0000000000 --- a/rd_ui/.jshintrc +++ /dev/null @@ -1,27 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": false, - "regexp": true, - "undef": true, - "unused": true, - "strict": false, - "trailing": true, - "smarttabs": true, - "globals": { - "angular": false, - "_": false, - "$": false, - "currentUser": false - } -} diff --git a/rd_ui/app/.buildignore b/rd_ui/app/.buildignore deleted file mode 100644 index fc98b8eb54..0000000000 --- a/rd_ui/app/.buildignore +++ /dev/null @@ -1 +0,0 @@ -*.coffee \ No newline at end of file diff --git a/rd_ui/app/404.html b/rd_ui/app/404.html deleted file mode 100644 index fdace4ab14..0000000000 --- a/rd_ui/app/404.html +++ /dev/null @@ -1,157 +0,0 @@ - - - - - Page Not Found :( - - - -
-

Not found :(

-

Sorry, but the page you were trying to view does not exist.

-

It looks like this was the result of either:

-
    -
  • a mistyped address
  • -
  • an out-of-date link
  • -
- - -
- - diff --git a/rd_ui/app/app_layout.html b/rd_ui/app/app_layout.html deleted file mode 100644 index fa4c61e261..0000000000 --- a/rd_ui/app/app_layout.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -{% block content %} -{% endblock %} - -
-
-
-
-
-

-

- You do not have permission to view the requested page. -

-
-
-
- - {% if not headless %} - {% raw %} -
-
- Source: {{location}} -
-
- - -{% endraw %} -{% include 'footer.html' %} -{% endif %} - -{% include 'vendor_scripts.html' %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -{% include '_includes/tail.html' %} - - - diff --git a/rd_ui/app/embed.html b/rd_ui/app/embed.html deleted file mode 100644 index 4bab15841e..0000000000 --- a/rd_ui/app/embed.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -{% include 'vendor_scripts.html' %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/rd_ui/app/favicon.ico b/rd_ui/app/favicon.ico deleted file mode 100755 index 9c648df83d..0000000000 Binary files a/rd_ui/app/favicon.ico and /dev/null differ diff --git a/rd_ui/app/fonts/glyphicons-halflings-regular.eot b/rd_ui/app/fonts/glyphicons-halflings-regular.eot deleted file mode 100755 index 4a4ca865d6..0000000000 Binary files a/rd_ui/app/fonts/glyphicons-halflings-regular.eot and /dev/null differ diff --git a/rd_ui/app/fonts/glyphicons-halflings-regular.svg b/rd_ui/app/fonts/glyphicons-halflings-regular.svg deleted file mode 100755 index 25691af8f1..0000000000 --- a/rd_ui/app/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/rd_ui/app/fonts/glyphicons-halflings-regular.ttf b/rd_ui/app/fonts/glyphicons-halflings-regular.ttf deleted file mode 100755 index 67fa00bf83..0000000000 Binary files a/rd_ui/app/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/rd_ui/app/fonts/glyphicons-halflings-regular.woff b/rd_ui/app/fonts/glyphicons-halflings-regular.woff deleted file mode 100755 index 8c54182aa5..0000000000 Binary files a/rd_ui/app/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/rd_ui/app/fonts/glyphicons-halflings-regular.woff2 b/rd_ui/app/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100755 index 8c54182aa5..0000000000 Binary files a/rd_ui/app/fonts/glyphicons-halflings-regular.woff2 and /dev/null differ diff --git a/rd_ui/app/index.html b/rd_ui/app/index.html deleted file mode 100644 index 4543fe5b7f..0000000000 --- a/rd_ui/app/index.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'app_layout.html' %} -{% block content %} - - -{% endblock %} diff --git a/rd_ui/app/public.html b/rd_ui/app/public.html deleted file mode 100644 index 219990087a..0000000000 --- a/rd_ui/app/public.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends 'app_layout.html' %} - -{% block content %} -{% if not headless %} - -{%endif%} - - -{% endblock %} - diff --git a/rd_ui/app/scripts/app.js b/rd_ui/app/scripts/app.js deleted file mode 100644 index 9cf836e2e3..0000000000 --- a/rd_ui/app/scripts/app.js +++ /dev/null @@ -1,185 +0,0 @@ -angular.module('redash', [ - 'redash.directives', - 'redash.admin_controllers', - 'redash.controllers', - 'redash.filters', - 'redash.services', - 'redash.visualization', - 'plotly', - 'angular-growl', - 'angularMoment', - 'ui.bootstrap', - 'ui.sortable', - 'smartTable.table', - 'ngResource', - 'ngRoute', - 'ui.select', - 'ui.ace', - 'naif.base64', - 'ui.bootstrap.showErrors', - 'angularResizable', - 'ngSanitize', - 'vs-repeat' -]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider', 'uiSelectConfig', '$httpProvider', - function ($routeProvider, $locationProvider, $compileProvider, growlProvider, uiSelectConfig, $httpProvider) { - function getQuery(Query, $route) { - var query = Query.get({'id': $route.current.params.queryId}); - return query.$promise; - }; - - if (currentUser.apiKey) { - $httpProvider.defaults.headers.common.Authorization = 'Key ' + currentUser.apiKey; - } - - uiSelectConfig.theme = "bootstrap"; - - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/); - $locationProvider.html5Mode(true); - growlProvider.globalTimeToLive(2000); - - $routeProvider.when('/admin/queries/outdated', { - templateUrl: '/views/admin/outdated_queries.html', - controller: 'AdminOutdatedQueriesCtrl' - }); - $routeProvider.when('/admin/queries/tasks', { - templateUrl: '/views/admin/tasks.html', - controller: 'AdminTasksCtrl' - }); - $routeProvider.when('/dashboard/:dashboardSlug', { - templateUrl: '/views/dashboard.html', - controller: 'DashboardCtrl', - reloadOnSearch: false - }); - $routeProvider.when('/public/dashboards/:token', { - templateUrl: '/views/dashboard.html', - controller: 'PublicDashboardCtrl', - reloadOnSearch: false - }); - $routeProvider.when('/queries', { - templateUrl: '/views/queries.html', - controller: 'QueriesCtrl', - reloadOnSearch: false - }); - $routeProvider.when('/queries/new', { - templateUrl: '/views/query.html', - controller: 'QuerySourceCtrl', - reloadOnSearch: false, - resolve: { - 'query': ['Query', function newQuery(Query) { - return Query.newQuery(); - }], - 'dataSources': ['DataSource', function (DataSource) { - return DataSource.query().$promise - }] - } - }); - $routeProvider.when('/queries/my', { - templateUrl: '/views/queries.html', - controller: 'QueriesCtrl', - reloadOnSearch: false - }); - $routeProvider.when('/queries/drafts', { - templateUrl: '/views/queries.html', - controller: 'QueriesCtrl', - reloadOnSearch: false - }); - $routeProvider.when('/queries/search', { - templateUrl: '/views/queries_search_results.html', - controller: 'QuerySearchCtrl', - reloadOnSearch: true, - }); - $routeProvider.when('/queries/:queryId', { - templateUrl: '/views/query.html', - controller: 'QueryViewCtrl', - reloadOnSearch: false, - resolve: { - 'query': ['Query', '$route', getQuery] - } - }); - $routeProvider.when('/queries/:queryId/source', { - templateUrl: '/views/query.html', - controller: 'QuerySourceCtrl', - reloadOnSearch: false, - resolve: { - 'query': ['Query', '$route', getQuery] - } - }); - $routeProvider.when('/admin/status', { - templateUrl: '/views/admin_status.html', - controller: 'AdminStatusCtrl' - }); - - $routeProvider.when('/alerts', { - templateUrl: '/views/alerts/list.html', - controller: 'AlertsCtrl' - }); - $routeProvider.when('/alerts/:alertId', { - templateUrl: '/views/alerts/edit.html', - controller: 'AlertCtrl' - }); - - $routeProvider.when('/data_sources/:dataSourceId', { - templateUrl: '/views/data_sources/edit.html', - controller: 'DataSourceCtrl' - }); - $routeProvider.when('/data_sources', { - templateUrl: '/views/data_sources/list.html', - controller: 'DataSourcesCtrl' - }); - - $routeProvider.when('/destinations/:destinationId', { - templateUrl: '/views/destinations/edit.html', - controller: 'DestinationCtrl' - }); - $routeProvider.when('/destinations', { - templateUrl: '/views/destinations/list.html', - controller: 'DestinationsCtrl' - }); - - $routeProvider.when('/users/new', { - templateUrl: '/views/users/new.html', - controller: 'NewUserCtrl' - }); - $routeProvider.when('/users/:userId', { - templateUrl: '/views/users/show.html', - reloadOnSearch: false, - controller: 'UserCtrl' - }); - $routeProvider.when('/users', { - templateUrl: '/views/users/list.html', - controller: 'UsersCtrl' - }); - $routeProvider.when('/groups/:groupId/data_sources', { - templateUrl: '/views/groups/show_data_sources.html', - controller: 'GroupDataSourcesCtrl' - }); - $routeProvider.when('/groups/:groupId', { - templateUrl: '/views/groups/show.html', - controller: 'GroupCtrl' - }); - $routeProvider.when('/groups', { - templateUrl: '/views/groups/list.html', - controller: 'GroupsCtrl' - }); - $routeProvider.when('/query_snippets/:snippetId', { - templateUrl: '/views/query_snippets/show.html', - controller: 'SnippetCtrl' - }); - $routeProvider.when('/query_snippets', { - templateUrl: '/views/query_snippets/list.html', - controller: 'SnippetsCtrl' - }); - $routeProvider.when('/', { - templateUrl: '/views/index.html', - controller: 'IndexCtrl' - }); - $routeProvider.when('/personal', { - redirectTo: '/' - }); - $routeProvider.otherwise({ - redirectTo: '/' - }); - - - } -]); diff --git a/rd_ui/app/scripts/controllers/admin_controllers.js b/rd_ui/app/scripts/controllers/admin_controllers.js deleted file mode 100644 index 41969f9548..0000000000 --- a/rd_ui/app/scripts/controllers/admin_controllers.js +++ /dev/null @@ -1,251 +0,0 @@ -(function () { - var AdminStatusCtrl = function ($scope, Events, $http, $timeout) { - Events.record(currentUser, "view", "page", "admin/status"); - $scope.$parent.pageTitle = "System Status"; - - var refresh = function () { - $http.get('/status.json').success(function (data) { - $scope.workers = data.workers; - delete data.workers; - $scope.manager = data.manager; - delete data.manager; - $scope.status = data; - }); - - var timer = $timeout(refresh, 59 * 1000); - - $scope.$on("$destroy", function () { - if (timer) { - $timeout.cancel(timer); - } - }); - }; - - refresh(); - }; - - var dateFormatter = function (value) { - if (!value) { - return "-"; - } - - return moment(value).format(clientConfig.dateTimeFormat); - }; - - var timestampFormatter = function(value) { - if (value) { - return dateFormatter(value * 1000.0); - } - - return "-"; - } - - var AdminTasksCtrl = function ($scope, $location, Events, $http, $timeout, $filter) { - Events.record(currentUser, "view", "page", "admin/tasks"); - $scope.$parent.pageTitle = "Running Queries"; - $scope.autoUpdate = true; - - $scope.gridConfig = { - isPaginationEnabled: true, - itemsByPage: 50, - maxSize: 8, - }; - $scope.selectedTab = 'in_progress'; - $scope.tasks = { - 'pending': [], - 'in_progress': [], - 'done': [] - }; - - $scope.allGridColumns = [ - { - label: 'Data Source ID', - map: 'data_source_id' - }, - { - label: 'Username', - map: 'username' - }, - { - 'label': 'State', - 'map': 'state', - "cellTemplate": '{{dataRow.state}} ' - }, - { - "label": "Query ID", - "map": "query_id" - }, - { - label: 'Query Hash', - map: 'query_hash' - }, - { - 'label': 'Runtime', - 'map': 'run_time', - 'formatFunction': function (value) { - return $filter('durationHumanize')(value); - } - }, - { - 'label': 'Created At', - 'map': 'created_at', - 'formatFunction': timestampFormatter - }, - { - 'label': 'Started At', - 'map': 'started_at', - 'formatFunction': timestampFormatter - }, - { - 'label': 'Updated At', - 'map': 'updated_at', - 'formatFunction': timestampFormatter - } - ]; - - $scope.inProgressGridColumns = angular.copy($scope.allGridColumns); - $scope.inProgressGridColumns.push({ - 'label': '', - "cellTemplate": '' - }); - - $scope.setTab = function(tab) { - $scope.selectedTab = tab; - $scope.showingTasks = $scope.tasks[tab]; - if (tab == 'in_progress') { - $scope.gridColumns = $scope.inProgressGridColumns; - } else { - $scope.gridColumns = $scope.allGridColumns; - } - }; - - $scope.setTab($location.hash() || 'in_progress'); - - var refresh = function () { - if ($scope.autoUpdate) { - $scope.refresh_time = moment().add(1, 'minutes'); - $http.get('/api/admin/queries/tasks').success(function (data) { - $scope.tasks = data; - $scope.showingTasks = $scope.tasks[$scope.selectedTab]; - }); - } - - var timer = $timeout(refresh, 5 * 1000); - - $scope.$on("$destroy", function () { - if (timer) { - $timeout.cancel(timer); - } - }); - }; - - refresh(); - }; - - var AdminOutdatedQueriesCtrl = function ($scope, Events, $http, $timeout, $filter) { - Events.record(currentUser, "view", "page", "admin/outdated_queries"); - $scope.$parent.pageTitle = "Outdated Queries"; - $scope.autoUpdate = true; - - $scope.gridConfig = { - isPaginationEnabled: true, - itemsByPage: 50, - maxSize: 8, - }; - - $scope.gridColumns = [ - { - label: 'Data Source ID', - map: 'data_source_id' - }, - { - "label": "Name", - "map": "name", - "cellTemplateUrl": "/views/queries_query_name_cell.html" - }, - { - 'label': 'Created By', - 'map': 'user.name' - }, - { - 'label': 'Runtime', - 'map': 'runtime', - 'formatFunction': function (value) { - return $filter('durationHumanize')(value); - } - }, - { - 'label': 'Last Executed At', - 'map': 'retrieved_at', - 'formatFunction': dateFormatter - }, - { - 'label': 'Created At', - 'map': 'created_at', - 'formatFunction': dateFormatter - }, - { - 'label': 'Update Schedule', - 'map': 'schedule', - 'formatFunction': function (value) { - return $filter('scheduleHumanize')(value); - } - } - ]; - - var refresh = function () { - if ($scope.autoUpdate) { - $scope.refresh_time = moment().add(1, 'minutes'); - $http.get('/api/admin/queries/outdated').success(function (data) { - $scope.queries = data.queries; - $scope.updatedAt = data.updated_at * 1000.0; - }); - } - - var timer = $timeout(refresh, 59 * 1000); - - $scope.$on("$destroy", function () { - if (timer) { - $timeout.cancel(timer); - } - }); - }; - - refresh(); - }; - - var cancelQueryButton = function () { - return { - restrict: 'E', - scope: { - 'queryId': '=', - 'taskId': '=' - }, - transclude: true, - template: '', - replace: true, - controller: ['$scope', '$http', 'Events', function ($scope, $http, Events) { - $scope.inProgress = false; - - $scope.cancelExecution = function() { - $http.delete('api/jobs/' + $scope.taskId).success(function() { - }); - - var queryId = $scope.queryId; - if ($scope.queryId == 'adhoc') { - queryId = null; - } - - Events.record(currentUser, 'cancel_execute', 'query', queryId, {'admin': true}); - $scope.inProgress = true; - } - }] - } - }; - - angular.module('redash.admin_controllers', []) - .controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl]) - .controller('AdminTasksCtrl', ['$scope', '$location', 'Events', '$http', '$timeout', '$filter', AdminTasksCtrl]) - .controller('AdminOutdatedQueriesCtrl', ['$scope', 'Events', '$http', '$timeout', '$filter', AdminOutdatedQueriesCtrl]) - .directive('cancelQueryButton', cancelQueryButton) -})(); diff --git a/rd_ui/app/scripts/controllers/alerts.js b/rd_ui/app/scripts/controllers/alerts.js deleted file mode 100644 index c444e388e1..0000000000 --- a/rd_ui/app/scripts/controllers/alerts.js +++ /dev/null @@ -1,228 +0,0 @@ -(function() { - - var AlertsCtrl = function($scope, Events, Alert) { - Events.record(currentUser, "view", "page", "alerts"); - $scope.$parent.pageTitle = "Alerts"; - - $scope.alerts = [] - Alert.query(function(alerts) { - var stateClass = { - 'ok': 'label label-success', - 'triggered': 'label label-danger', - 'unknown': 'label label-warning' - }; - _.each(alerts, function(alert) { - alert.class = stateClass[alert.state]; - }) - $scope.alerts = alerts; - - }); - - $scope.gridConfig = { - isPaginationEnabled: true, - itemsByPage: 50, - maxSize: 8, - }; - - - $scope.gridColumns = [ - { - "label": "Name", - "map": "name", - "cellTemplate": '{{dataRow.name}} (query)' - }, - { - 'label': 'Created By', - 'map': 'user.name' - }, - { - 'label': 'State', - 'cellTemplate': '{{dataRow.state | uppercase}} since ' - }, - { - 'label': 'Created At', - 'cellTemplate': '' - } - ]; - }; - - var AlertCtrl = function($scope, $routeParams, $location, growl, Query, Events, Alert, Destination) { - $scope.selectedTab = 'users'; - $scope.$parent.pageTitle = "Alerts"; - - $scope.alertId = $routeParams.alertId; - if ($scope.alertId === "new") { - Events.record(currentUser, 'view', 'page', 'alerts/new'); - } else { - Events.record(currentUser, 'view', 'alert', $scope.alertId); - } - - $scope.onQuerySelected = function(item) { - $scope.selectedQuery = item; - item.getQueryResultPromise().then(function(result) { - $scope.queryResult = result; - $scope.alert.options.column = $scope.alert.options.column || result.getColumnNames()[0]; - }); - }; - - if ($scope.alertId === "new") { - $scope.alert = new Alert({options: {}}); - $scope.canEdit = true; - } else { - $scope.alert = Alert.get({id: $scope.alertId}, function(alert) { - $scope.onQuerySelected(new Query($scope.alert.query)); - }); - $scope.canEdit = currentUser.canEdit($scope.alert); - } - - $scope.ops = ['greater than', 'less than', 'equals']; - $scope.selectedQuery = null; - - $scope.getDefaultName = function() { - if (!$scope.alert.query) { - return undefined; - } - return _.template("<%= query.name %>: <%= options.column %> <%= options.op %> <%= options.value %>", $scope.alert); - }; - - $scope.searchQueries = function (term) { - if (!term || term.length < 3) { - return; - } - - Query.search({q: term}, function(results) { - $scope.queries = results; - }); - }; - - $scope.saveChanges = function() { - if ($scope.alert.name === undefined || $scope.alert.name === '') { - $scope.alert.name = $scope.getDefaultName(); - } - if ($scope.alert.rearm === '' || $scope.alert.rearm === 0) { - $scope.alert.rearm = null; - } - $scope.alert.$save(function(alert) { - growl.addSuccessMessage("Saved."); - if ($scope.alertId === "new") { - $location.path('/alerts/' + alert.id).replace(); - } - }, function() { - growl.addErrorMessage("Failed saving alert."); - }); - }; - - $scope.delete = function() { - $scope.alert.$delete(function() { - $location.path('/alerts'); - growl.addSuccessMessage("Alert deleted."); - }, function() { - growl.addErrorMessage("Failed deleting alert."); - }); - } - - }; - - angular.module('redash.directives').directive('alertSubscriptions', ['$q', '$sce', 'AlertSubscription', 'Destination', 'growl', function ($q, $sce, AlertSubscription, Destination, growl) { - return { - restrict: 'E', - replace: true, - templateUrl: '/views/alerts/alert_subscriptions.html', - scope: { - 'alertId': '=' - }, - controller: function ($scope) { - $scope.newSubscription = {}; - $scope.subscribers = []; - $scope.destinations = []; - $scope.currentUser = currentUser; - - var destinations = Destination.query().$promise; - var subscribers = AlertSubscription.query({alertId: $scope.alertId}).$promise; - - $q.all([destinations, subscribers]).then(function(responses) { - var destinations = responses[0]; - var subscribers = responses[1]; - - var subscribedDestinations = _.compact(_.map(subscribers, function(s) { return s.destination && s.destination.id })); - var subscribedUsers = _.compact(_.map(subscribers, function(s) { if (!s.destination) { return s.user.id } })); - - $scope.destinations = _.filter(destinations, function(d) { return !_.contains(subscribedDestinations, d.id); }); - - if (!_.contains(subscribedUsers, currentUser.id)) { - $scope.destinations.unshift({user: {name: currentUser.name}}); - } - - $scope.newSubscription.destination = $scope.destinations[0]; - $scope.subscribers = subscribers; - }); - - $scope.destinationsDisplay = function(destination) { - if (!destination) { - return ''; - } - - if (destination.destination) { - destination = destination.destination; - } else if (destination.user) { - destination = { - name: destination.user.name + ' (Email)', - icon: 'fa-envelope', - type: 'user' - }; - } - - return $sce.trustAsHtml(' ' + destination.name); - }; - - $scope.saveSubscriber = function() { - var sub = new AlertSubscription({alert_id: $scope.alertId}); - if ($scope.newSubscription.destination.id) { - sub.destination_id = $scope.newSubscription.destination.id; - } - - sub.$save(function () { - growl.addSuccessMessage("Subscribed."); - $scope.subscribers.push(sub); - $scope.destinations = _.without($scope.destinations, $scope.newSubscription.destination); - if ($scope.destinations.length > 0) { - $scope.newSubscription.destination = $scope.destinations[0]; - } else { - $scope.newSubscription.destination = undefined; - } - console.log("dests: ", $scope.destinations); - }, function (response) { - growl.addErrorMessage("Failed saving subscription."); - }); - }; - - $scope.unsubscribe = function(subscriber) { - var destination = subscriber.destination; - var user = subscriber.user; - - subscriber.$delete(function () { - growl.addSuccessMessage("Unsubscribed"); - $scope.subscribers = _.without($scope.subscribers, subscriber); - if (destination) { - $scope.destinations.push(destination); - } else if (user.id == currentUser.id) { - $scope.destinations.push({user: {name: currentUser.name}}); - } - - if ($scope.destinations.length == 1) { - $scope.newSubscription.destination = $scope.destinations[0]; - } - - }, function () { - growl.addErrorMessage("Failed unsubscribing."); - }); - }; - } - } - }]); - - angular.module('redash.controllers') - .controller('AlertsCtrl', ['$scope', 'Events', 'Alert', AlertsCtrl]) - .controller('AlertCtrl', ['$scope', '$routeParams', '$location', 'growl', 'Query', 'Events', 'Alert', 'Destination', AlertCtrl]) - -})(); diff --git a/rd_ui/app/scripts/controllers/controllers.js b/rd_ui/app/scripts/controllers/controllers.js deleted file mode 100644 index 35406a050b..0000000000 --- a/rd_ui/app/scripts/controllers/controllers.js +++ /dev/null @@ -1,231 +0,0 @@ -(function () { - var dateFormatter = function (value) { - if (!value) { - return "-"; - } - - return value.format(clientConfig.dateTimeFormat); - }; - - var QuerySearchCtrl = function($scope, $location, $filter, Events, Query) { - $scope.$parent.pageTitle = "Queries Search"; - - $scope.gridConfig = { - isPaginationEnabled: true, - itemsByPage: 50, - maxSize: 8, - }; - - $scope.gridColumns = [ - { - "label": "Name", - "map": "name", - "cellTemplateUrl": "/views/queries_query_name_cell.html" - }, - { - 'label': 'Created By', - 'map': 'user.name' - }, - { - 'label': 'Created At', - 'map': 'created_at', - 'formatFunction': dateFormatter - }, - { - 'label': 'Update Schedule', - 'map': 'schedule', - 'formatFunction': function (value) { - return $filter('scheduleHumanize')(value); - } - } - ]; - - $scope.queries = []; - $scope.$parent.term = $location.search().q; - - Query.search({q: $scope.term }, function(results) { - $scope.queries = _.map(results, function(query) { - query.created_at = moment(query.created_at); - return query; - }); - }); - - $scope.search = function() { - if (!angular.isString($scope.term) || $scope.term.trim() == "") { - $scope.queries = []; - return; - } - - $location.search({q: $scope.term}); - }; - - Events.record(currentUser, "search", "query", "", {"term": $scope.term}); - }; - - var QueriesCtrl = function ($scope, $http, $location, $filter, Query) { - var loader; - - $scope.queries = []; - $scope.page = parseInt($location.search().page || 1); - $scope.total = undefined; - $scope.pageSize = 25; - - function loadQueries(resource, defaultOptions) { - return function(options) { - options = _.extend({}, defaultOptions, options); - resource(options, function (queries) { - $scope.totalQueriesCount = queries.count; - $scope.queries = _.map(queries.results, function (query) { - query.created_at = moment(query.created_at); - query.retrieved_at = moment(query.retrieved_at); - return query; - }); - }); - } - } - - switch($location.path()) { - case '/queries': - $scope.$parent.pageTitle = "Queries"; - // page title - loader = loadQueries(Query.query); - break; - case '/queries/drafts': - $scope.$parent.pageTitle = "Drafts"; - loader = loadQueries(Query.myQueries, {drafts: true}); - break; - case '/queries/my': - $scope.$parent.pageTitle = "My Queries"; - loader = loadQueries(Query.myQueries); - break; - } - - var loadAllQueries = loadQueries(Query.query); - var loadMyQueries = loadQueries(Query.myQueries); - - function load() { - var options = {page: $scope.page, page_size: $scope.pageSize}; - loader(options); - } - - $scope.selectPage = function(page) { - $location.search('page', page); - $scope.page = page; - load(); - } - - $scope.tabs = [ - {"name": "My Queries", "path": "queries/my", loader: loadMyQueries}, - {"path": "queries", "name": "All Queries", isActive: function(path) { - return path === '/queries'; - }, "loader": loadAllQueries}, - {"path": "queries/drafts", "name": "Drafts", loader: loadMyQueries}, - ]; - - load(); - } - - var MainCtrl = function ($scope, $location, Dashboard) { - $scope.$on("$routeChangeSuccess", function (event, current, previous, rejection) { - if ($scope.showPermissionError) { - $scope.showPermissionError = false; - } - }); - - $scope.$on("$routeChangeError", function (event, current, previous, rejection) { - if (rejection.status === 403) { - $scope.showPermissionError = true; - } - }); - - $scope.location = String(document.location); - $scope.version = clientConfig.version; - $scope.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.hasPermission("admin"); - - $scope.newDashboard = { - 'name': null, - 'layout': null - } - }; - - var IndexCtrl = function ($scope, Events, Dashboard, Query) { - Events.record(currentUser, "view", "page", "personal_homepage"); - $scope.$parent.pageTitle = "Home"; - - $scope.recentQueries = Query.recent(); - $scope.recentDashboards = Dashboard.recent(); - }; - - // Controller for modal window share_permissions, works for both query and dashboards, needs apiAccess set in scope - var ManagePermissionsCtrl = function ($scope, $http, $modalInstance, User) { - $scope.grantees = []; - $scope.newGrantees = {}; - - // List users that are granted permissions - var loadGrantees = function() { - $http.get($scope.apiAccess).success(function(result) { - $scope.grantees = []; - for(var access_type in result) { - result[access_type].forEach(function(grantee) { - var item = grantee; - item['access_type'] = access_type; - $scope.grantees.push(item); - }) - } - }); - }; - - loadGrantees(); - - // Search for user - $scope.findUser = function(search) { - if (search == "") { - return; - } - - if ($scope.foundUsers === undefined) { - User.query(function(users) { - var existingIds = _.map($scope.grantees, function(m) { return m.id; }); - _.each(users, function(user) { user.alreadyGrantee = _.contains(existingIds, user.id); }); - $scope.foundUsers = users; - }); - } - }; - - // Add new user to grantees list - $scope.addGrantee = function(user) { - $scope.newGrantees.selected = undefined; - var body = {'access_type': 'modify', 'user_id': user.id}; - $http.post($scope.apiAccess, body).success(function() { - user.alreadyGrantee = true; - loadGrantees(); - }); - }; - - // Remove user from grantees list - $scope.removeGrantee = function(user) { - var body = {'access_type': 'modify', 'user_id': user.id}; - $http({ url: $scope.apiAccess, method: 'DELETE', - data: body, headers: {"Content-Type": "application/json"} - }).success(function() { - $scope.grantees = _.filter($scope.grantees, function(m) { return m != user }); - - if ($scope.foundUsers) { - _.each($scope.foundUsers, function(u) { if (u.id == user.id) { u.alreadyGrantee = false }; }); - } - }); - }; - - $scope.close = function() { - $modalInstance.close(); - } - }; - - - angular.module('redash.controllers', []) - .controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl]) - .controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', 'Query', IndexCtrl]) - .controller('MainCtrl', ['$scope', '$location', 'Dashboard', MainCtrl]) - .controller('QuerySearchCtrl', ['$scope', '$location', '$filter', 'Events', 'Query', QuerySearchCtrl]) - .controller('ManagePermissionsCtrl', ['$scope', '$http', '$modalInstance', 'User', ManagePermissionsCtrl]); -})(); diff --git a/rd_ui/app/scripts/controllers/dashboard.js b/rd_ui/app/scripts/controllers/dashboard.js deleted file mode 100644 index bc8b0097b7..0000000000 --- a/rd_ui/app/scripts/controllers/dashboard.js +++ /dev/null @@ -1,290 +0,0 @@ -(function() { - var PublicDashboardCtrl = function($scope, Events, Widget, $routeParams, $location, $http, $timeout, $q, Dashboard) { - $scope.dashboard = seedData.dashboard; - $scope.public = true; - $scope.dashboard.widgets = _.map($scope.dashboard.widgets, function (row) { - return _.map(row, function (widget) { - return new Widget(widget); - }); - }); - }; - - var DashboardCtrl = function($scope, Events, Widget, $routeParams, $location, $http, $timeout, $q, $modal, Dashboard) { - $scope.refreshEnabled = false; - $scope.isFullscreen = false; - $scope.refreshRate = 60; - $scope.showPermissionsControl = clientConfig.showPermissionsControl; - - var renderDashboard = function (dashboard) { - $scope.$parent.pageTitle = dashboard.name; - - var promises = []; - - _.each($scope.dashboard.widgets, function (row) { - return _.each(row, function (widget) { - if (widget.visualization) { - var queryResult = widget.getQuery().getQueryResult(); - if (angular.isDefined(queryResult)) - promises.push(queryResult.toPromise()); - } - }); - }); - - $q.all(promises).then(function(queryResults) { - var filters = {}; - _.each(queryResults, function(queryResult) { - var queryFilters = queryResult.getFilters(); - _.each(queryFilters, function (queryFilter) { - var hasQueryStringValue = _.has($location.search(), queryFilter.name); - - if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) { - // If dashboard filters not enabled, or no query string value given, skip filters linking. - return; - } - - if (!_.has(filters, queryFilter.name)) { - var filter = _.extend({}, queryFilter); - filters[filter.name] = filter; - filters[filter.name].originFilters = []; - if (hasQueryStringValue) { - filter.current = $location.search()[filter.name]; - } - - $scope.$watch(function () { return filter.current }, function (value) { - _.each(filter.originFilters, function (originFilter) { - originFilter.current = value; - }); - }); - } - - // TODO: merge values. - filters[queryFilter.name].originFilters.push(queryFilter); - }); - }); - - $scope.filters = _.values(filters); - }); - } - - var loadDashboard = _.throttle(function () { - $scope.dashboard = Dashboard.get({slug: $routeParams.dashboardSlug}, function (dashboard) { - Events.record(currentUser, "view", "dashboard", dashboard.id); - renderDashboard(dashboard); - }, function () { - // error... - // try again. we wrap loadDashboard with throttle so it doesn't happen too often.\ - // we might want to consider exponential backoff and also move this as a general solution in $http/$resource for - // all AJAX calls. - loadDashboard(); - } - ); - }, 1000); - - loadDashboard(); - - var autoRefresh = function() { - if ($scope.refreshEnabled) { - $timeout(function() { - Dashboard.get({ - slug: $routeParams.dashboardSlug - }, function(dashboard) { - var newWidgets = _.groupBy(_.flatten(dashboard.widgets), 'id'); - - _.each($scope.dashboard.widgets, function(row) { - _.each(row, function(widget, i) { - var newWidget = newWidgets[widget.id][0]; - if (newWidget.visualization) { - if (newWidget && newWidget.visualization.query.latest_query_data_id != widget.visualization.query.latest_query_data_id) { - row[i] = new Widget(newWidget); - } - } - }); - }); - - autoRefresh(); - }); - - }, $scope.refreshRate); - } - }; - - $scope.archiveDashboard = function () { - if (confirm('Are you sure you want to archive the "' + $scope.dashboard.name + '" dashboard?')) { - Events.record(currentUser, "archive", "dashboard", $scope.dashboard.id); - $scope.dashboard.$delete(function () { - $scope.$parent.reloadDashboards(); - }); - } - }; - - $scope.showManagePermissionsModal = function() { - // Create scope for share permissions dialog and pass api path to it - var scope = $scope.$new(); - $scope.apiAccess = 'api/dashboards/' + $scope.dashboard.id + '/acl'; - - $modal.open({ - scope: scope, - templateUrl: '/views/dialogs/manage_permissions.html', - controller: 'ManagePermissionsCtrl' - }); - }; - - $scope.togglePublished = function () { - Events.record(currentUser, "toggle_published", "dashboard", $scope.dashboard.id); - $scope.dashboard.is_draft = !$scope.dashboard.is_draft; - $scope.saveInProgress = true; - Dashboard.save({slug: $scope.dashboard.id, name: $scope.dashboard.name, - layout: JSON.stringify($scope.dashboard.layout), - is_draft: $scope.dashboard.is_draft}, - function() {$scope.saveInProgress = false;}); - - }; - - $scope.toggleFullscreen = function() { - $scope.isFullscreen = !$scope.isFullscreen; - $('body').toggleClass('headless'); - if ($scope.isFullscreen) { - $location.search('fullscreen', true); - } else { - $location.search('fullscreen', null); - } - }; - - if (_.has($location.search(), 'fullscreen')) { - $scope.toggleFullscreen(); - } - - $scope.triggerRefresh = function() { - $scope.refreshEnabled = !$scope.refreshEnabled; - - Events.record(currentUser, "autorefresh", "dashboard", $scope.dashboard.id, {'enable': $scope.refreshEnabled}); - - if ($scope.refreshEnabled) { - var refreshRate = _.min(_.map(_.flatten($scope.dashboard.widgets), function(widget) { - if (widget.visualization) { - var schedule = widget.visualization.query.schedule; - if (schedule === null || schedule.match(/\d\d:\d\d/) !== null) { - return 60; - } - return widget.visualization.query.schedule; - } - })); - - $scope.refreshRate = _.min([300, refreshRate]) * 1000; - - autoRefresh(); - } - }; - - $scope.openShareForm = function() { - $modal.open({ - templateUrl: '/views/dashboard_share.html', - size: 'sm', - scope: $scope, - controller: ['$scope', '$modalInstance', '$http', function($scope, $modalInstance, $http) { - $scope.close = function() { - $modalInstance.close(); - }; - - $scope.toggleSharing = function() { - var url = 'api/dashboards/' + $scope.dashboard.id + '/share'; - if ($scope.dashboard.publicAccessEnabled) { - // disable - $http.delete(url).success(function() { - $scope.dashboard.publicAccessEnabled = false; - delete $scope.dashboard.public_url; - }).error(function() { - $scope.dashboard.publicAccessEnabled = true; - // TODO: show message - }) - } else { - $http.post(url).success(function(data) { - $scope.dashboard.publicAccessEnabled = true; - $scope.dashboard.public_url = data.public_url; - }).error(function() { - $scope.dashboard.publicAccessEnabled = false; - // TODO: show message - }); - } - }; - }] - }); - } - }; - - var WidgetCtrl = function($scope, $location, Events, Query, $modal) { - $scope.editTextBox = function() { - $modal.open({ - templateUrl: '/views/edit_text_box_form.html', - scope: $scope, - controller: ['$scope', '$modalInstance', 'growl', function($scope, $modalInstance, growl) { - $scope.close = function() { - $modalInstance.close(); - }; - - $scope.saveWidget = function() { - $scope.saveInProgress = true; - $scope.widget.$save().then(function(response) { - $scope.close(); - }).catch(function() { - growl.addErrorMessage("Widget can not be updated"); - }).finally(function() { - $scope.saveInProgress = false; - }); - }; - }], - }); - } - - $scope.deleteWidget = function() { - if (!confirm('Are you sure you want to remove "' + $scope.widget.getName() + '" from the dashboard?')) { - return; - } - - Events.record(currentUser, "delete", "widget", $scope.widget.id); - - $scope.widget.$delete(function(response) { - $scope.dashboard.widgets = _.map($scope.dashboard.widgets, function(row) { - return _.filter(row, function(widget) { - return widget.id != undefined; - }) - }); - - $scope.dashboard.widgets = _.filter($scope.dashboard.widgets, function(row) { return row.length > 0 }); - - $scope.dashboard.layout = response.layout; - $scope.dashboard.version = response.version; - }); - }; - - Events.record(currentUser, "view", "widget", $scope.widget.id); - - $scope.reload = function(force) { - var maxAge = $location.search()['maxAge']; - if (force) { - maxAge = 0; - } - $scope.queryResult = $scope.query.getQueryResult(maxAge); - }; - - if ($scope.widget.visualization) { - Events.record(currentUser, "view", "query", $scope.widget.visualization.query.id); - Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id); - - $scope.query = $scope.widget.getQuery(); - $scope.reload(false); - - $scope.type = 'visualization'; - } else if ($scope.widget.restricted) { - $scope.type = 'restricted'; - } else { - $scope.type = 'textbox'; - } - }; - - angular.module('redash.controllers') - .controller('DashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$location', '$http', '$timeout', '$q', '$modal', 'Dashboard', DashboardCtrl]) - .controller('PublicDashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$location', '$http', '$timeout', '$q', 'Dashboard', PublicDashboardCtrl]) - .controller('WidgetCtrl', ['$scope', '$location', 'Events', 'Query', '$modal', WidgetCtrl]) - -})(); diff --git a/rd_ui/app/scripts/controllers/data_sources.js b/rd_ui/app/scripts/controllers/data_sources.js deleted file mode 100644 index f0ef2f2bca..0000000000 --- a/rd_ui/app/scripts/controllers/data_sources.js +++ /dev/null @@ -1,66 +0,0 @@ -(function () { - var DataSourcesCtrl = function ($scope, $location, growl, Events, DataSource) { - Events.record(currentUser, "view", "page", "admin/data_sources"); - $scope.$parent.pageTitle = "Data Sources"; - - $scope.dataSources = DataSource.query(); - - }; - - var DataSourceCtrl = function ($scope, $routeParams, $http, $location, growl, Events, DataSource) { - Events.record(currentUser, "view", "page", "admin/data_source"); - $scope.$parent.pageTitle = "Data Sources"; - - $scope.dataSourceId = $routeParams.dataSourceId; - - if ($scope.dataSourceId == "new") { - $scope.dataSource = new DataSource({options: {}}); - } else { - $scope.dataSource = DataSource.get({id: $routeParams.dataSourceId}); - } - - $scope.$watch('dataSource.id', function(id) { - if (id != $scope.dataSourceId && id !== undefined) { - $location.path('/data_sources/' + id).replace(); - } - }); - - function deleteDataSource() { - Events.record(currentUser, "delete", "datasource", $scope.dataSource.id); - - $scope.dataSource.$delete(function (resource) { - growl.addSuccessMessage("Data source deleted successfully."); - $location.path('/data_sources/'); - }.bind(this), function (httpResponse) { - console.log("Failed to delete data source: ", httpResponse.status, httpResponse.statusText, httpResponse.data); - growl.addErrorMessage("Failed to delete data source."); - }); - } - - function testConnection (callback) { - Events.record(currentUser, "test", "datasource", $scope.dataSource.id); - - DataSource.test({id: $scope.dataSource.id}, function (httpResponse) { - if (httpResponse.ok) { - growl.addSuccessMessage(' Success.', {enableHtml: true, ttl: 3000}); - } else { - growl.addErrorMessage(' Connection Test Failed:
' + httpResponse.message, {enableHtml: true, ttl: -1}); - } - callback(); - }, function (httpResponse) { - console.log("Failed to test data source: ", httpResponse.status, httpResponse.statusText, httpResponse); - growl.addErrorMessage(' Unknown error occurred while performing connection test. Please try again later.', {enableHtml: true, ttl: -1}); - callback(); - }); - } - - $scope.actions = [ - {name: 'Delete', class: 'btn-danger', callback: deleteDataSource}, - {name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true} - ] - }; - - angular.module('redash.controllers') - .controller('DataSourcesCtrl', ['$scope', '$location', 'growl', 'Events', 'DataSource', DataSourcesCtrl]) - .controller('DataSourceCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'DataSource', DataSourceCtrl]) -})(); diff --git a/rd_ui/app/scripts/controllers/destinations.js b/rd_ui/app/scripts/controllers/destinations.js deleted file mode 100644 index 28474e6ac7..0000000000 --- a/rd_ui/app/scripts/controllers/destinations.js +++ /dev/null @@ -1,44 +0,0 @@ -(function () { - var DestinationsCtrl = function ($scope, $location, growl, Events, Destination) { - Events.record(currentUser, "view", "page", "admin/destinations"); - $scope.$parent.pageTitle = "Destinations"; - - $scope.destinations = Destination.query(); - - }; - - var DestinationCtrl = function ($scope, $routeParams, $http, $location, growl, Events, Destination) { - Events.record(currentUser, "view", "page", "admin/destination"); - $scope.$parent.pageTitle = "Destinations"; - - $scope.destinationId = $routeParams.destinationId; - - if ($scope.destinationId == "new") { - $scope.destination = new Destination({options: {}}); - } else { - $scope.destination = Destination.get({id: $routeParams.destinationId}); - } - - $scope.$watch('destination.id', function(id) { - if (id != $scope.destinationId && id !== undefined) { - $location.path('/destinations/' + id).replace(); - } - }); - - $scope.delete = function() { - Events.record(currentUser, "delete", "destination", $scope.destination.id); - - $scope.destination.$delete(function(resource) { - growl.addSuccessMessage("Destination deleted successfully."); - $location.path('/destinations/'); - }.bind(this), function(httpResponse) { - console.log("Failed to delete destination: ", httpResponse.status, httpResponse.statusText, httpResponse.data); - growl.addErrorMessage("Failed to delete destination."); - }); - } - }; - - angular.module('redash.controllers') - .controller('DestinationsCtrl', ['$scope', '$location', 'growl', 'Events', 'Destination', DestinationsCtrl]) - .controller('DestinationCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'Destination', DestinationCtrl]) -})(); diff --git a/rd_ui/app/scripts/controllers/query_source.js b/rd_ui/app/scripts/controllers/query_source.js deleted file mode 100644 index 30fc90dbc6..0000000000 --- a/rd_ui/app/scripts/controllers/query_source.js +++ /dev/null @@ -1,139 +0,0 @@ -(function() { - 'use strict'; - - function QuerySourceCtrl(Events, growl, $controller, $scope, $location, $http, Query, Visualization, KeyboardShortcuts) { - // extends QueryViewCtrl - $controller('QueryViewCtrl', {$scope: $scope}); - // TODO: - // This doesn't get inherited. Setting it on this didn't work either (which is weird). - // Obviously it shouldn't be repeated, but we got bigger fish to fry. - var DEFAULT_TAB = 'table'; - - Events.record(currentUser, 'view_source', 'query', $scope.query.id); - - var isNewQuery = !$scope.query.id, - queryText = $scope.query.query, - // ref to QueryViewCtrl.saveQuery - saveQuery = $scope.saveQuery, - forkQuery = $scope.forkQuery; - - $scope.sourceMode = true; - $scope.canEdit = currentUser.canEdit($scope.query) || $scope.query.can_edit;// TODO: bring this back? || clientConfig.allowAllToEditQueries; - $scope.isDirty = false; - $scope.base_url = $location.protocol()+"://"+$location.host()+":"+$location.port(); - - $scope.newVisualization = undefined; - - // @override - Object.defineProperty($scope, 'showDataset', { - get: function() { - return $scope.queryResult && $scope.queryResult.getStatus() == 'done'; - } - }); - - var shortcuts = { - 'meta+s': function () { - if ($scope.canEdit) { - $scope.saveQuery(); - } - }, - 'ctrl+s': function () { - if ($scope.canEdit) { - $scope.saveQuery(); - } - }, - // Cmd+Enter for Mac - 'meta+enter': $scope.executeQuery, - // Ctrl+Enter for PC - 'ctrl+enter': $scope.executeQuery - }; - - KeyboardShortcuts.bind(shortcuts); - - // @override - $scope.saveQuery = function(options, data) { - var savePromise = saveQuery(options, data); - - if (!savePromise) { - return; - } - - savePromise.then(function(savedQuery) { - queryText = savedQuery.query; - $scope.isDirty = $scope.query.query !== queryText; - // update to latest version number - $scope.query.version = savedQuery.version; - - if (isNewQuery) { - // redirect to new created query (keep hash) - $location.path(savedQuery.getSourceLink()); - } - }, function(error) { - if(error.status == 409) { - growl.addErrorMessage('It seems like the query has been modified by another user. ' + - 'Please copy/backup your changes and reload this page.', {ttl: -1}); - } - }); - - return savePromise; - }; - - $scope.forkQuery = function(options, data) { - var savePromise = forkQuery(options, data); - - if (!savePromise) { - return; - } - - savePromise.then(function(savedQuery) { - queryText = savedQuery.query; - }); - - return savePromise; - }; - - $scope.duplicateQuery = function() { - Events.record(currentUser, 'fork', 'query', $scope.query.id); - $scope.forkQuery({ - successMessage: 'Query forked', - errorMessage: 'Query could not be forked' - }).then(function redirect(savedQuery) { - // redirect to forked query (clear hash) - $location.url(savedQuery.getSourceLink()).replace() - }); - }; - - $scope.deleteVisualization = function($e, vis) { - $e.preventDefault(); - if (confirm('Are you sure you want to delete ' + vis.name + ' ?')) { - Events.record(currentUser, 'delete', 'visualization', vis.id); - - Visualization.delete(vis, function() { - if ($scope.selectedTab == vis.id) { - $scope.selectedTab = DEFAULT_TAB; - $location.hash($scope.selectedTab); - } - $scope.query.visualizations = - $scope.query.visualizations.filter(function (v) { - return vis.id !== v.id; - }); - }, function () { - growl.addErrorMessage("Error deleting visualization. Maybe it's used in a dashboard?"); - }); - } - }; - - $scope.$watch('query.query', function(newQueryText) { - $scope.isDirty = (newQueryText !== queryText); - }); - - $scope.$on('$destroy', function destroy() { - KeyboardShortcuts.unbind(shortcuts); - }); - } - - angular.module('redash.controllers').controller('QuerySourceCtrl', [ - 'Events', 'growl', '$controller', '$scope', '$location', '$http', - 'Query', 'Visualization', 'KeyboardShortcuts', QuerySourceCtrl - ]); -})(); diff --git a/rd_ui/app/scripts/controllers/query_view.js b/rd_ui/app/scripts/controllers/query_view.js deleted file mode 100644 index 72293dcfae..0000000000 --- a/rd_ui/app/scripts/controllers/query_view.js +++ /dev/null @@ -1,379 +0,0 @@ -(function() { - 'use strict'; - - function QueryViewCtrl($scope, Events, $route, $routeParams, $http, $location, notifications, growl, $modal, Query, DataSource, User) { - var DEFAULT_TAB = 'table'; - - var getQueryResult = function(maxAge) { - if (maxAge === undefined) { - maxAge = $location.search()['maxAge']; - } - - if (maxAge === undefined) { - maxAge = -1; - } - - $scope.showLog = false; - $scope.queryResult = $scope.query.getQueryResult(maxAge); - }; - - var getDataSourceId = function() { - // Try to get the query's data source id - var dataSourceId = $scope.query.data_source_id; - - // If there is no source yet, then parse what we have in localStorage - // e.g. `null` -> `NaN`, malformed data -> `NaN`, "1" -> 1 - if (dataSourceId === undefined) { - dataSourceId = parseInt(localStorage.lastSelectedDataSourceId, 10); - } - - // If we had an invalid value in localStorage (e.g. nothing, deleted source), then use the first data source - var isValidDataSourceId = !isNaN(dataSourceId) && _.some($scope.dataSources, function(ds) { - return ds.id == dataSourceId; - }); - - if (!isValidDataSourceId) { - dataSourceId = $scope.dataSources[0].id; - } - - // Return our data source id - return dataSourceId; - } - - var updateDataSources = function(dataSources) { - // Filter out data sources the user can't query (or used by current query): - $scope.dataSources = _.filter(dataSources, function(dataSource) { - return !dataSource.view_only || dataSource.id === $scope.query.data_source_id; - }); - - if ($scope.dataSources.length == 0) { - $scope.noDataSources = true; - return; - } - - if ($scope.query.isNew()) { - $scope.query.data_source_id = getDataSourceId(); - } - - $scope.dataSource = _.find(dataSources, function(ds) { return ds.id == $scope.query.data_source_id; }); - - //$scope.canExecuteQuery = $scope.canExecuteQuery && _.some(dataSources, function(ds) { return !ds.view_only }); - $scope.canCreateQuery = _.any(dataSources, function(ds) { return !ds.view_only }); - - updateSchema(); - } - - - $scope.dataSource = {}; - $scope.query = $route.current.locals.query; - $scope.showPermissionsControl = clientConfig.showPermissionsControl; - - var updateSchema = function() { - $scope.hasSchema = false; - $scope.editorSize = "col-md-12"; - DataSource.getSchema({id: $scope.query.data_source_id}, function(data) { - if (data && data.length > 0) { - $scope.schema = data; - _.each(data, function(table) { - table.collapsed = true; - }); - - $scope.editorSize = "col-md-9"; - $scope.hasSchema = true; - } else { - $scope.schema = undefined; - $scope.hasSchema = false; - $scope.editorSize = "col-md-12"; - } - }); - } - - Events.record(currentUser, 'view', 'query', $scope.query.id); - if ($scope.query.hasResult() || $scope.query.paramsRequired()) { - getQueryResult(); - } - $scope.queryExecuting = false; - - $scope.isQueryOwner = (currentUser.id === $scope.query.user.id) || currentUser.hasPermission('admin'); - $scope.canViewSource = currentUser.hasPermission('view_source'); - - $scope.canExecuteQuery = function() { - return currentUser.hasPermission('execute_query') && !$scope.dataSource.view_only; - } - - $scope.canScheduleQuery = currentUser.hasPermission('schedule_query'); - - if ($route.current.locals.dataSources) { - $scope.dataSources = $route.current.locals.dataSources; - updateDataSources($route.current.locals.dataSources); - } else { - $scope.dataSources = DataSource.query(updateDataSources); - } - - // in view mode, latest dataset is always visible - // source mode changes this behavior - $scope.showDataset = true; - $scope.showLog = false; - - $scope.lockButton = function(lock) { - $scope.queryExecuting = lock; - }; - - $scope.showApiKey = function() { - alert("API Key for this query:\n" + $scope.query.api_key); - }; - - $scope.saveQuery = function(options, data) { - if (data) { - // Don't save new query with partial data - if ($scope.query.isNew()) { - return; - } - data.id = $scope.query.id; - data.version = $scope.query.version; - } else { - data = _.pick($scope.query, ["schedule", "query", "id", "description", "name", "data_source_id", "options", "latest_query_data_id", "version", "is_draft"]); - } - - options = _.extend({}, { - successMessage: 'Query saved', - errorMessage: 'Query could not be saved' - }, options); - - return Query.save(data, function(updatedQuery) { - growl.addSuccessMessage(options.successMessage); - $scope.query.version = updatedQuery.version; - }, function(error) { - if(error.status == 409) { - growl.addErrorMessage('It seems like the query has been modified by another user. ' + - 'Please copy/backup your changes and reload this page.', {ttl: -1}); - } else { - growl.addErrorMessage(options.errorMessage); - } - }).$promise; - }; - - $scope.forkQuery = function(options, data) { - return Query.fork({id:$scope.query.id}, function() { - growl.addSuccessMessage(options.successMessage); - }, function(httpResponse) { - growl.addErrorMessage(options.errorMessage); - }).$promise; - }; - - $scope.saveDescription = function() { - Events.record(currentUser, 'edit_description', 'query', $scope.query.id); - $scope.saveQuery(undefined, {'description': $scope.query.description}); - }; - - $scope.saveName = function() { - Events.record(currentUser, 'edit_name', 'query', $scope.query.id); - $scope.saveQuery(undefined, {'name': $scope.query.name}); - }; - - $scope.togglePublished = function() { - Events.record(currentUser, 'toggle_published', 'query', $scope.query.id); - $scope.query.is_draft = !$scope.query.is_draft; - $scope.saveQuery(undefined, {'is_draft': $scope.query.is_draft}); - }; - - $scope.executeQuery = function() { - if (!$scope.canExecuteQuery()) { - return; - } - - if (!$scope.query.query) { - return; - } - - getQueryResult(0); - $scope.lockButton(true); - $scope.cancelling = false; - Events.record(currentUser, 'execute', 'query', $scope.query.id); - - notifications.getPermissions(); - }; - - $scope.cancelExecution = function() { - $scope.cancelling = true; - $scope.queryResult.cancelExecution(); - Events.record(currentUser, 'cancel_execute', 'query', $scope.query.id); - }; - - $scope.archiveQuery = function(options, data) { - if (data) { - data.id = $scope.query.id; - } else { - data = $scope.query; - } - - $scope.isDirty = false; - - options = _.extend({}, { - successMessage: 'Query archived', - errorMessage: 'Query could not be archived' - }, options); - - return Query.delete({id: data.id}, function() { - $scope.query.is_archived = true; - $scope.query.schedule = null; - growl.addSuccessMessage(options.successMessage); - // This feels dirty. - $('#archive-confirmation-modal').modal('hide'); - }, function(httpResponse) { - growl.addErrorMessage(options.errorMessage); - }).$promise; - } - - $scope.updateDataSource = function() { - Events.record(currentUser, 'update_data_source', 'query', $scope.query.id); - localStorage.lastSelectedDataSourceId = $scope.query.data_source_id; - - $scope.query.latest_query_data = null; - $scope.query.latest_query_data_id = null; - - if ($scope.query.id) { - Query.save({ - 'id': $scope.query.id, - 'data_source_id': $scope.query.data_source_id, - 'latest_query_data_id': null - }); - } - - updateSchema(); - $scope.dataSource = _.find($scope.dataSources, function(ds) { return ds.id == $scope.query.data_source_id; }); - $scope.executeQuery(); - }; - - $scope.setVisualizationTab = function (visualization) { - $scope.selectedTab = visualization.id; - $location.hash(visualization.id); - }; - - $scope.$watch('query.name', function() { - $scope.$parent.pageTitle = $scope.query.name; - }); - - $scope.$watch('queryResult && queryResult.getData()', function(data, oldData) { - if (!data) { - return; - } - - $scope.filters = $scope.queryResult.getFilters(); - }); - - $scope.$watch("queryResult && queryResult.getStatus()", function(status) { - if (!status) { - return; - } - - if (status == 'done') { - $scope.query.latest_query_data_id = $scope.queryResult.getId(); - $scope.query.queryResult = $scope.queryResult; - - notifications.showNotification("Re:dash", $scope.query.name + " updated."); - } else if (status == 'failed') { - notifications.showNotification("Re:dash", $scope.query.name + " failed to run: " + $scope.queryResult.getError()); - } - - if (status === 'done' || status === 'failed') { - $scope.lockButton(false); - } - - if ($scope.queryResult.getLog() != null) { - $scope.showLog = true; - } - }); - - $scope.openVisualizationEditor = function(visualization) { - function openModal() { - $modal.open({ - templateUrl: '/views/directives/visualization_editor.html', - windowClass:'modal-xl', - scope: $scope, - controller: ['$scope', '$modalInstance', function($scope, $modalInstance) { - $scope.modalInstance = $modalInstance; - $scope.visualization = visualization; - $scope.close = function() { - $modalInstance.close(); - } - }] - }); - } - - if ($scope.query.isNew()) { - $scope.saveQuery().then(function(query) { - // Because we have a path change, we need to "signal" the next page to open the visualization editor. - $location.path(query.getSourceLink()).hash('add'); - }); - } else { - openModal(); - } - }; - - if ($location.hash() === 'add') { - $location.hash(null); - $scope.openVisualizationEditor(); - } - - $scope.openScheduleForm = function() { - if (!$scope.isQueryOwner || !$scope.canScheduleQuery) { - return; - }; - - $modal.open({ - templateUrl: '/views/schedule_form.html', - size: 'sm', - scope: $scope, - controller: ['$scope', '$modalInstance', function($scope, $modalInstance) { - $scope.close = function() { - $modalInstance.close(); - } - if ($scope.query.hasDailySchedule()) { - $scope.refreshType = 'daily'; - } else { - $scope.refreshType = 'periodic'; - } - }] - }); - }; - - $scope.showEmbedDialog = function(query, visualization) { - $modal.open({ - templateUrl: '/views/dialogs/embed_code.html', - controller: ['$scope', '$modalInstance', function($scope, $modalInstance) { - $scope.close = function() { - $modalInstance.close(); - } - $scope.embedUrl = basePath + 'embed/query/' + query.id + '/visualization/' + visualization.id + '?api_key=' + query.api_key; - if (window.snapshotUrlBuilder) { - $scope.snapshotUrl = snapshotUrlBuilder(query, visualization); - } - }] - }) - } - - $scope.$watch(function() { - return $location.hash() - }, function(hash) { - if (hash == 'pivot') { - Events.record(currentUser, 'pivot', 'query', $scope.query && $scope.query.id); - } - $scope.selectedTab = hash || DEFAULT_TAB; - }); - - $scope.showManagePermissionsModal = function() { - // Create scope for share permissions dialog and pass api path to it - var scope = $scope.$new(); - $scope.apiAccess = 'api/queries/' + $routeParams.queryId + '/acl'; - - $modal.open({ - scope: scope, - templateUrl: '/views/dialogs/manage_permissions.html', - controller: 'ManagePermissionsCtrl' - }) - }; - }; - angular.module('redash.controllers') - .controller('QueryViewCtrl', ['$scope', 'Events', '$route', '$routeParams', '$http', '$location', 'notifications', 'growl', '$modal', 'Query', 'DataSource', 'User', QueryViewCtrl]); -})(); diff --git a/rd_ui/app/scripts/controllers/snippets.js b/rd_ui/app/scripts/controllers/snippets.js deleted file mode 100644 index e146e9bef2..0000000000 --- a/rd_ui/app/scripts/controllers/snippets.js +++ /dev/null @@ -1,93 +0,0 @@ -(function() { - var SnippetsCtrl = function ($scope, $location, growl, Events, QuerySnippet) { - Events.record(currentUser, "view", "page", "query_snippets"); - $scope.$parent.pageTitle = "Query Snippets"; - - $scope.gridConfig = { - isPaginationEnabled: true, - itemsByPage: 20, - maxSize: 8, - }; - - $scope.gridColumns = [ - { - "label": "Trigger", - "cellTemplate": '{{dataRow.trigger}}' - }, - { - "label": "Description", - "map": "description" - }, - { - "label": "Snippet", - "map": "snippet" - }, - { - 'label': 'Created By', - 'map': 'user.name' - }, - { - 'label': 'Updated At', - 'cellTemplate': '' - } - ]; - - $scope.snippets = []; - QuerySnippet.query(function(snippets) { - $scope.snippets = snippets; - }); - }; - - var SnippetCtrl = function ($scope, $routeParams, $http, $location, growl, Events, QuerySnippet) { - $scope.$parent.pageTitle = "Query Snippets"; - $scope.snippetId = $routeParams.snippetId; - Events.record(currentUser, "view", "query_snippet", $scope.snippetId); - - $scope.editorOptions = { - mode: 'snippets', - advanced: { - behavioursEnabled: true, - enableSnippets: false, - autoScrollEditorIntoView: true, - }, - onLoad: function(editor) { - editor.$blockScrolling = Infinity; - editor.getSession().setUseWrapMode(true); - editor.setShowPrintMargin(false); - } - }; - - $scope.saveChanges = function() { - $scope.snippet.$save(function(snippet) { - growl.addSuccessMessage("Saved."); - if ($scope.snippetId === "new") { - $location.path('/query_snippets/' + snippet.id).replace(); - } - }, function() { - growl.addErrorMessage("Failed saving snippet."); - }); - } - - $scope.delete = function() { - $scope.snippet.$delete(function() { - $location.path('/query_snippets'); - growl.addSuccessMessage("Query snippet deleted."); - }, function() { - growl.addErrorMessage("Failed deleting query snippet."); - }); - } - - if ($scope.snippetId == 'new') { - $scope.snippet = new QuerySnippet({description: ""}); - $scope.canEdit = true; - } else { - $scope.snippet = QuerySnippet.get({id: $scope.snippetId}, function(snippet) { - $scope.canEdit = currentUser.canEdit(snippet); - }); - } - }; - - angular.module('redash.controllers') - .controller('SnippetsCtrl', ['$scope', '$location', 'growl', 'Events', 'QuerySnippet', SnippetsCtrl]) - .controller('SnippetCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'QuerySnippet', SnippetCtrl]) -})(); diff --git a/rd_ui/app/scripts/controllers/users.js b/rd_ui/app/scripts/controllers/users.js deleted file mode 100644 index 0e76509027..0000000000 --- a/rd_ui/app/scripts/controllers/users.js +++ /dev/null @@ -1,353 +0,0 @@ -(function () { - var GroupsCtrl = function ($scope, $location, $modal, growl, Events, Group) { - Events.record(currentUser, "view", "page", "groups"); - $scope.$parent.pageTitle = "Groups"; - - $scope.gridConfig = { - isPaginationEnabled: true, - itemsByPage: 20, - maxSize: 8, - }; - - $scope.gridColumns = [ - { - "label": "Name", - "map": "name", - "cellTemplate": '{{dataRow.name}}' - } - ]; - - $scope.groups = []; - Group.query(function(groups) { - $scope.groups = groups; - }); - - $scope.newGroup = function() { - $modal.open({ - templateUrl: '/views/groups/edit_group_form.html', - size: 'sm', - resolve: { - group: function() { return new Group({}); } - }, - controller: ['$scope', '$modalInstance', 'group', function($scope, $modalInstance, group) { - $scope.group = group; - var newGroup = group.id === undefined; - - if (newGroup) { - $scope.saveButtonText = "Create"; - $scope.title = "Create a New Group"; - } else { - $scope.saveButtonText = "Save"; - $scope.title = "Edit Group"; - } - - $scope.ok = function() { - $scope.group.$save(function(group) { - if (newGroup) { - $location.path('/groups/' + group.id).replace(); - $modalInstance.close(); - } else { - $modalInstance.close(); - } - }); - } - - $scope.cancel = function() { - $modalInstance.close(); - } - }] - }); - } - }; - - var groupName = function ($location, growl) { - return { - restrict: 'E', - scope: { - 'group': '=' - }, - transclude: true, - template: - '

'+ - ' ' + - '' + - '

', - replace: true, - controller: ['$scope', function ($scope) { - $scope.canEdit = function() { - return currentUser.isAdmin && $scope.group.type != 'builtin'; - }; - - $scope.saveName = function() { - $scope.group.$save(); - }; - - $scope.deleteGroup = function() { - if (confirm("Are you sure you want to delete this group?")) { - $scope.group.$delete(function() { - $location.path('/groups').replace(); - growl.addSuccessMessage("Group deleted successfully."); - }) - } - } - }] - } - }; - - var GroupDataSourcesCtrl = function($scope, $routeParams, $http, $location, growl, Events, Group, DataSource) { - Events.record(currentUser, "view", "group_data_sources", $scope.groupId); - $scope.group = Group.get({id: $routeParams.groupId}); - $scope.dataSources = Group.dataSources({id: $routeParams.groupId}); - $scope.newDataSource = {}; - - $scope.findDataSource = function(search) { - if ($scope.foundDataSources === undefined) { - DataSource.query(function(dataSources) { - var existingIds = _.map($scope.dataSources, function(m) { return m.id; }); - $scope.foundDataSources = _.filter(dataSources, function(ds) { return !_.contains(existingIds, ds.id); }); - }); - } - }; - - $scope.addDataSource = function(dataSource) { - // Clear selection, to clear up the input control. - $scope.newDataSource.selected = undefined; - - $http.post('api/groups/' + $routeParams.groupId + '/data_sources', {'data_source_id': dataSource.id}).success(function(user) { - dataSource.view_only = false; - $scope.dataSources.unshift(dataSource); - - if ($scope.foundDataSources) { - $scope.foundDataSources = _.filter($scope.foundDataSources, function(ds) { return ds != dataSource; }); - } - }); - }; - - $scope.changePermission = function(dataSource, viewOnly) { - $http.post('api/groups/' + $routeParams.groupId + '/data_sources/' + dataSource.id, {view_only: viewOnly}).success(function() { - dataSource.view_only = viewOnly; - }); - }; - - $scope.removeDataSource = function(dataSource) { - $http.delete('api/groups/' + $routeParams.groupId + '/data_sources/' + dataSource.id).success(function() { - $scope.dataSources = _.filter($scope.dataSources, function(ds) { return dataSource != ds; }); - }); - }; - } - - var GroupCtrl = function($scope, $routeParams, $http, $location, growl, Events, Group, User) { - Events.record(currentUser, "view", "group", $scope.groupId); - $scope.group = Group.get({id: $routeParams.groupId}); - $scope.members = Group.members({id: $routeParams.groupId}); - $scope.newMember = {}; - - $scope.findUser = function(search) { - if (search == "") { - return; - } - - if ($scope.foundUsers === undefined) { - User.query(function(users) { - var existingIds = _.map($scope.members, function(m) { return m.id; }); - _.each(users, function(user) { user.alreadyMember = _.contains(existingIds, user.id); }); - $scope.foundUsers = users; - }); - } - }; - - $scope.addMember = function(user) { - // Clear selection, to clear up the input control. - $scope.newMember.selected = undefined; - - $http.post('api/groups/' + $routeParams.groupId + '/members', {'user_id': user.id}).success(function() { - $scope.members.unshift(user); - user.alreadyMember = true; - }); - }; - - $scope.removeMember = function(member) { - $http.delete('api/groups/' + $routeParams.groupId + '/members/' + member.id).success(function() { - $scope.members = _.filter($scope.members, function(m) { return m != member }); - - if ($scope.foundUsers) { - _.each($scope.foundUsers, function(user) { if (user.id == member.id) { user.alreadyMember = false }; }); - } - }); - }; - } - - var UsersCtrl = function ($scope, $location, growl, Events, User) { - Events.record(currentUser, "view", "page", "users"); - $scope.$parent.pageTitle = "Users"; - - $scope.gridConfig = { - isPaginationEnabled: true, - itemsByPage: 20, - maxSize: 8, - }; - - $scope.gridColumns = [ - { - "label": "Name", - "map": "name", - "cellTemplate": ' {{dataRow.name}}' - }, - { - 'label': 'Joined', - 'cellTemplate': '' - } - ]; - - $scope.users = []; - User.query(function(users) { - $scope.users = users; - }); - }; - - var UserCtrl = function ($scope, $routeParams, $http, $location, growl, Events, User) { - $scope.$parent.pageTitle = "Users"; - - $scope.userId = $routeParams.userId; - - if ($scope.userId === 'me') { - $scope.userId = currentUser.id; - } - Events.record(currentUser, "view", "user", $scope.userId); - $scope.canEdit = currentUser.hasPermission("admin") || currentUser.id === parseInt($scope.userId); - $scope.showSettings = false; - $scope.showPasswordSettings = false; - - $scope.selectTab = function(tab) { - $scope.selectedTab = tab; - _.each($scope.tabs, function(v, k) { - $scope.tabs[k] = (k === tab); - }); - }; - - $scope.setTab = function(tab) { - $scope.selectedTab = tab; - $location.hash(tab); - } - - $scope.tabs = { - profile: false, - apiKey: false, - settings: false, - password: false - }; - - $scope.selectTab($location.hash() || 'profile'); - - $scope.user = User.get({id: $scope.userId}, function(user) { - if (user.auth_type == 'password') { - $scope.showSettings = $scope.canEdit; - $scope.showPasswordSettings = $scope.canEdit; - } - }); - - $scope.password = { - current: '', - new: '', - newRepeat: '' - }; - - $scope.savePassword = function(form) { - $scope.$broadcast('show-errors-check-validity'); - - if (!form.$valid) { - return; - } - - var data = { - id: $scope.user.id, - password: $scope.password.new, - old_password: $scope.password.current - }; - - User.save(data, function() { - growl.addSuccessMessage("Password Saved.") - $scope.password = { - current: '', - new: '', - newRepeat: '' - }; - }, function(error) { - var message = error.data.message || "Failed saving password."; - growl.addErrorMessage(message); - }); - }; - - $scope.updateUser = function(form) { - $scope.$broadcast('show-errors-check-validity'); - - if (!form.$valid) { - return; - } - - var data = { - id: $scope.user.id, - name: $scope.user.name, - email: $scope.user.email - }; - - User.save(data, function(user) { - growl.addSuccessMessage("Saved.") - $scope.user = user; - }, function(error) { - var message = error.data.message || "Failed saving."; - growl.addErrorMessage(message); - }); - }; - - $scope.sendPasswordReset = function() { - $scope.disablePasswordResetButton = true; - $http.post('api/users/' + $scope.user.id + '/reset_password').success(function(data) { - $scope.disablePasswordResetButton = false; - $scope.passwordResetLink = data.reset_link; - }); - }; - }; - - var NewUserCtrl = function ($scope, $location, growl, Events, User) { - Events.record(currentUser, "view", "page", "users/new"); - }; - - var newUserForm = function (growl, User) { - return { - restrict: 'E', - scope: {}, - templateUrl: '/views/users/new_user_form.html', - replace: true, - link: function ($scope) { - $scope.user = new User({}); - $scope.saveUser = function() { - $scope.$broadcast('show-errors-check-validity'); - - if (!$scope.userForm.$valid) { - return; - } - - $scope.user.$save(function(user) { - $scope.user = user; - $scope.user.created = true; - growl.addSuccessMessage("Saved.") - }, function(error) { - var message = error.data.message || "Failed saving."; - growl.addErrorMessage(message); - }); - } - } - } - }; - - angular.module('redash.controllers') - .controller('GroupsCtrl', ['$scope', '$location', '$modal', 'growl', 'Events', 'Group', GroupsCtrl]) - .directive('groupName', ['$location', 'growl', groupName]) - .directive('newUserForm', ['growl', 'User', newUserForm]) - .controller('GroupCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'Group', 'User', GroupCtrl]) - .controller('GroupDataSourcesCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'Group', 'DataSource', GroupDataSourcesCtrl]) - .controller('UsersCtrl', ['$scope', '$location', 'growl', 'Events', 'User', UsersCtrl]) - .controller('UserCtrl', ['$scope', '$routeParams', '$http', '$location', 'growl', 'Events', 'User', UserCtrl]) - .controller('NewUserCtrl', ['$scope', '$location', 'growl', 'Events', 'User', NewUserCtrl]) -})(); diff --git a/rd_ui/app/scripts/directives/dashboard_directives.js b/rd_ui/app/scripts/directives/dashboard_directives.js deleted file mode 100644 index 4399358056..0000000000 --- a/rd_ui/app/scripts/directives/dashboard_directives.js +++ /dev/null @@ -1,232 +0,0 @@ -(function() { - 'use strict' - - var directives = angular.module('redash.directives'); - - directives.directive('editDashboardForm', ['Events', '$http', '$location', '$timeout', 'Dashboard', 'growl', - function(Events, $http, $location, $timeout, Dashboard, growl) { - return { - restrict: 'E', - scope: { - dashboard: '=' - }, - templateUrl: '/views/edit_dashboard.html', - replace: true, - link: function($scope, element, attrs) { - var gridster = element.find(".gridster ul").gridster({ - widget_margins: [5, 5], - widget_base_dimensions: [260, 100], - min_cols: 2, - max_cols: 2, - serialize_params: function($w, wgd) { - return { - col: wgd.col, - row: wgd.row, - id: $w.data('widget-id') - } - } - }).data('gridster'); - - var gsItemTemplate = '
  • ' + - '
    {name}' + - '
  • '; - - $scope.$watch('dashboard.layout', function() { - $timeout(function() { - gridster.remove_all_widgets(); - - if ($scope.dashboard.widgets && $scope.dashboard.widgets.length) { - var layout = []; - - _.each($scope.dashboard.widgets, function(row, rowIndex) { - _.each(row, function(widget, colIndex) { - layout.push({ - id: widget.id, - col: colIndex + 1, - row: rowIndex + 1, - ySize: 1, - xSize: widget.width, - name: widget.getName()//visualization.query.name - }); - }); - }); - - _.each(layout, function(item) { - var el = gsItemTemplate.replace('{id}', item.id).replace('{name}', item.name); - gridster.add_widget(el, item.xSize, item.ySize, item.col, item.row); - }); - } - }); - }, true); - - $scope.saveDashboard = function() { - $scope.saveInProgress = true; - // TODO: we should use the dashboard service here. - if ($scope.dashboard.id) { - var positions = $(element).find('.gridster ul').data('gridster').serialize(); - var layout = []; - _.each(_.sortBy(positions, function(pos) { - return pos.row * 10 + pos.col; - }), function(pos) { - var row = pos.row - 1; - var col = pos.col - 1; - layout[row] = layout[row] || []; - if (col > 0 && layout[row][col - 1] == undefined) { - layout[row][col - 1] = pos.id; - } else { - layout[row][col] = pos.id; - } - - }); - $scope.dashboard.layout = layout; - - layout = JSON.stringify(layout); - Dashboard.save({slug: $scope.dashboard.id, name: $scope.dashboard.name, - version: $scope.dashboard.version, layout: layout}, function(dashboard) { - $scope.dashboard = dashboard; - $scope.saveInProgress = false; - $(element).modal('hide'); - }, function(error) { - $scope.saveInProgress = false; - if(error.status == 403) { - growl.addErrorMessage("Unable to save dashboard: Permission denied."); - } else if(error.status == 409) { - growl.addErrorMessage('It seems like the dashboard has been modified by another user. ' + - 'Please copy/backup your changes and reload this page.', {ttl: -1}); - } - }); - Events.record(currentUser, 'edit', 'dashboard', $scope.dashboard.id); - } else { - - $http.post('api/dashboards', { - 'name': $scope.dashboard.name - }).success(function(response) { - $(element).modal('hide'); - $scope.dashboard = { - 'name': null, - 'layout': null - }; - $scope.saveInProgress = false; - $location.path('/dashboard/' + response.slug).replace(); - }); - Events.record(currentUser, 'create', 'dashboard'); - } - } - - } - } - } - ]); - - directives.directive('newWidgetForm', ['Query', 'Widget', 'growl', - function(Query, Widget, growl) { - return { - restrict: 'E', - scope: { - dashboard: '=' - }, - templateUrl: '/views/new_widget_form.html', - replace: true, - link: function($scope, element, attrs) { - $scope.widgetSizes = [{ - name: 'Regular', - value: 1 - }, { - name: 'Double', - value: 2 - }]; - - $scope.type = 'visualization'; - - $scope.isVisualization = function () { - return $scope.type == 'visualization'; - }; - - $scope.isTextBox = function () { - return $scope.type == 'textbox'; - }; - - $scope.setType = function (type) { - $scope.type = type; - if (type == 'textbox') { - $scope.widgetSizes.push({name: 'Hidden', value: 0}); - } else if ($scope.widgetSizes.length > 2) { - $scope.widgetSizes.pop(); - } - }; - - var reset = function() { - $scope.saveInProgress = false; - $scope.widgetSize = 1; - $scope.selectedVis = null; - $scope.query = {}; - $scope.selected_query = undefined; - $scope.text = ""; - }; - - reset(); - - $scope.loadVisualizations = function () { - if (!$scope.query.selected) { - return; - } - - Query.get({ id: $scope.query.selected.id }, function(query) { - if (query) { - $scope.selected_query = query; - if (query.visualizations.length) { - $scope.selectedVis = query.visualizations[0]; - } - } - }); - }; - - $scope.searchQueries = function (term) { - if (!term || term.length < 3) { - return; - } - - Query.search({q: term}, function(results) { - $scope.queries = results; - }); - }; - - $scope.$watch('query', function () { - $scope.loadVisualizations(); - }, true); - - $scope.saveWidget = function() { - $scope.saveInProgress = true; - var widget = new Widget({ - 'visualization_id': $scope.selectedVis && $scope.selectedVis.id, - 'dashboard_id': $scope.dashboard.id, - 'options': {}, - 'width': $scope.widgetSize, - 'text': $scope.text - }); - - widget.$save().then(function(response) { - // update dashboard layout - $scope.dashboard.layout = response['layout']; - $scope.dashboard.version = response['version']; - var newWidget = new Widget(response['widget']); - if (response['new_row']) { - $scope.dashboard.widgets.push([newWidget]); - } else { - $scope.dashboard.widgets[$scope.dashboard.widgets.length - 1].push(newWidget); - } - - // close the dialog - $('#add_query_dialog').modal('hide'); - reset(); - }).catch(function() { - growl.addErrorMessage("Widget can not be added"); - }).finally(function() { - $scope.saveInProgress = false; - }); - } - } - } - } - ]) -})(); diff --git a/rd_ui/app/scripts/directives/directives.js b/rd_ui/app/scripts/directives/directives.js deleted file mode 100644 index d8f530041b..0000000000 --- a/rd_ui/app/scripts/directives/directives.js +++ /dev/null @@ -1,625 +0,0 @@ -(function () { - 'use strict'; - - var directives = angular.module('redash.directives', []); - - directives.directive('appHeader', ['$location', 'Dashboard', 'notifications', function ($location, Dashboard) { - return { - restrict: 'E', - replace: true, - templateUrl: '/views/app_header.html', - link: function ($scope) { - $scope.dashboards = []; - $scope.logoUrl = clientConfig.logoUrl; - $scope.reloadDashboards = function () { - Dashboard.query(function (dashboards) { - $scope.dashboards = _.sortBy(dashboards, "name"); - $scope.allDashboards = _.groupBy($scope.dashboards, function (d) { - var parts = d.name.split(":"); - if (parts.length == 1) { - return "Other"; - } - return parts[0]; - }); - $scope.otherDashboards = $scope.allDashboards['Other'] || []; - $scope.groupedDashboards = _.omit($scope.allDashboards, 'Other'); - }); - }; - - $scope.searchQueries = function() { - $location.path('/queries/search').search({q: $scope.term}); - }; - - $scope.reloadDashboards(); - - $scope.currentUser = currentUser; - } - } - }]); - - directives.directive('alertUnsavedChanges', ['$window', function ($window) { - return { - restrict: 'E', - replace: true, - scope: { - 'isDirty': '=' - }, - link: function ($scope) { - var - - unloadMessage = "You will lose your changes if you leave", - confirmMessage = unloadMessage + "\n\nAre you sure you want to leave this page?", - - // store original handler (if any) - _onbeforeunload = $window.onbeforeunload; - - $window.onbeforeunload = function () { - return $scope.isDirty ? unloadMessage : null; - } - - $scope.$on('$locationChangeStart', function (event, next, current) { - if (next.split("?")[0] == current.split("?")[0] || next.split("#")[0] == current.split("#")[0]) { - return; - } - - if ($scope.isDirty && !confirm(confirmMessage)) { - event.preventDefault(); - } - }); - - $scope.$on('$destroy', function () { - $window.onbeforeunload = _onbeforeunload; - }); - } - } - }]); - - directives.directive('hashLink', ['$location', function($location) { - return { - restrict: 'A', - scope: { - 'hash': '@' - }, - link: function (scope, element) { - var basePath = $location.path().substring(1); - element[0].href = basePath + "#" + scope.hash; - } - }; - }]); - - directives.directive('rdTab', ['$location', function ($location) { - return { - restrict: 'E', - scope: { - 'tabId': '@', - 'name': '@', - 'basePath': '=?' - }, - transclude: true, - template: '
  • {{name}}
  • ', - replace: true, - link: function (scope) { - scope.basePath = scope.basePath || $location.path().substring(1); - scope.$watch(function () { - return scope.$parent.selectedTab - }, function (tab) { - scope.selectedTab = tab; - }); - } - } - }]); - - directives.directive('emailSettingsWarning', function() { - return { - restrict: 'E', - template: '

    It looks like your mail server isn\'t configured. Make sure to configure it for the {{function}} to work.

    ', - link: function(scope, elements, attrs) { - scope.showMailWarning = clientConfig.mailSettingsMissing && currentUser.isAdmin; - scope.function = attrs.function; - } - } - }); - - // From: http://jsfiddle.net/joshdmiller/NDFHg/ - directives.directive('editInPlace', function () { - return { - restrict: 'E', - scope: { - value: '=', - ignoreBlanks: '=', - editable: '=', - done: '=', - }, - template: function (tElement, tAttrs) { - var elType = tAttrs.editor || 'input'; - var placeholder = tAttrs.placeholder || 'Click to edit'; - - var viewMode = ''; - - if (tAttrs.markdown == "true") { - viewMode = ''; - } else { - viewMode = ''; - } - - var placeholderSpan = '' + placeholder + ''; - var editor = '<{elType} ng-model="value" class="rd-form-control">'.replace('{elType}', elType); - - return viewMode + placeholderSpan + editor; - }, - link: function ($scope, element, attrs) { - // Let's get a reference to the input element, as we'll want to reference it. - var inputElement = angular.element(element.children()[2]); - - // This directive should have a set class so we can style it. - element.addClass('edit-in-place'); - - // Initially, we're not editing. - $scope.editing = false; - - // ng-click handler to activate edit-in-place - $scope.edit = function () { - $scope.oldValue = $scope.value; - - $scope.editing = true; - - // We control display through a class on the directive itself. See the CSS. - element.addClass('active'); - - // And we must focus the element. - // `angular.element()` provides a chainable array, like jQuery so to access a native DOM function, - // we have to reference the first element in the array. - inputElement[0].focus(); - }; - - function save() { - if ($scope.editing) { - if ($scope.ignoreBlanks && _.isEmpty($scope.value)) { - $scope.value = $scope.oldValue; - } - $scope.editing = false; - element.removeClass('active'); - - if ($scope.value !== $scope.oldValue) { - $scope.done && $scope.done(); - } - } - } - - $(inputElement).keydown(function (e) { - // 'return' or 'enter' key pressed - // allow 'shift' to break lines - if (e.which === 13 && !e.shiftKey) { - save(); - } else if (e.which === 27) { - $scope.value = $scope.oldValue; - $scope.$apply(function () { - $(inputElement[0]).blur(); - }); - } - }).blur(function () { - save(); - }); - } - }; - }); - - // http://stackoverflow.com/a/17904092/1559840 - directives.directive('jsonText', function () { - return { - restrict: 'A', - require: 'ngModel', - link: function (scope, element, attr, ngModel) { - function into(input) { - return JSON.parse(input); - } - - function out(data) { - return JSON.stringify(data, undefined, 2); - } - - ngModel.$parsers.push(into); - ngModel.$formatters.push(out); - - scope.$watch(attr.ngModel, function (newValue) { - element[0].value = out(newValue); - }, true); - } - }; - }); - - directives.directive('rdTimer', [function () { - return { - restrict: 'E', - scope: { timestamp: '=' }, - template: '{{currentTime}}', - controller: ['$scope' , function ($scope) { - $scope.currentTime = "00:00:00"; - - // We're using setInterval directly instead of $timeout, to avoid using $apply, to - // prevent the digest loop being run every second. - var currentTimer = setInterval(function () { - $scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format("HH:mm:ss"); - $scope.$digest(); - }, 1000); - - $scope.$on('$destroy', function () { - if (currentTimer) { - clearInterval(currentTimer); - currentTimer = null; - } - }); - }] - }; - }]); - - directives.directive('rdTimeAgo', function () { - return { - restrict: 'E', - scope: { - value: '=' - }, - template: '' + - '' + - '-' + - '' - } - }); - - // Used instead of autofocus attribute, which doesn't work in Angular as there is no real page load. - directives.directive('autofocus', - ['$timeout', function ($timeout) { - return { - link: function (scope, element) { - $timeout(function () { - element[0].focus(); - }); - } - }; - }] - ); - - directives.directive('compareTo', function () { - return { - require: "ngModel", - scope: { - otherModelValue: "=compareTo" - }, - link: function (scope, element, attributes, ngModel) { - var validate = function(value) { - ngModel.$setValidity("compareTo", value === scope.otherModelValue); - }; - - scope.$watch("otherModelValue", function() { - validate(ngModel.$modelValue); - }); - - ngModel.$parsers.push(function(value) { - validate(value); - return value; - }); - } - }; - }); - - directives.directive('inputErrors', function () { - return { - restrict: "E", - templateUrl: "/views/directives/input_errors.html", - replace: true, - scope: { - errors: "=" - } - }; - }); - - directives.directive('onDestroy', function () { - /* This directive can be used to invoke a callback when an element is destroyed, - A useful example is the following: -
    - -
    - */ - return { - restrict: "A", - scope: { - onDestroy: "&", - }, - link: function(scope, elem, attrs) { - scope.$on('$destroy', function() { - scope.onDestroy(); - }); - } - }; - }); - - directives.directive('colorBox', function () { - return { - restrict: "E", - scope: {color: "="}, - template: "" - }; - }); - - directives.directive('overlay', function() { - return { - restrict: "E", - transclude: true, - template: "" + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' + - '
    ' - } - }); - - directives.directive('dynamicForm', ['$http', 'growl', '$q', function ($http, growl, $q) { - return { - restrict: 'E', - replace: 'true', - transclude: true, - templateUrl: '/views/directives/dynamic_form.html', - scope: { - 'target': '=', - 'type': '@type', - 'actions': '=' - }, - link: function ($scope) { - var setType = function(types) { - if ($scope.target.type === undefined) { - $scope.target.type = types[0].type; - return types[0]; - } - - $scope.type = _.find(types, function (t) { - return t.type == $scope.target.type; - }); - }; - - $scope.inProgressActions = {}; - _.each($scope.actions, function(action) { - var originalCallback = action.callback; - var name = action.name; - action.callback = function() { - action.name = ' ' + name; - - $scope.inProgressActions[action.name] = true; - function release() { - $scope.inProgressActions[action.name] = false; - action.name = name; - } - originalCallback(release); - } - }); - - $scope.files = {}; - - $scope.$watchCollection('files', function() { - _.each($scope.files, function(v, k) { - // THis is needed because angular-base64-upload sets the value to null at initialization, causing the field - // to be marked as dirty even if it wasn't changed. - if (!v && $scope.target.options[k]) { - $scope.dataSourceForm.$setPristine(); - } - if (v) { - $scope.target.options[k] = v.base64; - } - }); - }); - - var typesPromise = $http.get('api/' + $scope.type + '/types'); - - $q.all([typesPromise, $scope.target.$promise]).then(function(responses) { - var types = responses[0].data; - setType(types); - - $scope.types = types; - - _.each(types, function (type) { - _.each(type.configuration_schema.properties, function (prop, name) { - if (name == 'password' || name == 'passwd') { - prop.type = 'password'; - } - - if (_.string.endsWith(name, "File")) { - prop.type = 'file'; - } - - if (prop.type == 'boolean') { - prop.type = 'checkbox'; - } - - prop.required = _.contains(type.configuration_schema.required, name); - }); - }); - }); - - $scope.$watch('target.type', function(current, prev) { - if (prev !== current) { - if (prev !== undefined) { - $scope.target.options = {}; - } - setType($scope.types); - } - }); - - $scope.saveChanges = function() { - $scope.target.$save(function() { - growl.addSuccessMessage("Saved."); - $scope.dataSourceForm.$setPristine() - }, function() { - growl.addErrorMessage("Failed saving."); - }); - } - } - } - }]); - - directives.directive('pageHeader', function() { - return { - restrict: 'E', - transclude: true, - templateUrl: '/views/directives/page_header.html', - link: function(scope, elem, attrs) { - attrs.$observe('title', function(value){ - scope.title = value; - }); - } - } - }); - - directives.directive('settingsScreen', ['$location', function($location) { - return { - restrict: 'E', - transclude: true, - templateUrl: '/views/directives/settings_screen.html', - controller: ['$scope', function(scope) { - scope.usersPage = _.string.startsWith($location.path(), '/users'); - scope.groupsPage = _.string.startsWith($location.path(), '/groups'); - scope.dsPage = _.string.startsWith($location.path(), '/data_sources'); - scope.destinationsPage = _.string.startsWith($location.path(), '/destinations'); - scope.snippetsPage = _.string.startsWith($location.path(), '/query_snippets'); - - scope.showGroupsLink = currentUser.hasPermission('list_users'); - scope.showUsersLink = currentUser.hasPermission('list_users'); - scope.showDsLink = currentUser.hasPermission('admin'); - scope.showDestinationsLink = currentUser.hasPermission('admin'); - }] - } - }]); - - directives.directive('tabNav', ['$location', function($location) { - return { - restrict: 'E', - transclude: true, - scope: { - tabs: '=' - }, - template: '', - link: function($scope) { - _.each($scope.tabs, function(tab) { - if (tab.isActive) { - tab.active = tab.isActive($location.path()); - } else { - tab.active = _.string.startsWith($location.path(), "/" + tab.path); - } - }); - } - } - }]); - - directives.directive('queriesList', [function () { - return { - restrict: 'E', - replace: true, - scope: { - queries: '=', - total: '=', - selectPage: '=', - page: '=', - pageSize: '=' - }, - templateUrl: '/views/directives/queries_list.html', - link: function ($scope) { - function hasNext() { - return !($scope.page * $scope.pageSize >= $scope.total); - } - - function hasPrevious() { - return $scope.page !== 1; - } - - function updatePages() { - if ($scope.total === undefined) { - return; - } - - var maxSize = 5; - var pageCount = Math.ceil($scope.total/$scope.pageSize); - var pages = []; - - function makePage(title, page, disabled) { - return {title: title, page: page, active: page == $scope.page, disabled: disabled}; - } - - // Default page limits - var startPage = 1, endPage = pageCount; - - // recompute if maxSize - if (maxSize && maxSize < pageCount) { - startPage = Math.max($scope.page - Math.floor(maxSize / 2), 1); - endPage = startPage + maxSize - 1; - - // Adjust if limit is exceeded - if (endPage > pageCount) { - endPage = pageCount; - startPage = endPage - maxSize + 1; - } - } - - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, false); - pages.push(page); - } - - // Add previous & next links - var previousPage = makePage('<', $scope.page - 1, !hasPrevious()); - pages.unshift(previousPage); - - var nextPage = makePage('>', $scope.page + 1, !hasNext()); - pages.push(nextPage); - - $scope.pages = pages; - } - - $scope.$watch('total', updatePages); - $scope.$watch('page', updatePages); - } - } - }]); - - - directives.directive('parameters', ['$location', '$modal', function($location, $modal) { - return { - restrict: 'E', - transclude: true, - scope: { - 'parameters': '=', - 'syncValues': '=?', - 'editable': '=?' - }, - templateUrl: '/views/directives/parameters.html', - link: function(scope, elem, attrs) { - // is this the correct location for this logic? - if (scope.syncValues !== false) { - scope.$watch('parameters', function() { - _.each(scope.parameters, function(param) { - if (param.value !== null || param.value !== '') { - $location.search('p_' + param.name, param.value); - } - }) - }, true); - } - - scope.showParameterSettings = function(param) { - $modal.open({ - templateUrl: '/views/dialogs/parameter_settings.html', - controller: ['$scope', '$modalInstance', function($scope, $modalInstance) { - $scope.close = function() { - $modalInstance.close(); - }; - $scope.parameter = param; - }] - }) - } - } - } - }]); - -})(); diff --git a/rd_ui/app/scripts/directives/plotly.js b/rd_ui/app/scripts/directives/plotly.js deleted file mode 100644 index aa1e96e532..0000000000 --- a/rd_ui/app/scripts/directives/plotly.js +++ /dev/null @@ -1,391 +0,0 @@ -(function () { - 'use strict'; - - // The following colors will be used if you pick "Automatic" color. - var BaseColors = { - 'Blue': '#4572A7', - 'Red': '#AA4643', - 'Green': '#89A54E', - 'Purple': '#80699B', - 'Cyan': '#3D96AE', - 'Orange': '#DB843D', - 'Light Blue': '#92A8CD', - 'Lilac': '#A47D7C', - 'Light Green': '#B5CA92', - 'Brown': '#A52A2A', - 'Black': '#000000', - 'Gray': '#808080', - 'Pink': '#FFC0CB', - 'Dark Blue': '#00008b' - } - - // Additional colors for the user to choose from: - var ColorPalette = _.extend({}, BaseColors, { - 'Indian Red': '#F8766D', - 'Green 2': '#53B400', - 'Green 3': '#00C094', - 'DarkTurquoise': '#00B6EB', - 'Dark Violet': '#A58AFF', - 'Pink 2' : '#FB61D7' - }); - - var ColorPaletteArray = _.values(BaseColors); - - var fillXValues = function(seriesList) { - var xValues = _.sortBy(_.union.apply(_, _.pluck(seriesList, 'x')), _.identity); - _.each(seriesList, function(series) { - series.x = _.sortBy(series.x, _.identity); - - _.each(xValues, function(value, index) { - if (series.x[index] !== value) { - series.x.splice(index, 0, value); - series.y.splice(index, 0, null); - } - }); - }); - }; - - var storeOriginalHeightForEachSeries = function(seriesList) { - _.each(seriesList, function(series) { - if(!_.has(series,'visible')){ - series.visible = true; - series.original_y = series.y.slice(); - } - }); - }; - - var getEnabledSeries = function(seriesList){ - return _.filter(seriesList, function(series) { - return series.visible === true; - }); - }; - - var initializeTextAndHover = function(seriesList){ - _.each(seriesList, function(series) { - series.text = []; - series.hoverinfo = 'text+name'; - }); - }; - - var normalAreaStacking = function(seriesList) { - fillXValues(seriesList); - storeOriginalHeightForEachSeries(seriesList); - initializeTextAndHover(seriesList); - seriesList = getEnabledSeries(seriesList); - - _.each(seriesList, function(series, seriesIndex, list){ - _.each(series.y, function(undefined, yIndex, undefined2){ - var cumulativeHeightOfPreviousSeries = seriesIndex > 0 ? list[seriesIndex-1].y[yIndex] : 0; - var cumulativeHeightWithThisSeries = cumulativeHeightOfPreviousSeries + series.original_y[yIndex]; - series.y[yIndex] = cumulativeHeightWithThisSeries; - series.text.push('Value: ' + series.original_y[yIndex] + '
    Sum: ' + cumulativeHeightWithThisSeries); - }); - }); - }; - - var lastVisibleY = function(seriesList, lastSeriesIndex, yIndex){ - for(; lastSeriesIndex >= 0; lastSeriesIndex--){ - if(seriesList[lastSeriesIndex].visible === true){ - return seriesList[lastSeriesIndex].y[yIndex]; - } - } - return 0; - } - - var percentAreaStacking = function(seriesList) { - if (seriesList.length === 0) { - return; - } - fillXValues(seriesList); - storeOriginalHeightForEachSeries(seriesList); - initializeTextAndHover(seriesList); - - _.each(seriesList[0].y, function(seriesY, yIndex, undefined){ - - var sumOfCorrespondingDataPoints = _.reduce(seriesList, function(total, series){ - return total + series.original_y[yIndex]; - }, 0); - - _.each(seriesList, function(series, seriesIndex, list){ - var percentage = (series.original_y[yIndex] / sumOfCorrespondingDataPoints ) * 100; - var previousVisiblePercentage = lastVisibleY(seriesList, seriesIndex-1, yIndex); - series.y[yIndex] = percentage + previousVisiblePercentage; - series.text.push('Value: ' + series.original_y[yIndex] + '
    Relative: ' + percentage.toFixed(2) + '%'); - }); - }); - }; - - var percentBarStacking = function(seriesList) { - if (seriesList.length === 0) { - return; - } - fillXValues(seriesList); - initializeTextAndHover(seriesList); - for (var i = 0; i < seriesList[0].y.length; i++) { - var sum = 0; - for(var j = 0; j < seriesList.length; j++) { - sum += seriesList[j].y[i]; - } - for(var j = 0; j < seriesList.length; j++) { - var value = seriesList[j].y[i] / sum * 100; - seriesList[j].text.push('Value: ' + seriesList[j].y[i] + '
    Relative: ' + value.toFixed(2) + '%'); - seriesList[j].y[i] = value; - } - } - } - - var normalizeValue = function(value) { - if (moment.isMoment(value)) { - return value.format("YYYY-MM-DD HH:mm:ss"); - } - return value; - } - - function seriesMinValue(series) { - return _.min(_.map(series, function(s) { return _.min(series.y) })); - } - - function seriesMaxValue(series) { - return _.max(_.map(series, function(s) { return _.max(series.y) })); - } - - function leftAxisSeries(series) { - return _.filter(series, function(s) { return s.yaxis !== 'y2' }); - } - - function rightAxisSeries(series) { - return _.filter(series, function(s) { return s.yaxis === 'y2' }); - } - - angular.module('plotly', []) - .constant('ColorPalette', ColorPalette) - .directive('plotlyChart', function () { - var bottomMargin = 50; - return { - restrict: 'E', - template: '
    ', - scope: { - options: "=", - series: "=", - height: "=" - }, - link: function (scope, element) { - var getScaleType = function(scale) { - if (scale === 'datetime') { - return 'date'; - } - if (scale === 'logarithmic') { - return 'log'; - } - return scale; - }; - - var setType = function(series, type) { - if (type === 'column') { - series.type = 'bar'; - } else if (type === 'line') { - series.mode = 'lines'; - } else if (type === 'area') { - series.fill = scope.options.series.stacking === null ? 'tozeroy' : 'tonexty'; - series.mode = 'lines'; - } else if (type === 'scatter') { - series.type = 'scatter'; - series.mode = 'markers'; - } - }; - - var getColor = function(index) { - return ColorPaletteArray[index % ColorPaletteArray.length]; - }; - - var calculateHeight = function() { - var height = Math.max(scope.height, (scope.height - 50) + bottomMargin); - return height; - } - - var recalculateOptions = function() { - scope.data.length = 0; - scope.layout.showlegend = _.has(scope.options, 'legend') ? scope.options.legend.enabled : true; - if(_.has(scope.options, 'bottomMargin')) { - bottomMargin = parseInt(scope.options.bottomMargin); - scope.layout.margin.b = bottomMargin; - } - delete scope.layout.barmode; - delete scope.layout.xaxis; - delete scope.layout.yaxis; - delete scope.layout.yaxis2; - - if (scope.options.globalSeriesType === 'pie') { - var hasX = _.contains(_.values(scope.options.columnMapping), 'x'); - var rows = scope.series.length > 2 ? 2 : 1; - var cellsInRow = Math.ceil(scope.series.length / rows); - var cellWidth = 1 / cellsInRow; - var cellHeight = 1 / rows; - var xPadding = 0.02; - var yPadding = 0.05; - _.each(scope.series, function(series, index) { - var xPosition = (index % cellsInRow) * cellWidth; - var yPosition = Math.floor(index / cellsInRow) * cellHeight; - var plotlySeries = {values: [], labels: [], type: 'pie', hole: .4, - marker: {colors: ColorPaletteArray}, - text: series.name, textposition: 'inside', name: series.name, - domain: {x: [xPosition, xPosition + cellWidth - xPadding], - y: [yPosition, yPosition + cellHeight - yPadding]}}; - _.each(series.data, function(row, index) { - plotlySeries.values.push(row.y); - plotlySeries.labels.push(hasX ? row.x : 'Slice ' + index); - }); - scope.data.push(plotlySeries); - }); - return; - } - - var hasY2 = false; - var sortX = scope.options.sortX === true || scope.options.sortX === undefined; - var useUnifiedXaxis = sortX && scope.options.xAxis.type === 'category'; - - var unifiedX = null; - if (useUnifiedXaxis) { - unifiedX = _.sortBy(_.union.apply(_, _.map(scope.series, function(s) { return _.pluck(s.data, 'x'); })), _.identity); - } - - _.each(scope.series, function(series, index) { - var seriesOptions = scope.options.seriesOptions[series.name] || {type: scope.options.globalSeriesType}; - var plotlySeries = {x: [], - y: [], - name: seriesOptions.name || series.name, - marker: {color: seriesOptions.color ? seriesOptions.color : getColor(index)}}; - - if (seriesOptions.yAxis === 1 && (scope.options.series.stacking === null || seriesOptions.type === 'line')) { - hasY2 = true; - plotlySeries.yaxis = 'y2'; - } - - setType(plotlySeries, seriesOptions.type); - var data = series.data; - if (sortX) { - data = _.sortBy(data, 'x'); - } - - if (useUnifiedXaxis && index === 0) { - var values = {}; - _.each(data, function(row) { - values[row.x] = row.y; - }); - - _.each(unifiedX, function(x) { - plotlySeries.x.push(normalizeValue(x)); - plotlySeries.y.push(normalizeValue(values[x] || null)); - }); - } else { - _.each(data, function(row) { - plotlySeries.x.push(normalizeValue(row.x)); - plotlySeries.y.push(normalizeValue(row.y)); - }); - } - - scope.data.push(plotlySeries); - }); - - var getTitle = function(axis) { - if (angular.isDefined(axis) && angular.isDefined(axis.title)) { - return axis.title.text; - } - return null; - }; - - - scope.layout.xaxis = {title: getTitle(scope.options.xAxis), - type: getScaleType(scope.options.xAxis.type)}; - if (angular.isDefined(scope.options.xAxis.labels)) { - scope.layout.xaxis.showticklabels = scope.options.xAxis.labels.enabled; - } - if (angular.isArray(scope.options.yAxis)) { - scope.layout.yaxis = {title: getTitle(scope.options.yAxis[0]), - type: getScaleType(scope.options.yAxis[0].type)}; - - if (angular.isNumber(scope.options.yAxis[0].rangeMin) || angular.isNumber(scope.options.yAxis[0].rangeMax)) { - var min = scope.options.yAxis[0].rangeMin || Math.min(0, seriesMinValue(leftAxisSeries(scope.data))); - var max = scope.options.yAxis[0].rangeMax || seriesMaxValue(leftAxisSeries(scope.data)); - - scope.layout.yaxis.range = [min, max]; - } - } - if (hasY2 && angular.isDefined(scope.options.yAxis)) { - scope.layout.yaxis2 = {title: getTitle(scope.options.yAxis[1]), - type: getScaleType(scope.options.yAxis[1].type), - overlaying: 'y', - side: 'right'}; - - if (angular.isNumber(scope.options.yAxis[1].rangeMin) || angular.isNumber(scope.options.yAxis[1].rangeMax)) { - var min = scope.options.yAxis[1].rangeMin || Math.min(0, seriesMinValue(rightAxisSeries(scope.data))); - var max = scope.options.yAxis[1].rangeMax || seriesMaxValue(rightAxisSeries(scope.data)); - - scope.layout.yaxis2.range = [min, max]; - } - } else { - delete scope.layout.yaxis2; - } - - if (scope.options.series.stacking === 'normal') { - scope.layout.barmode = 'stack'; - if (scope.options.globalSeriesType === 'area') { - normalAreaStacking(scope.data); - } - } else if (scope.options.series.stacking === 'percent') { - scope.layout.barmode = 'stack'; - if (scope.options.globalSeriesType === 'area') { - percentAreaStacking(scope.data); - } else if (scope.options.globalSeriesType === 'column') { - percentBarStacking(scope.data); - } - } - - scope.layout.margin.b = bottomMargin; - scope.layout.height = calculateHeight(); - }; - - scope.$watch('series', recalculateOptions); - scope.$watch('options', recalculateOptions, true); - - scope.layout = {margin: {l: 50, r: 50, b: bottomMargin, t: 20, pad: 4}, height: calculateHeight(), autosize: true, hovermode: 'closest'}; - scope.plotlyOptions = {showLink: false, displaylogo: false}; - scope.data = []; - - var element = element[0].children[0]; - Plotly.newPlot(element, scope.data, scope.layout, scope.plotlyOptions); - - element.on('plotly_afterplot', function(d) { - if(scope.options.globalSeriesType === 'area' && (scope.options.series.stacking === 'normal' || scope.options.series.stacking === 'percent')){ - $(element).find(".legendtoggle").each(function(i, rectDiv) { - d3.select(rectDiv).on('click', function () { - var maxIndex = scope.data.length - 1; - var itemClicked = scope.data[maxIndex - i]; - - itemClicked.visible = (itemClicked.visible === true) ? 'legendonly' : true; - if (scope.options.series.stacking === 'normal') { - normalAreaStacking(scope.data); - } else if (scope.options.series.stacking === 'percent') { - percentAreaStacking(scope.data); - } - Plotly.redraw(element); - }); - }); - } - }); - scope.$watch('layout', function (layout, old) { - if (angular.equals(layout, old)) { - return; - } - Plotly.relayout(element, layout); - }, true); - - scope.$watch('data', function (data, old) { - if (!_.isEmpty(data)) { - Plotly.redraw(element); - } - }, true); - } - }; - }); -})(); diff --git a/rd_ui/app/scripts/directives/query_directives.js b/rd_ui/app/scripts/directives/query_directives.js deleted file mode 100644 index 03e93f26a9..0000000000 --- a/rd_ui/app/scripts/directives/query_directives.js +++ /dev/null @@ -1,363 +0,0 @@ -(function() { - 'use strict' - - function queryLink() { - return { - restrict: 'E', - scope: { - 'query': '=', - 'visualization': '=?' - }, - template: '{{query.name}}', - link: function(scope, element) { - var hash = null; - if (scope.visualization) { - if (scope.visualization.type === 'TABLE') { - // link to hard-coded table tab instead of the (hidden) visualization tab - hash = 'table'; - } else { - hash = scope.visualization.id; - } - } - scope.link = scope.query.getUrl(false, hash); - } - } - } - - function querySourceLink($location) { - return { - restrict: 'E', - template: '\ - Show Source\ - \ - Hide Source\ - \ - ' - } - } - - function queryResultLink() { - return { - restrict: 'A', - link: function (scope, element, attrs) { - - var fileType = attrs.fileType ? attrs.fileType : "csv"; - scope.$watch('queryResult && queryResult.getData()', function(data) { - if (!data) { - return; - } - - if (scope.queryResult.getId() == null) { - element.attr('href', ''); - } else { - element.attr('href', 'api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.' + fileType + (scope.embed ? '?api_key=' + scope.apiKey : '')); - element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + "." + fileType); - } - }); - } - } - } - - // By default Ace will try to load snippet files for the different modes and fail. We don't need them, so we use these - // placeholders until we define our own. - function defineDummySnippets(mode) { - ace.define("ace/snippets/" + mode, ["require", "exports", "module"], function(require, exports, module) { - "use strict"; - - exports.snippetText = ""; - exports.scope = mode; - }); - }; - - defineDummySnippets("python"); - defineDummySnippets("sql"); - defineDummySnippets("json"); - - function queryEditor(QuerySnippet) { - return { - restrict: 'E', - scope: { - 'query': '=', - 'lock': '=', - 'schema': '=', - 'syntax': '=' - }, - template: '
    ', - link: { - pre: function ($scope, element) { - $scope.syntax = $scope.syntax || 'sql'; - - $scope.editorOptions = { - mode: 'json', - require: ['ace/ext/language_tools'], - advanced: { - behavioursEnabled: true, - enableSnippets: true, - enableBasicAutocompletion: true, - enableLiveAutocompletion: true, - autoScrollEditorIntoView: true, - }, - onLoad: function(editor) { - QuerySnippet.query(function(snippets) { - var snippetManager = ace.require("ace/snippets").snippetManager; - var m = { - snippetText: '' - }; - m.snippets = snippetManager.parseSnippetFile(m.snippetText); - _.each(snippets, function(snippet) { - m.snippets.push(snippet.getSnippet()); - }); - - snippetManager.register(m.snippets || [], m.scope); - }); - - editor.$blockScrolling = Infinity; - editor.getSession().setUseWrapMode(true); - editor.setShowPrintMargin(false); - - $scope.$watch('syntax', function(syntax) { - var newMode = 'ace/mode/' + syntax; - editor.getSession().setMode(newMode); - }); - - $scope.$watch('schema', function(newSchema, oldSchema) { - if (newSchema !== oldSchema) { - var tokensCount = _.reduce(newSchema, function(totalLength, table) { return totalLength + table.columns.length }, 0); - // If there are too many tokens we disable live autocomplete, as it makes typing slower. - if (tokensCount > 5000) { - editor.setOption('enableLiveAutocompletion', false); - } else { - editor.setOption('enableLiveAutocompletion', true); - } - } - - }); - - $scope.$parent.$on("angular-resizable.resizing", function (event, args) { - editor.resize(); - }); - - editor.focus(); - } - }; - - var langTools = ace.require("ace/ext/language_tools"); - - var schemaCompleter = { - getCompletions: function(state, session, pos, prefix, callback) { - if (prefix.length === 0 || !$scope.schema) { - callback(null, []); - return; - } - - if (!$scope.schema.keywords) { - var keywords = {}; - - _.each($scope.schema, function (table) { - keywords[table.name] = 'Table'; - - _.each(table.columns, function (c) { - keywords[c] = 'Column'; - keywords[table.name + "." + c] = 'Column'; - }); - }); - - $scope.schema.keywords = _.map(keywords, function(v, k) { - return { - name: k, - value: k, - score: 0, - meta: v - }; - }); - } - callback(null, $scope.schema.keywords); - } - }; - - langTools.addCompleter(schemaCompleter); - } - } - }; - } - - function queryFormatter($http, growl) { - return { - restrict: 'E', - // don't create new scope to avoid ui-codemirror bug - // seehttps://github.com/angular-ui/ui-codemirror/pull/37 - scope: false, - template: '', - link: function($scope) { - $scope.formatQuery = function formatQuery() { - if ($scope.dataSource.syntax == 'json') { - try { - $scope.query.query = JSON.stringify(JSON.parse($scope.query.query), ' ', 4); - } catch(err) { - growl.addErrorMessage(err); - } - } else if ($scope.dataSource.syntax =='sql') { - - $scope.queryFormatting = true; - $http.post('api/queries/format', { - 'query': $scope.query.query - }).success(function (response) { - $scope.query.query = response; - }).finally(function () { - $scope.queryFormatting = false; - }); - } else { - growl.addInfoMessage("Query formatting is not supported for your data source syntax."); - } - }; - } - } - } - - function schemaBrowser() { - return { - restrict: 'E', - scope: { - schema: '=' - }, - templateUrl: '/views/directives/schema_browser.html', - link: function ($scope) { - $scope.showTable = function(table) { - table.collapsed = !table.collapsed; - $scope.$broadcast('vsRepeatTrigger'); - } - - $scope.getSize = function(table) { - var size = 18; - - if (!table.collapsed) { - size += 18 * table.columns.length; - } - - return size; - } - } - } - } - - function queryTimePicker() { - return { - restrict: 'E', - template: ' :\ - ', - link: function($scope) { - var padWithZeros = function(size, v) { - v = String(v); - if (v.length < size) { - v = "0" + v; - } - return v; - }; - - $scope.hourOptions = _.map(_.range(0, 24), _.partial(padWithZeros, 2)); - $scope.minuteOptions = _.map(_.range(0, 60, 5), _.partial(padWithZeros, 2)); - - if ($scope.query.hasDailySchedule()) { - var parts = $scope.query.scheduleInLocalTime().split(':'); - $scope.minute = parts[1]; - $scope.hour = parts[0]; - } else { - $scope.minute = "15"; - $scope.hour = "00"; - } - - $scope.updateSchedule = function() { - var newSchedule = moment().hour($scope.hour).minute($scope.minute).utc().format('HH:mm'); - if (newSchedule != $scope.query.schedule) { - $scope.query.schedule = newSchedule; - $scope.saveQuery(); - } - }; - - $scope.$watch('refreshType', function() { - if ($scope.refreshType == 'daily') { - $scope.updateSchedule(); - } - }); - } - } - } - - function queryRefreshSelect() { - return { - restrict: 'E', - template: '\ - \ - ', - link: function($scope) { - $scope.refreshOptions = [ - { - value: "60", - name: 'Every minute' - } - ]; - - _.each([5, 10, 15, 30], function(i) { - $scope.refreshOptions.push({ - value: String(i*60), - name: "Every " + i + " minutes" - }) - }); - - _.each(_.range(1, 13), function (i) { - $scope.refreshOptions.push({ - value: String(i * 3600), - name: 'Every ' + i + 'h' - }); - }) - - $scope.refreshOptions.push({ - value: String(24 * 3600), - name: 'Every 24h' - }); - $scope.refreshOptions.push({ - value: String(7 * 24 * 3600), - name: 'Every 7 days' - }); - $scope.refreshOptions.push({ - value: String(14 * 24 * 3600), - name: 'Every 14 days' - }); - $scope.refreshOptions.push({ - value: String(30 * 24 * 3600), - name: 'Every 30 days' - }); - - $scope.$watch('refreshType', function() { - if ($scope.refreshType == 'periodic') { - if ($scope.query.hasDailySchedule()) { - $scope.query.schedule = null; - $scope.saveQuery(); - } - } - }); - } - - } - } - - angular.module('redash.directives') - .directive('queryLink', queryLink) - .directive('querySourceLink', ['$location', querySourceLink]) - .directive('queryResultLink', queryResultLink) - .directive('queryEditor', ['QuerySnippet', queryEditor]) - .directive('queryRefreshSelect', queryRefreshSelect) - .directive('queryTimePicker', queryTimePicker) - .directive('schemaBrowser', schemaBrowser) - .directive('queryFormatter', ['$http', 'growl', queryFormatter]); -})(); diff --git a/rd_ui/app/scripts/embed.js b/rd_ui/app/scripts/embed.js deleted file mode 100644 index 0fc71b8c20..0000000000 --- a/rd_ui/app/scripts/embed.js +++ /dev/null @@ -1,56 +0,0 @@ -angular.module('redash', [ - 'redash.directives', - 'redash.admin_controllers', - 'redash.controllers', - 'redash.filters', - 'redash.services', - 'redash.visualization', - 'plotly', - 'angular-growl', - 'angularMoment', - 'ui.bootstrap', - 'ui.sortable', - 'smartTable.table', - 'ngResource', - 'ngRoute', - 'ui.select', - 'naif.base64', - 'ui.bootstrap.showErrors', - 'ngSanitize' - ]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider', 'uiSelectConfig', - function ($routeProvider, $locationProvider, $compileProvider, growlProvider, uiSelectConfig) { - function getQuery(Query, $route) { - var query = Query.get({'id': $route.current.params.queryId }); - return query.$promise; - }; - - uiSelectConfig.theme = "bootstrap"; - - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/); - $locationProvider.html5Mode(true); - growlProvider.globalTimeToLive(2000); - - $routeProvider.when('/embed/query/:queryId/visualization/:visualizationId', { - templateUrl: '/views/visualization-embed.html', - controller: 'EmbedCtrl', - reloadOnSearch: false - }); - $routeProvider.otherwise({ - redirectTo: '/embed' - }); - - - } - ]) - .controller('EmbedCtrl', ['$scope', function ($scope) {} ]) - .controller('EmbeddedVisualizationCtrl', ['$scope', '$location', 'Query', 'QueryResult', - function ($scope, $location, Query, QueryResult) { - $scope.showQueryDescription = $location.search()['showDescription']; - $scope.embed = true; - $scope.visualization = visualization; - $scope.query = visualization.query; - $scope.apiKey = $location.search()['api_key']; - query = new Query(visualization.query); - $scope.queryResult = new QueryResult({query_result: query_result}); - }]) - ; diff --git a/rd_ui/app/scripts/filters.js b/rd_ui/app/scripts/filters.js deleted file mode 100644 index 41ad9ba51a..0000000000 --- a/rd_ui/app/scripts/filters.js +++ /dev/null @@ -1,133 +0,0 @@ -var durationHumanize = function (duration) { - var humanized = ""; - if (duration == undefined) { - humanized = "-"; - } else if (duration < 60) { - humanized = Math.round(duration) + "s"; - } else if (duration > 3600 * 24) { - var days = Math.round(parseFloat(duration) / 60.0 / 60.0 / 24.0); - humanized = days + "days"; - } else if (duration >= 3600) { - var hours = Math.round(parseFloat(duration) / 60.0 / 60.0); - humanized = hours + "h"; - } else { - var minutes = Math.round(parseFloat(duration) / 60.0); - humanized = minutes + "m"; - } - return humanized; -}; - -var urlPattern = /(^|[\s\n]|)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi; - -angular.module('redash.filters', []). - filter('durationHumanize', function () { - return durationHumanize; - }) - - .filter('scheduleHumanize', function() { - return function (schedule) { - if (schedule === null) { - return "Never"; - } else if (schedule.match(/\d\d:\d\d/) !== null) { - var parts = schedule.split(':'); - var localTime = moment.utc().hour(parts[0]).minute(parts[1]).local().format('HH:mm'); - return "Every day at " + localTime; - } - - return "Every " + durationHumanize(parseInt(schedule)); - } - }) - - .filter('toHuman', function () { - return function (text) { - return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, function (a) { - return a.toUpperCase(); - }); - } - }) - - .filter('colWidth', function () { - return function (widgetWidth) { - if (widgetWidth === 0) { - return 0; - } else if (widgetWidth === 1) { - return 6; - } else if (widgetWidth === 2) { - return 12; - } - return widgetWidth; - }; - }) - - .filter('capitalize', function () { - return function (text) { - if (text) { - return _.str.capitalize(text); - } else { - return null; - } - - } - }) - - .filter('dateTime', function() { - return function(value) { - if (!value) { - return '-'; - } - return moment(value).format(clientConfig.dateTimeFormat); - } - }) - - .filter('linkify', function () { - return function (text) { - return text.replace(urlPattern, "$1$2"); - }; - }) - - .filter('markdown', ['$sce', function ($sce) { - return function (text) { - if (!text) { - return ""; - } - - var html = marked(text); - if (clientConfig.allowScriptsInUserInput) { - html = $sce.trustAsHtml(html); - } - - return html; - } - }]) - - .filter('trustAsHtml', ['$sce', function ($sce) { - return function (text) { - if (!text) { - return ""; - } - return $sce.trustAsHtml(text); - } - }]) - - .filter('remove', function() { - return function(items, item) { - if (items == undefined) - return items; - if (item instanceof Array) { - var notEquals = function(other) { return item.indexOf(other) == -1; } - } else { - var notEquals = function(other) { return item != other; } - } - var filtered = []; - for (var i = 0; i < items.length; i++) - if (notEquals(items[i])) - filtered.push(items[i]) - return filtered; - }; - }) - - .filter('notEmpty', function() { - return function(collection) { - return !_.isEmpty(collection); - } - }); diff --git a/rd_ui/app/scripts/ng_smart_table.js b/rd_ui/app/scripts/ng_smart_table.js deleted file mode 100644 index bd3951022b..0000000000 --- a/rd_ui/app/scripts/ng_smart_table.js +++ /dev/null @@ -1,977 +0,0 @@ -/* Column module */ - -function getNestedValue (obj, keys) { - if (keys.length == 1) { - return obj[keys[0]]; - } - return getNestedValue(obj[keys[0]], keys.splice(1)); -} - -function getKeyFromObject(obj, key) { - var value = obj[key]; - - if ((!_.has(obj, key) && _.string.include(key, '.'))) { - var keys = key.split("."); - - value = getNestedValue(obj, keys); - } - - return value; -} - -(function (global, angular) { - "use strict"; - - var smartTableColumnModule = angular.module('smartTable.column', ['smartTable.templateUrlList']).constant('DefaultColumnConfiguration', { - isSortable: true, - isEditable: false, - type: 'text', - - - //it is useless to have that empty strings, but it reminds what is available - headerTemplateUrl: '', - map: '', - label: '', - sortPredicate: '', - formatFunction: '', - formatParameter: '', - filterPredicate: undefined, - cellTemplateUrl: '', - headerClass: '', - cellClass: '' - }); - - function ColumnProvider(DefaultColumnConfiguration, templateUrlList) { - - function Column(config) { - if (!(this instanceof Column)) { - return new Column(config); - } - angular.extend(this, config); - } - - this.setDefaultOption = function (option) { - angular.extend(Column.prototype, option); - }; - - DefaultColumnConfiguration.headerTemplateUrl = templateUrlList.defaultHeader; - this.setDefaultOption(DefaultColumnConfiguration); - - this.$get = function () { - return Column; - }; - } - - ColumnProvider.$inject = ['DefaultColumnConfiguration', 'templateUrlList']; - smartTableColumnModule.provider('Column', ColumnProvider); - - //make it global so it can be tested - global.ColumnProvider = ColumnProvider; -})(window, angular); - - -/* Directives */ -(function (angular) { - "use strict"; - - angular.module('smartTable.directives', ['smartTable.templateUrlList', 'smartTable.templates']) - .directive('smartTable', ['templateUrlList', 'DefaultTableConfiguration', function (templateList, defaultConfig) { - return { - restrict: 'EA', - scope: { - columnCollection: '=columns', - dataCollection: '=rows', - config: '=' - }, - replace: 'true', - templateUrl: templateList.smartTable, - controller: 'TableCtrl', - link: function (scope, element, attr, ctrl) { - - var templateObject; - - scope.$watch('config', function (config) { - var newConfig = angular.extend({}, defaultConfig, config), - length = scope.columns !== undefined ? scope.columns.length : 0; - - ctrl.setGlobalConfig(newConfig); - - //remove the checkbox column if needed - if (newConfig.selectionMode !== 'multiple' || newConfig.displaySelectionCheckbox !== true) { - for (var i = length - 1; i >= 0; i--) { - if (scope.columns[i].isSelectionColumn === true) { - ctrl.removeColumn(i); - } - } - } else { - //add selection box column if required - ctrl.insertColumn({ - cellTemplateUrl: templateList.selectionCheckbox, - headerTemplateUrl: templateList.selectAllCheckbox, - isSelectionColumn: true - }, 0); - } - }, true); - - //insert columns from column config - //TODO add a way to clean all columns - scope.$watchCollection('columnCollection', function (oldValue, newValue) { - if (scope.columnCollection) { - scope.columns.length = 0; - for (var i = 0, l = scope.columnCollection.length; i < l; i++) { - - ctrl.insertColumn(scope.columnCollection[i]); - } - } else { - //or guess data Structure - if (scope.dataCollection && scope.dataCollection.length > 0) { - templateObject = scope.dataCollection[0]; - angular.forEach(templateObject, function (value, key) { - if (key[0] != '$') { - ctrl.insertColumn({label: key, map: key}); - } - }); - } - } - }, true); - - //if item are added or removed into the data model from outside the grid - scope.$watch('dataCollection', function (oldValue, newValue) { - // evme: - // reset sorting when data updates (executing query again) - if (newValue) { - ctrl.resetSort(); - } - }); - - } - }; - }]) - //just to be able to select the row - .directive('smartTableDataRow', function () { - - return { - require: '^smartTable', - restrict: 'C', - link: function (scope, element, attr, ctrl) { - - element.bind('click', function () { - scope.$apply(function () { - ctrl.toggleSelection(scope.dataRow); - }) - }); - } - }; - }) - //header cell with sorting functionality or put a checkbox if this column is a selection column - .directive('smartTableHeaderCell', function () { - return { - restrict: 'C', - require: '^smartTable', - link: function (scope, element, attr, ctrl) { - element.bind('click', function () { - scope.$apply(function () { - ctrl.sortBy(scope.column); - }); - }) - } - }; - }).directive('smartTableSelectAll', function () { - return { - restrict: 'C', - require: '^smartTable', - link: function (scope, element, attr, ctrl) { - element.bind('click', function (event) { - ctrl.toggleSelectionAll(element[0].checked === true); - }) - } - }; - }) - //credit to Valentyn shybanov : http://stackoverflow.com/questions/14544741/angularjs-directive-to-stoppropagation - .directive('stopEvent', function () { - return { - restrict: 'A', - link: function (scope, element, attr) { - element.bind(attr.stopEvent, function (e) { - e.stopPropagation(); - }); - } - } - }) - //the global filter - .directive('smartTableGlobalSearch', ['templateUrlList', function (templateList) { - return { - restrict: 'C', - require: '^smartTable', - scope: { - columnSpan: '@' - }, - templateUrl: templateList.smartTableGlobalSearch, - replace: false, - link: function (scope, element, attr, ctrl) { - - scope.searchValue = undefined; - - scope.$watch('searchValue', function (value) { - //todo perf improvement only filter on blur ? - ctrl.search(value); - }); - } - } - }]) - //a customisable cell (see templateUrl) and editable - //TODO check with the ng-include strategy - .directive('smartTableDataCell', ['$filter', '$http', '$templateCache', '$compile', '$parse', '$sanitize', function (filter, http, templateCache, compile, parse, sanitize) { - return { - restrict: 'C', - link: function (scope, element) { - var - column = scope.column, - row = scope.dataRow, - format = filter('format'), - childScope; - - var value = getKeyFromObject(row, column.map); - - //can be useful for child directives - scope.formatedValue = format(value, column.formatFunction, column.formatParameter); - - function defaultContent() { - //clear content - if (column.isEditable) { - element.html('
    '); - compile(element.contents())(scope); - } else if (column.cellTemplate) { - //create a scope - childScope = scope.$new(); - //compile the element with its new content and new scope - element.html(column.cellTemplate); - compile(element.contents())(childScope); - } else { - if (typeof scope.formatedValue === 'string' || scope.formatedValue instanceof String) { - element.html(sanitize(scope.formatedValue)); - } else { - element.text(scope.formatedValue); - } - - } - } - - scope.$watch('column.cellTemplateUrl', function (value) { - - if (value) { - //we have to load the template (and cache it) : a kind of ngInclude - http.get(value, {cache: templateCache}).success(function (response) { - - //create a scope - childScope = scope.$new(); - //compile the element with its new content and new scope - element.html(response); - compile(element.contents())(childScope); - }).error(defaultContent); - - } else { - defaultContent(); - } - }); - } - }; - }]) - //directive that allows type to be bound in input - .directive('inputType', function () { - return { - restrict: 'A', - priority: 1, - link: function (scope, ielement, iattr) { - //force the type to be set before inputDirective is called - var type = scope.$eval(iattr.type); - iattr.$set('type', type); - } - }; - }) - //an editable content in the context of a cell (see row, column) - .directive('editableCell', ['templateUrlList', '$parse', function (templateList, parse) { - return { - restrict: 'EA', - require: '^smartTable', - templateUrl: templateList.editableCell, - scope: { - row: '=', - column: '=', - type: '=' - }, - replace: true, - link: function (scope, element, attrs, ctrl) { - var form = angular.element(element.children()[1]), - input = angular.element(form.children()[0]); - - //init values - scope.isEditMode = false; - scope.value = scope.row[scope.column.map]; - - - scope.submit = function () { - //update model if valid - if (scope.myForm.$valid === true) { - ctrl.updateDataRow(scope.row, scope.column.map, scope.value); - ctrl.sortBy();//it will trigger the refresh... (ie it will sort, filter, etc with the new value) - } - scope.toggleEditMode(); - }; - - scope.toggleEditMode = function () { - scope.value = scope.row[scope.column.map]; - scope.isEditMode = scope.isEditMode !== true; - }; - - scope.$watch('isEditMode', function (newValue, oldValue) { - if (newValue) { - input[0].select(); - input[0].focus(); - } - }); - - input.bind('blur', function () { - scope.$apply(function () { - scope.submit(); - }); - }); - } - }; - }]); -})(angular); - -/* Filters */ -(function (angular) { - "use strict"; - angular.module('smartTable.filters', []). - constant('DefaultFilters', ['currency', 'date', 'json', 'lowercase', 'number', 'uppercase']). - filter('format', ['$filter', 'DefaultFilters', function (filter, defaultfilters) { - return function (value, formatFunction, filterParameter) { - - var returnFunction; - - if (formatFunction && angular.isFunction(formatFunction)) { - returnFunction = formatFunction; - } else { - returnFunction = defaultfilters.indexOf(formatFunction) !== -1 ? filter(formatFunction) : function (value) { - return value; - }; - } - return returnFunction(value, filterParameter); - }; - }]); -})(angular); - - -/*table module */ - -(function (angular) { - "use strict"; - angular.module('smartTable.table', ['smartTable.column', 'smartTable.utilities', 'smartTable.directives', 'smartTable.filters', 'ui.bootstrap.pagination.smartTable']) - .constant('DefaultTableConfiguration', { - selectionMode: 'none', - isGlobalSearchActivated: false, - displaySelectionCheckbox: false, - isPaginationEnabled: true, - itemsByPage: 10, - maxSize: 5, - - //just to remind available option - sortAlgorithm: '', - filterAlgorithm: '' - }) - .controller('TableCtrl', ['$scope', 'Column', '$filter', '$parse', 'ArrayUtility', 'DefaultTableConfiguration', function (scope, Column, filter, parse, arrayUtility, defaultConfig) { - - scope.columns = []; - scope.dataCollection = scope.dataCollection || []; - scope.displayedCollection = []; //init empty array so that if pagination is enabled, it does not spoil performances - scope.numberOfPages = calculateNumberOfPages(scope.dataCollection); - scope.currentPage = 1; - scope.holder = {isAllSelected: false}; - - var predicate = {}, - lastColumnSort; - - function isAllSelected() { - var i, - l = scope.displayedCollection.length; - for (i = 0; i < l; i++) { - if (scope.displayedCollection[i].isSelected !== true) { - return false; - } - } - return true; - } - - function calculateNumberOfPages(array) { - - if (!angular.isArray(array)) { - return 1; - } - if (array.length === 0 || scope.itemsByPage < 1) { - return 1; - } - return Math.ceil(array.length / scope.itemsByPage); - } - - function sortDataRow(array, column) { - var sortAlgo = (scope.sortAlgorithm && angular.isFunction(scope.sortAlgorithm)) === true ? scope.sortAlgorithm : filter('orderBy'); - if (column) { - var predicate = function (o) { - return getKeyFromObject(o, column.sortPredicate); - }; - return arrayUtility.sort(array, sortAlgo, predicate, column.reverse); - } else { - return array; - } - } - - function selectDataRow(array, selectionMode, index, select) { - - var dataRow, oldValue; - - if ((!angular.isArray(array)) || (selectionMode !== 'multiple' && selectionMode !== 'single')) { - return; - } - - if (index >= 0 && index < array.length) { - dataRow = array[index]; - if (selectionMode === 'single') { - //unselect all the others - for (var i = 0, l = array.length; i < l; i++) { - oldValue = array[i].isSelected; - array[i].isSelected = false; - if (oldValue === true) { - scope.$emit('selectionChange', {item: array[i]}); - } - } - } - dataRow.isSelected = select; - scope.holder.isAllSelected = isAllSelected(); - scope.$emit('selectionChange', {item: dataRow}); - } - } - - /** - * set the config (config parameters will be available through scope - * @param config - */ - this.setGlobalConfig = function (config) { - angular.extend(scope, defaultConfig, config); - }; - - /** - * change the current page displayed - * @param page - */ - this.changePage = function (page) { - var oldPage = scope.currentPage; - if (angular.isNumber(page.page)) { - scope.currentPage = page.page; - scope.displayedCollection = this.pipe(scope.dataCollection); - scope.holder.isAllSelected = isAllSelected(); - scope.$emit('changePage', {oldValue: oldPage, newValue: scope.currentPage}); - } - }; - - /** - * set column as the column used to sort the data (if it is already the case, it will change the reverse value) - * @method sortBy - * @param column - */ - this.sortBy = function (column) { - var index = scope.columns.indexOf(column); - if (index !== -1) { - if (column.isSortable === true) { - // reset the last column used - if (lastColumnSort && lastColumnSort !== column) { - lastColumnSort.reverse = 'none'; - } - - column.sortPredicate = column.sortPredicate || column.map; - column.reverse = column.reverse !== true; - lastColumnSort = column; - } - } - - scope.displayedCollection = this.pipe(scope.dataCollection); - }; - - /** - * set the filter predicate used for searching - * @param input - * @param column - */ - this.search = function (input, column) { - //update column and global predicate - if (column && scope.columns.indexOf(column) !== -1) { - predicate.$ = ''; - column.filterPredicate = input; - } else { - for (var j = 0, l = scope.columns.length; j < l; j++) { - scope.columns[j].filterPredicate = undefined; - } - predicate.$ = input; - } - - for (var j = 0, l = scope.columns.length; j < l; j++) { - predicate[scope.columns[j].map] = scope.columns[j].filterPredicate; - } - scope.displayedCollection = this.pipe(scope.dataCollection); - - }; - - /** - * combine sort, search and limitTo operations on an array, - * @param array - * @returns Array, an array result of the operations on input array - */ - this.pipe = function (array) { - var filterAlgo = (scope.filterAlgorithm && angular.isFunction(scope.filterAlgorithm)) === true ? scope.filterAlgorithm : filter('filter'), - output; - //filter and sort are commutative - output = sortDataRow(arrayUtility.filter(array, filterAlgo, predicate), lastColumnSort); - scope.numberOfPages = calculateNumberOfPages(output); - return scope.isPaginationEnabled ? arrayUtility.fromTo(output, (scope.currentPage - 1) * scope.itemsByPage, scope.itemsByPage) : output; - }; - - this.resetSort = function () { - lastColumnSort = null; - predicate = {}; - this.sortBy(); - }; - - /*//////////// - Column API - ///////////*/ - - - /** - * insert a new column in scope.collection at index or push at the end if no index - * @param columnConfig column configuration used to instantiate the new Column - * @param index where to insert the column (at the end if not specified) - */ - this.insertColumn = function (columnConfig, index) { - var column = new Column(columnConfig); - arrayUtility.insertAt(scope.columns, index, column); - }; - - /** - * remove the column at columnIndex from scope.columns - * @param columnIndex index of the column to be removed - */ - this.removeColumn = function (columnIndex) { - arrayUtility.removeAt(scope.columns, columnIndex); - }; - - /** - * move column located at oldIndex to the newIndex in scope.columns - * @param oldIndex index of the column before it is moved - * @param newIndex index of the column after the column is moved - */ - this.moveColumn = function (oldIndex, newIndex) { - arrayUtility.moveAt(scope.columns, oldIndex, newIndex); - }; - - - /*/////////// - ROW API - */ - - /** - * select or unselect the item of the displayedCollection with the selection mode set in the scope - * @param dataRow - */ - this.toggleSelection = function (dataRow) { - var index = scope.dataCollection.indexOf(dataRow); - if (index !== -1) { - selectDataRow(scope.dataCollection, scope.selectionMode, index, dataRow.isSelected !== true); - } - }; - - /** - * select/unselect all the currently displayed rows - * @param value if true select, else unselect - */ - this.toggleSelectionAll = function (value) { - var i = 0, - l = scope.displayedCollection.length; - - if (scope.selectionMode !== 'multiple') { - return; - } - for (; i < l; i++) { - selectDataRow(scope.displayedCollection, scope.selectionMode, i, value === true); - } - }; - - /** - * remove the item at index rowIndex from the displayed collection - * @param rowIndex - * @returns {*} item just removed or undefined - */ - this.removeDataRow = function (rowIndex) { - var toRemove = arrayUtility.removeAt(scope.displayedCollection, rowIndex); - arrayUtility.removeAt(scope.dataCollection, scope.dataCollection.indexOf(toRemove)); - }; - - /** - * move an item from oldIndex to newIndex in displayedCollection - * @param oldIndex - * @param newIndex - */ - this.moveDataRow = function (oldIndex, newIndex) { - arrayUtility.moveAt(scope.displayedCollection, oldIndex, newIndex); - }; - - /** - * update the model, it can be a non existing yet property - * @param dataRow the dataRow to update - * @param propertyName the property on the dataRow ojbect to update - * @param newValue the value to set - */ - this.updateDataRow = function (dataRow, propertyName, newValue) { - var index = scope.displayedCollection.indexOf(dataRow), - oldValue; - if (index !== -1) { - oldValue = scope.displayedCollection[index][propertyName]; - if (oldValue !== newValue) { - scope.displayedCollection[index][propertyName] = newValue; - scope.$emit('updateDataRow', {item: scope.displayedCollection[index]}); - } - } - }; - - - }]); - -})(angular); - - -angular.module('smartTable.templates', ['partials/defaultCell.html', 'partials/defaultHeader.html', 'partials/editableCell.html', 'partials/globalSearchCell.html', 'partials/pagination.html', 'partials/selectAllCheckbox.html', 'partials/selectionCheckbox.html', 'partials/smartTable.html']); - -angular.module("partials/defaultCell.html", []).run(["$templateCache", function ($templateCache) { - $templateCache.put("partials/defaultCell.html", - "{{formatedValue}}"); -}]); - -angular.module("partials/defaultHeader.html", []).run(["$templateCache", function ($templateCache) { - $templateCache.put("partials/defaultHeader.html", - "{{column.label}}"); -}]); - -angular.module("partials/editableCell.html", []).run(["$templateCache", function ($templateCache) { - $templateCache.put("partials/editableCell.html", - "
    \n" + - " {{value | format:column.formatFunction:column.formatParameter}}\n" + - "\n" + - "
    \n" + - " \n" + - "
    \n" + - "
    "); -}]); - -angular.module("partials/globalSearchCell.html", []).run(["$templateCache", function ($templateCache) { - $templateCache.put("partials/globalSearchCell.html", - "\n" + - ""); -}]); - -angular.module("partials/pagination.html", []).run(["$templateCache", function ($templateCache) { - $templateCache.put("partials/pagination.html", - "
      \n" + - "
    • {{page.text}}
    • \n" + - "
    "); -}]); - -angular.module("partials/selectAllCheckbox.html", []).run(["$templateCache", function ($templateCache) { - $templateCache.put("partials/selectAllCheckbox.html", - ""); -}]); - -angular.module("partials/selectionCheckbox.html", []).run(["$templateCache", function ($templateCache) { - $templateCache.put("partials/selectionCheckbox.html", - ""); -}]); - -angular.module("partials/smartTable.html", []).run(["$templateCache", function ($templateCache) { - $templateCache.put("partials/smartTable.html", - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
    \n" + - "
    \n" + - "
    \n" + - "\n" + - "\n" + - ""); -}]); - -(function (angular) { - "use strict"; - angular.module('smartTable.templateUrlList', []) - .constant('templateUrlList', { - smartTable: 'partials/smartTable.html', - smartTableGlobalSearch: 'partials/globalSearchCell.html', - editableCell: 'partials/editableCell.html', - selectionCheckbox: 'partials/selectionCheckbox.html', - selectAllCheckbox: 'partials/selectAllCheckbox.html', - defaultHeader: 'partials/defaultHeader.html', - pagination: 'partials/pagination.html' - }); -})(angular); - - -(function (angular) { - "use strict"; - angular.module('smartTable.utilities', []) - - .factory('ArrayUtility', function () { - - /** - * remove the item at index from arrayRef and return the removed item - * @param arrayRef - * @param index - * @returns {*} - */ - var removeAt = function (arrayRef, index) { - if (index >= 0 && index < arrayRef.length) { - return arrayRef.splice(index, 1)[0]; - } - }, - - /** - * insert item in arrayRef at index or a the end if index is wrong - * @param arrayRef - * @param index - * @param item - */ - insertAt = function (arrayRef, index, item) { - if (index >= 0 && index < arrayRef.length) { - arrayRef.splice(index, 0, item); - } else { - arrayRef.push(item); - } - }, - - /** - * move the item at oldIndex to newIndex in arrayRef - * @param arrayRef - * @param oldIndex - * @param newIndex - */ - moveAt = function (arrayRef, oldIndex, newIndex) { - var elementToMove; - if (oldIndex >= 0 && oldIndex < arrayRef.length && newIndex >= 0 && newIndex < arrayRef.length) { - elementToMove = arrayRef.splice(oldIndex, 1)[0]; - arrayRef.splice(newIndex, 0, elementToMove); - } - }, - - /** - * sort arrayRef according to sortAlgorithm following predicate and reverse - * @param arrayRef - * @param sortAlgorithm - * @param predicate - * @param reverse - * @returns {*} - */ - sort = function (arrayRef, sortAlgorithm, predicate, reverse) { - - if (!sortAlgorithm || !angular.isFunction(sortAlgorithm)) { - return arrayRef; - } else { - return sortAlgorithm(arrayRef, predicate, reverse === true);//excpet if reverse is true it will take it as false - } - }, - - /** - * filter arrayRef according with filterAlgorithm and predicate - * @param arrayRef - * @param filterAlgorithm - * @param predicate - * @returns {*} - */ - filter = function (arrayRef, filterAlgorithm, predicate) { - if (!filterAlgorithm || !angular.isFunction(filterAlgorithm)) { - return arrayRef; - } else { - return filterAlgorithm(arrayRef, predicate); - } - }, - - /** - * return an array, part of array ref starting at min and the size of length - * @param arrayRef - * @param min - * @param length - * @returns {*} - */ - fromTo = function (arrayRef, min, length) { - - var out = [], - limit, - start; - - if (!angular.isArray(arrayRef)) { - return arrayRef; - } - - start = Math.max(min, 0); - start = Math.min(start, (arrayRef.length - 1) > 0 ? arrayRef.length - 1 : 0); - - length = Math.max(0, length); - limit = Math.min(start + length, arrayRef.length); - - for (var i = start; i < limit; i++) { - out.push(arrayRef[i]); - } - return out; - }; - - - return { - removeAt: removeAt, - insertAt: insertAt, - moveAt: moveAt, - sort: sort, - filter: filter, - fromTo: fromTo - }; - }); -})(angular); - - -(function (angular) { - angular.module('ui.bootstrap.pagination.smartTable', ['smartTable.templateUrlList']) - - .constant('paginationConfig', { - boundaryLinks: false, - directionLinks: true, - firstText: 'First', - previousText: '<', - nextText: '>', - lastText: 'Last' - }) - - .directive('paginationSmartTable', ['paginationConfig', 'templateUrlList', function (paginationConfig, templateUrlList) { - return { - restrict: 'EA', - require: '^smartTable', - scope: { - numPages: '=', - currentPage: '=', - maxSize: '=' - }, - templateUrl: templateUrlList.pagination, - replace: true, - link: function (scope, element, attrs, ctrl) { - - // Setup configuration parameters - var boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - var directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - var firstText = angular.isDefined(attrs.firstText) ? attrs.firstText : paginationConfig.firstText; - var previousText = angular.isDefined(attrs.previousText) ? attrs.previousText : paginationConfig.previousText; - var nextText = angular.isDefined(attrs.nextText) ? attrs.nextText : paginationConfig.nextText; - var lastText = angular.isDefined(attrs.lastText) ? attrs.lastText : paginationConfig.lastText; - - // Create page object used in template - function makePage(number, text, isActive, isDisabled) { - return { - number: number, - text: text, - active: isActive, - disabled: isDisabled - }; - } - - scope.$watch('numPages + currentPage + maxSize', function () { - scope.pages = []; - - // Default page limits - var startPage = 1, endPage = scope.numPages; - - // recompute if maxSize - if (scope.maxSize && scope.maxSize < scope.numPages) { - startPage = Math.max(scope.currentPage - Math.floor(scope.maxSize / 2), 1); - endPage = startPage + scope.maxSize - 1; - - // Adjust if limit is exceeded - if (endPage > scope.numPages) { - endPage = scope.numPages; - startPage = endPage - scope.maxSize + 1; - } - } - - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, scope.isActive(number), false); - scope.pages.push(page); - } - - // Add previous & next links - if (directionLinks) { - var previousPage = makePage(scope.currentPage - 1, previousText, false, scope.noPrevious()); - scope.pages.unshift(previousPage); - - var nextPage = makePage(scope.currentPage + 1, nextText, false, scope.noNext()); - scope.pages.push(nextPage); - } - - // Add first & last links - if (boundaryLinks) { - var firstPage = makePage(1, firstText, false, scope.noPrevious()); - scope.pages.unshift(firstPage); - - var lastPage = makePage(scope.numPages, lastText, false, scope.noNext()); - scope.pages.push(lastPage); - } - - - if (scope.currentPage > scope.numPages) { - scope.selectPage(scope.numPages); - } - }); - scope.noPrevious = function () { - return scope.currentPage === 1; - }; - scope.noNext = function () { - return scope.currentPage === scope.numPages; - }; - scope.isActive = function (page) { - return scope.currentPage === page; - }; - - scope.selectPage = function (page) { - if (!scope.isActive(page) && page > 0 && page <= scope.numPages) { - scope.currentPage = page; - ctrl.changePage({page: page}); - } - }; - } - }; - }]); -})(angular); - diff --git a/rd_ui/app/scripts/services/dashboards.js b/rd_ui/app/scripts/services/dashboards.js deleted file mode 100644 index 5504eac01b..0000000000 --- a/rd_ui/app/scripts/services/dashboards.js +++ /dev/null @@ -1,42 +0,0 @@ -(function () { - var Dashboard = function($resource, $http, Widget) { - - var transformSingle = function(dashboard) { - dashboard.widgets = _.map(dashboard.widgets, function (row) { - return _.map(row, function (widget) { - return new Widget(widget); - }); - }); - dashboard.publicAccessEnabled = dashboard.public_url !== undefined; - }; - - var transform = $http.defaults.transformResponse.concat(function(data, headers) { - if (_.isArray(data)) { - _.each(data, transformSingle); - } else { - transformSingle(data); - } - return data; - }); - - var resource = $resource('api/dashboards/:slug', {slug: '@slug'}, { - 'get': {method: 'GET', transformResponse: transform}, - 'save': {method: 'POST', transformResponse: transform}, - 'query': {method: 'GET', isArray: true, transformResponse: transform}, - recent: { - method: 'get', - isArray: true, - url: "api/dashboards/recent", - transformResponse: transform - }}); - - resource.prototype.canEdit = function() { - return currentUser.canEdit(this) || this.can_edit; - }; - - return resource; - } - - angular.module('redash.services') - .factory('Dashboard', ['$resource', '$http', 'Widget', Dashboard]) -})(); diff --git a/rd_ui/app/scripts/services/notifications.js b/rd_ui/app/scripts/services/notifications.js deleted file mode 100644 index 9312f81e28..0000000000 --- a/rd_ui/app/scripts/services/notifications.js +++ /dev/null @@ -1,76 +0,0 @@ -(function () { - var notifications = function (Events) { - var notificationService = {pageVisible: true}; - - notificationService.monitorVisibility = function() { - var hidden, visibilityState, visibilityChange; - - if (typeof document.hidden !== "undefined") { - hidden = "hidden", visibilityChange = "visibilitychange", visibilityState = "visibilityState"; - } else if (typeof document.msHidden !== "undefined") { - hidden = "msHidden", visibilityChange = "msvisibilitychange", visibilityState = "msVisibilityState"; - } - - var documentHidden = document[hidden]; - - document.addEventListener(visibilityChange, function () { - if (documentHidden != document[hidden]) { - if (document[hidden]) { - notificationService.pageVisible = false; - } else { - notificationService.pageVisible = true; - } - - documentHidden = document[hidden]; - } - }); - }; - - notificationService.monitorVisibility(); - - notificationService.isSupported = function () { - if ("Notification" in window) { - return true; - } else { - console.log("HTML5 notifications are not supported."); - return false; - } - }; - - notificationService.getPermissions = function () { - if (!this.isSupported()) { - return; - } - - if (Notification.permission === "default") { - Notification.requestPermission(function (status) { - if (Notification.permission !== status) { - Notification.permission = status; - } - }); - } - } - - notificationService.showNotification = function (title, content) { - if (!this.isSupported() || this.pageVisible || Notification.permission !== "granted") { - return; - } - - //using the 'tag' to avoid showing duplicate notifications - var notification = new Notification(title, {'tag': title + content, 'body': content, 'icon': '/images/redash_icon_small.png'}); - setTimeout(function () { - notification.close(); - }, 3000); - notification.onclick = function () { - window.focus(); - this.close(); - Events.record(currentUser, 'click', 'notification'); - }; - } - - return notificationService; - } - - angular.module('redash.services') - .factory('notifications', ['Events', notifications]); -})(); diff --git a/rd_ui/app/scripts/services/resources.js b/rd_ui/app/scripts/services/resources.js deleted file mode 100644 index 7606bdba72..0000000000 --- a/rd_ui/app/scripts/services/resources.js +++ /dev/null @@ -1,823 +0,0 @@ -(function () { - function QueryResultError(errorMessage) { - this.errorMessage = errorMessage; - } - - QueryResultError.prototype.getError = function() { - return this.errorMessage; - }; - - QueryResultError.prototype.getStatus = function() { - return 'failed'; - }; - - QueryResultError.prototype.getData = function() { - return null; - }; - - QueryResultError.prototype.getLog = function() { - return null; - }; - - QueryResultError.prototype.getChartData = function() { - return null; - }; - - var QueryResult = function ($resource, $timeout, $q) { - var QueryResultResource = $resource('api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}}); - var Job = $resource('api/jobs/:id', {id: '@id'}); - - var updateFunction = function (props) { - angular.extend(this, props); - if ('query_result' in props) { - this.status = "done"; - this.filters = undefined; - this.filterFreeze = undefined; - - var columnTypes = {}; - - // TODO: we should stop manipulating incoming data, and switch to relaying on the column type set by the backend. - // This logic is prone to errors, and better be removed. Kept for now, for backward compatability. - _.each(this.query_result.data.rows, function (row) { - _.each(row, function (v, k) { - if (angular.isNumber(v)) { - columnTypes[k] = 'float'; - } else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}T/)) { - row[k] = moment.utc(v); - columnTypes[k] = 'datetime'; - } else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}$/)) { - row[k] = moment.utc(v); - columnTypes[k] = 'date'; - } else if (typeof(v) == 'object' && v !== null) { - row[k] = JSON.stringify(v); - } - }, this); - }, this); - - _.each(this.query_result.data.columns, function(column) { - if (columnTypes[column.name]) { - if (column.type == null || column.type == 'string') { - column.type = columnTypes[column.name]; - } - } - }); - - this.deferred.resolve(this); - } else if (this.job.status == 3) { - this.status = "processing"; - } else { - this.status = undefined; - } - }; - - function QueryResult(props) { - this.deferred = $q.defer(); - this.job = {}; - this.query_result = {}; - this.status = "waiting"; - this.filters = undefined; - this.filterFreeze = undefined; - - this.updatedAt = moment(); - - if (props) { - updateFunction.apply(this, [props]); - } - } - - var statuses = { - 1: "waiting", - 2: "processing", - 3: "done", - 4: "failed" - } - - QueryResult.prototype.update = updateFunction; - - QueryResult.prototype.getId = function () { - var id = null; - if ('query_result' in this) { - id = this.query_result.id; - } - return id; - } - - QueryResult.prototype.cancelExecution = function () { - Job.delete({id: this.job.id}); - } - - QueryResult.prototype.getStatus = function () { - return this.status || statuses[this.job.status]; - } - - QueryResult.prototype.getError = function () { - // TODO: move this logic to the server... - if (this.job.error == "None") { - return undefined; - } - - return this.job.error; - } - - QueryResult.prototype.getLog = function() { - if (!this.query_result.data || !this.query_result.data.log || this.query_result.data.log.length == 0) { - return null; - } - - return this.query_result.data.log; - } - - QueryResult.prototype.getUpdatedAt = function () { - return this.query_result.retrieved_at || this.job.updated_at * 1000.0 || this.updatedAt; - } - - QueryResult.prototype.getRuntime = function () { - return this.query_result.runtime; - } - - QueryResult.prototype.getRawData = function () { - if (!this.query_result.data) { - return null; - } - - var data = this.query_result.data.rows; - - return data; - } - - QueryResult.prototype.getData = function () { - if (!this.query_result.data) { - return null; - } - - var filterValues = function (filters) { - if (!filters) { - return null; - } - - return _.reduce(filters, function (str, filter) { - return str + filter.current; - }, "") - } - - var filters = this.getFilters(); - var filterFreeze = filterValues(filters); - - if (this.filterFreeze != filterFreeze) { - this.filterFreeze = filterFreeze; - - if (filters) { - this.filteredData = _.filter(this.query_result.data.rows, function (row) { - return _.reduce(filters, function (memo, filter) { - if (!_.isArray(filter.current)) { - filter.current = [filter.current]; - }; - - return (memo && _.some(filter.current, function(v) { - var value = row[filter.name]; - if (moment.isMoment(value)) { - return value.isSame(v); - } else { - // We compare with either the value or the String representation of the value, - // because Select2 casts true/false to "true"/"false". - return (v == value || String(value) == v); - } - })); - }, true); - }); - } else { - this.filteredData = this.query_result.data.rows; - } - } - - return this.filteredData; - }; - - /** - * Helper function to add a point into a series - */ - QueryResult.prototype._addPointToSeries = function (point, seriesCollection, seriesName) { - if (seriesCollection[seriesName] == undefined) { - seriesCollection[seriesName] = { - name: seriesName, - type: 'column', - data: [] - }; - } - - seriesCollection[seriesName]['data'].push(point); - }; - - QueryResult.prototype.getChartData = function (mapping) { - var series = {}; - - _.each(this.getData(), function (row) { - var point = {}; - var seriesName = undefined; - var xValue = 0; - var yValues = {}; - - _.each(row, function (value, definition) { - var name = definition.split("::")[0] || definition.split("__")[0]; - var type = definition.split("::")[1] || definition.split("__")[1]; - if (mapping) { - type = mapping[definition]; - } - - if (type == 'unused') { - return; - } - - if (type == 'x') { - xValue = value; - point[type] = value; - } - if (type == 'y') { - if (value == null) { - value = 0; - } - yValues[name] = value; - point[type] = value; - } - - if (type == 'series') { - seriesName = String(value); - } - - if (type == 'multiFilter' || type == 'multi-filter') { - seriesName = String(value); - } - }); - - if (seriesName === undefined) { - _.each(yValues, function (yValue, seriesName) { - this._addPointToSeries({'x': xValue, 'y': yValue}, series, seriesName); - }.bind(this)); - } - else { - this._addPointToSeries(point, series, seriesName); - } - }.bind(this)); - - return _.values(series); - }; - - QueryResult.prototype.getColumns = function () { - if (this.columns == undefined && this.query_result.data) { - this.columns = this.query_result.data.columns; - } - - return this.columns; - } - - QueryResult.prototype.getColumnNames = function () { - if (this.columnNames == undefined && this.query_result.data) { - this.columnNames = _.map(this.query_result.data.columns, function (v) { - return v.name; - }); - } - - return this.columnNames; - } - - QueryResult.prototype.getColumnNameWithoutType = function (column) { - var typeSplit; - if (column.indexOf("::") != -1) { - typeSplit = "::"; - } else if (column.indexOf("__") != -1) { - typeSplit = "__"; - } else { - return column; - } - - var parts = column.split(typeSplit); - if (parts[0] == "" && parts.length == 2) { - return parts[1]; - } - return parts[0]; - }; - - QueryResult.prototype.getColumnCleanName = function (column) { - var name = this.getColumnNameWithoutType(column); - - return name; - } - - QueryResult.prototype.getColumnFriendlyName = function (column) { - return this.getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, function (a) { - return a.toUpperCase(); - }); - } - - QueryResult.prototype.getColumnCleanNames = function () { - return _.map(this.getColumnNames(), function (col) { - return this.getColumnCleanName(col); - }, this); - } - - QueryResult.prototype.getColumnFriendlyNames = function () { - return _.map(this.getColumnNames(), function (col) { - return this.getColumnFriendlyName(col); - }, this); - } - - QueryResult.prototype.getFilters = function () { - if (!this.filters) { - this.prepareFilters(); - } - - return this.filters; - }; - - QueryResult.prototype.prepareFilters = function () { - var filters = []; - var filterTypes = ['filter', 'multi-filter', 'multiFilter']; - _.each(this.getColumns(), function (col) { - var name = col.name; - var type = name.split('::')[1] || name.split('__')[1]; - if (_.contains(filterTypes, type)) { - // filter found - var filter = { - name: name, - friendlyName: this.getColumnFriendlyName(name), - column: col, - values: [], - multiple: (type=='multiFilter') || (type=='multi-filter') - } - filters.push(filter); - } - }, this); - - _.each(this.getRawData(), function (row) { - _.each(filters, function (filter) { - filter.values.push(row[filter.name]); - if (filter.values.length == 1) { - filter.current = row[filter.name]; - } - }) - }); - - _.each(filters, function(filter) { - filter.values = _.uniq(filter.values, function(v) { - if (moment.isMoment(v)) { - return v.unix(); - } else { - return v; - } - }); - }); - - this.filters = filters; - } - - var refreshStatus = function (queryResult, query) { - Job.get({'id': queryResult.job.id}, function (response) { - queryResult.update(response); - - if (queryResult.getStatus() == "processing" && queryResult.job.query_result_id && queryResult.job.query_result_id != "None") { - QueryResultResource.get({'id': queryResult.job.query_result_id}, function (response) { - queryResult.update(response); - }); - } else if (queryResult.getStatus() != "failed") { - $timeout(function () { - refreshStatus(queryResult, query); - }, 3000); - } - }, function(error) { - console.log("Connection error", error); - queryResult.update({job: {error: 'failed communicating with server. Please check your Internet connection and try again.', status: 4}}) - }); - } - - QueryResult.getById = function (id) { - var queryResult = new QueryResult(); - - QueryResultResource.get({'id': id}, function (response) { - queryResult.update(response); - }); - - return queryResult; - }; - - QueryResult.prototype.toPromise = function() { - return this.deferred.promise; - } - - QueryResult.get = function (data_source_id, query, maxAge, queryId) { - var queryResult = new QueryResult(); - - var params = {'data_source_id': data_source_id, 'query': query, 'max_age': maxAge}; - if (queryId !== undefined) { - params['query_id'] = queryId; - }; - - QueryResultResource.post(params, function (response) { - queryResult.update(response); - - if ('job' in response) { - refreshStatus(queryResult, query); - } - }, function(error) { - if (error.status === 403) { - queryResult.update(error.data); - } else if (error.status === 400 && 'job' in error.data) { - queryResult.update(error.data); - } else { - console.log("Unknown error", error); - queryResult.update({job: {error: 'unknown error occurred. Please try again later.', status: 4}}) - } - }); - - return queryResult; - } - - return QueryResult; - }; - - var Query = function ($resource, $location, QueryResult) { - var Query = $resource('api/queries/:id', {id: '@id'}, - { - search: { - method: 'get', - isArray: true, - url: "api/queries/search" - }, - recent: { - method: 'get', - isArray: true, - url: "api/queries/recent" - }, - query: { - isArray: false - }, - myQueries: { - method: 'get', - isArray: false, - url: "api/queries/my" - }, - fork: { - method: 'post', - isArray: false, - url: "api/queries/:id/fork", - params: {id: '@id'} - } - }); - - Query.newQuery = function () { - return new Query({ - query: "", - name: "New Query", - schedule: null, - user: currentUser, - options: {} - }); - }; - - Query.prototype.getSourceLink = function () { - return '/queries/' + this.id + '/source'; - }; - - Query.prototype.isNew = function() { - return this.id === undefined; - }; - - Query.prototype.hasDailySchedule = function() { - return (this.schedule && this.schedule.match(/\d\d:\d\d/) !== null); - }; - - Query.prototype.scheduleInLocalTime = function() { - var parts = this.schedule.split(':'); - return moment.utc().hour(parts[0]).minute(parts[1]).local().format('HH:mm'); - }; - - Query.prototype.hasResult = function() { - return !!(this.latest_query_data || this.latest_query_data_id); - }; - - Query.prototype.paramsRequired = function() { - return this.getParameters().isRequired(); - }; - - Query.prototype.getQueryResult = function (maxAge) { - if (!this.query) { - return; - } - var queryText = this.query; - - var parameters = this.getParameters(); - var missingParams = parameters.getMissing(); - - if (missingParams.length > 0) { - var paramsWord = "parameter"; - var valuesWord = "value"; - if (missingParams.length > 1) { - paramsWord = "parameters"; - valuesWord = "values"; - } - - return new QueryResult({job: {error: "missing " + valuesWord + " for " + missingParams.join(', ') + " "+paramsWord+".", status: 4}}); - } - - if (parameters.isRequired()) { - queryText = Mustache.render(queryText, parameters.getValues()); - - // Need to clear latest results, to make sure we don't use results for different params. - this.latest_query_data = null; - this.latest_query_data_id = null; - } - - if (this.latest_query_data && maxAge != 0) { - if (!this.queryResult) { - this.queryResult = new QueryResult({'query_result': this.latest_query_data}); - } - } else if (this.latest_query_data_id && maxAge != 0) { - if (!this.queryResult) { - this.queryResult = QueryResult.getById(this.latest_query_data_id); - } - } else if (this.data_source_id) { - this.queryResult = QueryResult.get(this.data_source_id, queryText, maxAge, this.id); - } else { - return new QueryResultError("Please select data source to run this query."); - } - - return this.queryResult; - }; - - Query.prototype.getUrl = function(source, hash) { - var url = "queries/" + this.id; - - if (source) { - url += '/source'; - } - - var params = ""; - if (this.getParameters().isRequired()) { - _.each(this.getParameters().getValues(), function(value, name) { - if (value === null) { - return; - } - - if (params !== "") { - params += "&"; - } - - params += 'p_' + encodeURIComponent(name) + "=" + encodeURIComponent(value); - }); - } - - if (params !== "") { - url += "?" + params; - } - - if (hash) { - url += "#" + hash; - } - - return url; - } - - Query.prototype.getQueryResultPromise = function() { - return this.getQueryResult().toPromise(); - }; - - - var Parameters = function(query) { - this.query = query; - - this.parseQuery = function() { - var parts = Mustache.parse(this.query.query); - var parameters = []; - var collectParams = function(parts) { - parameters = []; - _.each(parts, function(part) { - if (part[0] == 'name' || part[0] == '&') { - parameters.push(part[1]); - } else if (part[0] == '#') { - parameters = _.union(parameters, collectParams(part[4])); - } - }); - return parameters; - }; - - parameters = _.uniq(collectParams(parts)); - - return parameters; - } - - this.updateParameters = function() { - if (this.query.query === this.cachedQueryText) { - return; - } - - this.cachedQueryText = this.query.query; - var parameterNames = this.parseQuery(); - - this.query.options.parameters = this.query.options.parameters || []; - - var parametersMap = {}; - _.each(this.query.options.parameters, function(param) { - parametersMap[param.name] = param; - }); - - _.each(parameterNames, function(param) { - if (!_.has(parametersMap, param)) { - this.query.options.parameters.push({ - 'title': param, - 'name': param, - 'type': 'text', - 'value': null - }); - } - }.bind(this)); - - this.query.options.parameters = _.filter(this.query.options.parameters, function(p) { return _.indexOf(parameterNames, p.name) !== -1}); - } - - this.initFromQueryString = function() { - var queryString = $location.search(); - _.each(this.get(), function(param) { - var queryStringName = 'p_' + param.name; - if (_.has(queryString, queryStringName)) { - param.value = queryString[queryStringName]; - } - }); - } - - this.updateParameters(); - this.initFromQueryString(); - } - - Parameters.prototype.get = function() { - this.updateParameters(); - return this.query.options.parameters; - }; - - Parameters.prototype.getMissing = function() { - return _.pluck(_.filter(this.get(), function(p) { return p.value === null || p.value === ''; }), 'title'); - } - - Parameters.prototype.isRequired = function() { - return !_.isEmpty(this.get()); - } - - Parameters.prototype.getValues = function() { - var params = this.get(); - return _.object(_.pluck(params, 'name'), _.pluck(params, 'value')); - } - - Query.prototype.getParameters = function() { - if (!this.$parameters) { - this.$parameters = new Parameters(this); - } - - return this.$parameters; - } - - Query.prototype.getParametersDefs = function() { - return this.getParameters().get(); - } - - return Query; - }; - - var DataSource = function ($resource) { - var actions = { - 'get': {'method': 'GET', 'cache': false, 'isArray': false}, - 'query': {'method': 'GET', 'cache': false, 'isArray': true}, - 'test': {'method': 'POST', 'cache': false, 'isArray': false, 'url': 'api/data_sources/:id/test'}, - 'getSchema': {'method': 'GET', 'cache': true, 'isArray': true, 'url': 'api/data_sources/:id/schema'} - }; - - var DataSourceResource = $resource('api/data_sources/:id', {id: '@id'}, actions); - - return DataSourceResource; - }; - - var Destination = function ($resource) { - var actions = { - 'get': {'method': 'GET', 'cache': false, 'isArray': false}, - 'query': {'method': 'GET', 'cache': false, 'isArray': true} - }; - - var DestinationResource = $resource('api/destinations/:id', {id: '@id'}, actions); - - return DestinationResource; - }; - - var User = function ($resource, $http) { - var transformSingle = function(user) { - if (user.groups !== undefined) { - user.admin = user.groups.indexOf("admin") != -1; - } - }; - - var transform = $http.defaults.transformResponse.concat(function(data, headers) { - if (_.isArray(data)) { - _.each(data, transformSingle); - } else { - transformSingle(data); - } - return data; - }); - - var actions = { - 'get': {method: 'GET', transformResponse: transform}, - 'save': {method: 'POST', transformResponse: transform}, - 'query': {method: 'GET', isArray: true, transformResponse: transform}, - 'delete': {method: 'DELETE', transformResponse: transform} - }; - - var UserResource = $resource('api/users/:id', {id: '@id'}, actions); - - return UserResource; - }; - - var Group = function ($resource) { - var actions = { - 'get': {'method': 'GET', 'cache': false, 'isArray': false}, - 'query': {'method': 'GET', 'cache': false, 'isArray': true}, - 'members': {'method': 'GET', 'cache': false, 'isArray': true, 'url': 'api/groups/:id/members'}, - 'dataSources': {'method': 'GET', 'cache': false, 'isArray': true, 'url': 'api/groups/:id/data_sources'} - }; - var resource = $resource('api/groups/:id', {id: '@id'}, actions); - return resource; - }; - - var AlertSubscription = function ($resource) { - var resource = $resource('api/alerts/:alertId/subscriptions/:subscriberId', {alertId: '@alert_id', subscriberId: '@id'}); - return resource; - }; - - var Alert = function ($resource, $http) { - var actions = { - save: { - method: 'POST', - transformRequest: [function(data) { - var newData = _.extend({}, data); - if (newData.query_id === undefined) { - newData.query_id = newData.query.id; - newData.destination_id = newData.destinations; - delete newData.query; - delete newData.destinations; - } - - return newData; - }].concat($http.defaults.transformRequest) - } - }; - var resource = $resource('api/alerts/:id', {id: '@id'}, actions); - - return resource; - }; - - var QuerySnippet = function ($resource) { - var resource = $resource('api/query_snippets/:id', {id: '@id'}); - resource.prototype.getSnippet = function() { - var name = this.trigger; - if (this.description !== "") { - name = this.trigger + ": " + this.description; - } - - return { - "name": name, - "content": this.snippet, - "tabTrigger": this.trigger - }; - } - - return resource; - }; - - var Widget = function ($resource, Query) { - var WidgetResource = $resource('api/widgets/:id', {id: '@id'}); - - WidgetResource.prototype.getQuery = function () { - if (!this.query && this.visualization) { - this.query = new Query(this.visualization.query); - } - - return this.query; - }; - - WidgetResource.prototype.getName = function () { - if (this.visualization) { - return this.visualization.query.name + ' (' + this.visualization.name + ')'; - } - return _.str.truncate(this.text, 20); - }; - - return WidgetResource; - } - - angular.module('redash.services') - .factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult]) - .factory('Query', ['$resource', '$location', 'QueryResult', Query]) - .factory('DataSource', ['$resource', DataSource]) - .factory('Destination', ['$resource', Destination]) - .factory('Alert', ['$resource', '$http', Alert]) - .factory('AlertSubscription', ['$resource', AlertSubscription]) - .factory('Widget', ['$resource', 'Query', Widget]) - .factory('User', ['$resource', '$http', User]) - .factory('Group', ['$resource', Group]) - .factory('QuerySnippet', ['$resource', QuerySnippet]); -})(); diff --git a/rd_ui/app/scripts/services/services.js b/rd_ui/app/scripts/services/services.js deleted file mode 100644 index e913bcb759..0000000000 --- a/rd_ui/app/scripts/services/services.js +++ /dev/null @@ -1,52 +0,0 @@ -(function () { - 'use strict' - - function KeyboardShortcuts() { - this.bind = function bind(keymap) { - _.forEach(keymap, function (fn, key) { - Mousetrap.bindGlobal(key, function (e) { - e.preventDefault(); - fn(); - }); - }); - - } - - this.unbind = function unbind(keymap) { - _.forEach(keymap, function (fn, key) { - Mousetrap.unbind(key); - }); - } - } - - function Events($http) { - this.events = []; - - this.post = _.debounce(function() { - var events = this.events; - this.events = []; - - $http.post('api/events', events); - - }, 1000); - - this.record = function (user, action, object_type, object_id, additional_properties) { - - var event = { - "user_id": user.id, - "action": action, - "object_type": object_type, - "object_id": object_id, - "timestamp": Date.now()/1000.0 - }; - _.extend(event, additional_properties); - this.events.push(event); - - this.post(); - }; - } - - angular.module('redash.services', []) - .service('KeyboardShortcuts', [KeyboardShortcuts]) - .service('Events', ['$http', Events]) -})(); diff --git a/rd_ui/app/scripts/vendor/cloud.js b/rd_ui/app/scripts/vendor/cloud.js deleted file mode 100644 index 223d891a07..0000000000 --- a/rd_ui/app/scripts/vendor/cloud.js +++ /dev/null @@ -1,505 +0,0 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.d3||(g.d3 = {}));g=(g.layout||(g.layout = {}));g.cloud = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o> 5, - ch = 1 << 11; - -d3.cloud = function() { - var size = [256, 256], - text = cloudText, - font = cloudFont, - fontSize = cloudFontSize, - fontStyle = cloudFontNormal, - fontWeight = cloudFontNormal, - rotate = cloudRotate, - padding = cloudPadding, - spiral = archimedeanSpiral, - words = [], - timeInterval = Infinity, - event = dispatch("word", "end"), - timer = null, - random = Math.random, - cloud = {}, - canvas = cloudCanvas; - - cloud.canvas = function(_) { - return arguments.length ? (canvas = functor(_), cloud) : canvas; - }; - - cloud.start = function() { - var contextAndRatio = getContext(canvas()), - board = zeroArray((size[0] >> 5) * size[1]), - bounds = null, - n = words.length, - i = -1, - tags = [], - data = words.map(function(d, i) { - d.text = text.call(this, d, i); - d.font = font.call(this, d, i); - d.style = fontStyle.call(this, d, i); - d.weight = fontWeight.call(this, d, i); - d.rotate = rotate.call(this, d, i); - d.size = ~~fontSize.call(this, d, i); - d.padding = padding.call(this, d, i); - return d; - }).sort(function(a, b) { return b.size - a.size; }); - - if (timer) clearInterval(timer); - timer = setInterval(step, 0); - step(); - - return cloud; - - function step() { - var start = Date.now(); - while (Date.now() - start < timeInterval && ++i < n && timer) { - var d = data[i]; - d.x = (size[0] * (random() + .5)) >> 1; - d.y = (size[1] * (random() + .5)) >> 1; - cloudSprite(contextAndRatio, d, data, i); - if (d.hasText && place(board, d, bounds)) { - tags.push(d); - event.word(d); - if (bounds) cloudBounds(bounds, d); - else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}]; - // Temporary hack - d.x -= size[0] >> 1; - d.y -= size[1] >> 1; - } - } - if (i >= n) { - cloud.stop(); - event.end(tags, bounds); - } - } - } - - cloud.stop = function() { - if (timer) { - clearInterval(timer); - timer = null; - } - return cloud; - }; - - function getContext(canvas) { - canvas.width = canvas.height = 1; - var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2); - canvas.width = (cw << 5) / ratio; - canvas.height = ch / ratio; - - var context = canvas.getContext("2d"); - context.fillStyle = context.strokeStyle = "red"; - context.textAlign = "center"; - - return {context: context, ratio: ratio}; - } - - function place(board, tag, bounds) { - var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}], - startX = tag.x, - startY = tag.y, - maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]), - s = spiral(size), - dt = random() < .5 ? 1 : -1, - t = -dt, - dxdy, - dx, - dy; - - while (dxdy = s(t += dt)) { - dx = ~~dxdy[0]; - dy = ~~dxdy[1]; - - if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break; - - tag.x = startX + dx; - tag.y = startY + dy; - - if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 || - tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue; - // TODO only check for collisions within current bounds. - if (!bounds || !cloudCollide(tag, board, size[0])) { - if (!bounds || collideRects(tag, bounds)) { - var sprite = tag.sprite, - w = tag.width >> 5, - sw = size[0] >> 5, - lx = tag.x - (w << 4), - sx = lx & 0x7f, - msx = 32 - sx, - h = tag.y1 - tag.y0, - x = (tag.y + tag.y0) * sw + (lx >> 5), - last; - for (var j = 0; j < h; j++) { - last = 0; - for (var i = 0; i <= w; i++) { - board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0); - } - x += sw; - } - delete tag.sprite; - return true; - } - } - } - return false; - } - - cloud.timeInterval = function(_) { - return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval; - }; - - cloud.words = function(_) { - return arguments.length ? (words = _, cloud) : words; - }; - - cloud.size = function(_) { - return arguments.length ? (size = [+_[0], +_[1]], cloud) : size; - }; - - cloud.font = function(_) { - return arguments.length ? (font = functor(_), cloud) : font; - }; - - cloud.fontStyle = function(_) { - return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle; - }; - - cloud.fontWeight = function(_) { - return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight; - }; - - cloud.rotate = function(_) { - return arguments.length ? (rotate = functor(_), cloud) : rotate; - }; - - cloud.text = function(_) { - return arguments.length ? (text = functor(_), cloud) : text; - }; - - cloud.spiral = function(_) { - return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral; - }; - - cloud.fontSize = function(_) { - return arguments.length ? (fontSize = functor(_), cloud) : fontSize; - }; - - cloud.padding = function(_) { - return arguments.length ? (padding = functor(_), cloud) : padding; - }; - - cloud.random = function(_) { - return arguments.length ? (random = _, cloud) : random; - }; - - cloud.on = function() { - var value = event.on.apply(event, arguments); - return value === event ? cloud : value; - }; - - return cloud; -}; - -function cloudText(d) { - return d.text; -} - -function cloudFont() { - return "serif"; -} - -function cloudFontNormal() { - return "normal"; -} - -function cloudFontSize(d) { - return Math.sqrt(d.value); -} - -function cloudRotate() { - return (~~(Math.random() * 6) - 3) * 30; -} - -function cloudPadding() { - return 1; -} - -// Fetches a monochrome sprite bitmap for the specified text. -// Load in batches for speed. -function cloudSprite(contextAndRatio, d, data, di) { - if (d.sprite) return; - var c = contextAndRatio.context, - ratio = contextAndRatio.ratio; - - c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio); - var x = 0, - y = 0, - maxh = 0, - n = data.length; - --di; - while (++di < n) { - d = data[di]; - c.save(); - c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font; - var w = c.measureText(d.text + "m").width * ratio, - h = d.size << 1; - if (d.rotate) { - var sr = Math.sin(d.rotate * cloudRadians), - cr = Math.cos(d.rotate * cloudRadians), - wcr = w * cr, - wsr = w * sr, - hcr = h * cr, - hsr = h * sr; - w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5; - h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr)); - } else { - w = (w + 0x1f) >> 5 << 5; - } - if (h > maxh) maxh = h; - if (x + w >= (cw << 5)) { - x = 0; - y += maxh; - maxh = 0; - } - if (y + h >= ch) break; - c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio); - if (d.rotate) c.rotate(d.rotate * cloudRadians); - c.fillText(d.text, 0, 0); - if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0); - c.restore(); - d.width = w; - d.height = h; - d.xoff = x; - d.yoff = y; - d.x1 = w >> 1; - d.y1 = h >> 1; - d.x0 = -d.x1; - d.y0 = -d.y1; - d.hasText = true; - x += w; - } - var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data, - sprite = []; - while (--di >= 0) { - d = data[di]; - if (!d.hasText) continue; - var w = d.width, - w32 = w >> 5, - h = d.y1 - d.y0; - // Zero the buffer - for (var i = 0; i < h * w32; i++) sprite[i] = 0; - x = d.xoff; - if (x == null) return; - y = d.yoff; - var seen = 0, - seenRow = -1; - for (var j = 0; j < h; j++) { - for (var i = 0; i < w; i++) { - var k = w32 * j + (i >> 5), - m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0; - sprite[k] |= m; - seen |= m; - } - if (seen) seenRow = j; - else { - d.y0++; - h--; - j--; - y++; - } - } - d.y1 = d.y0 + seenRow; - d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32); - } -} - -// Use mask-based collision detection. -function cloudCollide(tag, board, sw) { - sw >>= 5; - var sprite = tag.sprite, - w = tag.width >> 5, - lx = tag.x - (w << 4), - sx = lx & 0x7f, - msx = 32 - sx, - h = tag.y1 - tag.y0, - x = (tag.y + tag.y0) * sw + (lx >> 5), - last; - for (var j = 0; j < h; j++) { - last = 0; - for (var i = 0; i <= w; i++) { - if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) - & board[x + i]) return true; - } - x += sw; - } - return false; -} - -function cloudBounds(bounds, d) { - var b0 = bounds[0], - b1 = bounds[1]; - if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0; - if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0; - if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1; - if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1; -} - -function collideRects(a, b) { - return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y; -} - -function archimedeanSpiral(size) { - var e = size[0] / size[1]; - return function(t) { - return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)]; - }; -} - -function rectangularSpiral(size) { - var dy = 4, - dx = dy * size[0] / size[1], - x = 0, - y = 0; - return function(t) { - var sign = t < 0 ? -1 : 1; - // See triangular numbers: T_n = n * (n + 1) / 2. - switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) { - case 0: x += dx; break; - case 1: y += dy; break; - case 2: x -= dx; break; - default: y -= dy; break; - } - return [x, y]; - }; -} - -// TODO reuse arrays? -function zeroArray(n) { - var a = [], - i = -1; - while (++i < n) a[i] = 0; - return a; -} - -function cloudCanvas() { - return document.createElement("canvas"); -} - -function functor(d) { - return typeof d === "function" ? d : function() { return d; }; -} - -var spirals = { - archimedean: archimedeanSpiral, - rectangular: rectangularSpiral -}; - -},{"d3-dispatch":2}],2:[function(require,module,exports){ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : - typeof define === 'function' && define.amd ? define(['exports'], factory) : - factory((global.dispatch = {})); -}(this, function (exports) { 'use strict'; - - function Dispatch(types) { - var i = -1, - n = types.length, - callbacksByType = {}, - callbackByName = {}, - type, - that = this; - - that.on = function(type, callback) { - type = parseType(type); - - // Return the current callback, if any. - if (arguments.length < 2) { - return (callback = callbackByName[type.name]) && callback.value; - } - - // If a type was specified… - if (type.type) { - var callbacks = callbacksByType[type.type], - callback0 = callbackByName[type.name], - i; - - // Remove the current callback, if any, using copy-on-remove. - if (callback0) { - callback0.value = null; - i = callbacks.indexOf(callback0); - callbacksByType[type.type] = callbacks = callbacks.slice(0, i).concat(callbacks.slice(i + 1)); - delete callbackByName[type.name]; - } - - // Add the new callback, if any. - if (callback) { - callback = {value: callback}; - callbackByName[type.name] = callback; - callbacks.push(callback); - } - } - - // Otherwise, if a null callback was specified, remove all callbacks with the given name. - else if (callback == null) { - for (var otherType in callbacksByType) { - if (callback = callbackByName[otherType + type.name]) { - callback.value = null; - var callbacks = callbacksByType[otherType], i = callbacks.indexOf(callback); - callbacksByType[otherType] = callbacks.slice(0, i).concat(callbacks.slice(i + 1)); - delete callbackByName[callback.name]; - } - } - } - - return that; - }; - - while (++i < n) { - type = types[i] + ""; - if (!type || (type in that)) throw new Error("illegal or duplicate type: " + type); - callbacksByType[type] = []; - that[type] = applier(type); - } - - function parseType(type) { - var i = (type += "").indexOf("."), name = type; - if (i >= 0) type = type.slice(0, i); else name += "."; - if (type && !callbacksByType.hasOwnProperty(type)) throw new Error("unknown type: " + type); - return {type: type, name: name}; - } - - function applier(type) { - return function() { - var callbacks = callbacksByType[type], // Defensive reference; copy-on-remove. - callback, - callbackValue, - i = -1, - n = callbacks.length; - - while (++i < n) { - if (callbackValue = (callback = callbacks[i]).value) { - callbackValue.apply(this, arguments); - } - } - - return that; - }; - } - } - - function dispatch() { - return new Dispatch(arguments); - } - - dispatch.prototype = Dispatch.prototype; // allow instanceof - - exports.dispatch = dispatch; - -})); -},{}]},{},[1])(1) -}); diff --git a/rd_ui/app/scripts/visualizations/base.js b/rd_ui/app/scripts/visualizations/base.js deleted file mode 100644 index c96ea4b1d1..0000000000 --- a/rd_ui/app/scripts/visualizations/base.js +++ /dev/null @@ -1,226 +0,0 @@ -(function () { - var VisualizationProvider = function () { - this.visualizations = {}; - this.visualizationTypes = {}; - var defaultConfig = { - defaultOptions: {}, - skipTypes: false, - editorTemplate: null - }; - - this.registerVisualization = function (config) { - var visualization = _.extend({}, defaultConfig, config); - - // TODO: this is prone to errors; better refactor. - if (_.isEmpty(this.visualizations)) { - this.defaultVisualization = visualization; - } - - this.visualizations[config.type] = visualization; - - if (!config.skipTypes) { - this.visualizationTypes[config.name] = config.type; - } - }; - - this.getSwitchTemplate = function (property) { - var pattern = /(<[a-zA-Z0-9-]*?)( |>)/; - - var mergedTemplates = _.reduce(this.visualizations, function (templates, visualization) { - if (visualization[property]) { - var ngSwitch = '$1 ng-switch-when="' + visualization.type + '" $2'; - var template = visualization[property].replace(pattern, ngSwitch); - - return templates + "\n" + template; - } - - return templates; - }, ""); - - mergedTemplates = '
    ' + mergedTemplates + "
    "; - - return mergedTemplates; - }; - - this.$get = ['$resource', function ($resource) { - var Visualization = $resource('api/visualizations/:id', {id: '@id'}); - Visualization.visualizations = this.visualizations; - Visualization.visualizationTypes = this.visualizationTypes; - Visualization.renderVisualizationsTemplate = this.getSwitchTemplate('renderTemplate'); - Visualization.editorTemplate = this.getSwitchTemplate('editorTemplate'); - Visualization.defaultVisualization = this.defaultVisualization; - - return Visualization; - }]; - }; - - var VisualizationName = function(Visualization) { - return { - restrict: 'E', - scope: { - visualization: '=' - }, - template: '{{name}}', - replace: false, - link: function (scope) { - if (Visualization.visualizations[scope.visualization.type].name !== scope.visualization.name) { - scope.name = scope.visualization.name; - } - } - }; - }; - - var VisualizationRenderer = function ($location, Visualization) { - return { - restrict: 'E', - scope: { - visualization: '=', - queryResult: '=' - }, - // TODO: using switch here (and in the options editor) might introduce errors and bad - // performance wise. It's better to eventually show the correct template based on the - // visualization type and not make the browser render all of them. - template: '\n' + Visualization.renderVisualizationsTemplate, - replace: false, - link: function (scope) { - scope.$watch('queryResult && queryResult.getFilters()', function (filters) { - if (filters) { - scope.filters = filters; - } - }); - } - }; - }; - - var VisualizationOptionsEditor = function (Visualization) { - return { - restrict: 'E', - template: Visualization.editorTemplate, - replace: false - }; - }; - - var Filters = function () { - return { - restrict: 'E', - templateUrl: '/views/visualizations/filters.html' - }; - }; - - var FilterValueFilter = function() { - return function(value, filter) { - if (_.isArray(value)) { - value = value[0]; - } - - // TODO: deduplicate code with table.js: - if (filter.column.type === 'date') { - if (value && moment.isMoment(value)) { - return value.format(clientConfig.dateFormat); - } - } else if (filter.column.type === 'datetime') { - if (value && moment.isMoment(value)) { - return value.format(clientConfig.dateTimeFormat); - } - } - - return value; - }; - }; - - var EditVisualizationForm = function (Events, Visualization, growl) { - return { - restrict: 'E', - templateUrl: '/views/visualizations/edit_visualization.html', - replace: true, - scope: { - query: '=', - queryResult: '=', - originalVisualization: '=?', - onNewSuccess: '=?', - modalInstance: '=?' - }, - link: function (scope) { - scope.visualization = angular.copy(scope.originalVisualization); - scope.editRawOptions = currentUser.hasPermission('edit_raw_chart'); - scope.visTypes = Visualization.visualizationTypes; - - scope.newVisualization = function () { - return { - 'type': Visualization.defaultVisualization.type, - 'name': Visualization.defaultVisualization.name, - 'description': '', - 'options': Visualization.defaultVisualization.defaultOptions - }; - }; - - if (!scope.visualization) { - var unwatch = scope.$watch('query.id', function (queryId) { - if (queryId) { - unwatch(); - - scope.visualization = scope.newVisualization(); - } - }); - } - - scope.$watch('visualization.type', function (type, oldType) { - // if not edited by user, set name to match type - if (type && oldType !== type && scope.visualization && !scope.visForm.name.$dirty) { - scope.visualization.name = Visualization.visualizations[scope.visualization.type].name; - } - - if (type && oldType !== type && scope.visualization) { - scope.visualization.options = Visualization.visualizations[scope.visualization.type].defaultOptions; - } - }); - - scope.submit = function () { - if (scope.visualization.id) { - Events.record(currentUser, "update", "visualization", scope.visualization.id, {'type': scope.visualization.type}); - } else { - Events.record(currentUser, "create", "visualization", null, {'type': scope.visualization.type}); - } - - scope.visualization.query_id = scope.query.id; - - Visualization.save(scope.visualization, function success(result) { - growl.addSuccessMessage("Visualization saved"); - - var visIds = _.pluck(scope.query.visualizations, 'id'); - var index = visIds.indexOf(result.id); - if (index > -1) { - scope.query.visualizations[index] = result; - } else { - // new visualization - scope.query.visualizations.push(result); - scope.onNewSuccess && scope.onNewSuccess(result); - } - scope.modalInstance.close(); - }, function error() { - growl.addErrorMessage("Visualization could not be saved"); - }); - }; - - scope.close = function() { - if (scope.visForm.$dirty) { - if (confirm("Are you sure you want to close the editor without saving?")) { - scope.modalInstance.close(); - } - } else { - scope.modalInstance.close(); - } - } - } - }; - }; - - angular.module('redash.visualization', []) - .provider('Visualization', VisualizationProvider) - .directive('visualizationRenderer', ['$location', 'Visualization', VisualizationRenderer]) - .directive('visualizationOptionsEditor', ['Visualization', VisualizationOptionsEditor]) - .directive('visualizationName', ['Visualization', VisualizationName]) - .directive('filters', Filters) - .filter('filterValue', FilterValueFilter) - .directive('editVisulatizationForm', ['Events', 'Visualization', 'growl', EditVisualizationForm]); -})(); diff --git a/rd_ui/app/scripts/visualizations/box.js b/rd_ui/app/scripts/visualizations/box.js deleted file mode 100644 index c98b9d07b2..0000000000 --- a/rd_ui/app/scripts/visualizations/box.js +++ /dev/null @@ -1,307 +0,0 @@ -(function() { - -// Inspired by http://informationandvisualization.de/blog/box-plot -d3.box = function() { - var width = 1, - height = 1, - duration = 0, - domain = null, - value = Number, - whiskers = boxWhiskers, - quartiles = boxQuartiles, - tickFormat = null; - - // For each small multiple… - function box(g) { - g.each(function(d, i) { - d = d.map(value).sort(d3.ascending); - var g = d3.select(this), - n = d.length, - min = d[0], - max = d[n - 1]; - - // Compute quartiles. Must return exactly 3 elements. - var quartileData = d.quartiles = quartiles(d); - - // Compute whiskers. Must return exactly 2 elements, or null. - var whiskerIndices = whiskers && whiskers.call(this, d, i), - whiskerData = whiskerIndices && whiskerIndices.map(function(i) { return d[i]; }); - - // Compute outliers. If no whiskers are specified, all data are "outliers". - // We compute the outliers as indices, so that we can join across transitions! - var outlierIndices = whiskerIndices - ? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n)) - : d3.range(n); - - // Compute the new x-scale. - var x1 = d3.scale.linear() - .domain(domain && domain.call(this, d, i) || [min, max]) - .range([height, 0]); - - // Retrieve the old x-scale, if this is an update. - var x0 = this.__chart__ || d3.scale.linear() - .domain([0, Infinity]) - .range(x1.range()); - - // Stash the new scale. - this.__chart__ = x1; - - // Note: the box, median, and box tick elements are fixed in number, - // so we only have to handle enter and update. In contrast, the outliers - // and other elements are variable, so we need to exit them! Variable - // elements also fade in and out. - - // Update center line: the vertical line spanning the whiskers. - var center = g.selectAll("line.center") - .data(whiskerData ? [whiskerData] : []); - - center.enter().insert("line", "rect") - .attr("class", "center") - .attr("x1", width / 2) - .attr("y1", function(d) { return x0(d[0]); }) - .attr("x2", width / 2) - .attr("y2", function(d) { return x0(d[1]); }) - .style("opacity", 1e-6) - .transition() - .duration(duration) - .style("opacity", 1) - .attr("y1", function(d) { return x1(d[0]); }) - .attr("y2", function(d) { return x1(d[1]); }); - - center.transition() - .duration(duration) - .style("opacity", 1) - .attr("y1", function(d) { return x1(d[0]); }) - .attr("y2", function(d) { return x1(d[1]); }); - - center.exit().transition() - .duration(duration) - .style("opacity", 1e-6) - .attr("y1", function(d) { return x1(d[0]); }) - .attr("y2", function(d) { return x1(d[1]); }) - .remove(); - - // Update innerquartile box. - var box = g.selectAll("rect.box") - .data([quartileData]); - - box.enter().append("rect") - .attr("class", "box") - .attr("x", 0) - .attr("y", function(d) { return x0(d[2]); }) - .attr("width", width) - .attr("height", function(d) { return x0(d[0]) - x0(d[2]); }) - .transition() - .duration(duration) - .attr("y", function(d) { return x1(d[2]); }) - .attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); - - box.transition() - .duration(duration) - .attr("y", function(d) { return x1(d[2]); }) - .attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); - - box.exit().remove() - - // Update median line. - var medianLine = g.selectAll("line.median") - .data([quartileData[1]]); - - medianLine.enter().append("line") - .attr("class", "median") - .attr("x1", 0) - .attr("y1", x0) - .attr("x2", width) - .attr("y2", x0) - .transition() - .duration(duration) - .attr("y1", x1) - .attr("y2", x1); - - medianLine.transition() - .duration(duration) - .attr("y1", x1) - .attr("y2", x1); - - medianLine.exit().remove() - - // Update whiskers. - var whisker = g.selectAll("line.whisker") - .data(whiskerData || []); - - whisker.enter().insert("line", "circle, text") - .attr("class", "whisker") - .attr("x1", 0) - .attr("y1", x0) - .attr("x2", width) - .attr("y2", x0) - .style("opacity", 1e-6) - .transition() - .duration(duration) - .attr("y1", x1) - .attr("y2", x1) - .style("opacity", 1); - - whisker.transition() - .duration(duration) - .attr("y1", x1) - .attr("y2", x1) - .style("opacity", 1); - - whisker.exit().transition() - .duration(duration) - .attr("y1", x1) - .attr("y2", x1) - .style("opacity", 1e-6) - .remove(); - - // Update outliers. - var outlier = g.selectAll("circle.outlier") - .data(outlierIndices, Number); - - outlier.enter().insert("circle", "text") - .attr("class", "outlier") - .attr("r", 5) - .attr("cx", width / 2) - .attr("cy", function(i) { return x0(d[i]); }) - .style("opacity", 1e-6) - .transition() - .duration(duration) - .attr("cy", function(i) { return x1(d[i]); }) - .style("opacity", 1); - - outlier.transition() - .duration(duration) - .attr("cy", function(i) { return x1(d[i]); }) - .style("opacity", 1); - - outlier.exit().transition() - .duration(duration) - .attr("cy", function(i) { return x1(d[i]); }) - .style("opacity", 1e-6) - .remove(); - - // Compute the tick format. - var format = tickFormat || x1.tickFormat(8); - - // Update box ticks. - var boxTick = g.selectAll("text.box") - .data(quartileData); - - boxTick.enter().append("text") - .attr("class", "box") - .attr("dy", ".3em") - .attr("dx", function(d, i) { return i & 1 ? 6 : -6 }) - .attr("x", function(d, i) { return i & 1 ? width : 0 }) - .attr("y", x0) - .attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; }) - .text(format) - .transition() - .duration(duration) - .attr("y", x1); - - boxTick.transition() - .duration(duration) - .text(format) - .attr("y", x1); - - boxTick.exit().remove() - - // Update whisker ticks. These are handled separately from the box - // ticks because they may or may not exist, and we want don't want - // to join box ticks pre-transition with whisker ticks post-. - var whiskerTick = g.selectAll("text.whisker") - .data(whiskerData || []); - - whiskerTick.enter().append("text") - .attr("class", "whisker") - .attr("dy", ".3em") - .attr("dx", 6) - .attr("x", width) - .attr("y", x0) - .text(format) - .style("opacity", 1e-6) - .transition() - .duration(duration) - .attr("y", x1) - .style("opacity", 1); - - whiskerTick.transition() - .duration(duration) - .text(format) - .attr("y", x1) - .style("opacity", 1); - - whiskerTick.exit().transition() - .duration(duration) - .attr("y", x1) - .style("opacity", 1e-6) - .remove(); - }); - d3.timer.flush(); - } - - box.width = function(x) { - if (!arguments.length) return width; - width = x; - return box; - }; - - box.height = function(x) { - if (!arguments.length) return height; - height = x; - return box; - }; - - box.tickFormat = function(x) { - if (!arguments.length) return tickFormat; - tickFormat = x; - return box; - }; - - box.duration = function(x) { - if (!arguments.length) return duration; - duration = x; - return box; - }; - - box.domain = function(x) { - if (!arguments.length) return domain; - domain = x == null ? x : d3.functor(x); - return box; - }; - - box.value = function(x) { - if (!arguments.length) return value; - value = x; - return box; - }; - - box.whiskers = function(x) { - if (!arguments.length) return whiskers; - whiskers = x; - return box; - }; - - box.quartiles = function(x) { - if (!arguments.length) return quartiles; - quartiles = x; - return box; - }; - - return box; -}; - -function boxWhiskers(d) { - return [0, d.length - 1]; -} - -function boxQuartiles(d) { - return [ - d3.quantile(d, .25), - d3.quantile(d, .5), - d3.quantile(d, .75) - ]; -} - -})(); \ No newline at end of file diff --git a/rd_ui/app/scripts/visualizations/boxplot.js b/rd_ui/app/scripts/visualizations/boxplot.js deleted file mode 100644 index 41a9c79e90..0000000000 --- a/rd_ui/app/scripts/visualizations/boxplot.js +++ /dev/null @@ -1,176 +0,0 @@ -(function() { - var module = angular.module('redash.visualization'); - - module.config(['VisualizationProvider', function(VisualizationProvider) { - var renderTemplate = - '' + - ''; - - var editTemplate = ''; - - VisualizationProvider.registerVisualization({ - type: 'BOXPLOT', - name: 'Boxplot', - renderTemplate: renderTemplate, - editorTemplate: editTemplate - }); - } - ]); - module.directive('boxplotRenderer', function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/boxplot.html', - link: function($scope, elm, attrs) { - - function iqr(k) { - return function(d, i) { - var q1 = d.quartiles[0], - q3 = d.quartiles[2], - iqr = (q3 - q1) * k, - i = -1, - j = d.length; - while (d[++i] < q1 - iqr); - while (d[--j] > q3 + iqr); - return [i, j]; - }; - }; - - $scope.$watch('[queryResult && queryResult.getData(), visualization.options]', function () { - if ($scope.queryResult.getData() === null) { - return; - } - - var data = $scope.queryResult.getData(); - var parentWidth = d3.select(elm[0].parentNode).node().getBoundingClientRect().width; - var margin = {top: 10, right: 50, bottom: 40, left: 50, inner: 25}, - width = parentWidth - margin.right - margin.left - height = 500 - margin.top - margin.bottom; - - var min = Infinity, - max = -Infinity; - var mydata = []; - var value = 0; - var d = []; - var xAxisLabel = $scope.visualization.options.xAxisLabel; - var yAxisLabel = $scope.visualization.options.yAxisLabel; - - var columns = $scope.queryResult.getColumnNames(); - var xscale = d3.scale.ordinal() - .domain(columns) - .rangeBands([0, parentWidth-margin.left-margin.right]); - - if (columns.length > 1){ - boxWidth = Math.min(xscale(columns[1]),120.0); - } else { - boxWidth=120.0; - }; - margin.inner = boxWidth/3.0; - - _.each(columns, function(column, i){ - d = mydata[i] = []; - _.each(data, function (row) { - value = row[column]; - d.push(value); - if (value > max) max = Math.ceil(value); - if (value < min) min = Math.floor(value); - }); - }); - - var yscale = d3.scale.linear() - .domain([min*0.99,max*1.01]) - .range([height, 0]); - - var chart = d3.box() - .whiskers(iqr(1.5)) - .width(boxWidth-2*margin.inner) - .height(height) - .domain([min*0.99,max*1.01]); - var xAxis = d3.svg.axis() - .scale(xscale) - .orient("bottom"); - - - var yAxis = d3.svg.axis() - .scale(yscale) - .orient("left"); - - var xLines = d3.svg.axis() - .scale(xscale) - .tickSize(height) - .orient("bottom"); - - var yLines = d3.svg.axis() - .scale(yscale) - .tickSize(width) - .orient("right"); - - var barOffset = function(i){ - return xscale(columns[i]) + (xscale(columns[1]) - margin.inner)/2.0; - }; - - d3.select(elm[0]).selectAll("svg").remove(); - - var plot = d3.select(elm[0]) - .append("svg") - .attr("width",parentWidth) - .attr("height",height + margin.bottom + margin.top) - .append("g") - .attr("width",parentWidth-margin.left-margin.right) - .attr("transform", "translate(" + margin.left + "," + margin.top + ")") - - d3.select("svg").append("text") - .attr("class", "box") - .attr("x", parentWidth/2.0) - .attr("text-anchor", "middle") - .attr("y", height+margin.bottom) - .text(xAxisLabel) - - d3.select("svg").append("text") - .attr("class", "box") - .attr("transform","translate(10,"+(height+margin.top+margin.bottom)/2.0+")rotate(-90)") - .attr("text-anchor", "middle") - .text(yAxisLabel) - - plot.append("rect") - .attr("class", "grid-background") - .attr("width", width) - .attr("height", height); - - plot.append("g") - .attr("class","grid") - .call(yLines) - - plot.append("g") - .attr("class","grid") - .call(xLines) - - plot.append("g") - .attr("class", "x axis") - .attr("transform", "translate(0," + height + ")") - .call(xAxis); - - plot.append("g") - .attr("class", "y axis") - .call(yAxis); - - plot.selectAll(".box").data(mydata) - .enter().append("g") - .attr("class", "box") - .attr("width", boxWidth) - .attr("height", height) - .attr("transform", function(d,i) { return "translate(" + barOffset(i) + "," + 0 + ")"; } ) - .call(chart); - }, true); - } - } - }); - - module.directive('boxplotEditor', function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/boxplot_editor.html' - }; - }); - -})(); diff --git a/rd_ui/app/scripts/visualizations/chart.js b/rd_ui/app/scripts/visualizations/chart.js deleted file mode 100644 index f93e7df165..0000000000 --- a/rd_ui/app/scripts/visualizations/chart.js +++ /dev/null @@ -1,216 +0,0 @@ -(function () { - var chartVisualization = angular.module('redash.visualization'); - - chartVisualization.config(['VisualizationProvider', function (VisualizationProvider) { - var renderTemplate = ''; - var editTemplate = ''; - - var defaultOptions = { - globalSeriesType: 'column', - sortX: true, - legend: {enabled: true}, - yAxis: [{type: 'linear'}, {type: 'linear', opposite: true}], - xAxis: {type: 'datetime', labels: {enabled: true}}, - series: {stacking: null}, - seriesOptions: {}, - columnMapping: {}, - bottomMargin: 50 - }; - - VisualizationProvider.registerVisualization({ - type: 'CHART', - name: 'Chart', - renderTemplate: renderTemplate, - editorTemplate: editTemplate, - defaultOptions: defaultOptions - }); - }]); - - chartVisualization.directive('chartRenderer', function () { - return { - restrict: 'E', - scope: { - queryResult: '=', - options: '=?' - }, - templateUrl: '/views/visualizations/chart.html', - replace: false, - controller: ['$scope', function ($scope) { - $scope.chartSeries = []; - - var reloadChart = function() { - reloadData(); - $scope.plotlyOptions = $scope.options; - }; - - var reloadData = function() { - if (angular.isDefined($scope.queryResult)) { - $scope.chartSeries = _.sortBy($scope.queryResult.getChartData($scope.options.columnMapping), - function(series) { - if ($scope.options.seriesOptions[series.name]) { - return $scope.options.seriesOptions[series.name].zIndex; - } - return 0; - }); - } - }; - - $scope.$watch('options', reloadChart, true); - $scope.$watch('queryResult && queryResult.getData()', reloadData); - }] - }; - }); - - chartVisualization.directive('chartEditor', function (ColorPalette) { - return { - restrict: 'E', - templateUrl: '/views/visualizations/chart_editor.html', - scope: { - queryResult: '=', - options: '=?' - }, - link: function (scope, element, attrs) { - scope.currentTab = 'general'; - scope.colors = _.extend({'Automatic': null}, ColorPalette); - - scope.stackingOptions = { - 'Disabled': null, - 'Enabled': 'normal', - 'Percent': 'percent' - }; - - scope.chartTypes = { - 'line': {name: 'Line', icon: 'line-chart'}, - 'column': {name: 'Bar', icon: 'bar-chart'}, - 'area': {name: 'Area', icon: 'area-chart'}, - 'pie': {name: 'Pie', icon: 'pie-chart'}, - 'scatter': {name: 'Scatter', icon: 'circle-o'} - }; - - scope.chartTypeChanged = function() { - _.each(scope.options.seriesOptions, function(options) { - options.type = scope.options.globalSeriesType; - }); - }; - - scope.xAxisScales = ['datetime', 'linear', 'logarithmic', 'category']; - scope.yAxisScales = ['linear', 'logarithmic', 'datetime']; - - var refreshColumns = function() { - scope.columns = scope.queryResult.getColumns(); - scope.columnNames = _.pluck(scope.columns, 'name'); - if (scope.columnNames.length > 0) { - _.each(_.difference(_.keys(scope.options.columnMapping), scope.columnNames), function(column) { - delete scope.options.columnMapping[column]; - }); - } - }; - - refreshColumns(); - - var refreshColumnsAndForm = function() { - refreshColumns(); - if (!scope.queryResult.getData() || scope.queryResult.getData().length == 0 || scope.columns.length == 0) - return; - scope.form.yAxisColumns = _.intersection(scope.form.yAxisColumns, scope.columnNames); - if (!_.contains(scope.columnNames, scope.form.xAxisColumn)) - scope.form.xAxisColumn = undefined; - if (!_.contains(scope.columnNames, scope.form.groupby)) - scope.form.groupby = undefined; - } - - var refreshSeries = function() { - var seriesNames = _.pluck(scope.queryResult.getChartData(scope.options.columnMapping), 'name'); - var existing = _.keys(scope.options.seriesOptions); - _.each(_.difference(seriesNames, existing), function(name) { - scope.options.seriesOptions[name] = { - 'type': scope.options.globalSeriesType, - 'yAxis': 0, - }; - scope.form.seriesList.push(name); - }); - _.each(_.difference(existing, seriesNames), function(name) { - scope.form.seriesList = _.without(scope.form.seriesList, name) - delete scope.options.seriesOptions[name]; - }); - }; - - scope.$watch('options.columnMapping', function() { - if (scope.queryResult.status === "done") { - refreshSeries(); - } - }, true); - - scope.$watch(function() {return [scope.queryResult.getId(), scope.queryResult.status];}, function(changed) { - if (!changed[0] || changed[1] !== "done") { - return; - } - - refreshColumnsAndForm(); - refreshSeries(); - }, true); - - scope.form = { - yAxisColumns: [], - seriesList: _.sortBy(_.keys(scope.options.seriesOptions), function(name) { - return scope.options.seriesOptions[name].zIndex; - }) - }; - - scope.$watchCollection('form.seriesList', function(value, old) { - _.each(value, function(name, index) { - scope.options.seriesOptions[name].zIndex = index; - scope.options.seriesOptions[name].index = 0; // is this needed? - }); - }); - - var setColumnRole = function(role, column) { - scope.options.columnMapping[column] = role; - } - var unsetColumn = function(column) { - setColumnRole('unused', column); - } - - scope.$watchCollection('form.yAxisColumns', function(value, old) { - _.each(old, unsetColumn); - _.each(value, _.partial(setColumnRole, 'y')); - }); - - scope.$watch('form.xAxisColumn', function(value, old) { - if (old !== undefined) - unsetColumn(old); - if (value !== undefined) - setColumnRole('x', value); - }); - - scope.$watch('form.groupby', function(value, old) { - if (old !== undefined) - unsetColumn(old) - if (value !== undefined) { - setColumnRole('series', value); - } - }); - - if (!_.has(scope.options, 'legend')) { - scope.options.legend = {enabled: true}; - } - - if (!_.has(scope.options, 'bottomMargin')) { - scope.options.bottomMargin = 50; - } - - if (scope.columnNames) - _.each(scope.options.columnMapping, function(value, key) { - if (scope.columnNames.length > 0 && !_.contains(scope.columnNames, key)) - return; - if (value == 'x') - scope.form.xAxisColumn = key; - else if (value == 'y') - scope.form.yAxisColumns.push(key); - else if (value == 'series') - scope.form.groupby = key; - }); - } - } - }); -}()); diff --git a/rd_ui/app/scripts/visualizations/cohort.js b/rd_ui/app/scripts/visualizations/cohort.js deleted file mode 100644 index 9cff000cc4..0000000000 --- a/rd_ui/app/scripts/visualizations/cohort.js +++ /dev/null @@ -1,87 +0,0 @@ -(function () { - var cohortVisualization = angular.module('redash.visualization'); - - cohortVisualization.config(['VisualizationProvider', function (VisualizationProvider) { - - var editTemplate = ''; - var defaultOptions = { - 'timeInterval': 'daily' - }; - - VisualizationProvider.registerVisualization({ - type: 'COHORT', - name: 'Cohort', - renderTemplate: '', - editorTemplate: editTemplate, - defaultOptions: defaultOptions - }); - }]); - - cohortVisualization.directive('cohortRenderer', function () { - return { - restrict: 'E', - scope: { - queryResult: '=', - options: '=' - }, - template: "", - replace: false, - link: function ($scope, element, attrs) { - $scope.options.timeInterval = $scope.options.timeInterval || 'daily'; - - var updateCohort = function () { - if ($scope.queryResult.getData() === null) { - return; - } - - var sortedData = _.sortBy($scope.queryResult.getData(), function (r) { - return r['date'] + r['day_number']; - }); - - var grouped = _.groupBy(sortedData, "date"); - - var maxColumns = _.reduce(grouped, function (memo, data) { - return (data.length > memo) ? data.length : memo; - }, 0); - - var data = _.map(grouped, function (values, date) { - var row = [values[0].total]; - _.each(values, function (value) { - row.push(value.value); - }); - _.each(_.range(values.length, maxColumns), function () { - row.push(null); - }); - return row; - }); - - var initialDate = moment(sortedData[0].date).toDate(), - container = angular.element(element)[0]; - - Cornelius.draw({ - initialDate: initialDate, - container: container, - cohort: data, - title: null, - timeInterval: $scope.options.timeInterval, - labels: { - time: 'Time', - people: 'Users', - weekOf: 'Week of' - } - }); - } - - $scope.$watch('queryResult && queryResult.getData()', updateCohort); - $scope.$watch('options.timeInterval', updateCohort); - } - } - }); - cohortVisualization.directive('cohortEditor', function () { - return { - restrict: 'E', - templateUrl: '/views/visualizations/cohort_editor.html' - } - }); - -}()); diff --git a/rd_ui/app/scripts/visualizations/counter.js b/rd_ui/app/scripts/visualizations/counter.js deleted file mode 100644 index c2c3223e52..0000000000 --- a/rd_ui/app/scripts/visualizations/counter.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -(function() { - var module = angular.module('redash.visualization'); - - module.config(['VisualizationProvider', function(VisualizationProvider) { - var renderTemplate = - '' + - ''; - - var editTemplate = ''; - var defaultOptions = { - counterColName: 'counter', - rowNumber: 1, - targetRowNumber: 1 - }; - - VisualizationProvider.registerVisualization({ - type: 'COUNTER', - name: 'Counter', - renderTemplate: renderTemplate, - editorTemplate: editTemplate, - defaultOptions: defaultOptions - }); - } - ]); - - module.directive('counterRenderer', function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/counter.html', - link: function($scope, elm, attrs) { - var refreshData = function() { - var queryData = $scope.queryResult.getData(); - if (queryData) { - var rowNumber = $scope.visualization.options.rowNumber - 1; - var targetRowNumber = $scope.visualization.options.targetRowNumber - 1; - var counterColName = $scope.visualization.options.counterColName; - var targetColName = $scope.visualization.options.targetColName; - - if (counterColName) { - $scope.counterValue = queryData[rowNumber][counterColName]; - } - - if (targetColName) { - $scope.targetValue = queryData[targetRowNumber][targetColName]; - - if ($scope.targetValue) { - $scope.delta = $scope.counterValue - $scope.targetValue; - $scope.trendPositive = $scope.delta >= 0; - } - } else { - $scope.targetValue = null; - } - } - }; - - $scope.$watch("visualization.options", refreshData, true); - $scope.$watch("queryResult && queryResult.getData()", refreshData); - } - } - }); - - module.directive('counterEditor', function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/counter_editor.html' - } - }); - -})(); diff --git a/rd_ui/app/scripts/visualizations/map.js b/rd_ui/app/scripts/visualizations/map.js deleted file mode 100644 index a0033af918..0000000000 --- a/rd_ui/app/scripts/visualizations/map.js +++ /dev/null @@ -1,293 +0,0 @@ -'use strict'; - -(function() { - var module = angular.module('redash.visualization'); - - module.config(['VisualizationProvider', function(VisualizationProvider) { - var renderTemplate = - '' + - ''; - - var editTemplate = ''; - var defaultOptions = { - height: 500, - classify: 'none', - clusterMarkers: true - }; - - VisualizationProvider.registerVisualization({ - type: 'MAP', - name: 'Map', - renderTemplate: renderTemplate, - editorTemplate: editTemplate, - defaultOptions: defaultOptions - }); - } - ]); - - module.directive('mapRenderer', function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/map.html', - link: function($scope, elm, attrs) { - $scope.$watch('queryResult && queryResult.getData()', render, true); - $scope.$watch('visualization.options', render, true); - angular.element(window).on("resize", resize); - $scope.$watch('visualization.options.height', resize); - - var color = d3.scale.category10(); - var map = L.map(elm[0].children[0].children[0], {scrollWheelZoom: false}); - var mapControls = L.control.layers().addTo(map); - var layers = {}; - var tileLayer = L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(map); - - map.on('focus',function(){ - map.on('moveend', getBounds); - }); - - map.on('blur',function(){ - map.off('moveend', getBounds); - }); - - // Following line is used to avoid "Couldn't autodetect L.Icon.Default.imagePath" error - // https://github.com/Leaflet/Leaflet/issues/766#issuecomment-7741039 - L.Icon.Default.imagePath = L.Icon.Default.imagePath || "//api.tiles.mapbox.com/mapbox.js/v2.2.1/images"; - - - function resize() { - if (!map) return; - map.invalidateSize(false); - setBounds(); - } - - function setBounds (){ - var b = $scope.visualization.options.bounds; - - if(b){ - map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]); - } else if (layers){ - var allMarkers = _.flatten(_.map(_.values(layers), function(l) { return l.getLayers() })); - var group = new L.featureGroup(allMarkers); - map.fitBounds(group.getBounds()); - } - }; - - var createMarker = function(lat,lon){ - if (lat == null || lon == null) return; - - return L.marker([lat, lon]); - }; - - var heatpoint = function(lat, lon, color){ - if (lat == null || lon == null) return; - - var style = { - fillColor:color, - fillOpacity:0.9, - stroke:false - }; - - return L.circleMarker([lat,lon],style) - }; - - function getBounds() { - $scope.visualization.options.bounds = map.getBounds(); - } - - function createDescription(latCol, lonCol, row) { - var lat = row[latCol]; - var lon = row[lonCol]; - - var description = '
      '; - description += "
    • "+lat+ ", " + lon + ""; - - for (var k in row){ - if (!(k == latCol || k == lonCol)) { - description += "
    • " + k + ": " + row[k] + "
    • "; - } - } - - return description; - } - - function removeLayer(layer) { - if (layer) { - mapControls.removeLayer(layer); - map.removeLayer(layer); - } - } - - function addLayer(name, points) { - var latCol = $scope.visualization.options.latColName || 'lat'; - var lonCol = $scope.visualization.options.lonColName || 'lon'; - var classify = $scope.visualization.options.classify; - - var markers; - if ($scope.visualization.options.clusterMarkers) { - var color = $scope.visualization.options.groups[name].color; - var options = {}; - - if (classify) { - options.iconCreateFunction = function (cluster) { - var childCount = cluster.getChildCount(); - - var c = ' marker-cluster-'; - if (childCount < 10) { - c += 'small'; - } else if (childCount < 100) { - c += 'medium'; - } else { - c += 'large'; - } - - c = ''; - - - var style = 'color: white; background-color: '+color+';'; - - return L.divIcon({ html: '
      ' + childCount + '
      ', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) }); - } - } - - markers = L.markerClusterGroup(options); - } else { - markers = L.layerGroup(); - } - - // create markers - _.each(points, function(row) { - var marker; - - var lat = row[latCol]; - var lon = row[lonCol]; - - if (classify && classify != 'none') { - var color = $scope.visualization.options.groups[name].color; - marker = heatpoint(lat, lon, color); - } else { - marker = createMarker(lat, lon); - } - - if (!marker) return; - - marker.bindPopup(createDescription(latCol, lonCol, row)); - markers.addLayer(marker); - }); - - markers.addTo(map); - - layers[name] = markers; - mapControls.addOverlay(markers, name); - } - - function render() { - var queryData = $scope.queryResult.getData(); - var classify = $scope.visualization.options.classify; - - $scope.visualization.options.mapTileUrl = $scope.visualization.options.mapTileUrl || '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; - - tileLayer.setUrl($scope.visualization.options.mapTileUrl); - - if ($scope.visualization.options.clusterMarkers === undefined) { - $scope.visualization.options.clusterMarkers = true; - } - - if (queryData) { - var pointGroups; - if (classify && classify != 'none') { - pointGroups = _.groupBy(queryData, classify); - } else { - pointGroups = {'All': queryData}; - } - - var groupNames = _.keys(pointGroups); - var options = _.map(groupNames, function(group) { - if ($scope.visualization.options.groups && $scope.visualization.options.groups[group]) { - return $scope.visualization.options.groups[group]; - } - return {color: color(group)}; - }); - - $scope.visualization.options.groups = _.object(groupNames, options); - - _.each(layers, function(v, k) { - removeLayer(v); - }); - - _.each(pointGroups, function(v, k) { - addLayer(k, v); - }); - - setBounds(); - } - } - - } - } - }); - - module.directive('mapEditor', function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/map_editor.html', - link: function($scope, elm, attrs) { - $scope.currentTab = 'general'; - $scope.classify_columns = $scope.queryResult.columnNames.concat('none'); - $scope.mapTiles = [ - { - name: 'OpenStreetMap', - url: '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' - }, - { - name: 'OpenStreetMap BW', - url: '//{s}.tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png' - }, - { - name: 'OpenStreetMap DE', - url: '//{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png' - }, - { - name: 'OpenStreetMap FR', - url: '//{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png' - }, - { - name: 'OpenStreetMap Hot', - url: '//{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png' - }, - { - name: 'Thunderforest', - url: '//{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png' - }, - { - name: 'Thunderforest Spinal', - url: '//{s}.tile.thunderforest.com/spinal-map/{z}/{x}/{y}.png' - }, - { - name: 'OpenMapSurfer', - url: '//korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}' - }, - { - name: 'Stamen Toner', - url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png' - }, - { - name: 'Stamen Toner Background', - url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner-background/{z}/{x}/{y}.png' - }, - { - name: 'Stamen Toner Lite', - url: '//stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.png' - }, - { - name: 'OpenTopoMap', - url: '//{s}.tile.opentopomap.org/{z}/{x}/{y}.png' - } - ]; - } - } - }); - -})(); diff --git a/rd_ui/app/scripts/visualizations/pivot.js b/rd_ui/app/scripts/visualizations/pivot.js deleted file mode 100644 index 32e804923a..0000000000 --- a/rd_ui/app/scripts/visualizations/pivot.js +++ /dev/null @@ -1,65 +0,0 @@ -(function() { - var module = angular.module('redash.visualization'); - - module.directive('pivotTableRenderer', function () { - return { - restrict: 'E', - scope: { - queryResult: '=', - visualization: '=' - }, - template: "", - replace: false, - link: function($scope, element) { - $scope.$watch('queryResult && queryResult.getData()', function (data) { - if (!data) { - return; - } - - if ($scope.queryResult.getData() === null) { - } else { - // We need to give the pivot table its own copy of the data, because it changes - // it which interferes with other visualizations. - data = $.extend(true, [], $scope.queryResult.getRawData()); - var options = { - renderers: $.pivotUtilities.renderers, - onRefresh: function(config) { - var configCopy = $.extend(true, {}, config); - //delete some values which are functions - delete configCopy.aggregators; - delete configCopy.renderers; - delete configCopy.onRefresh; - //delete some bulky default values - delete configCopy.rendererOptions; - delete configCopy.localeStrings; - - if ($scope.visualization) { - $scope.visualization.options = configCopy; - } - } - }; - - if ($scope.visualization) { - $.extend(options, $scope.visualization.options); - } - $(element).pivotUI(data, options, true); - } - }); - } - }; - }); - - module.config(['VisualizationProvider', function (VisualizationProvider) { - var editTemplate = '
      '; - var defaultOptions = { - }; - - VisualizationProvider.registerVisualization({ - type: 'PIVOT', - name: 'Pivot Table', - renderTemplate: '', - editorTemplate: editTemplate, - defaultOptions: defaultOptions - }); - }]); -})(); diff --git a/rd_ui/app/scripts/visualizations/sankey.js b/rd_ui/app/scripts/visualizations/sankey.js deleted file mode 100644 index 20c7938cfb..0000000000 --- a/rd_ui/app/scripts/visualizations/sankey.js +++ /dev/null @@ -1,268 +0,0 @@ -(function() { - 'use strict'; - - var module = angular.module('redash.visualization'); - - module.directive('sankeyRenderer', function() { - return { - restrict: 'E', - link: function(scope, element) { - var refreshData = function() { - var queryData = scope.queryResult.getData(); - if (queryData) { - // do the render logic. - angular.element(element[0]).empty(); - createSankey(element[0], scope.visualization.options.height, queryData); - } - }; - - angular.element(window).on("resize", refreshData); - scope.$watch("queryResult && queryResult.getData()", refreshData); - scope.$watch('visualization.options.height', function(oldValue, newValue) { - if (oldValue !== newValue) { - refreshData(); - } - }); - } - } - }); - - module.directive('sankeyEditor', function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/sankey_editor.html' - } - }); - - module.config(['VisualizationProvider', function(VisualizationProvider) { - var renderTemplate = - ''; - - var editTemplate = ''; - var defaultOptions = { - height: 300 - }; - - VisualizationProvider.registerVisualization({ - type: 'SANKEY', - name: 'Sankey', - renderTemplate: renderTemplate, - editorTemplate: editTemplate, - defaultOptions: defaultOptions - }); - } - ]); - - function createSankey(element, height, data) { - var margin = {top: 10, right: 10, bottom: 10, left: 10}, - width = $(element).parent().width() - margin.left - margin.right, - height = height - margin.top - margin.bottom; - - data = graph(data); - - var formatNumber = d3.format(",.0f"); // zero decimal places - var format = function(d) { return formatNumber(d); }; - var color = d3.scale.category20(); - - // append the svg canvas to the page - var svg = d3.select(element).append("svg") - .attr("class", "sankey") - .attr("width", width + margin.left + margin.right) - .attr("height", height + margin.top + margin.bottom) - .append("g") - .attr("transform", - "translate(" + margin.left + "," + margin.top + ")"); - - // Set the sankey diagram properties - var sankey = d3.sankey() - .nodeWidth(15) - .nodePadding(10) - .size([width, height]); - - var path = sankey.link(); - - sankey - .nodes(data.nodes) - .links(data.links) - .layout(0); - - spreadNodes(height, data); - sankey.relayout(); - - // add in the links - var link = svg.append("g").selectAll(".link") - .data(data.links) - .enter().append("path") - .filter(function(link) { - return link.target.name != 'Exit'; - }) - .attr("class", "link") - .attr("d", path) - .style("stroke-width", function(d) { return Math.max(1, d.dy); }) - .sort(function(a, b) { return b.dy - a.dy; }); - - // add the link titles - link.append("title") - .text(function(d) { - return d.source.name + " → " + d.target.name + "\n" + format(d.value); - }); - - // add in the nodes - var node = svg.append("g").selectAll(".node") - .data(data.nodes) - .enter().append("g") - .filter(function(node) { - return node.name != 'Exit'; - }) - .attr("class", "node") - .attr("transform", function(d) { - return "translate(" + d.x + "," + d.y + ")"; - }) - .on("mouseover", nodeMouseOver) - .on("mouseout", nodeMouseOut); - - // add the rectangles for the nodes - node.append("rect") - .attr("height", function(d) { return d.dy; }) - .attr("width", sankey.nodeWidth()) - .style("fill", function(d) { - return d.color = color(d.name.replace(/ .*/, "")); - }) - .style("stroke", function(d) { - return d3.rgb(d.color).darker(2); - }) - .append("title").text(function(d) { - return d.name + "\n" + format(d.value); - }); - - // add in the title for the nodes - node.append("text") - .attr("x", -6) - .attr("y", function(d) { return d.dy / 2; }) - .attr("dy", ".35em") - .attr("text-anchor", "end") - .attr("transform", null) - .text(function(d) { return d.name; }) - .filter(function(d) { return d.x < width / 2; }) - .attr("x", 6 + sankey.nodeWidth()) - .attr("text-anchor", "start"); - - function nodeMouseOver(currentNode) { - var nodes = getConnectedNodes(currentNode); - nodes = _.pluck(nodes, 'id'); - node.filter(function(d) { - if (d === currentNode) { - return false; - } - - if (_.contains(nodes, d.id)) { - return false; - } - - return true; - }).style('opacity', 0.2); - link.filter(function(l) { - return !(_.include(currentNode.sourceLinks, l) || _.include(currentNode.targetLinks, l)); - }).style('opacity', 0.2); - } - - function nodeMouseOut(currentNode) { - node.style('opacity', 1); - link.style('opacity', 1); - } - - function spreadNodes(height, data) { - var nodesByBreadth = d3.nest() - .key(function(d) { return d.x; }) - .entries(data.nodes) - .map(function(d) { return d.values; }); - - nodesByBreadth.forEach(function(nodes) { - nodes = _.filter(_.sortBy(nodes, function(node) { return -node.value; }), function(node) { - return node.name !== 'Exit'; - }); - - var sum = d3.sum(nodes, function(o) { return o.dy; }); - var padding = (height - sum) / nodes.length; - - _.reduce(nodes, function(y0, node) { - node.y = y0; - return y0 + node.dy + padding; - }, 0); - }); - } - - function getConnectedNodes(node) { - // source link = this node is the source, I need the targets - var nodes = []; - _.each(node.sourceLinks, function(link) { - nodes.push(link.target); - }); - _.each(node.targetLinks, function(link) { - nodes.push(link.source); - }); - - return nodes; - } - - function graph(data) { - var nodesDict = {}; - var links = {}; - var nodes = []; - - var keys = _.sortBy(_.without(_.keys(data[0]), 'value'), _.identity); - - data.forEach(function(row) { - addLink(row[keys[0]], row[keys[1]], row.value, 1); - addLink(row[keys[1]], row[keys[2]], row.value, 2); - addLink(row[keys[2]], row[keys[3]], row.value, 3); - addLink(row[keys[3]], row[keys[4]], row.value, 4); - }); - - return {nodes: nodes, links: _.values(links)}; - - function normalizeName(name) { - if (name) { - return name; - } - - return 'Exit'; - } - - function getNode(name, level) { - name = normalizeName(name); - var key = name + ":" + String(level); - var node = nodesDict[key]; - if (!node) { - node = {name: name}; - var id = nodes.push(node) - 1; - node.id = id; - nodesDict[key] = node; - } - return node; - } - - function getLink(source, target) { - var link = links[[source, target]]; - if (!link) { - link = {target: target, source: source, value: 0}; - links[[source, target]] = link; - } - - return link; - } - - function addLink(sourceName, targetName, value, depth) { - if ((sourceName === '' || !sourceName) && depth > 1) { - return; - } - - var source = getNode(sourceName, depth); - var target = getNode(targetName, depth+1); - var link = getLink(source.id, target.id); - link.value += parseInt(value); - } - } - } - -})(); diff --git a/rd_ui/app/scripts/visualizations/sunburst_sequence.js b/rd_ui/app/scripts/visualizations/sunburst_sequence.js deleted file mode 100644 index 2565f229f7..0000000000 --- a/rd_ui/app/scripts/visualizations/sunburst_sequence.js +++ /dev/null @@ -1,495 +0,0 @@ -'use strict'; - -(function () { - var module = angular.module('redash.visualization'); - - module.directive('sunburstSequenceRenderer', function () { - return { - restrict: 'E', - link: function(scope, element) { - var sunburst = new Sunburst(scope, element); - - function resize() { - sunburst.remove(); - sunburst = new Sunburst(scope, element); - } - - angular.element(window).on("resize", resize); - scope.$watch('visualization.options.height', function(oldValue, newValue) { - if (oldValue !== newValue) { - resize(); - } - }); - } - } - }); - - module.directive('sunburstSequenceEditor', function () { - return { - restrict: 'E', - templateUrl: '/views/visualizations/sunburst_sequence_editor.html' - } - }); - - module.config(['VisualizationProvider', function (VisualizationProvider) { - var renderTemplate = - ''; - - var editTemplate = ''; - var defaultOptions = { - height: 300, - // - }; - - VisualizationProvider.registerVisualization({ - type: 'SUNBURST_SEQUENCE', - name: 'Sunburst Sequence', - renderTemplate: renderTemplate, - editorTemplate: editTemplate, - defaultOptions: defaultOptions - }); - } - ]); - - - // The following is based on @chrisrzhou's example from: http://bl.ocks.org/chrisrzhou/d5bdd8546f64ca0e4366. - function Sunburst(scope, element) { - this.element = element; - - var refreshData = function () { - var queryData = scope.queryResult.getData(); - if (queryData) { - render(queryData); - } - }; - - this.watches = []; - this.watches.push(scope.$watch("visualization.options", refreshData, true)); - this.watches.push(scope.$watch("queryResult && queryResult.getData()", refreshData)); - - var exitNode = "<<>>"; - // svg dimensions - var width = element[0].parentElement.clientWidth; - var height = scope.visualization.options.height; - var radius = Math.min(width, height) / 2; - - // Breadcrumb dimensions: width, height, spacing, width of tip/tail. - var b = { - w: width / 6, - h: 30, - s: 3, - t: 10 - }; - - // Legend dimensions: width, height, spacing, radius of rounded rect. - var li = { - w: 75, - h: 30, - s: 3, - r: 3 - }; - - // margins - var margin = { - top: radius, - bottom: 50, - left: radius, - right: 0 - }; - - /** - * Drawing variables: - * - * e.g. colors, totalSize, partitions, arcs - */ - // Mapping of nodes to colorscale. - var colors = d3.scale.category10(); - - // Total size of all nodes, to be used later when data is loaded - var totalSize = 0; - - // create d3.layout.partition - var partition = d3.layout.partition() - .size([2 * Math.PI, radius * radius]) - .value(function (d) { - return d.size; - }); - - // create arcs for drawing D3 paths - var arc = d3.svg.arc() - .startAngle(function (d) { - return d.x; - }) - .endAngle(function (d) { - return d.x + d.dx; - }) - .innerRadius(function (d) { - return Math.sqrt(d.y); - }) - .outerRadius(function (d) { - return Math.sqrt(d.y + d.dy); - }); - - - /** - * Define and initialize D3 select references and div-containers - * - * e.g. vis, breadcrumbs, lastCrumb, summary, sunburst, legend - */ - // create main vis selection - var vis = d3.select(element[0]) - .append("div").classed("vis-container", true) - .style("position", "relative") - .style("margin-top", "5px") - .style("height", height + 2 * b.h + "px"); - - // create and position breadcrumbs container and svg - var breadcrumbs = vis - .append("div").classed("breadcrumbs-container", true) - .append("svg") - .attr("width", width) - .attr("height", b.h) - .attr("fill", "white") - .attr("font-weight", 600); - - var marginLeft = (width - radius * 2) / 2; - - // create and position SVG - var sunburst = vis - .append("div").classed("sunburst-container", true) - .style('z-index', '2') - // .style("margin-left", marginLeft + "px") - .style("left", marginLeft + "px") - .style('position', 'absolute') - .append("svg") - .attr("width", width) - .attr("height", height) - .append("g") - .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); - - // create last breadcrumb element - var lastCrumb = breadcrumbs.append("text").classed("lastCrumb", true); - - // create and position summary container - var summary = vis - .append("div").classed("summary-container", true) - .style("position", "absolute") - .style("top", b.h + radius * 0.80 + "px") - .style("left", marginLeft + radius / 2 + "px") - .style("width", radius + "px") - .style("height", radius + "px") - .style("text-align", "center") - .style("font-size", "11px") - .style("color", "#666") - .style('z-index', '1'); - - refreshData(); - - /** - * Render process: - * - * 1) Load data - * 2) Build Tree - * 3) Draw visualization - */ - // render visualization - function render(data) { - var json = buildHierarchy(data); // build json tree - removeVisualization(); // remove existing visualization if any - createVisualization(json); // visualize json tree - } - - - /** - * Helper functions: - * - * @function removeVisualization(): removes existing SVG components - * @function createVisualization(json): create visualization from json tree structure - * @function colorMap(d): color nodes with colors mapping - * @function mouseover(d): mouseover function - * @function mouseleave(d): mouseleave function - * @function getAncestors(node): get ancestors of a specified node - * @function buildHierarchy(data): generate json nested structure from csv data input - */ - // removes existing SVG components - function removeVisualization() { - sunburst.selectAll(".nodePath").remove(); - // legend.selectAll("g").remove(); - } - - - // visualize json tree structure - function createVisualization(json) { - drawSunburst(json); // draw sunburst - // drawLegend(); // draw legend - }; - - // helper function colorMap - color gray if "end" is detected - function colorMap(d) { - return colors(d.name); - } - - - // helper function to draw the sunburst and breadcrumbs - function drawSunburst(json) { - // Build only nodes of a threshold "visible" sizes to improve efficiency - var nodes = partition.nodes(json) - .filter(function (d) { - return (d.dx > 0.005) && d.name !== exitNode; // 0.005 radians = 0.29 degrees - }); - - // this section is required to update the colors.domain() every time the data updates - var uniqueNames = (function (a) { - var output = []; - a.forEach(function (d) { - if (output.indexOf(d.name) === -1) output.push(d.name); - }); - return output; - })(nodes); - colors.domain(uniqueNames); // update domain colors - - // create path based on nodes - var path = sunburst.data([json]).selectAll("path") - .data(nodes).enter() - .append("path").classed("nodePath", true) - .attr("display", function (d) { - return d.depth ? null : "none"; - }) - .attr("d", arc) - .attr("fill", colorMap) - .attr("opacity", 1) - .attr("stroke", "white") - .on("mouseover", mouseover); - - - // // trigger mouse click over sunburst to reset visualization summary - vis.on("click", click); - - // Update totalSize of the tree = value of root node from partition. - totalSize = path.node().__data__.value; - } - - // helper function mouseover to handle mouseover events/animations and calculation of ancestor nodes etc - function mouseover(d) { - // build percentage string - var percentage = (100 * d.value / totalSize).toPrecision(3); - var percentageString = percentage + "%"; - if (percentage < 1) { - percentageString = "< 1.0%"; - } - - // update breadcrumbs (get all ancestors) - var ancestors = getAncestors(d); - updateBreadcrumbs(ancestors, percentageString); - - // update sunburst (Fade all the segments and highlight only ancestors of current segment) - sunburst.selectAll("path") - .attr("opacity", 0.3); - sunburst.selectAll("path") - .filter(function (node) { - return (ancestors.indexOf(node) >= 0); - }) - .attr("opacity", 1); - - // update summary - summary.html( - "Stage: " + d.depth + "
      " + - "" + percentageString + "
      " + - d.value + " of " + totalSize + "
      " - ); - - // display summary and breadcrumbs if hidden - summary.style("visibility", ""); - breadcrumbs.style("visibility", ""); - } - - - // helper function click to handle mouseleave events/animations - function click(d) { - // Deactivate all segments then retransition each segment to full opacity. - sunburst.selectAll("path").on("mouseover", null); - sunburst.selectAll("path") - .transition() - .duration(1000) - .attr("opacity", 1) - .each("end", function () { - d3.select(this).on("mouseover", mouseover); - }); - - // hide summary and breadcrumbs if visible - breadcrumbs.style("visibility", "hidden"); - summary.style("visibility", "hidden"); - } - - - // Return array of ancestors of nodes, highest first, but excluding the root. - function getAncestors(node) { - var path = []; - var current = node; - - while (current.parent) { - path.unshift(current); - current = current.parent; - } - return path; - } - - - // Generate a string representation for drawing a breadcrumb polygon. - function breadcrumbPoints(d, i) { - var points = []; - points.push("0,0"); - points.push(b.w + ",0"); - points.push(b.w + b.t + "," + (b.h / 2)); - points.push(b.w + "," + b.h); - points.push("0," + b.h); - - if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex. - points.push(b.t + "," + (b.h / 2)); - } - return points.join(" "); - } - - - // Update the breadcrumb breadcrumbs to show the current sequence and percentage. - function updateBreadcrumbs(ancestors, percentageString) { - // Data join, where primary key = name + depth. - var g = breadcrumbs.selectAll("g") - .data(ancestors, function (d) { - return d.name + d.depth; - }); - - // Add breadcrumb and label for entering nodes. - var breadcrumb = g.enter().append("g"); - - breadcrumb - .append("polygon").classed("breadcrumbs-shape", true) - .attr("points", breadcrumbPoints) - .attr("fill", colorMap); - - breadcrumb - .append("text").classed("breadcrumbs-text", true) - .attr("x", (b.w + b.t) / 2) - .attr("y", b.h / 2) - .attr("dy", "0.35em") - .attr("font-size", "10px") - .attr("text-anchor", "middle") - .text(function (d) { - return d.name; - }); - - // Set position for entering and updating nodes. - g.attr("transform", function (d, i) { - return "translate(" + i * (b.w + b.s) + ", 0)"; - }); - - // Remove exiting nodes. - g.exit().remove(); - - // Update percentage at the lastCrumb. - lastCrumb - .attr("x", (ancestors.length + 0.5) * (b.w + b.s)) - .attr("y", b.h / 2) - .attr("dy", "0.35em") - .attr("text-anchor", "middle") - .attr("fill", "black") - .attr("font-weight", 600) - .text(percentageString); - } - - function buildHierarchy(csv) { - var data = buildNodes(csv); - - // build tree - var root = { - name: "root", - children: [] - }; - - data.forEach(function (d) { - var nodes = d.nodes; - var size = parseInt(d.size); - - // build graph, nodes, and child nodes - var currentNode = root; - for (var j = 0; j < nodes.length; j++) { - var children = currentNode.children; - var nodeName = nodes[j]; - var isLeaf = j + 1 === nodes.length; - - - if (!children) { - currentNode.children = children = []; - children.push({ - name: exitNode, - size: currentNode.size - }) - } - - var childNode = _.find(children, function(child) { return child.name == nodeName }); - - if (isLeaf && childNode) { - childNode.children.push({ - name: exitNode, - size: size - }) - } else if (isLeaf) { - children.push({ - name: nodeName, - size: size - }) - } else { - if (!childNode) { - childNode = { - name: nodeName, - children: [] - }; - children.push(childNode); - } - - currentNode = childNode; - } - } - }); - - return root; - } - - function buildNodes(raw) { - var values; - - if (_.has(raw[0], 'sequence') && _.has(raw[0], 'stage') && _.has(raw[0], 'node') && _.has(raw[0], 'value')) { - - var grouped = _.groupBy(raw, 'sequence'); - - var values = _.map(grouped, function(value, key) { - var sorted = _.sortBy(value, 'stage'); - return { - size: value[0].value, - sequence: value[0].sequence, - nodes: _.pluck(sorted, 'node') - } - }); - } else { - var keys = _.sortBy(_.without(_.keys(raw[0]), 'value'), _.identity); - - values = _.map(raw, function(row, sequence) { - return { - size: row.value, - sequence: sequence, - nodes: _.compact(_.map(keys, function(key) { return row[key] })) - } - }) - } - - return values; - } - } - - Sunburst.prototype.remove = function() { - _.each(this.watches, function(unregister) { unregister() }); - angular.element(this.element[0]).empty('.vis-container'); - } - - -})(); diff --git a/rd_ui/app/scripts/visualizations/table.js b/rd_ui/app/scripts/visualizations/table.js deleted file mode 100644 index 4c1efe11c0..0000000000 --- a/rd_ui/app/scripts/visualizations/table.js +++ /dev/null @@ -1,109 +0,0 @@ -(function () { - var tableVisualization = angular.module('redash.visualization'); - - tableVisualization.config(['VisualizationProvider', function (VisualizationProvider) { - VisualizationProvider.registerVisualization({ - type: 'TABLE', - name: 'Table', - renderTemplate: '', - skipTypes: true - }); - }]); - - tableVisualization.directive('gridRenderer', function () { - return { - restrict: 'E', - scope: { - queryResult: '=', - itemsPerPage: '=' - }, - templateUrl: "/views/grid_renderer.html", - replace: false, - controller: ['$scope', '$filter', function ($scope, $filter) { - $scope.gridColumns = []; - $scope.gridData = []; - $scope.gridConfig = { - isPaginationEnabled: true, - itemsByPage: $scope.itemsPerPage || 15, - maxSize: 8 - }; - - $scope.$watch('queryResult && queryResult.getData()', function (data) { - if (!data) { - return; - } - - if ($scope.queryResult.getData() == null) { - $scope.gridColumns = []; - $scope.gridData = []; - $scope.filters = []; - } else { - $scope.filters = $scope.queryResult.getFilters(); - - var prepareGridData = function (data) { - var gridData = _.map(data, function (row) { - var newRow = {}; - _.each(row, function (val, key) { - newRow[$scope.queryResult.getColumnCleanName(key)] = val; - }) - return newRow; - }); - - return gridData; - }; - - $scope.gridData = prepareGridData($scope.queryResult.getData()); - - var columns = $scope.queryResult.getColumns(); - $scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) { - var columnDefinition = { - 'label': $scope.queryResult.getColumnFriendlyNames()[i], - 'map': col - }; - - var columnType = columns[i].type; - - if (columnType === 'integer') { - columnDefinition.formatFunction = 'number'; - columnDefinition.formatParameter = 0; - } else if (columnType === 'float') { - columnDefinition.formatFunction = 'number'; - columnDefinition.formatParameter = 2; - } else if (columnType === 'boolean') { - columnDefinition.formatFunction = function (value) { - if (value !== undefined) { - return "" + value; - } - return value; - }; - } else if (columnType === 'date') { - columnDefinition.formatFunction = function (value) { - if (value && moment.isMoment(value)) { - return value.format(clientConfig.dateFormat); - } - return value; - }; - } else if (columnType === 'datetime') { - columnDefinition.formatFunction = function (value) { - if (value && moment.isMoment(value)) { - return value.format(clientConfig.dateTimeFormat); - } - return value; - }; - } else { - columnDefinition.formatFunction = function (value) { - if (angular.isString(value)) { - value = $filter('linkify')(value); - } - return value; - } - } - - return columnDefinition; - }); - } - }); - }] - } - }) -}()); \ No newline at end of file diff --git a/rd_ui/app/scripts/visualizations/wordcloud.js b/rd_ui/app/scripts/visualizations/wordcloud.js deleted file mode 100644 index e2bc5404c4..0000000000 --- a/rd_ui/app/scripts/visualizations/wordcloud.js +++ /dev/null @@ -1,97 +0,0 @@ -(function () { - var wordCloudVisualization = angular.module('redash.visualization'); - - wordCloudVisualization.config(['VisualizationProvider', function (VisualizationProvider) { - VisualizationProvider.registerVisualization({ - type: 'WORD_CLOUD', - name: 'Word Cloud', - renderTemplate: '', - editorTemplate: '' - }); - }]); - - wordCloudVisualization.directive('wordCloudRenderer', function () { - return { - restrict: 'E', - link: function($scope, elem, attrs) { - - reloadCloud = function () { - - if (!angular.isDefined($scope.queryResult)) retun; - data = $scope.queryResult.getData(); - cloud = d3.cloud; - - wordsHash = {}; - if($scope.visualization.options.column){ - data.map(function(d) { - d[$scope.visualization.options.column] - .toString() - .split(' ') - .map(function(d) { - if (d in wordsHash) { - wordsHash[d]+=1; - } else { - wordsHash[d]=1; - } - }) - }) - } - - wordList = []; - for(var key in wordsHash) { - wordList.push({text: key, size: 10 + Math.pow(wordsHash[key],2)}); - } - - var fill = d3.scale.category20(); - - var layout = cloud() - .size([500, 500]) - .words(wordList) - .padding(5) - .rotate(function() { return ~~(Math.random() * 2) * 90; }) - .font("Impact") - .fontSize(function(d) { return d.size; }) - .on("end", draw); - - layout.start(); - - function draw(words) { - d3.select(elem[0].parentNode) - .select("svg") - .remove(); - - d3.select(elem[0].parentNode) - .append("svg") - .attr("width", layout.size()[0]) - .attr("height", layout.size()[1]) - .append("g") - .attr("transform", "translate(" + layout.size()[0] / 2 + "," + layout.size()[1] / 2 + ")") - .selectAll("text") - .data(words) - .enter().append("text") - .style("font-size", function(d) { return d.size + "px"; }) - .style("font-family", "Impact") - .style("fill", function(d, i) { return fill(i); }) - .attr("text-anchor", "middle") - .attr("transform", function(d) { - return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")"; - }) - .text(function(d) { return d.text; }); - } - - } - - $scope.$watch('queryResult && queryResult.getData()', reloadCloud); - $scope.$watch('visualization.options.column', reloadCloud); - } - } - }); - - wordCloudVisualization.directive('wordCloudEditor', function() { - return { - restrict: 'E', - templateUrl: '/views/visualizations/word_cloud_editor.html' - }; - }); - -})(); diff --git a/rd_ui/app/styles/select2-spinner.gif b/rd_ui/app/styles/select2-spinner.gif deleted file mode 100644 index 5b33f7e54f..0000000000 Binary files a/rd_ui/app/styles/select2-spinner.gif and /dev/null differ diff --git a/rd_ui/app/styles/select2.png b/rd_ui/app/styles/select2.png deleted file mode 100644 index 1d804ffb99..0000000000 Binary files a/rd_ui/app/styles/select2.png and /dev/null differ diff --git a/rd_ui/app/styles/select2x2.png b/rd_ui/app/styles/select2x2.png deleted file mode 100644 index 4bdd5c961d..0000000000 Binary files a/rd_ui/app/styles/select2x2.png and /dev/null differ diff --git a/rd_ui/app/vendor_scripts.html b/rd_ui/app/vendor_scripts.html deleted file mode 100644 index 9302888de2..0000000000 --- a/rd_ui/app/vendor_scripts.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/rd_ui/app/views/admin/outdated_queries.html b/rd_ui/app/views/admin/outdated_queries.html deleted file mode 100644 index 7ff0fae2ee..0000000000 --- a/rd_ui/app/views/admin/outdated_queries.html +++ /dev/null @@ -1,20 +0,0 @@ - - - -
      -
      - - - -
      - Last update: -
      - () -
      -
      diff --git a/rd_ui/app/views/admin/tasks.html b/rd_ui/app/views/admin/tasks.html deleted file mode 100644 index 06f12d2fc1..0000000000 --- a/rd_ui/app/views/admin/tasks.html +++ /dev/null @@ -1,25 +0,0 @@ - - - -
      -
      - - -
        - - - -
      - - - - -
      -
      - diff --git a/rd_ui/app/views/alerts/edit.html b/rd_ui/app/views/alerts/edit.html deleted file mode 100644 index ee3dc19749..0000000000 --- a/rd_ui/app/views/alerts/edit.html +++ /dev/null @@ -1,70 +0,0 @@ - - - -
      - - -
      -
      -
      -
      -
      - - - {{$select.selected.name}} - -
      -
      -
      -
      - -
      - - -
      - -
      -
      - -
      - -
      - -
      -

      {{queryResult.getData()[0][alert.options.column]}}

      -
      -
      -
      - -
      - -
      - -
      - -
      -
      -
      - -
      - -
      -
      -
      - -
      - - -
      -
      -
      -
      - -
      -
      -
      -
      diff --git a/rd_ui/app/views/alerts/list.html b/rd_ui/app/views/alerts/list.html deleted file mode 100644 index 348246dc68..0000000000 --- a/rd_ui/app/views/alerts/list.html +++ /dev/null @@ -1,11 +0,0 @@ - - New Alert - - -
      -
      - -
      -
      diff --git a/rd_ui/app/views/dashboard.html b/rd_ui/app/views/dashboard.html deleted file mode 100644 index a4b093afda..0000000000 --- a/rd_ui/app/views/dashboard.html +++ /dev/null @@ -1,121 +0,0 @@ - - - -
      - - - - - - - - -
      - This dashboard is archived and won't appear in the dashboards list or search results. -
      -
      - This dashboard is a draft. -
      - -
      - -
      - -
      -
      -
      -
      -
      -

      - {{query.name}} - - -

      -

      - {{query.name}} - -

      -
      -
      - -
      - - - -
      -
      -
      Error running query: {{queryResult.getError()}}
      -
      -
      - -
      -
      - -
      -
      - -
      - Updated: - - Updated: {{queryResult.getUpdatedAt() | dateTime}} - - -
      -
      - -
      -
      -
      -

      -

      - This widget requires access to a data source you don't have access to. -

      -
      -
      -
      - -
      -
      - -

      -
      -
      -
      -
      -
      diff --git a/rd_ui/app/views/dashboard_share.html b/rd_ui/app/views/dashboard_share.html deleted file mode 100644 index 760a5f0c6f..0000000000 --- a/rd_ui/app/views/dashboard_share.html +++ /dev/null @@ -1,17 +0,0 @@ - - diff --git a/rd_ui/app/views/directives/input_errors.html b/rd_ui/app/views/directives/input_errors.html deleted file mode 100644 index 2bb72ac65a..0000000000 --- a/rd_ui/app/views/directives/input_errors.html +++ /dev/null @@ -1,5 +0,0 @@ -
      - This field is required. - This field is too short. - This needs to be a valid email. -
      diff --git a/rd_ui/app/views/directives/queries_list.html b/rd_ui/app/views/directives/queries_list.html deleted file mode 100644 index 9efb61ea8b..0000000000 --- a/rd_ui/app/views/directives/queries_list.html +++ /dev/null @@ -1,30 +0,0 @@ -
      - - - - - - - - - - - - - - - - - - - - - -
      NameCreated ByCreated AtRuntimeLast Executed AtUpdate Schedule
      {{query.name}}{{query.user.name}}{{query.created_at | dateTime}}{{query.runtime | durationHumanize}}{{query.retrieved_at | dateTime}}{{query.schedule | scheduleHumanize}}
      - -
      - -
      -
      diff --git a/rd_ui/app/views/directives/visualization_editor.html b/rd_ui/app/views/directives/visualization_editor.html deleted file mode 100644 index be50e4a4c1..0000000000 --- a/rd_ui/app/views/directives/visualization_editor.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/rd_ui/app/views/edit_dashboard.html b/rd_ui/app/views/edit_dashboard.html deleted file mode 100644 index 77020d99c6..0000000000 --- a/rd_ui/app/views/edit_dashboard.html +++ /dev/null @@ -1,25 +0,0 @@ - \ No newline at end of file diff --git a/rd_ui/app/views/edit_text_box_form.html b/rd_ui/app/views/edit_text_box_form.html deleted file mode 100644 index bd47de6556..0000000000 --- a/rd_ui/app/views/edit_text_box_form.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/rd_ui/app/views/grid_renderer.html b/rd_ui/app/views/grid_renderer.html deleted file mode 100644 index 012a0e6ffa..0000000000 --- a/rd_ui/app/views/grid_renderer.html +++ /dev/null @@ -1,5 +0,0 @@ -
      - -
      diff --git a/rd_ui/app/views/groups/edit_group_form.html b/rd_ui/app/views/groups/edit_group_form.html deleted file mode 100644 index afa3774a88..0000000000 --- a/rd_ui/app/views/groups/edit_group_form.html +++ /dev/null @@ -1,12 +0,0 @@ - - - \ No newline at end of file diff --git a/rd_ui/app/views/groups/list.html b/rd_ui/app/views/groups/list.html deleted file mode 100644 index 72f2f9a41e..0000000000 --- a/rd_ui/app/views/groups/list.html +++ /dev/null @@ -1,13 +0,0 @@ - -
      -
      -

      - New Group -

      - - -
      -
      -
      diff --git a/rd_ui/app/views/new_widget_form.html b/rd_ui/app/views/new_widget_form.html deleted file mode 100644 index 4aa2575bf5..0000000000 --- a/rd_ui/app/views/new_widget_form.html +++ /dev/null @@ -1,59 +0,0 @@ - diff --git a/rd_ui/app/views/queries.html b/rd_ui/app/views/queries.html deleted file mode 100644 index 7bb2ff13fc..0000000000 --- a/rd_ui/app/views/queries.html +++ /dev/null @@ -1,6 +0,0 @@ -
      - - - - -
      diff --git a/rd_ui/app/views/queries_query_name_cell.html b/rd_ui/app/views/queries_query_name_cell.html deleted file mode 100644 index 8f0db69e56..0000000000 --- a/rd_ui/app/views/queries_query_name_cell.html +++ /dev/null @@ -1 +0,0 @@ -{{dataRow.name}} \ No newline at end of file diff --git a/rd_ui/app/views/queries_search_results.html b/rd_ui/app/views/queries_search_results.html deleted file mode 100644 index ef48c4d28f..0000000000 --- a/rd_ui/app/views/queries_search_results.html +++ /dev/null @@ -1,19 +0,0 @@ -
      -
      -

      -

      -
      - -
      - -
      -

      - - -
      - -
      diff --git a/rd_ui/app/views/query_snippets/list.html b/rd_ui/app/views/query_snippets/list.html deleted file mode 100644 index e0e7b5ac9f..0000000000 --- a/rd_ui/app/views/query_snippets/list.html +++ /dev/null @@ -1,13 +0,0 @@ - -
      -
      -

      - New Snippet -

      - - -
      -
      -
      diff --git a/rd_ui/app/views/query_snippets/show.html b/rd_ui/app/views/query_snippets/show.html deleted file mode 100644 index 2dfbf9c43d..0000000000 --- a/rd_ui/app/views/query_snippets/show.html +++ /dev/null @@ -1,36 +0,0 @@ - - - -
      - - - - -
      -
      - - -
      - -
      - - -
      - -
      - -
      {{snippet.snippet}}
      -
      -
      - -
      - - -
      - - Created by: {{snippet.user.name}} - -
      - -
      -
      diff --git a/rd_ui/app/views/schedule_form.html b/rd_ui/app/views/schedule_form.html deleted file mode 100644 index 5710cdd98c..0000000000 --- a/rd_ui/app/views/schedule_form.html +++ /dev/null @@ -1,18 +0,0 @@ - - diff --git a/rd_ui/app/views/users/list.html b/rd_ui/app/views/users/list.html deleted file mode 100644 index f3430208a6..0000000000 --- a/rd_ui/app/views/users/list.html +++ /dev/null @@ -1,13 +0,0 @@ - -
      -
      -

      - New User -

      - - -
      -
      -
      diff --git a/rd_ui/app/views/users/new.html b/rd_ui/app/views/users/new.html deleted file mode 100644 index c32dfd4d35..0000000000 --- a/rd_ui/app/views/users/new.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/rd_ui/app/views/users/new_user_form.html b/rd_ui/app/views/users/new_user_form.html deleted file mode 100644 index 43770db042..0000000000 --- a/rd_ui/app/views/users/new_user_form.html +++ /dev/null @@ -1,26 +0,0 @@ -
      -
      - - - -
      -
      - - - -
      - -
      - -
      - -
      -

      - The user has been created and should receive an invite email soon. -

      -

      - You can use the following link to invite them yourself:
      - {{user.invite_link}} -

      -
      -
      diff --git a/rd_ui/app/views/visualization-embed.html b/rd_ui/app/views/visualization-embed.html deleted file mode 100644 index 0182a1001f..0000000000 --- a/rd_ui/app/views/visualization-embed.html +++ /dev/null @@ -1,48 +0,0 @@ -
      -
      -

      -

      - - {{query.name}} - -

      - -
      -
      -

      -
      - - - - -
      -
      - Updated: - Updated: {{queryResult.getUpdatedAt() | dateTime}} -
      -
      - - - - -
      - - -
      -
      -
      -
      diff --git a/rd_ui/app/views/visualizations/boxplot.html b/rd_ui/app/views/visualizations/boxplot.html deleted file mode 100644 index 7f02a45896..0000000000 --- a/rd_ui/app/views/visualizations/boxplot.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/rd_ui/app/views/visualizations/chart.html b/rd_ui/app/views/visualizations/chart.html deleted file mode 100644 index e9197b0b8a..0000000000 --- a/rd_ui/app/views/visualizations/chart.html +++ /dev/null @@ -1,3 +0,0 @@ -
      - -
      diff --git a/rd_ui/app/views/visualizations/edit_visualization.html b/rd_ui/app/views/visualizations/edit_visualization.html deleted file mode 100644 index 9bba1835da..0000000000 --- a/rd_ui/app/views/visualizations/edit_visualization.html +++ /dev/null @@ -1,44 +0,0 @@ -
      - - - -
      diff --git a/rd_ui/app/views/visualizations/sunburst_sequence.html b/rd_ui/app/views/visualizations/sunburst_sequence.html deleted file mode 100644 index d58db9e7bf..0000000000 --- a/rd_ui/app/views/visualizations/sunburst_sequence.html +++ /dev/null @@ -1,2 +0,0 @@ -
      -
      diff --git a/rd_ui/app/views/visualizations/word_cloud.html b/rd_ui/app/views/visualizations/word_cloud.html deleted file mode 100644 index b80e6e0678..0000000000 --- a/rd_ui/app/views/visualizations/word_cloud.html +++ /dev/null @@ -1,3 +0,0 @@ -
      - -
      diff --git a/rd_ui/test/.jshintrc b/rd_ui/test/.jshintrc deleted file mode 100644 index b1be025b81..0000000000 --- a/rd_ui/test/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "node": true, - "browser": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "immed": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "quotmark": "single", - "regexp": true, - "undef": true, - "unused": true, - "strict": true, - "trailing": true, - "smarttabs": true, - "globals": { - "after": false, - "afterEach": false, - "angular": false, - "before": false, - "beforeEach": false, - "browser": false, - "describe": false, - "expect": false, - "inject": false, - "it": false, - "jasmine": false, - "spyOn": false - } -} - diff --git a/rd_ui/test/karma.conf.js b/rd_ui/test/karma.conf.js deleted file mode 100644 index 352152b3ab..0000000000 --- a/rd_ui/test/karma.conf.js +++ /dev/null @@ -1,133 +0,0 @@ -// Karma configuration -// http://karma-runner.github.io/0.12/config/configuration-file.html -// Generated on 2014-07-30 using -// generator-karma 0.8.3 - -module.exports = function(config) { - 'use strict'; - - config.set({ - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - // base path, that will be used to resolve files and exclude - basePath: '../', - - // testing framework to use (jasmine/mocha/qunit/...) - frameworks: ['jasmine'], - - // list of files / patterns to load in the browser - files: [ - 'app/bower_components/jquery/jquery.js', - 'app/bower_components/jquery-ui/ui/jquery-ui.js', - - 'app/bower_components/angular/angular.js', - 'app/bower_components/angular-route/angular-route.js', - 'app/bower_components/angular-mocks/angular-mocks.js', - - 'app/bower_components/bootstrap/js/collapse.js', - 'app/bower_components/bootstrap/js/modal.js', - 'app/bower_components/angular-resource/angular-resource.js', - 'app/bower_components/underscore/underscore.js', - 'app/bower_components/moment/moment.js', - 'app/bower_components/angular-moment/angular-moment.js', - 'app/bower_components/codemirror/lib/codemirror.js', - 'app/bower_components/codemirror/addon/edit/matchbrackets.js', - 'app/bower_components/codemirror/addon/edit/closebrackets.js', - 'app/bower_components/codemirror/mode/sql/sql.js', - 'app/bower_components/codemirror/mode/javascript/javascript.js', - 'app/bower_components/angular-ui-codemirror/ui-codemirror.js', - 'app/bower_components/plotly/plotly.js', - 'app/bower_components/angular-plotly/src/angular-plotly.js', - 'app/bower_components/gridster/dist/jquery.gridster.js', - 'app/bower_components/angular-growl/build/angular-growl.js', - 'app/bower_components/pivottable/dist/pivot.js', - 'app/bower_components/cornelius/src/cornelius.js', - 'app/bower_components/mousetrap/mousetrap.js', - 'app/bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.js', - 'app/bower_components/select2/select2.js', - 'app/bower_components/angular-ui-select2/src/select2.js', - 'app/bower_components/angular-ui-select/dist/select.js', - 'app/bower_components/underscore.string/lib/underscore.string.js', - 'app/bower_components/marked/lib/marked.js', - 'app/scripts/ng_smart_table.js', - 'app/scripts/ui-bootstrap-tpls-0.5.0.min.js', - 'app/bower_components/bucky/bucky.js', - 'app/bower_components/pace/pace.js', - 'app/bower_components/mustache/mustache.js', - - 'app/scripts/app.js', - 'app/scripts/services/services.js', - 'app/scripts/services/resources.js', - 'app/scripts/services/notifications.js', - 'app/scripts/services/dashboards.js', - 'app/scripts/controllers/controllers.js', - 'app/scripts/controllers/dashboard.js', - 'app/scripts/controllers/admin_controllers.js', - 'app/scripts/controllers/query_view.js', - 'app/scripts/controllers/query_source.js', - 'app/scripts/visualizations/base.js', - 'app/scripts/visualizations/chart.js', - 'app/scripts/visualizations/cohort.js', - 'app/scripts/visualizations/table.js', - 'app/scripts/visualizations/pivot.js', - 'app/scripts/directives/directives.js', - 'app/scripts/directives/query_directives.js', - 'app/scripts/directives/dashboard_directives.js', - 'app/scripts/directives/plotly.js', - 'app/scripts/filters.js', - - 'app/views/**/*.html', - - 'test/mocks/*.js', - 'test/unit/*.js' - ], - - // generate js files from html templates - preprocessors: { - 'app/views/**/*.html': 'ng-html2js' - }, - - // list of files / patterns to exclude - exclude: [], - - // web server port - port: 8080, - - // Start these browsers, currently available: - // - Chrome - // - ChromeCanary - // - Firefox - // - Opera - // - Safari (only Mac) - // - PhantomJS - // - IE (only Windows) - browsers: [ - 'PhantomJS' - ], - - // Which plugins to enable - plugins: [ - 'karma-phantomjs-launcher', - 'karma-jasmine', - 'karma-ng-html2js-preprocessor' - ], - - // Continuous Integration mode - // if true, it capture browsers, run tests and exit - singleRun: false, - - colors: true, - - // level of logging - // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG - logLevel: config.LOG_INFO, - - // Uncomment the following lines if you are using grunt's server to run the tests - // proxies: { - // '/': 'http://localhost:9000/' - // }, - // URL root prevent conflicts with the site root - // urlRoot: '_karma_' - }); -}; diff --git a/rd_ui/test/mocks/redash_mocks.js b/rd_ui/test/mocks/redash_mocks.js deleted file mode 100644 index ddd24c86e0..0000000000 --- a/rd_ui/test/mocks/redash_mocks.js +++ /dev/null @@ -1,108 +0,0 @@ -clientConfig = {}; -currentUser = { - id: 1, - name: 'John Mock', - email: 'john@example.com', - groups: ['default'], - permissions: [], - canEdit: function(object) { - var user_id = object.user_id || (object.user && object.user.id); - return user_id && (user_id == currentUser.id); - }, - hasPermission: function(permission) { - return this.permissions.indexOf(permission) != -1; - } -}; - - -angular.module('redashMocks', []) - .value('MockData', { - query: { - "ttl": -1, - "query": "select name from users;", - "id": 1803, - "description": "", - "name": "my test query", - "created_at": "2014-01-07T16:11:31.859528+02:00", - "query_hash": "c89c235bc73e462e9702debc56adc309", - - "user": { - "email": "amirn@everything.me", - "id": 48, - "name": "Amir Nissim" - }, - - "visualizations": [{ - "description": "", - "options": {}, - "type": "TABLE", - "id": 636, - "name": "Table" - }], - - "api_key": "123456789", - - "data_source_id": 1, - - "latest_query_data_id": 106632, - - "latest_query_data": { - "retrieved_at": "2014-07-29T10:49:10.951364+03:00", - "query_hash": "c89c235bc73e462e9702debc56adc309", - "query": "select name from users;", - "runtime": 0.0139260292053223, - "data": { - "rows": [{ - "name": "Amir Nissim" - }, { - "name": "Arik Fraimovich" - }], - "columns": [{ - "friendly_name": "name", - "type": null, - "name": "name" - }, { - "friendly_name": "mail::filter", - "type": null, - "name": "mail::filter" - }] - }, - "id": 106632, - "data_source_id": 1 - } - - }, - - queryResult: { - "job": {}, - "query_result": { - "retrieved_at": "2014-08-04T13:33:45.563486+03:00", - "query_hash": "9951c38c9cf00e6ee8aecce026b51c19", - "query": "select name as \"name::filter\" from users", - "runtime": 0.00896096229553223, - "data": { - "rows": [], - "columns": [{ - "friendly_name": "name::filter", - "type": null, - "name": "name::filter" - }] - }, - "id": 106673, - "data_source_id": 1 - }, - "status": "done", - "filters": [], - "filterFreeze": "test@example.com", - "updatedAt": "2014-08-05T13:13:40.833Z", - "columnNames": ["name::filter"], - "filteredData": [{ - "name::filter": "test@example.com" - }], - "columns": [{ - "friendly_name": "name::filter", - "type": null, - "name": "name::filter" - }] - } - }); diff --git a/rd_ui/test/runner.html b/rd_ui/test/runner.html deleted file mode 100644 index f4a00a12b0..0000000000 --- a/rd_ui/test/runner.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - End2end Test Runner - - - - - - \ No newline at end of file diff --git a/rd_ui/test/unit/example_test.js b/rd_ui/test/unit/example_test.js deleted file mode 100644 index 8079f8b1a2..0000000000 --- a/rd_ui/test/unit/example_test.js +++ /dev/null @@ -1,5 +0,0 @@ -describe('example test', function() { - it('should expect the obvious', function() { - expect(0).toBe(0); - }); -}); diff --git a/rd_ui/test/unit/test_query_view.js b/rd_ui/test/unit/test_query_view.js deleted file mode 100644 index 4e6b3dfe1f..0000000000 --- a/rd_ui/test/unit/test_query_view.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -describe('QueryViewCtrl', function() { - var scope; - var MockData; - - beforeEach(module('redash', 'redashMocks')); - - beforeEach(inject(function($injector, $controller, $rootScope, Query, _MockData_) { - MockData = _MockData_; - scope = $rootScope.$new(); - - var route = { - current: { - locals: { - query: new Query(MockData.query) - } - } - }; - - $controller('QueryViewCtrl', {$scope: scope, $route: route}); - })); - - it('should have a query', function() { - expect(scope.query).toBeDefined(); - }); - - it('should update the executing state', function() { - expect(scope.queryExecuting).toBe(false); - scope.executeQuery(); - expect(scope.queryExecuting).toBe(true); - }); - -}); diff --git a/rd_ui/test/unit/test_visualization_renderer.js b/rd_ui/test/unit/test_visualization_renderer.js deleted file mode 100644 index 22859e760b..0000000000 --- a/rd_ui/test/unit/test_visualization_renderer.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -describe('VisualizationRenderer', function() { - var element; - var scope; - - var filters = [{ - "name": "name::filter", - "friendlyName": "Name", - "values": ["test@example.com", "amirn@example.com"], - "multiple": false - }]; - - beforeEach(module('redash', 'redashMocks')); - - // loading templates - beforeEach(module('app/views/grid_renderer.html', - 'app/views/visualizations/filters.html')); - - // serving templates - beforeEach(inject(function($httpBackend, $templateCache) { - $httpBackend.whenGET('/views/grid_renderer.html') - .respond($templateCache.get('app/views/grid_renderer.html')); - - $httpBackend.whenGET('/views/visualizations/filters.html') - .respond($templateCache.get('app/views/visualizations/filters.html')); - })); - - // directive setup - beforeEach(inject(function($rootScope, $compile, MockData, QueryResult) { - var qr = new QueryResult(MockData.queryResult) - qr.filters = filters; - - $rootScope.queryResult = qr; - - element = angular.element( - '' + - ''); - })); - - - describe('scope', function() { - beforeEach(inject(function($rootScope, $compile) { - $compile(element)($rootScope); - - // we will test the isolated scope of the directive - scope = element.isolateScope(); - scope.$digest(); - })); - - it('should have filters', function() { - expect(scope.filters).toBeDefined(); - }); - }); - - - /*describe('URL binding', function() { - - beforeEach(inject(function($rootScope, $compile, $location) { - spyOn($location, 'search').andCallThrough(); - - // set initial search - var initialSearch = {}; - initialSearch[filters[0].friendlyName] = filters[0].values[0]; - $location.search('filters', initialSearch); - - $compile(element)($rootScope); - - // we will test the isolated scope of the directive - scope = element.isolateScope(); - scope.$digest(); - })); - - it('should update scope from URL', - inject(function($location) { - expect($location.search).toHaveBeenCalled(); - expect(scope.filters[0].current).toEqual(filters[0].values[0]); - })); - - it('should update URL from scope', - inject(function($location) { - scope.filters[0].current = 'newValue'; - scope.$digest(); - - var searchFilters = angular.fromJson($location.search().filters); - expect(searchFilters[filters[0].friendlyName]).toEqual('newValue'); - })); - });*/ -}); diff --git a/redash/handlers/__init__.py b/redash/handlers/__init__.py index f0771c33f9..3e6ae8b43b 100644 --- a/redash/handlers/__init__.py +++ b/redash/handlers/__init__.py @@ -1,23 +1,12 @@ -from flask import jsonify, url_for +from flask import jsonify from flask_login import login_required -from redash import settings -from redash.authentication.org_resolving import current_org from redash.handlers.api import api from redash.handlers.base import routes from redash.monitor import get_status from redash.permissions import require_super_admin -def base_href(): - if settings.MULTI_ORG: - base_href = url_for('redash.index', _external=True, org_slug=current_org.slug) - else: - base_href = url_for('redash.index', _external=True) - - return base_href - - @routes.route('/ping', methods=['GET']) def ping(): return 'PONG.' @@ -31,13 +20,6 @@ def status_api(): return jsonify(status) -@routes.app_context_processor -def inject_variables(): - return dict(name=settings.NAME, - logo_url=settings.LOGO_URL, - base_href=base_href()) - - def init_app(app): from redash.handlers import embed, queries, static, authentication, admin app.register_blueprint(routes) diff --git a/redash/handlers/admin.py b/redash/handlers/admin.py index 2c61be3677..1a24a75b43 100644 --- a/redash/handlers/admin.py +++ b/redash/handlers/admin.py @@ -1,18 +1,13 @@ import json -from flask import current_app -from flask_login import login_required +from flask_login import login_required from redash import models, redis_connection -from redash.utils import json_dumps from redash.handlers import routes +from redash.handlers.base import json_response from redash.permissions import require_super_admin from redash.tasks.queries import QueryTaskTracker -def json_response(response): - return current_app.response_class(json_dumps(response), mimetype='application/json') - - @routes.route('/api/admin/queries/outdated', methods=['GET']) @require_super_admin @login_required @@ -45,4 +40,3 @@ def queries_tasks(): } return json_response(response) - diff --git a/redash/handlers/api.py b/redash/handlers/api.py index 2db4f63c71..0e1003c7e6 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -6,7 +6,7 @@ from redash.handlers.base import org_scoped_rule from redash.handlers.permissions import ObjectPermissionsListResource, CheckPermissionResource from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource -from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource +from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource, PublicDashboardResource from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource from redash.handlers.events import EventResource from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource @@ -47,6 +47,7 @@ def json_representation(data, code, headers=None): api.add_org_resource(DashboardListResource, '/api/dashboards', endpoint='dashboards') api.add_org_resource(RecentDashboardsResource, '/api/dashboards/recent', endpoint='recent_dashboards') api.add_org_resource(DashboardResource, '/api/dashboards/', endpoint='dashboard') +api.add_org_resource(PublicDashboardResource, '/api/dashboards/public/', endpoint='public_dashboard') api.add_org_resource(DashboardShareResource, '/api/dashboards//share', endpoint='dashboard_share') api.add_org_resource(DataSourceTypeListResource, '/api/data_sources/types', endpoint='data_source_types') diff --git a/redash/handlers/authentication.py b/redash/handlers/authentication.py index 0600dc2adc..9c492f3e05 100644 --- a/redash/handlers/authentication.py +++ b/redash/handlers/authentication.py @@ -1,12 +1,16 @@ +import hashlib import logging -from flask import render_template, request, redirect, url_for, flash -from flask_login import current_user, login_user, logout_user -from redash import models, settings, limiter -from redash.handlers import routes -from redash.handlers.base import org_scoped_rule +from flask import flash, redirect, render_template, request, url_for +from flask_login import current_user, login_required, login_user, logout_user +from redash import __version__, limiter, models, settings from redash.authentication import current_org, get_login_url -from redash.authentication.account import validate_token, BadSignature, SignatureExpired, send_password_reset_email +from redash.authentication.account import (BadSignature, SignatureExpired, + send_password_reset_email, + validate_token) +from redash.handlers import routes +from redash.handlers.base import json_response, org_scoped_rule +from redash.version_check import get_latest_version logger = logging.getLogger(__name__) @@ -125,3 +129,65 @@ def login(org_slug=None): def logout(org_slug=None): logout_user() return redirect(get_login_url(next=None)) + + +def base_href(): + if settings.MULTI_ORG: + base_href = url_for('redash.index', _external=True, org_slug=current_org.slug) + else: + base_href = url_for('redash.index', _external=True) + + return base_href + + +def client_config(): + if not isinstance(current_user._get_current_object(), models.ApiUser) and current_user.is_authenticated: + client_config = { + 'newVersionAvailable': get_latest_version(), + 'version': __version__ + } + else: + client_config = {} + + client_config.update(settings.COMMON_CLIENT_CONFIG) + client_config.update({ + 'basePath': base_href() + }) + + return client_config + + +@routes.route(org_scoped_rule('/api/config'), methods=['GET']) +def config(org_slug=None): + return json_response({ + 'org_slug': current_org.slug, + 'client_config': client_config() + }) + + +@routes.route(org_scoped_rule('/api/session'), methods=['GET']) +@login_required +def session(org_slug=None): + if not isinstance(current_user._get_current_object(), models.ApiUser): + email_md5 = hashlib.md5(current_user.email.lower()).hexdigest() + gravatar_url = "https://www.gravatar.com/avatar/%s?s=40" % email_md5 + + user = { + 'gravatar_url': gravatar_url, + 'id': current_user.id, + 'name': current_user.name, + 'email': current_user.email, + 'groups': current_user.groups, + 'permissions': current_user.permissions + } + else: + user = { + 'permissions': [], + 'apiKey': current_user.id + } + + return json_response({ + 'user': user, + 'org_slug': current_org.slug, + 'client_config': client_config() + }) diff --git a/redash/handlers/base.py b/redash/handlers/base.py index 2d963ff773..f5c5506ccd 100644 --- a/redash/handlers/base.py +++ b/redash/handlers/base.py @@ -1,13 +1,14 @@ import time -from flask import request, Blueprint -from flask_restful import Resource, abort + +from flask import Blueprint, current_app, request from flask_login import current_user, login_required +from flask_restful import Resource, abort from peewee import DoesNotExist - from redash import settings -from redash.tasks import record_event as record_event_task -from redash.models import ApiUser from redash.authentication import current_org +from redash.models import ApiUser +from redash.tasks import record_event as record_event_task +from redash.utils import json_dumps routes = Blueprint('redash', __name__, template_folder=settings.fix_assets_path('templates')) @@ -99,3 +100,7 @@ def org_scoped_rule(rule): return "/{}".format(rule) return rule + + +def json_response(response): + return current_app.response_class(json_dumps(response), mimetype='application/json') diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py index 39109af670..34c1500917 100644 --- a/redash/handlers/dashboards.py +++ b/redash/handlers/dashboards.py @@ -1,13 +1,14 @@ -from flask import request, url_for -from flask_restful import abort - -from funcy import distinct, take, project from itertools import chain -from redash import models -from redash.models import ConflictDetectedError -from redash.permissions import require_permission, require_admin_or_owner, require_object_modify_permission, can_modify +from flask import request, url_for +from flask_restful import abort +from funcy import distinct, project, take +from redash import models, serializers from redash.handlers.base import BaseResource, get_object_or_404 +from redash.models import ConflictDetectedError +from redash.permissions import (can_modify, require_admin_or_owner, + require_object_modify_permission, + require_permission) class RecentDashboardsResource(BaseResource): @@ -25,17 +26,17 @@ def get(self): class DashboardListResource(BaseResource): @require_permission('list_dashboards') def get(self): - dashboards = [d.to_dict() for d in models.Dashboard.all(self.current_org, self.current_user.groups, self.current_user)] - return dashboards + results = models.Dashboard.all(self.current_org, self.current_user.groups, self.current_user) + return [q.to_dict() for q in results] @require_permission('create_dashboard') def post(self): dashboard_properties = request.get_json(force=True) - dashboard = models.Dashboard(name=dashboard_properties['name'], - org=self.current_org, - user=self.current_user, - is_draft=True, - layout='[]') + dashboard = models.Dashboard.create(name=dashboard_properties['name'], + org=self.current_org, + user=self.current_user, + is_draft=True, + layout='[]') return dashboard.to_dict() @@ -83,6 +84,17 @@ def delete(self, dashboard_slug): return dashboard.to_dict(with_widgets=True, user=self.current_user) +class PublicDashboardResource(BaseResource): + def get(self, token): + if not isinstance(self.current_user, models.ApiUser): + api_key = get_object_or_404(models.ApiKey.get_by_api_key, token) + dashboard = api_key.object + else: + dashboard = self.current_user.object + + return serializers.public_dashboard(dashboard) + + class DashboardShareResource(BaseResource): def post(self, dashboard_id): dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org) @@ -112,5 +124,3 @@ def delete(self, dashboard_id): 'object_id': dashboard.id, 'object_type': 'dashboard', }) - - diff --git a/redash/handlers/embed.py b/redash/handlers/embed.py index 0eabe2bac2..47731b9795 100644 --- a/redash/handlers/embed.py +++ b/redash/handlers/embed.py @@ -1,21 +1,22 @@ import json -import pystache -import time import logging +import time -from funcy import project -from flask import render_template, request -from flask_login import login_required, current_user +import pystache +from authentication import current_org +from flask import current_app, render_template, request, safe_join, send_file +from flask_login import current_user, login_required from flask_restful import abort - -from redash import models, settings, utils -from redash import serializers -from redash.utils import json_dumps, collect_parameters_from_request, gen_query_hash +from funcy import project +from redash import models, serializers, settings, utils from redash.handlers import routes -from redash.handlers.base import org_scoped_rule, record_event, get_object_or_404 +from redash.handlers.base import (get_object_or_404, org_scoped_rule, + record_event) from redash.handlers.query_results import collect_query_parameters from redash.permissions import require_access, view_only -from authentication import current_org +from redash.utils import (collect_parameters_from_request, gen_query_hash, + json_dumps) + # # Run a parameterized query synchronously and return the result @@ -68,35 +69,6 @@ def run_query_sync(data_source, parameter_values, query_text, max_age=0): @routes.route(org_scoped_rule('/embed/query//visualization/'), methods=['GET']) @login_required def embed(query_id, visualization_id, org_slug=None): - query = models.Query.get_by_id_and_org(query_id, current_org) - require_access(query.groups, current_user, view_only) - vis = query.visualizations.where(models.Visualization.id == visualization_id).first() - qr = {} - - parameter_values = collect_parameters_from_request(request.args) - - if vis is not None: - vis = vis.to_dict() - qr = query.latest_query_data - if settings.ALLOW_PARAMETERS_IN_EMBEDS == True and len(parameter_values) > 0: - # run parameterized query - # - # WARNING: Note that the external query parameters - # are a potential risk of SQL injections. - # - max_age = int(request.args.get('maxAge', 0)) - results = run_query_sync(query.data_source, parameter_values, query.query, max_age=max_age) - if results is None: - abort(400, message="Unable to get results for this query") - else: - qr = {"data": json.loads(results)} - elif qr is None: - abort(400, message="No Results for this query") - else: - qr = qr.to_dict() - else: - abort(404, message="Visualization not found.") - record_event(current_org, current_user, { 'action': 'view', 'object_id': visualization_id, @@ -106,54 +78,22 @@ def embed(query_id, visualization_id, org_slug=None): 'referer': request.headers.get('Referer') }) - client_config = {} - client_config.update(settings.COMMON_CLIENT_CONFIG) - - qr = project(qr, ('data', 'id', 'retrieved_at')) - vis = project(vis, ('description', 'name', 'id', 'options', 'query', 'type', 'updated_at')) - vis['query'] = project(vis['query'], ('created_at', 'description', 'name', 'id', 'latest_query_data_id', 'name', 'updated_at')) - - return render_template("embed.html", - client_config=json_dumps(client_config), - visualization=json_dumps(vis), - query_result=json_dumps(qr)) - + full_path = safe_join(settings.STATIC_ASSETS_PATHS[-2], 'index.html') + return send_file(full_path, **dict(cache_timeout=0, conditional=True)) @routes.route(org_scoped_rule('/public/dashboards/'), methods=['GET']) @login_required def public_dashboard(token, org_slug=None): - # TODO: verify object is a dashboard? - if not isinstance(current_user, models.ApiUser): - api_key = get_object_or_404(models.ApiKey.get_by_api_key, token) - dashboard = api_key.object - else: - dashboard = current_user.object - - user = { - 'permissions': [], - 'apiKey': current_user.id - } - - headers = { - 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate' - } - - record_event(current_org, current_user, { - 'action': 'view', - 'object_id': dashboard.id, - 'object_type': 'dashboard', - 'public': True, - 'headless': 'embed' in request.args, - 'referer': request.headers.get('Referer') - }) - - response = render_template("public.html", - headless='embed' in request.args, - user=json.dumps(user), - seed_data=json_dumps({ - 'dashboard': serializers.public_dashboard(dashboard) - }), - client_config=json.dumps(settings.COMMON_CLIENT_CONFIG)) - - return response, 200, headers + # TODO: bring this back. + # record_event(current_org, current_user, { + # 'action': 'view', + # 'object_id': dashboard.id, + # 'object_type': 'dashboard', + # 'public': True, + # 'headless': 'embed' in request.args, + # 'referer': request.headers.get('Referer') + # }) + + full_path = safe_join(settings.STATIC_ASSETS_PATHS[-2], 'index.html') + return send_file(full_path, **dict(cache_timeout=0, conditional=True)) diff --git a/redash/handlers/queries.py b/redash/handlers/queries.py index 649a3996a0..764ea8b94b 100644 --- a/redash/handlers/queries.py +++ b/redash/handlers/queries.py @@ -1,17 +1,18 @@ -from flask import request -from flask_restful import abort -from flask_login import login_required -import sqlparse - -from funcy import distinct, take from itertools import chain -from redash.handlers.base import routes, org_scoped_rule, paginate -from redash.handlers.query_results import run_query +import sqlparse +from flask import jsonify, request +from flask_login import login_required +from flask_restful import abort +from funcy import distinct, take from redash import models -from redash.permissions import require_permission, require_access, require_admin_or_owner, not_view_only, view_only, \ - require_object_modify_permission, can_modify -from redash.handlers.base import BaseResource, get_object_or_404 +from redash.handlers.base import (BaseResource, get_object_or_404, + org_scoped_rule, paginate, routes) +from redash.handlers.query_results import run_query +from redash.permissions import (can_modify, not_view_only, require_access, + require_admin_or_owner, + require_object_modify_permission, + require_permission, view_only) from redash.utils import collect_parameters_from_request @@ -21,7 +22,7 @@ def format_sql_query(org_slug=None): arguments = request.get_json(force=True) query = arguments.get("query", "") - return sqlparse.format(query, reindent=True, keyword_case='upper') + return jsonify({'query': sqlparse.format(query, reindent=True, keyword_case='upper')}) class QuerySearchResource(BaseResource): @@ -116,10 +117,6 @@ def post(self, query_id): except models.ConflictDetectedError: abort(409) - # old_query = copy.deepcopy(query.to_dict()) - # new_change = query.update_instance_tracked(changing_user=self.current_user, old_object=old_query, **query_def) - # abort(409) # HTTP 'Conflict' status code - result = query.to_dict(with_visualizations=True) return result @@ -155,5 +152,3 @@ def post(self, query_id): parameter_values = collect_parameters_from_request(request.args) return run_query(query.data_source, parameter_values, query.query, query.id) - - diff --git a/redash/handlers/static.py b/redash/handlers/static.py index 7706b27337..a2997c1c53 100644 --- a/redash/handlers/static.py +++ b/redash/handlers/static.py @@ -1,16 +1,11 @@ import os -import hashlib -import json -from flask import render_template, safe_join, send_file, current_app -from flask_login import current_user, login_required -from werkzeug.exceptions import NotFound - -from redash import settings, __version__ +from flask import current_app, safe_join, send_file +from flask_login import login_required +from redash import settings from redash.handlers import routes from redash.handlers.base import org_scoped_rule -from redash.version_check import get_latest_version -from authentication import current_org +from werkzeug.exceptions import NotFound @routes.route('/') @@ -31,35 +26,8 @@ def send_static(filename): @login_required def index(**kwargs): - email_md5 = hashlib.md5(current_user.email.lower()).hexdigest() - gravatar_url = "https://www.gravatar.com/avatar/%s?s=40" % email_md5 - - user = { - 'gravatar_url': gravatar_url, - 'id': current_user.id, - 'name': current_user.name, - 'email': current_user.email, - 'groups': current_user.groups, - 'permissions': current_user.permissions - } - - client_config = { - 'newVersionAvailable': get_latest_version(), - 'version': __version__ - } - - client_config.update(settings.COMMON_CLIENT_CONFIG) - - headers = { - 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate' - } - - response = render_template("index.html", - user=json.dumps(user), - org_slug=current_org.slug, - client_config=json.dumps(client_config)) - - return response, 200, headers + full_path = safe_join(settings.STATIC_ASSETS_PATHS[-2], 'index.html') + return send_file(full_path, **dict(cache_timeout=0, conditional=True)) def register_static_routes(rules): @@ -70,24 +38,26 @@ def register_static_routes(rules): routes.add_url_rule(org_scoped_rule(rule), None, index) rules = ['/admin//', - '/admin/', - '/dashboard/', - '/alerts', - '/alerts/', - '/queries', - '/data_sources', - '/data_sources/', - '/users', - '/users/', - '/destinations', - '/destinations/', - '/query_snippets', - '/query_snippets/', - '/groups', - '/groups/', - '/groups//data_sources', - '/queries/', - '/queries//', - '/personal'] + '/admin/', + '/dashboards', + '/dashboard/', + '/dashboards/', + '/alerts', + '/alerts/', + '/queries', + '/data_sources', + '/data_sources/', + '/users', + '/users/', + '/destinations', + '/destinations/', + '/query_snippets', + '/query_snippets/', + '/groups', + '/groups/', + '/groups//data_sources', + '/queries/', + '/queries//', + '/personal'] register_static_routes(rules) diff --git a/redash/query_runner/script.py b/redash/query_runner/script.py index 632f0c8a94..e1c26c7fcd 100644 --- a/redash/query_runner/script.py +++ b/redash/query_runner/script.py @@ -18,18 +18,27 @@ def configuration_schema(cls): 'path': { 'type': 'string', 'title': 'Scripts path' + }, + 'shell': { + 'type': 'boolean', + 'title': 'Execute command through the shell' } }, 'required': ['path'] } @classmethod - def annotate_query(cls): - return False + def type(cls): + return "insecure_script" + def __init__(self, configuration): super(Script, self).__init__(configuration) + # If path is * allow any execution path + if self.configuration["path"] == "*": + return + # Poor man's protection against running scripts from outside the scripts directory if self.configuration["path"].find("../") > -1: raise ValueError("Scripts can only be run from the configured scripts directory") @@ -44,13 +53,16 @@ def run_query(self, query, user): query = query.strip() - script = os.path.join(self.configuration["path"], query.split(" ")[0]) - if not os.path.exists(script): - return None, "Script '%s' not found in script directory" % query + if self.configuration["path"] != "*": + script = os.path.join(self.configuration["path"], query.split(" ")[0]) + if not os.path.exists(script): + return None, "Script '%s' not found in script directory" % query - script = os.path.join(self.configuration["path"], query) + script = os.path.join(self.configuration["path"], query).split(" ") + else: + script = query.split("*/ ")[1] - output = subprocess.check_output(script.split(" "), shell=False) + output = subprocess.check_output(script, shell=self.configuration['shell']) if output is not None: output = output.strip() if output != "": @@ -67,4 +79,5 @@ def run_query(self, query, user): return json_data, error + register(Script) diff --git a/redash/settings.py b/redash/settings.py index 84b9ef86be..a42ff0f3dd 100644 --- a/redash/settings.py +++ b/redash/settings.py @@ -1,6 +1,7 @@ import json import os import urlparse + from funcy import distinct, remove @@ -123,7 +124,8 @@ def all_settings(): # Usually it will be a single path, but we allow to specify additional ones to override the default assets. Only the # last one will be used for Flask templates. -STATIC_ASSETS_PATHS = [fix_assets_path(path) for path in os.environ.get("REDASH_STATIC_ASSETS_PATH", "../rd_ui/app/").split(',')] +STATIC_ASSETS_PATHS = [fix_assets_path(path) for path in os.environ.get("REDASH_STATIC_ASSETS_PATH", "../client/dist/").split(',')] +STATIC_ASSETS_PATHS.append(fix_assets_path('./static/')) JOB_EXPIRY_TIME = int(os.environ.get("REDASH_JOB_EXPIRY_TIME", 3600 * 6)) COOKIE_SECRET = os.environ.get("REDASH_COOKIE_SECRET", "c292a0a3aa32397cdb050e233733900f") @@ -217,6 +219,8 @@ def all_settings(): FEATURE_DISABLE_REFRESH_QUERIES = parse_boolean(os.environ.get("REDASH_FEATURE_DISABLE_REFRESH_QUERIES", "false")) FEATURE_SHOW_QUERY_RESULTS_COUNT = parse_boolean(os.environ.get("REDASH_FEATURE_SHOW_QUERY_RESULTS_COUNT", "true")) FEATURE_SHOW_PERMISSIONS_CONTROL = parse_boolean(os.environ.get("REDASH_FEATURE_SHOW_PERMISSIONS_CONTROL", "false")) +FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS = parse_boolean(os.environ.get("REDASH_FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS", + "false")) # BigQuery BIGQUERY_HTTP_TIMEOUT = int(os.environ.get("REDASH_BIGQUERY_HTTP_TIMEOUT", "600")) @@ -228,10 +232,11 @@ def all_settings(): # WARNING: With this option enabled, Redash reads query parameters from the request URL (risk of SQL injection!) ALLOW_PARAMETERS_IN_EMBEDS = parse_boolean(os.environ.get("REDASH_ALLOW_PARAMETERS_IN_EMBEDS", "false")) -### Common Client config +# Common Client config COMMON_CLIENT_CONFIG = { 'allowScriptsInUserInput': ALLOW_SCRIPTS_IN_USER_INPUT, 'showPermissionsControl': FEATURE_SHOW_PERMISSIONS_CONTROL, + 'allowCustomJSVisualizations': FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS, 'dateFormat': DATE_FORMAT, 'dateTimeFormat': "{0} HH:mm".format(DATE_FORMAT), 'allowAllToEditQueries': FEATURE_ALLOW_ALL_TO_EDIT_QUERIES, diff --git a/redash/static/images/favicon-16x16.png b/redash/static/images/favicon-16x16.png new file mode 120000 index 0000000000..0c74c25f59 --- /dev/null +++ b/redash/static/images/favicon-16x16.png @@ -0,0 +1 @@ +../../../frontend/app/assets/images/favicon-16x16.png \ No newline at end of file diff --git a/redash/static/images/favicon-32x32.png b/redash/static/images/favicon-32x32.png new file mode 120000 index 0000000000..3b34fa1f23 --- /dev/null +++ b/redash/static/images/favicon-32x32.png @@ -0,0 +1 @@ +../../../frontend/app/assets/images/favicon-32x32.png \ No newline at end of file diff --git a/redash/static/images/favicon-96x96.png b/redash/static/images/favicon-96x96.png new file mode 120000 index 0000000000..345ca99c7c --- /dev/null +++ b/redash/static/images/favicon-96x96.png @@ -0,0 +1 @@ +../../../frontend/app/assets/images/favicon-96x96.png \ No newline at end of file diff --git a/rd_ui/app/google_login.png b/redash/static/images/google_login.png similarity index 100% rename from rd_ui/app/google_login.png rename to redash/static/images/google_login.png diff --git a/rd_ui/app/images/logo.png b/redash/static/images/logo.png similarity index 100% rename from rd_ui/app/images/logo.png rename to redash/static/images/logo.png diff --git a/rd_ui/app/images/logo_white.png b/redash/static/images/logo_white.png similarity index 100% rename from rd_ui/app/images/logo_white.png rename to redash/static/images/logo_white.png diff --git a/redash/static/images/redash_icon_small.png b/redash/static/images/redash_icon_small.png new file mode 100644 index 0000000000..5e73cc6f61 Binary files /dev/null and b/redash/static/images/redash_icon_small.png differ diff --git a/redash/static/js/jquery.min.js b/redash/static/js/jquery.min.js new file mode 100755 index 0000000000..006e953102 --- /dev/null +++ b/redash/static/js/jquery.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.9.1 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license +//@ sourceMappingURL=jquery.min.map +*/(function(e,t){var n,r,i=typeof t,o=e.document,a=e.location,s=e.jQuery,u=e.$,l={},c=[],p="1.9.1",f=c.concat,d=c.push,h=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,b=function(e,t){return new b.fn.init(e,t,r)},x=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,N=/^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^[\],:{}\s]*$/,E=/(?:^|:|,)(?:\s*\[)+/g,S=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,A=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,j=/^-ms-/,D=/-([\da-z])/gi,L=function(e,t){return t.toUpperCase()},H=function(e){(o.addEventListener||"load"===e.type||"complete"===o.readyState)&&(q(),b.ready())},q=function(){o.addEventListener?(o.removeEventListener("DOMContentLoaded",H,!1),e.removeEventListener("load",H,!1)):(o.detachEvent("onreadystatechange",H),e.detachEvent("onload",H))};b.fn=b.prototype={jquery:p,constructor:b,init:function(e,n,r){var i,a;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof b?n[0]:n,b.merge(this,b.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:o,!0)),C.test(i[1])&&b.isPlainObject(n))for(i in n)b.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(a=o.getElementById(i[2]),a&&a.parentNode){if(a.id!==i[2])return r.find(e);this.length=1,this[0]=a}return this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):b.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),b.makeArray(e,this))},selector:"",length:0,size:function(){return this.length},toArray:function(){return h.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=b.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return b.each(this,e,t)},ready:function(e){return b.ready.promise().done(e),this},slice:function(){return this.pushStack(h.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(b.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:d,sort:[].sort,splice:[].splice},b.fn.init.prototype=b.fn,b.extend=b.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},u=1,l=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},u=2),"object"==typeof s||b.isFunction(s)||(s={}),l===u&&(s=this,--u);l>u;u++)if(null!=(o=arguments[u]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(b.isPlainObject(r)||(n=b.isArray(r)))?(n?(n=!1,a=e&&b.isArray(e)?e:[]):a=e&&b.isPlainObject(e)?e:{},s[i]=b.extend(c,a,r)):r!==t&&(s[i]=r));return s},b.extend({noConflict:function(t){return e.$===b&&(e.$=u),t&&e.jQuery===b&&(e.jQuery=s),b},isReady:!1,readyWait:1,holdReady:function(e){e?b.readyWait++:b.ready(!0)},ready:function(e){if(e===!0?!--b.readyWait:!b.isReady){if(!o.body)return setTimeout(b.ready);b.isReady=!0,e!==!0&&--b.readyWait>0||(n.resolveWith(o,[b]),b.fn.trigger&&b(o).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===b.type(e)},isArray:Array.isArray||function(e){return"array"===b.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if(!e||"object"!==b.type(e)||e.nodeType||b.isWindow(e))return!1;try{if(e.constructor&&!y.call(e,"constructor")&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||y.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=b.buildFragment([e],t,i),i&&b(i).remove(),b.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=b.trim(n),n&&k.test(n.replace(S,"@").replace(A,"]").replace(E,"")))?Function("return "+n)():(b.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||b.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&b.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(j,"ms-").replace(D,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:v&&!v.call("\ufeff\u00a0")?function(e){return null==e?"":v.call(e)}:function(e){return null==e?"":(e+"").replace(T,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?b.merge(n,"string"==typeof e?[e]:e):d.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(g)return g.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return f.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),b.isFunction(e)?(r=h.call(arguments,2),i=function(){return e.apply(n||this,r.concat(h.call(arguments)))},i.guid=e.guid=e.guid||b.guid++,i):t},access:function(e,n,r,i,o,a,s){var u=0,l=e.length,c=null==r;if("object"===b.type(r)){o=!0;for(u in r)b.access(e,n,u,r[u],!0,a,s)}else if(i!==t&&(o=!0,b.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(b(e),n)})),n))for(;l>u;u++)n(e[u],r,s?i:i.call(e[u],u,n(e[u],r)));return o?e:c?n.call(e):l?n(e[0],r):a},now:function(){return(new Date).getTime()}}),b.ready.promise=function(t){if(!n)if(n=b.Deferred(),"complete"===o.readyState)setTimeout(b.ready);else if(o.addEventListener)o.addEventListener("DOMContentLoaded",H,!1),e.addEventListener("load",H,!1);else{o.attachEvent("onreadystatechange",H),e.attachEvent("onload",H);var r=!1;try{r=null==e.frameElement&&o.documentElement}catch(i){}r&&r.doScroll&&function a(){if(!b.isReady){try{r.doScroll("left")}catch(e){return setTimeout(a,50)}q(),b.ready()}}()}return n.promise(t)},b.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=b.type(e);return b.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=b(o);var _={};function F(e){var t=_[e]={};return b.each(e.match(w)||[],function(e,n){t[n]=!0}),t}b.Callbacks=function(e){e="string"==typeof e?_[e]||F(e):b.extend({},e);var n,r,i,o,a,s,u=[],l=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=u.length,n=!0;u&&o>a;a++)if(u[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,u&&(l?l.length&&c(l.shift()):r?u=[]:p.disable())},p={add:function(){if(u){var t=u.length;(function i(t){b.each(t,function(t,n){var r=b.type(n);"function"===r?e.unique&&p.has(n)||u.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=u.length:r&&(s=t,c(r))}return this},remove:function(){return u&&b.each(arguments,function(e,t){var r;while((r=b.inArray(t,u,r))>-1)u.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?b.inArray(e,u)>-1:!(!u||!u.length)},empty:function(){return u=[],this},disable:function(){return u=l=r=t,this},disabled:function(){return!u},lock:function(){return l=t,r||p.disable(),this},locked:function(){return!l},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],!u||i&&!l||(n?l.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},b.extend({Deferred:function(e){var t=[["resolve","done",b.Callbacks("once memory"),"resolved"],["reject","fail",b.Callbacks("once memory"),"rejected"],["notify","progress",b.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return b.Deferred(function(n){b.each(t,function(t,o){var a=o[0],s=b.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&b.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?b.extend(e,r):r}},i={};return r.pipe=r.then,b.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=h.call(arguments),r=n.length,i=1!==r||e&&b.isFunction(e.promise)?r:0,o=1===i?e:b.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?h.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,u,l;if(r>1)for(s=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&b.isFunction(n[t].promise)?n[t].promise().done(a(t,l,n)).fail(o.reject).progress(a(t,u,s)):--i;return i||o.resolveWith(l,n),o.promise()}}),b.support=function(){var t,n,r,a,s,u,l,c,p,f,d=o.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
      a",n=d.getElementsByTagName("*"),r=d.getElementsByTagName("a")[0],!n||!r||!n.length)return{};s=o.createElement("select"),l=s.appendChild(o.createElement("option")),a=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={getSetAttribute:"t"!==d.className,leadingWhitespace:3===d.firstChild.nodeType,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:"/a"===r.getAttribute("href"),opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:!!a.value,optSelected:l.selected,enctype:!!o.createElement("form").enctype,html5Clone:"<:nav>"!==o.createElement("nav").cloneNode(!0).outerHTML,boxModel:"CSS1Compat"===o.compatMode,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},a.checked=!0,t.noCloneChecked=a.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!l.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}a=o.createElement("input"),a.setAttribute("value",""),t.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),t.radioValue="t"===a.value,a.setAttribute("checked","t"),a.setAttribute("name","t"),u=o.createDocumentFragment(),u.appendChild(a),t.appendChecked=a.checked,t.checkClone=u.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;return d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip,b(function(){var n,r,a,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",u=o.getElementsByTagName("body")[0];u&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",u.appendChild(n).appendChild(d),d.innerHTML="
      t
      ",a=d.getElementsByTagName("td"),a[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===a[0].offsetHeight,a[0].style.display="",a[1].style.display="none",t.reliableHiddenOffsets=p&&0===a[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=4===d.offsetWidth,t.doesNotIncludeMarginInBodyOffset=1!==u.offsetTop,e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(o.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
      ",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(u.style.zoom=1)),u.removeChild(n),n=d=a=r=null)}),n=s=u=l=r=a=null,t}();var O=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,B=/([A-Z])/g;function P(e,n,r,i){if(b.acceptData(e)){var o,a,s=b.expando,u="string"==typeof n,l=e.nodeType,p=l?b.cache:e,f=l?e[s]:e[s]&&s;if(f&&p[f]&&(i||p[f].data)||!u||r!==t)return f||(l?e[s]=f=c.pop()||b.guid++:f=s),p[f]||(p[f]={},l||(p[f].toJSON=b.noop)),("object"==typeof n||"function"==typeof n)&&(i?p[f]=b.extend(p[f],n):p[f].data=b.extend(p[f].data,n)),o=p[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[b.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[b.camelCase(n)])):a=o,a}}function R(e,t,n){if(b.acceptData(e)){var r,i,o,a=e.nodeType,s=a?b.cache:e,u=a?e[b.expando]:b.expando;if(s[u]){if(t&&(o=n?s[u]:s[u].data)){b.isArray(t)?t=t.concat(b.map(t,b.camelCase)):t in o?t=[t]:(t=b.camelCase(t),t=t in o?[t]:t.split(" "));for(r=0,i=t.length;i>r;r++)delete o[t[r]];if(!(n?$:b.isEmptyObject)(o))return}(n||(delete s[u].data,$(s[u])))&&(a?b.cleanData([e],!0):b.support.deleteExpando||s!=s.window?delete s[u]:s[u]=null)}}}b.extend({cache:{},expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?b.cache[e[b.expando]]:e[b.expando],!!e&&!$(e)},data:function(e,t,n){return P(e,t,n)},removeData:function(e,t){return R(e,t)},_data:function(e,t,n){return P(e,t,n,!0)},_removeData:function(e,t){return R(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&b.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),b.fn.extend({data:function(e,n){var r,i,o=this[0],a=0,s=null;if(e===t){if(this.length&&(s=b.data(o),1===o.nodeType&&!b._data(o,"parsedAttrs"))){for(r=o.attributes;r.length>a;a++)i=r[a].name,i.indexOf("data-")||(i=b.camelCase(i.slice(5)),W(o,i,s[i]));b._data(o,"parsedAttrs",!0)}return s}return"object"==typeof e?this.each(function(){b.data(this,e)}):b.access(this,function(n){return n===t?o?W(o,e,b.data(o,e)):null:(this.each(function(){b.data(this,e,n)}),t)},null,n,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){b.removeData(this,e)})}});function W(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(B,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:O.test(r)?b.parseJSON(r):r}catch(o){}b.data(e,n,r)}else r=t}return r}function $(e){var t;for(t in e)if(("data"!==t||!b.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}b.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=b._data(e,n),r&&(!i||b.isArray(r)?i=b._data(e,n,b.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=b.queue(e,t),r=n.length,i=n.shift(),o=b._queueHooks(e,t),a=function(){b.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),o.cur=i,i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return b._data(e,n)||b._data(e,n,{empty:b.Callbacks("once memory").add(function(){b._removeData(e,t+"queue"),b._removeData(e,n)})})}}),b.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?b.queue(this[0],e):n===t?this:this.each(function(){var t=b.queue(this,e,n);b._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&b.dequeue(this,e)})},dequeue:function(e){return this.each(function(){b.dequeue(this,e)})},delay:function(e,t){return e=b.fx?b.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=b.Deferred(),a=this,s=this.length,u=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=b._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(u));return u(),o.promise(n)}});var I,z,X=/[\t\r\n]/g,U=/\r/g,V=/^(?:input|select|textarea|button|object)$/i,Y=/^(?:a|area)$/i,J=/^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i,G=/^(?:checked|selected)$/i,Q=b.support.getSetAttribute,K=b.support.input;b.fn.extend({attr:function(e,t){return b.access(this,b.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){b.removeAttr(this,e)})},prop:function(e,t){return b.access(this,b.prop,e,t,arguments.length>1)},removeProp:function(e){return e=b.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,u="string"==typeof e&&e;if(b.isFunction(e))return this.each(function(t){b(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(X," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=b.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,u=0===arguments.length||"string"==typeof e&&e;if(b.isFunction(e))return this.each(function(t){b(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(X," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?b.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e,r="boolean"==typeof t;return b.isFunction(e)?this.each(function(n){b(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var o,a=0,s=b(this),u=t,l=e.match(w)||[];while(o=l[a++])u=r?u:!s.hasClass(o),s[u?"addClass":"removeClass"](o)}else(n===i||"boolean"===n)&&(this.className&&b._data(this,"__className__",this.className),this.className=this.className||e===!1?"":b._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(X," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=b.isFunction(e),this.each(function(n){var o,a=b(this);1===this.nodeType&&(o=i?e.call(this,n,a.val()):e,null==o?o="":"number"==typeof o?o+="":b.isArray(o)&&(o=b.map(o,function(e){return null==e?"":e+""})),r=b.valHooks[this.type]||b.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=b.valHooks[o.type]||b.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(U,""):null==n?"":n)}}}),b.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,u=0>i?s:o?i:0;for(;s>u;u++)if(n=r[u],!(!n.selected&&u!==i||(b.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&b.nodeName(n.parentNode,"optgroup"))){if(t=b(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n=b.makeArray(t);return b(e).find("option").each(function(){this.selected=b.inArray(b(this).val(),n)>=0}),n.length||(e.selectedIndex=-1),n}}},attr:function(e,n,r){var o,a,s,u=e.nodeType;if(e&&3!==u&&8!==u&&2!==u)return typeof e.getAttribute===i?b.prop(e,n,r):(a=1!==u||!b.isXMLDoc(e),a&&(n=n.toLowerCase(),o=b.attrHooks[n]||(J.test(n)?z:I)),r===t?o&&a&&"get"in o&&null!==(s=o.get(e,n))?s:(typeof e.getAttribute!==i&&(s=e.getAttribute(n)),null==s?t:s):null!==r?o&&a&&"set"in o&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r):(b.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=b.propFix[n]||n,J.test(n)?!Q&&G.test(n)?e[b.camelCase("default-"+n)]=e[r]=!1:e[r]=!1:b.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!b.support.radioValue&&"radio"===t&&b.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!b.isXMLDoc(e),a&&(n=b.propFix[n]||n,o=b.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var n=e.getAttributeNode("tabindex");return n&&n.specified?parseInt(n.value,10):V.test(e.nodeName)||Y.test(e.nodeName)&&e.href?0:t}}}}),z={get:function(e,n){var r=b.prop(e,n),i="boolean"==typeof r&&e.getAttribute(n),o="boolean"==typeof r?K&&Q?null!=i:G.test(n)?e[b.camelCase("default-"+n)]:!!i:e.getAttributeNode(n);return o&&o.value!==!1?n.toLowerCase():t},set:function(e,t,n){return t===!1?b.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&b.propFix[n]||n,n):e[b.camelCase("default-"+n)]=e[n]=!0,n}},K&&Q||(b.attrHooks.value={get:function(e,n){var r=e.getAttributeNode(n);return b.nodeName(e,"input")?e.defaultValue:r&&r.specified?r.value:t},set:function(e,n,r){return b.nodeName(e,"input")?(e.defaultValue=n,t):I&&I.set(e,n,r)}}),Q||(I=b.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&("id"===n||"name"===n||"coords"===n?""!==r.value:r.specified)?r.value:t},set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},b.attrHooks.contenteditable={get:I.get,set:function(e,t,n){I.set(e,""===t?!1:t,n)}},b.each(["width","height"],function(e,n){b.attrHooks[n]=b.extend(b.attrHooks[n],{set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}})})),b.support.hrefNormalized||(b.each(["href","src","width","height"],function(e,n){b.attrHooks[n]=b.extend(b.attrHooks[n],{get:function(e){var r=e.getAttribute(n,2);return null==r?t:r}})}),b.each(["href","src"],function(e,t){b.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}})),b.support.style||(b.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),b.support.optSelected||(b.propHooks.selected=b.extend(b.propHooks.selected,{get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}})),b.support.enctype||(b.propFix.enctype="encoding"),b.support.checkOn||b.each(["radio","checkbox"],function(){b.valHooks[this]={get:function(e){return null===e.getAttribute("value")?"on":e.value}}}),b.each(["radio","checkbox"],function(){b.valHooks[this]=b.extend(b.valHooks[this],{set:function(e,n){return b.isArray(n)?e.checked=b.inArray(b(e).val(),n)>=0:t}})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}b.event={global:{},add:function(e,n,r,o,a){var s,u,l,c,p,f,d,h,g,m,y,v=b._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=b.guid++),(u=v.events)||(u=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof b===i||e&&b.event.triggered===e.type?t:b.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(w)||[""],l=n.length;while(l--)s=rt.exec(n[l])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),p=b.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=b.event.special[g]||{},d=b.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&b.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=u[g])||(h=u[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),b.event.global[g]=!0;e=null}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,p,f,d,h,g,m=b.hasData(e)&&b._data(e);if(m&&(c=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(s=rt.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=b.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),u=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));u&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||b.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)b.event.remove(e,d+t[l],n,r,!0);b.isEmptyObject(c)&&(delete m.handle,b._removeData(e,"events"))}},trigger:function(n,r,i,a){var s,u,l,c,p,f,d,h=[i||o],g=y.call(n,"type")?n.type:n,m=y.call(n,"namespace")?n.namespace.split("."):[];if(l=f=i=i||o,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+b.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),u=0>g.indexOf(":")&&"on"+g,n=n[b.expando]?n:new b.Event(g,"object"==typeof n&&n),n.isTrigger=!0,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:b.makeArray(r,[n]),p=b.event.special[g]||{},a||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!a&&!p.noBubble&&!b.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(l=l.parentNode);l;l=l.parentNode)h.push(l),f=l;f===(i.ownerDocument||o)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((l=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(b._data(l,"events")||{})[n.type]&&b._data(l,"handle"),s&&s.apply(l,r),s=u&&l[u],s&&b.acceptData(l)&&s.apply&&s.apply(l,r)===!1&&n.preventDefault();if(n.type=g,!(a||n.isDefaultPrevented()||p._default&&p._default.apply(i.ownerDocument,r)!==!1||"click"===g&&b.nodeName(i,"a")||!b.acceptData(i)||!u||!i[g]||b.isWindow(i))){f=i[u],f&&(i[u]=null),b.event.triggered=g;try{i[g]()}catch(v){}b.event.triggered=t,f&&(i[u]=f)}return n.result}},dispatch:function(e){e=b.event.fix(e);var n,r,i,o,a,s=[],u=h.call(arguments),l=(b._data(this,"events")||{})[e.type]||[],c=b.event.special[e.type]||{};if(u[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=b.event.handlers.call(this,e,l),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((b.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,u),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],u=n.delegateCount,l=e.target;if(u&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(o=[],a=0;u>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?b(r,this).index(l)>=0:b.find(r,this,null,[l]).length),o[r]&&o.push(i);o.length&&s.push({elem:l,handlers:o})}return n.length>u&&s.push({elem:this,handlers:n.slice(u)}),s},fix:function(e){if(e[b.expando])return e;var t,n,r,i=e.type,a=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new b.Event(a),t=r.length;while(t--)n=r[t],e[n]=a[n];return e.target||(e.target=a.srcElement||o),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,a):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,a,s=n.button,u=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||o,a=i.documentElement,r=i.body,e.pageX=n.clientX+(a&&a.scrollLeft||r&&r.scrollLeft||0)-(a&&a.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(a&&a.scrollTop||r&&r.scrollTop||0)-(a&&a.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&u&&(e.relatedTarget=u===e.target?n.toElement:u),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},click:{trigger:function(){return b.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t}},focus:{trigger:function(){if(this!==o.activeElement&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===o.activeElement&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=b.extend(new b.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?b.event.trigger(i,null,t):b.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},b.removeEvent=o.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},b.Event=function(e,n){return this instanceof b.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&b.extend(this,n),this.timeStamp=e&&e.timeStamp||b.now(),this[b.expando]=!0,t):new b.Event(e,n)},b.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},b.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){b.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj; +return(!i||i!==r&&!b.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),b.support.submitBubbles||(b.event.special.submit={setup:function(){return b.nodeName(this,"form")?!1:(b.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=b.nodeName(n,"input")||b.nodeName(n,"button")?n.form:t;r&&!b._data(r,"submitBubbles")&&(b.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),b._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&b.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return b.nodeName(this,"form")?!1:(b.event.remove(this,"._submit"),t)}}),b.support.changeBubbles||(b.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(b.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),b.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),b.event.simulate("change",this,e,!0)})),!1):(b.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!b._data(t,"changeBubbles")&&(b.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||b.event.simulate("change",this.parentNode,e,!0)}),b._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return b.event.remove(this,"._change"),!Z.test(this.nodeName)}}),b.support.focusinBubbles||b.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){b.event.simulate(t,e.target,b.event.fix(e),!0)};b.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),b.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return b().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=b.guid++)),this.each(function(){b.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,b(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){b.event.remove(this,e,r,n)})},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},trigger:function(e,t){return this.each(function(){b.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?b.event.trigger(e,n,r,!0):t}}),function(e,t){var n,r,i,o,a,s,u,l,c,p,f,d,h,g,m,y,v,x="sizzle"+-new Date,w=e.document,T={},N=0,C=0,k=it(),E=it(),S=it(),A=typeof t,j=1<<31,D=[],L=D.pop,H=D.push,q=D.slice,M=D.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},_="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=F.replace("w","w#"),B="([*^$|!~]?=)",P="\\["+_+"*("+F+")"+_+"*(?:"+B+_+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+O+")|)|)"+_+"*\\]",R=":("+F+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+P.replace(3,8)+")*)|.*)\\)|)",W=RegExp("^"+_+"+|((?:^|[^\\\\])(?:\\\\.)*)"+_+"+$","g"),$=RegExp("^"+_+"*,"+_+"*"),I=RegExp("^"+_+"*([\\x20\\t\\r\\n\\f>+~])"+_+"*"),z=RegExp(R),X=RegExp("^"+O+"$"),U={ID:RegExp("^#("+F+")"),CLASS:RegExp("^\\.("+F+")"),NAME:RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:RegExp("^("+F.replace("w","w*")+")"),ATTR:RegExp("^"+P),PSEUDO:RegExp("^"+R),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+_+"*(even|odd|(([+-]|)(\\d*)n|)"+_+"*(?:([+-]|)"+_+"*(\\d+)|))"+_+"*\\)|)","i"),needsContext:RegExp("^"+_+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+_+"*((?:-\\d)?\\d*)"+_+"*\\)|)(?=[^-]|$)","i")},V=/[\x20\t\r\n\f]*[+~]/,Y=/^[^{]+\{\s*\[native code/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,G=/^(?:input|select|textarea|button)$/i,Q=/^h\d$/i,K=/'|\\/g,Z=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,et=/\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g,tt=function(e,t){var n="0x"+t-65536;return n!==n?t:0>n?String.fromCharCode(n+65536):String.fromCharCode(55296|n>>10,56320|1023&n)};try{q.call(w.documentElement.childNodes,0)[0].nodeType}catch(nt){q=function(e){var t,n=[];while(t=this[e++])n.push(t);return n}}function rt(e){return Y.test(e+"")}function it(){var e,t=[];return e=function(n,r){return t.push(n+=" ")>i.cacheLength&&delete e[t.shift()],e[n]=r}}function ot(e){return e[x]=!0,e}function at(e){var t=p.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}}function st(e,t,n,r){var i,o,a,s,u,l,f,g,m,v;if((t?t.ownerDocument||t:w)!==p&&c(t),t=t||p,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(!d&&!r){if(i=J.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&y(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return H.apply(n,q.call(t.getElementsByTagName(e),0)),n;if((a=i[3])&&T.getByClassName&&t.getElementsByClassName)return H.apply(n,q.call(t.getElementsByClassName(a),0)),n}if(T.qsa&&!h.test(e)){if(f=!0,g=x,m=t,v=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){l=ft(e),(f=t.getAttribute("id"))?g=f.replace(K,"\\$&"):t.setAttribute("id",g),g="[id='"+g+"'] ",u=l.length;while(u--)l[u]=g+dt(l[u]);m=V.test(e)&&t.parentNode||t,v=l.join(",")}if(v)try{return H.apply(n,q.call(m.querySelectorAll(v),0)),n}catch(b){}finally{f||t.removeAttribute("id")}}}return wt(e.replace(W,"$1"),t,n,r)}a=st.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},c=st.setDocument=function(e){var n=e?e.ownerDocument||e:w;return n!==p&&9===n.nodeType&&n.documentElement?(p=n,f=n.documentElement,d=a(n),T.tagNameNoComments=at(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),T.attributes=at(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return"boolean"!==t&&"string"!==t}),T.getByClassName=at(function(e){return e.innerHTML="",e.getElementsByClassName&&e.getElementsByClassName("e").length?(e.lastChild.className="e",2===e.getElementsByClassName("e").length):!1}),T.getByName=at(function(e){e.id=x+0,e.innerHTML="
      ",f.insertBefore(e,f.firstChild);var t=n.getElementsByName&&n.getElementsByName(x).length===2+n.getElementsByName(x+0).length;return T.getIdNotName=!n.getElementById(x),f.removeChild(e),t}),i.attrHandle=at(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==A&&"#"===e.firstChild.getAttribute("href")})?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},T.getIdNotName?(i.find.ID=function(e,t){if(typeof t.getElementById!==A&&!d){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(et,tt);return function(e){return e.getAttribute("id")===t}}):(i.find.ID=function(e,n){if(typeof n.getElementById!==A&&!d){var r=n.getElementById(e);return r?r.id===e||typeof r.getAttributeNode!==A&&r.getAttributeNode("id").value===e?[r]:t:[]}},i.filter.ID=function(e){var t=e.replace(et,tt);return function(e){var n=typeof e.getAttributeNode!==A&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=T.tagNameNoComments?function(e,n){return typeof n.getElementsByTagName!==A?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.NAME=T.getByName&&function(e,n){return typeof n.getElementsByName!==A?n.getElementsByName(name):t},i.find.CLASS=T.getByClassName&&function(e,n){return typeof n.getElementsByClassName===A||d?t:n.getElementsByClassName(e)},g=[],h=[":focus"],(T.qsa=rt(n.querySelectorAll))&&(at(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||h.push("\\["+_+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||h.push(":checked")}),at(function(e){e.innerHTML="",e.querySelectorAll("[i^='']").length&&h.push("[*^$]="+_+"*(?:\"\"|'')"),e.querySelectorAll(":enabled").length||h.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),h.push(",.*:")})),(T.matchesSelector=rt(m=f.matchesSelector||f.mozMatchesSelector||f.webkitMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&at(function(e){T.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",R)}),h=RegExp(h.join("|")),g=RegExp(g.join("|")),y=rt(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},v=f.compareDocumentPosition?function(e,t){var r;return e===t?(u=!0,0):(r=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t))?1&r||e.parentNode&&11===e.parentNode.nodeType?e===n||y(w,e)?-1:t===n||y(w,t)?1:0:4&r?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return u=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:0;if(o===a)return ut(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?ut(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},u=!1,[0,0].sort(v),T.detectDuplicates=u,p):p},st.matches=function(e,t){return st(e,null,null,t)},st.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Z,"='$1']"),!(!T.matchesSelector||d||g&&g.test(t)||h.test(t)))try{var n=m.call(e,t);if(n||T.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(r){}return st(t,p,null,[e]).length>0},st.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},st.attr=function(e,t){var n;return(e.ownerDocument||e)!==p&&c(e),d||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):d||T.attributes?e.getAttribute(t):((n=e.getAttributeNode(t))||e.getAttribute(t))&&e[t]===!0?t:n&&n.specified?n.value:null},st.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},st.uniqueSort=function(e){var t,n=[],r=1,i=0;if(u=!T.detectDuplicates,e.sort(v),u){for(;t=e[r];r++)t===e[r-1]&&(i=n.push(r));while(i--)e.splice(n[i],1)}return e};function ut(e,t){var n=t&&e,r=n&&(~t.sourceIndex||j)-(~e.sourceIndex||j);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function lt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ct(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function pt(e){return ot(function(t){return t=+t,ot(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}o=st.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=st.selectors={cacheLength:50,createPseudo:ot,match:U,find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(et,tt),e[3]=(e[4]||e[5]||"").replace(et,tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||st.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&st.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return U.CHILD.test(e[0])?null:(e[4]?e[2]=e[4]:n&&z.test(n)&&(t=ft(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){return"*"===e?function(){return!0}:(e=e.replace(et,tt).toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[e+" "];return t||(t=RegExp("(^|"+_+")"+e+"("+_+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==A&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=st.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!u&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[x]||(m[x]={}),l=c[e]||[],d=l[0]===N&&l[1],f=l[0]===N&&l[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[N,d,f];break}}else if(v&&(l=(t[x]||(t[x]={}))[e])&&l[0]===N)f=l[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[x]||(p[x]={}))[e]=[N,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||st.error("unsupported pseudo: "+e);return r[x]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?ot(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=M.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:ot(function(e){var t=[],n=[],r=s(e.replace(W,"$1"));return r[x]?ot(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:ot(function(e){return function(t){return st(e,t).length>0}}),contains:ot(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:ot(function(e){return X.test(e||"")||st.error("unsupported lang: "+e),e=e.replace(et,tt).toLowerCase(),function(t){var n;do if(n=d?t.getAttribute("xml:lang")||t.getAttribute("lang"):t.lang)return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return Q.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:pt(function(){return[0]}),last:pt(function(e,t){return[t-1]}),eq:pt(function(e,t,n){return[0>n?n+t:n]}),even:pt(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:pt(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:pt(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:pt(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}};for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[n]=lt(n);for(n in{submit:!0,reset:!0})i.pseudos[n]=ct(n);function ft(e,t){var n,r,o,a,s,u,l,c=E[e+" "];if(c)return t?0:c.slice(0);s=e,u=[],l=i.preFilter;while(s){(!n||(r=$.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),u.push(o=[])),n=!1,(r=I.exec(s))&&(n=r.shift(),o.push({value:n,type:r[0].replace(W," ")}),s=s.slice(n.length));for(a in i.filter)!(r=U[a].exec(s))||l[a]&&!(r=l[a](r))||(n=r.shift(),o.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?st.error(e):E(e,u).slice(0)}function dt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function ht(e,t,n){var i=t.dir,o=n&&"parentNode"===i,a=C++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,s){var u,l,c,p=N+" "+a;if(s){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[x]||(t[x]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,s)||r,l[1]===!0)return!0}}function gt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function mt(e,t,n,r,i){var o,a=[],s=0,u=e.length,l=null!=t;for(;u>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),l&&t.push(s));return a}function yt(e,t,n,r,i,o){return r&&!r[x]&&(r=yt(r)),i&&!i[x]&&(i=yt(i,o)),ot(function(o,a,s,u){var l,c,p,f=[],d=[],h=a.length,g=o||xt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:mt(g,f,e,s,u),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,u),r){l=mt(y,d),r(l,[],s,u),c=l.length;while(c--)(p=l[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?M.call(o,p):f[c])>-1&&(o[l]=!(a[l]=p))}}else y=mt(y===a?y.splice(h,y.length):y),i?i(null,a,y,u):H.apply(a,y)})}function vt(e){var t,n,r,o=e.length,a=i.relative[e[0].type],s=a||i.relative[" "],u=a?1:0,c=ht(function(e){return e===t},s,!0),p=ht(function(e){return M.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>u;u++)if(n=i.relative[e[u].type])f=[ht(gt(f),n)];else{if(n=i.filter[e[u].type].apply(null,e[u].matches),n[x]){for(r=++u;o>r;r++)if(i.relative[e[r].type])break;return yt(u>1&>(f),u>1&&dt(e.slice(0,u-1)).replace(W,"$1"),n,r>u&&vt(e.slice(u,r)),o>r&&vt(e=e.slice(r)),o>r&&dt(e))}f.push(n)}return gt(f)}function bt(e,t){var n=0,o=t.length>0,a=e.length>0,s=function(s,u,c,f,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,T=l,C=s||a&&i.find.TAG("*",d&&u.parentNode||u),k=N+=null==T?1:Math.random()||.1;for(w&&(l=u!==p&&u,r=n);null!=(h=C[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,u,c)){f.push(h);break}w&&(N=k,r=++n)}o&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,o&&b!==v){g=0;while(m=t[g++])m(x,y,u,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=L.call(f));y=mt(y)}H.apply(f,y),w&&!s&&y.length>0&&v+t.length>1&&st.uniqueSort(f)}return w&&(N=k,l=T),x};return o?ot(s):s}s=st.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=ft(e)),n=t.length;while(n--)o=vt(t[n]),o[x]?r.push(o):i.push(o);o=S(e,bt(i,r))}return o};function xt(e,t,n){var r=0,i=t.length;for(;i>r;r++)st(e,t[r],n);return n}function wt(e,t,n,r){var o,a,u,l,c,p=ft(e);if(!r&&1===p.length){if(a=p[0]=p[0].slice(0),a.length>2&&"ID"===(u=a[0]).type&&9===t.nodeType&&!d&&i.relative[a[1].type]){if(t=i.find.ID(u.matches[0].replace(et,tt),t)[0],!t)return n;e=e.slice(a.shift().value.length)}o=U.needsContext.test(e)?0:a.length;while(o--){if(u=a[o],i.relative[l=u.type])break;if((c=i.find[l])&&(r=c(u.matches[0].replace(et,tt),V.test(a[0].type)&&t.parentNode||t))){if(a.splice(o,1),e=r.length&&dt(a),!e)return H.apply(n,q.call(r,0)),n;break}}}return s(e,p)(r,t,d,n,V.test(e)),n}i.pseudos.nth=i.pseudos.eq;function Tt(){}i.filters=Tt.prototype=i.pseudos,i.setFilters=new Tt,c(),st.attr=b.attr,b.find=st,b.expr=st.selectors,b.expr[":"]=b.expr.pseudos,b.unique=st.uniqueSort,b.text=st.getText,b.isXMLDoc=st.isXML,b.contains=st.contains}(e);var at=/Until$/,st=/^(?:parents|prev(?:Until|All))/,ut=/^.[^:#\[\.,]*$/,lt=b.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};b.fn.extend({find:function(e){var t,n,r,i=this.length;if("string"!=typeof e)return r=this,this.pushStack(b(e).filter(function(){for(t=0;i>t;t++)if(b.contains(r[t],this))return!0}));for(n=[],t=0;i>t;t++)b.find(e,this[t],n);return n=this.pushStack(i>1?b.unique(n):n),n.selector=(this.selector?this.selector+" ":"")+e,n},has:function(e){var t,n=b(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(b.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e,!1))},filter:function(e){return this.pushStack(ft(this,e,!0))},is:function(e){return!!e&&("string"==typeof e?lt.test(e)?b(e,this.context).index(this[0])>=0:b.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,o=[],a=lt.test(e)||"string"!=typeof e?b(e,t||this.context):0;for(;i>r;r++){n=this[r];while(n&&n.ownerDocument&&n!==t&&11!==n.nodeType){if(a?a.index(n)>-1:b.find.matchesSelector(n,e)){o.push(n);break}n=n.parentNode}}return this.pushStack(o.length>1?b.unique(o):o)},index:function(e){return e?"string"==typeof e?b.inArray(this[0],b(e)):b.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?b(e,t):b.makeArray(e&&e.nodeType?[e]:e),r=b.merge(this.get(),n);return this.pushStack(b.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),b.fn.andSelf=b.fn.addBack;function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}b.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return b.dir(e,"parentNode")},parentsUntil:function(e,t,n){return b.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return b.dir(e,"nextSibling")},prevAll:function(e){return b.dir(e,"previousSibling")},nextUntil:function(e,t,n){return b.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return b.dir(e,"previousSibling",n)},siblings:function(e){return b.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return b.sibling(e.firstChild)},contents:function(e){return b.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:b.merge([],e.childNodes)}},function(e,t){b.fn[e]=function(n,r){var i=b.map(this,t,n);return at.test(e)||(r=n),r&&"string"==typeof r&&(i=b.filter(r,i)),i=this.length>1&&!ct[e]?b.unique(i):i,this.length>1&&st.test(e)&&(i=i.reverse()),this.pushStack(i)}}),b.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),1===t.length?b.find.matchesSelector(t[0],e)?[t[0]]:[]:b.find.matches(e,t)},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!b(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(t=t||0,b.isFunction(t))return b.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return b.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=b.grep(e,function(e){return 1===e.nodeType});if(ut.test(t))return b.filter(t,r,!n);t=b.filter(t,r)}return b.grep(e,function(e){return b.inArray(e,t)>=0===n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
      ","
      "],area:[1,"",""],param:[1,"",""],thead:[1,"","
      "],tr:[2,"","
      "],col:[2,"","
      "],td:[3,"","
      "],_default:b.support.htmlSerialize?[0,"",""]:[1,"X
      ","
      "]},jt=dt(o),Dt=jt.appendChild(o.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,b.fn.extend({text:function(e){return b.access(this,function(e){return e===t?b.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(b.isFunction(e))return this.each(function(t){b(this).wrapAll(e.call(this,t))});if(this[0]){var t=b(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return b.isFunction(e)?this.each(function(t){b(this).wrapInner(e.call(this,t))}):this.each(function(){var t=b(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=b.isFunction(e);return this.each(function(n){b(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){b.nodeName(this,"body")||b(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&this.insertBefore(e,this.firstChild)})},before:function(){return this.domManip(arguments,!1,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,!1,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=0;for(;null!=(n=this[r]);r++)(!e||b.filter(e,[n]).length>0)&&(t||1!==n.nodeType||b.cleanData(Ot(n)),n.parentNode&&(t&&b.contains(n.ownerDocument,n)&&Mt(Ot(n,"script")),n.parentNode.removeChild(n)));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&b.cleanData(Ot(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&b.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return b.clone(this,e,t)})},html:function(e){return b.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!b.support.htmlSerialize&&mt.test(e)||!b.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(b.cleanData(Ot(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(e){var t=b.isFunction(e);return t||"string"==typeof e||(e=b(e).not(this).detach()),this.domManip([e],!0,function(e){var t=this.nextSibling,n=this.parentNode;n&&(b(this).remove(),n.insertBefore(e,t))})},detach:function(e){return this.remove(e,!0)},domManip:function(e,n,r){e=f.apply([],e);var i,o,a,s,u,l,c=0,p=this.length,d=this,h=p-1,g=e[0],m=b.isFunction(g);if(m||!(1>=p||"string"!=typeof g||b.support.checkClone)&&Ct.test(g))return this.each(function(i){var o=d.eq(i);m&&(e[0]=g.call(this,i,n?o.html():t)),o.domManip(e,n,r)});if(p&&(l=b.buildFragment(e,this[0].ownerDocument,!1,this),i=l.firstChild,1===l.childNodes.length&&(l=i),i)){for(n=n&&b.nodeName(i,"tr"),s=b.map(Ot(l,"script"),Ht),a=s.length;p>c;c++)o=l,c!==h&&(o=b.clone(o,!0,!0),a&&b.merge(s,Ot(o,"script"))),r.call(n&&b.nodeName(this[c],"table")?Lt(this[c],"tbody"):this[c],o,c);if(a)for(u=s[s.length-1].ownerDocument,b.map(s,qt),c=0;a>c;c++)o=s[c],kt.test(o.type||"")&&!b._data(o,"globalEval")&&b.contains(u,o)&&(o.src?b.ajax({url:o.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):b.globalEval((o.text||o.textContent||o.innerHTML||"").replace(St,"")));l=i=null}return this}});function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function Ht(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function Mt(e,t){var n,r=0;for(;null!=(n=e[r]);r++)b._data(n,"globalEval",!t||b._data(t[r],"globalEval"))}function _t(e,t){if(1===t.nodeType&&b.hasData(e)){var n,r,i,o=b._data(e),a=b._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)b.event.add(t,n,s[n][r])}a.data&&(a.data=b.extend({},a.data))}}function Ft(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!b.support.noCloneEvent&&t[b.expando]){i=b._data(t);for(r in i.events)b.removeEvent(t,r,i.handle);t.removeAttribute(b.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),b.support.html5Clone&&e.innerHTML&&!b.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Nt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}b.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){b.fn[e]=function(e){var n,r=0,i=[],o=b(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),b(o[r])[t](n),d.apply(i,n.get());return this.pushStack(i)}});function Ot(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||b.nodeName(o,n)?s.push(o):b.merge(s,Ot(o,n));return n===t||n&&b.nodeName(e,n)?b.merge([e],s):s}function Bt(e){Nt.test(e.type)&&(e.defaultChecked=e.checked)}b.extend({clone:function(e,t,n){var r,i,o,a,s,u=b.contains(e.ownerDocument,e);if(b.support.html5Clone||b.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(b.support.noCloneEvent&&b.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||b.isXMLDoc(e)))for(r=Ot(o),s=Ot(e),a=0;null!=(i=s[a]);++a)r[a]&&Ft(i,r[a]);if(t)if(n)for(s=s||Ot(e),r=r||Ot(o),a=0;null!=(i=s[a]);a++)_t(i,r[a]);else _t(e,o);return r=Ot(o,"script"),r.length>0&&Mt(r,!u&&Ot(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,u,l,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===b.type(o))b.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),u=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[u]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!b.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!b.support.tbody){o="table"!==u||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)b.nodeName(l=o.childNodes[i],"tbody")&&!l.childNodes.length&&o.removeChild(l) +}b.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),b.support.appendChecked||b.grep(Ot(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===b.inArray(o,r))&&(a=b.contains(o.ownerDocument,o),s=Ot(f.appendChild(o),"script"),a&&Mt(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,u=b.expando,l=b.cache,p=b.support.deleteExpando,f=b.event.special;for(;null!=(n=e[s]);s++)if((t||b.acceptData(n))&&(o=n[u],a=o&&l[o])){if(a.events)for(r in a.events)f[r]?b.event.remove(n,r):b.removeEvent(n,r,a.handle);l[o]&&(delete l[o],p?delete n[u]:typeof n.removeAttribute!==i?n.removeAttribute(u):n[u]=null,c.push(o))}}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+x+")(.*)$","i"),Yt=RegExp("^("+x+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+x+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===b.css(e,"display")||!b.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=b._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=b._data(r,"olddisplay",un(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&b._data(r,"olddisplay",i?n:b.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}b.fn.extend({css:function(e,n){return b.access(this,function(e,n,r){var i,o,a={},s=0;if(b.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=b.css(e,n[s],!1,o);return a}return r!==t?b.style(e,n,r):b.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){var t="boolean"==typeof e;return this.each(function(){(t?e:nn(this))?b(this).show():b(this).hide()})}}),b.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":b.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,u=b.camelCase(n),l=e.style;if(n=b.cssProps[u]||(b.cssProps[u]=tn(l,u)),s=b.cssHooks[n]||b.cssHooks[u],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:l[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(b.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||b.cssNumber[u]||(r+="px"),b.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(l[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{l[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,u=b.camelCase(n);return n=b.cssProps[u]||(b.cssProps[u]=tn(e.style,u)),s=b.cssHooks[n]||b.cssHooks[u],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||b.isNumeric(o)?o||0:a):a},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),u=s?s.getPropertyValue(n)||s[n]:t,l=e.style;return s&&(""!==u||b.contains(e.ownerDocument,e)||(u=b.style(e,n)),Yt.test(u)&&Ut.test(n)&&(i=l.width,o=l.minWidth,a=l.maxWidth,l.minWidth=l.maxWidth=l.width=u,u=s.width,l.width=i,l.minWidth=o,l.maxWidth=a)),u}):o.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),u=s?s[n]:t,l=e.style;return null==u&&l&&l[n]&&(u=l[n]),Yt.test(u)&&!zt.test(n)&&(i=l.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),l.left="fontSize"===n?"1em":u,u=l.pixelLeft+"px",l.left=i,a&&(o.left=a)),""===u?"auto":u});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=b.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=b.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=b.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=b.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=b.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=b.support.boxSizing&&"border-box"===b.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(b.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function un(e){var t=o,n=Gt[e];return n||(n=ln(e,t),"none"!==n&&n||(Pt=(Pt||b("