Compare commits

...

538 Commits

Author SHA1 Message Date
Arik Fraimovich
b1576b5a91 Merge pull request #531 from EverythingMe/feature/users_admin
Feature: users admin in the web interface
2015-09-06 10:25:18 +03:00
Arik Fraimovich
6f2ee2c0bb Update vagrant instructions 2015-09-06 10:18:33 +03:00
Arik Fraimovich
eec5e3290b Return dashboard after archive 2015-09-06 10:17:38 +03:00
Arik Fraimovich
aaac5928c4 Fix: tests w/ celery breaking 2015-09-06 10:15:26 +03:00
Arik Fraimovich
b97b35d9b5 Update icons size in navbar 2015-09-06 10:15:26 +03:00
Arik Fraimovich
6955514ec3 Update documentation re. users admin 2015-09-06 10:15:26 +03:00
Arik Fraimovich
c8d5267bc7 Bump version 2015-09-06 10:15:25 +03:00
Arik Fraimovich
993a861c78 Users UI. 2015-09-06 10:15:25 +03:00
Arik Fraimovich
a11e100050 Tests for users API 2015-09-06 10:15:25 +03:00
Arik Fraimovich
470ec4924c Remove redash.cache module 2015-09-06 10:15:25 +03:00
Arik Fraimovich
cdb6aaac6e Split the giant redash.controllers module into a package 2015-09-06 10:15:25 +03:00
Arik Fraimovich
580d33a6f8 API for users resource 2015-09-06 10:14:56 +03:00
Arik Fraimovich
8686694be9 Merge pull request #544 from EverythingMe/fix/archived_dashboard_in_recent
Fix: archived dashboards were shown in recent list (and move archive button)
2015-09-03 08:44:10 +03:00
Arik Fraimovich
795a9fe011 Move dashboard archive to dashboard page. 2015-09-03 08:31:51 +03:00
Arik Fraimovich
4b08a3a5f2 Fix #541: archived dashboards appear in recent list. 2015-09-03 08:31:17 +03:00
Arik Fraimovich
7000547419 Merge pull request #537 from ekampf/patch-3
Docs: Missing --source-uri in command
2015-08-25 23:46:45 +03:00
Arik Fraimovich
e0100543cd Merge pull request #538 from ekampf/patch-4
Docs: Space-separated list of scopes is deprecated. Need to use comma-seper…
2015-08-25 23:46:07 +03:00
Arik Fraimovich
7ea640927f Merge pull request #539 from ekampf/patch-5
Docs: Misc. UI fixes because Google changed their console
2015-08-25 23:45:48 +03:00
Eran Kampf
db26cafc41 Misc. UI fixes because Google changed their console 2015-08-24 17:07:11 -07:00
Eran Kampf
100b9e7c71 Space-separated list of scopes is deprecated. Need to use comma-seperated 2015-08-24 16:53:29 -07:00
Eran Kampf
d3391db8f0 Missing --source-uri in command 2015-08-24 16:50:36 -07:00
Arik Fraimovich
1ad01d8394 Warn about using the bootstrap script with existing machines. 2015-08-19 21:25:36 +03:00
Arik Fraimovich
3ef3f2c01b Merge pull request #529 from rm420/fix/multifilters_for_bq
Enables multi-filtering for big-query.
2015-08-16 11:07:02 +03:00
Ryan McClarnon
371422a9ae Merge multi-filter check into one statement 2015-08-10 23:58:46 +01:00
Ryan McClarnon
f4af650292 Enables multi-filtering for big-query. Before had issue with hyphen in field name 2015-08-10 16:17:39 +01:00
Arik Fraimovich
5f38e87f01 Fix images links. 2015-08-09 13:26:23 +03:00
Arik Fraimovich
b98e4a27ce Update images. 2015-08-09 13:25:25 +03:00
Arik Fraimovich
9ff8db31d2 Merge pull request #528 from EverythingMe/chore/packer_update
Cleanup packer.json (no longer building GCE image with it).
2015-08-09 11:40:07 +03:00
Arik Fraimovich
446148d07f Run dist-upgrade before apt-get install 2015-08-09 11:29:01 +03:00
Arik Fraimovich
2d6ca50568 Remove google compute 2015-08-09 11:29:01 +03:00
Arik Fraimovich
650ccac501 Merge pull request #527 from EverythingMe/fix/filters_for_bq
Fix: support for filters in BigQuery.
2015-08-09 11:09:21 +03:00
Arik Fraimovich
ab507f0fd5 Update path to tarball. 2015-08-09 11:09:05 +03:00
Arik Fraimovich
7187b5ffee Fix: support for filters in BigQuery.
BigQuery doesn't support :: in column names, so using __ to split instead of ::.
2015-08-09 11:00:35 +03:00
Arik Fraimovich
5e73da1df4 Fix link to tarball (it changes on every build). 2015-08-07 19:34:00 +03:00
Arik Fraimovich
244d25b12c Fix #524: use v0.7.1 in bootstrap.sh. 2015-08-07 19:18:18 +03:00
Arik Fraimovich
2dcf676cf2 Fix #525: make sure we're in right path for requirements_all_ds.txt 2015-08-07 19:14:46 +03:00
Arik Fraimovich
e07af676a5 Fix #526: install latest setuptools in bootstrap.sh. 2015-08-07 19:12:17 +03:00
Arik Fraimovich
3dea6302de Merge pull request #523 from EverythingMe/feature/new_home
Fix: global_recent isnt set if user has enough recents
2015-08-06 16:58:44 +03:00
Arik Fraimovich
b1ceb60360 Fix: global_recent isnt set if user has enough recents 2015-08-06 16:58:15 +03:00
Arik Fraimovich
1ef94b77e9 Merge pull request #522 from EverythingMe/feature/new_home
Feature: "personalized" homepage with recent queries and dashboards
2015-08-06 16:48:29 +03:00
Arik Fraimovich
292d31e490 Improve /personal and use it as default home 2015-08-06 16:42:29 +03:00
Arik Fraimovich
6f0ac1e730 Merge pull request #521 from kataring/update-doc
Update docs about data sources for Presto
2015-08-06 10:16:39 +03:00
Noriaki Katayama
9f82e5850d Update docs about data sources for Presto 2015-08-06 16:02:18 +09:00
Arik Fraimovich
4a18fa07ec Merge pull request #518 from massaru129/feature/bootstrap_amazon_linux
Other: created bootstrap script for amazon linux
2015-08-04 16:17:18 +03:00
Arik Fraimovich
05d1886467 Merge pull request #520 from EverythingMe/docs-datasources
Docs: update documentation about data sources
2015-08-02 10:49:57 +03:00
Arik Fraimovich
6e45706825 Update docs about data sources 2015-08-02 10:15:11 +03:00
Arik Fraimovich
464402a233 Merge pull request #519 from EverythingMe/feature/disable_collaborative_editing
Feature: ability to disable the ability to edit anyone's query by everyone
2015-08-01 16:41:08 +03:00
Arik Fraimovich
3a56b9ded7 Don't set last_modified_by if only changing ref to last result 2015-08-01 16:36:56 +03:00
Arik Fraimovich
142295671b Feature flag to control if everyone can edit queries 2015-08-01 16:30:03 +03:00
masaru
0e46a24112 fixed config file pathes 2015-07-31 19:21:36 +09:00
masaru
a3cb698be0 fixed file path
the fixed pathes will work if my original files are uploaded
2015-07-31 14:34:25 +09:00
masaru
08730ad113 created bootstrap script for amazon linux 2015-07-31 14:05:48 +09:00
Arik Fraimovich
d155f166d7 Merge pull request #517 from EverythingMe/cleanup
Fix: URL query runner was failing without base URL
2015-07-30 21:27:00 +03:00
Arik Fraimovich
ca95e9252f Fix: URL query runner was failing without base URL 2015-07-30 21:26:14 +03:00
Arik Fraimovich
d078e80e79 Grammar fix. 2015-07-30 14:50:52 +03:00
Arik Fraimovich
8ad1d2672c Clarify about -i. 2015-07-30 14:49:55 +03:00
Arik Fraimovich
735130efc9 Merge pull request #510 from rghose/patch-1
for aws based and password less logins to boxes
2015-07-30 14:46:44 +03:00
Arik Fraimovich
7e6b7398a4 Remove confusing exception logging 2015-07-30 14:36:27 +03:00
Arik Fraimovich
edf8f5b1fd Fix tests post field name change in #515 2015-07-30 14:36:01 +03:00
Arik Fraimovich
08c09d896a Merge pull request #516 from EverythingMe/cleanup
Fix: multi-filter was broken in newer version of angular-ui-select
2015-07-30 13:36:50 +03:00
Arik Fraimovich
58403634cf Fix: multi-filter was broken in newer version of angular-ui-select 2015-07-30 13:35:50 +03:00
Arik Fraimovich
2eb171e40d Merge pull request #515 from moyomot/fix/only_email_authentication
Fix: change wording on login screen "username or email" -> "email"
2015-07-30 12:25:37 +03:00
moyomot
3753f58980 authentication are allowed e-mail only. 2015-07-30 17:56:57 +09:00
Arik Fraimovich
fe1cc78ab3 Merge pull request #514 from alexanderlz/master
Feature: Support Hive as datasource
2015-07-30 08:48:26 +03:00
Alexander Leibzon
c140668648 minor fixes. working version of Hive datasource. 2015-07-29 23:39:39 +03:00
Arik Fraimovich
41ca1321cf Merge pull request #513 from EverythingMe/cleanup
Cleanup: remove select2 and use ui-select.
2015-07-29 09:36:13 +03:00
Alexander Leibzon
d88340158a add Hive as datasource 2015-07-29 02:01:22 +03:00
Arik Fraimovich
52f335edd5 Cleanup: remove select2 and use ui-select. 2015-07-28 10:03:56 +03:00
Arik Fraimovich
22200ec7b2 Merge pull request #511 from stanhu/add-primary-key-to-flask-admin
Add the primary key to Flask admin to make it possible to lookup queries
2015-07-27 23:40:15 +03:00
Arik Fraimovich
e458ed03c8 Bump version. 2015-07-27 23:38:10 +03:00
Arik Fraimovich
e9f1e3a189 Merge pull request #512 from johnkearney/tidier-requirements
Move datasource requirements from bootstrap to own requirements file
2015-07-27 23:32:44 +03:00
John Kearney
d202570b0d Move datasource requirements from bootstrap to own requirements file 2015-07-27 11:46:53 -07:00
Stan Hu
9b6edde5c8 Add the primary key to Flask admin to make it possible to lookup queries 2015-07-27 10:24:34 -07:00
Rahul Ghose
975c92d40d for aws based and password less logins to boxes 2015-07-27 13:23:38 +05:30
Arik Fraimovich
27639f83c7 Update index.rst 2015-07-26 22:38:37 +03:00
Arik Fraimovich
c08e6791df Remove version info from conf.py -- rtd doesn't use it 2015-07-26 15:54:56 +03:00
Arik Fraimovich
5c7158b6ae Update vagrant instructions 2015-07-26 15:46:26 +03:00
Arik Fraimovich
b886067a9f Merge pull request #509 from EverythingMe/docs
Moving documentation to ReatTheDocs
2015-07-26 15:25:29 +03:00
Arik Fraimovich
2421de8819 Add Sphinx based documentation to the project. 2015-07-26 15:24:16 +03:00
Arik Fraimovich
9e87e42400 Merge pull request #508 from EverythingMe/cleanup
Some cleanup (updated settings and bootstrap script)
2015-07-26 12:30:31 +03:00
Arik Fraimovich
8c750826e3 Install dependencies for new sources 2015-07-26 11:49:23 +03:00
Arik Fraimovich
b14b6d1773 Give permission to read user(id, name) to redash_reader 2015-07-26 11:45:58 +03:00
Arik Fraimovich
76cb73f4ce Add description to the server param of elastic search 2015-07-26 11:45:25 +03:00
Arik Fraimovich
8854a45598 Update to settings:
1. Enable API key auth by default.
2. Enable query results cleanup by default.
3. Add ElasticSearch to the enabled query runners list.
2015-07-26 11:44:11 +03:00
Arik Fraimovich
228b8c7614 Merge pull request #507 from EverythingMe/cleanup
Fix: when editing alerts show correct column
2015-07-26 11:39:55 +03:00
Arik Fraimovich
5de79213ae Fix: when editing alerts show correct column 2015-07-26 11:39:22 +03:00
Arik Fraimovich
c7d30c8b87 Merge pull request #498 from EverythingMe/feature/ds_admin
Feature: datasources web admin (closes #193)
2015-07-26 11:35:52 +03:00
Arik Fraimovich
076710f0c6 Bump version 2015-07-26 10:24:09 +03:00
Arik Fraimovich
a9172dac00 Fix: if connection fails connection isn't set 2015-07-26 10:24:08 +03:00
Arik Fraimovich
accca51f39 Feature: web interface to edit datasources
* Web interface to add and delete data sources, without the need to ssh
into the server.
* Ability to safely delete datasources -- query results from this data sources
are deleted, while queries get assigned null datasource.
* Updated the BigQuery datasource to use the JSON key file from Google Developer
console. Also both BigQuery and the Google Spreadsheets datasource no longer store
their key on the filesystem, but rather in the DB.
* Minor updates to the Flask Admin.
2015-07-26 10:24:08 +03:00
Arik Fraimovich
5f5774d01b Merge pull request #506 from EverythingMe/small_fixes
Fix: makes sure the totals series is sorted
2015-07-23 15:03:36 +03:00
Arik Fraimovich
00e99d858c Fix: makes sure the totals series is sorted 2015-07-23 15:03:18 +03:00
Arik Fraimovich
da56dc883f Merge pull request #505 from EverythingMe/small_fixes
Fix: Update URL after creating an alert
2015-07-22 20:38:52 +03:00
Arik Fraimovich
02582cab65 Update URL after creating an alert 2015-07-22 20:38:22 +03:00
Arik Fraimovich
bff4d31ada Read HOST from env. 2015-07-22 18:19:27 +03:00
Arik Fraimovich
83554207e1 Merge pull request #504 from EverythingMe/fix/python_result_set
Fix: cohort was wrong if values were not sorted
2015-07-22 18:06:41 +03:00
Arik Fraimovich
1c0c3e0b93 Fix: cohort was wrong if values were not sorted 2015-07-22 18:05:54 +03:00
Arik Fraimovich
5feb563dc9 Merge pull request #503 from EverythingMe/fix/python_result_set
Fix: if you change the result object, python runner wouldn't return any results
2015-07-22 18:03:29 +03:00
Arik Fraimovich
07b88d0b53 Fix: log results were lost 2015-07-22 17:56:49 +03:00
Arik Fraimovich
21f33462d5 Anoter try in removing optipng from build 2015-07-22 17:43:05 +03:00
Arik Fraimovich
6a9d95f1ac Fix: if you change the result object, python runner wouldn't return any results 2015-07-22 17:36:46 +03:00
Arik Fraimovich
36b80fc4ef Remove optipng from build artifacts 2015-07-22 17:21:50 +03:00
Arik Fraimovich
d89dd2c9af Merge pull request #502 from EverythingMe/feature/alerts
Feature: alerts on query results
2015-07-22 17:14:29 +03:00
Arik Fraimovich
658af526c7 Add alerts to menu 2015-07-22 17:05:31 +03:00
Arik Fraimovich
3d859ec5f3 Feature: alerts for query results.
This is basic implementation for alerts feature, where you can
define a simple rule on the last query result to send an alert.

As part of the implementation added Flask-Mail to the project,
to send emails. Should be useful to make re:dash more "self aware"
(notify users about potential issues, when queries done executing
and more).
2015-07-22 17:05:31 +03:00
Arik Fraimovich
fdff799d23 ng_smart_table: support for inline templates 2015-07-22 17:05:09 +03:00
Arik Fraimovich
5fc0b88b23 ng_smart_table: support for nested objects 2015-07-22 17:05:09 +03:00
Alexander Leibzon
63de247478 add datasources 2015-07-22 14:55:26 +03:00
Arik Fraimovich
5d3caac1b5 Merge pull request #499 from alexanderlz/master
Feature: Support Impala as DataSource
2015-07-22 14:23:55 +03:00
Alexander Leibzon
e4b9d23dfe minor fixes 2015-07-22 14:21:40 +03:00
Alexander Leibzon
890f59a4c9 add get_schema ability to impala 2015-07-22 13:54:00 +03:00
Arik Fraimovich
d4a18ba611 Merge pull request #501 from johnkearney/all_pg_queries_with_no_results
All pg queries with no results
2015-07-21 06:47:33 +03:00
John Kearney
c4502b2925 Add a more use-friendly message when redshift returns no rows 2015-07-20 14:17:51 -07:00
Alexander Leibzon
1d5efdd93f fixes in accordance with pull req spec 2015-07-20 23:21:02 +03:00
John Kearney
2b95da102e Remove unused exports for env examples 2015-07-20 12:36:10 -07:00
Arik Fraimovich
d512cd0c1d Merge pull request #500 from EverythingMe/feature/login_events
Feature: add event for users logining in
2015-07-20 18:30:57 +03:00
Arik Fraimovich
3dc9c84a98 Feature: add event for users logining in 2015-07-20 18:26:45 +03:00
Alexander Leibzon
4a33b987b8 datasource rename 2015-07-20 02:07:17 +03:00
Alexander Leibzon
f7041977d5 impala datasource fixes 2015-07-20 02:06:15 +03:00
Alexander Leibzon
83bc38579e impala data source 2015-07-19 01:44:48 +03:00
Arik Fraimovich
4b8a94e795 Merge pull request #495 from EverythingMe/fix/bq_param
Fix: Update BigQuery configuration parameter name to avoid confusion.
2015-07-15 19:49:56 +03:00
Arik Fraimovich
406010a7a6 Fix: Update BigQuery configuration parameter name to avoid confusion. 2015-07-15 19:45:55 +03:00
Arik Fraimovich
4f11f28efa Merge pull request #494 from erans/master
MongoDB query runner: cleanup
2015-07-15 18:24:28 +03:00
Eran Sandler
c919602b20 cleanups and shit. 2015-07-15 18:17:55 +03:00
Arik Fraimovich
7702b05635 Merge pull request #493 from erans/master
Fix: a generic way to parse all the input JSON and make sure we replace ISODate to Python date times.
2015-07-15 17:50:21 +03:00
Eran Sandler
5fc7c499a3 stupid auto merge. 2015-07-15 17:48:03 +03:00
Eran Sandler
628240906e Fix: a generic way to parse all the input JSON and make sure we replace ISODate to Python date times. 2015-07-15 17:44:33 +03:00
Arik Fraimovich
41b9b21a20 Merge pull request #492 from erans/master
Fix: MongoDB: Date parsing and dates in aggregation $match
2015-07-15 17:03:17 +03:00
Eran Sandler
dbd3f754ba - Fixed parsing dates in the format of YYYY-MM-DDTHH:mm
- Added handling dates in the aggregate $match
2015-07-15 16:58:10 +03:00
Arik Fraimovich
4ef3c27fe6 Merge pull request #489 from kataring/suport-presto
Feature: Support Presto
2015-07-15 16:50:51 +03:00
Arik Fraimovich
58a005c71b Merge pull request #484 from alexanderlz/master
Feature: Google Spreadsheets support (alpha)
2015-07-14 12:14:57 +03:00
Alexander Leibzon
9d7ff31178 replace camelcase vars 2015-07-14 12:13:04 +03:00
Noriaki Katayama
93d6b01fbf add bigint 2015-07-14 16:59:25 +09:00
Arik Fraimovich
7d57f9d0f1 Merge pull request #488 from EverythingMe/fix/mongo-support-sandbox
Fix: Drop database name check in MongoDB queries  to support sandboxed environments
2015-07-14 08:56:47 +03:00
Arik Fraimovich
e80f470255 Mongo: Drop database name check to support sandboxed environments 2015-07-14 08:51:31 +03:00
Arik Fraimovich
5636cec0eb Merge pull request #487 from erans/master
Fix: Support newer as well as older PyMongo versions
2015-07-14 08:23:58 +03:00
Eran Sandler
912bbc1a4a Added backwards compatibility mode with older versions of PyMongo.
It appears that older versions would return a dictionary from an aggregate operation that had the cursor inside the "result" key.
Newer versions return a new type of cursor called CommandCursor.
2015-07-14 08:19:25 +03:00
Arik Fraimovich
d3bb58167e Merge pull request #486 from EverythingMe/fix/mysql-noerror-onconnect
Fix: no error when failing to connect to a MySQL data source
2015-07-13 19:04:16 +03:00
Arik Fraimovich
2911fa8af7 Bump version. 2015-07-13 18:31:31 +03:00
Arik Fraimovich
4503c6af66 Move the MySQL connect to the try/except block 2015-07-13 18:31:00 +03:00
Arik Fraimovich
7fc2d5ee0b Update bootstrap.sh to use 0.6.3. 2015-07-13 12:00:28 +03:00
Arik Fraimovich
3c9c1466a3 Merge pull request #483 from erans/master
Feature: Updated more fine grained support for CORS in QueryResultAPI
2015-07-13 11:04:10 +03:00
Eran Sandler
4a7c066bf0 Too many languages... :-( 2015-07-13 10:05:07 +03:00
Alexander Leibzon
b850da52a2 remove .nojekyll, naming convention 2015-07-13 09:56:11 +03:00
Eran Sandler
1a3657572e Added fine grind control of CORS header for QueryResultAPI and possibly future APIs. 2015-07-13 09:42:23 +03:00
Arik Fraimovich
666e3281e4 Merge pull request #482 from erans/master
Feature: CORS support for the Query Result API to allow embedding in other domains
2015-07-13 08:41:38 +03:00
Eran Sandler
66084b1a3b minor fixes 2015-07-12 23:07:06 +03:00
Eran Sandler
421470666a use set_from_string. 2015-07-12 23:06:00 +03:00
Eran Sandler
f8e2bc9eca Added configurable CORS support for the Query Result API (to export data as JSON or CSV). Configuration is via an environment variable that is comma separated to include one or more domains (REDASH_QUERIES_RESULT_CORS) 2015-07-12 23:00:50 +03:00
Alexander Leibzon
079fbf33f4 don't execute query if the query string is empty (when changing datasource) 2015-07-12 22:53:08 +03:00
Arik Fraimovich
c195362710 Merge pull request #479 from erans/master
Feature: improved error reporting and new log information support for the Python query runner
2015-07-12 22:30:21 +03:00
Arik Fraimovich
b671dd0431 Merge pull request #480 from EverythingMe/feature/multiple-domains
Feature: support for multiple domains with Google OAuth
2015-07-12 13:36:17 +03:00
Arik Fraimovich
7793f3b257 Feature: support for multiple Google Auth domains 2015-07-12 12:51:45 +03:00
Arik Fraimovich
e09aa6f81a Show message for wrong user/password (closes #275) 2015-07-12 12:43:37 +03:00
Eran Sandler
780e0c0418 - refactored the Python query runner
- Improved error handling to show the real error (including indentation and syntax errors - it should show the right row number)
- Added support for "print" statements which becomes log messages show in a single query window (where you edit). It's on by default. To remove it simply called 'disable_print_log()'
2015-07-12 12:14:46 +03:00
Alexander Leibzon
43edb009d6 safer handling of worksheet num 2015-07-12 01:28:52 +03:00
Alexander Leibzon
81978c5049 jekyll disable 2015-07-11 22:51:31 +03:00
Alexander Leibzon
239813e195 modify google spreadsheed datasource params, only the credentials file needed 2015-07-11 22:43:07 +03:00
Alexander Leibzon
28dd571a03 google spreadsheets working version 2015-07-10 21:27:16 +03:00
Alexander Leibzon
808126cf91 forgot type 2015-07-09 01:22:08 +03:00
Alexander Leibzon
69a8295f4c forgot type 2015-07-09 01:18:31 +03:00
Arik Fraimovich
a692e3f664 Merge pull request #476 from EverythingMe/feature/api
Feature: support for per user API keys
2015-07-08 21:34:06 +03:00
Arik Fraimovich
6860dde1f7 Set api_key to be unique 2015-07-08 21:29:32 +03:00
Arik Fraimovich
e183affdd0 Feature: support for per user api keys 2015-07-08 20:59:07 +03:00
Noriaki Katayama
6338be3811 modified response 2015-07-08 10:33:55 +09:00
Alexander Leibzon
3ee6371250 initial work on google spreadsheets 2015-07-07 01:39:59 +03:00
Noriaki Katayama
4f38d42182 add presto 2015-07-06 18:22:23 +09:00
Arik Fraimovich
39db74ff20 Merge pull request #475 from hakobera/support-infuxdb
Feature: Support InfluxDB v0.9+
2015-07-05 22:39:40 +03:00
Arik Fraimovich
05c2c21a85 Bump version. 2015-07-05 22:39:12 +03:00
Kazuyuki Honda
00edc29e50 Support influxdb 0.9+ 2015-07-06 01:15:43 +09:00
Arik Fraimovich
3771af0a8c Update bootstrap.sh to use 0.6.2. 2015-07-05 08:30:11 +03:00
Arik Fraimovich
c32c2d43f7 Bump version. 2015-07-04 10:41:06 +03:00
Arik Fraimovich
4e2e3f9077 Merge pull request #472 from BrunoSalerno/map_visualization_options_fixed
map visualization: options fixed
2015-07-04 10:23:55 +03:00
Bruno Salerno
2a27422df9 map visualization: draw_options to scope 2015-07-03 18:02:22 -03:00
Bruno Salerno
f9e0ce8e9c map visualization: options fixed 2015-07-03 16:45:55 -03:00
Arik Fraimovich
a1d49f13d3 Merge pull request #471 from EverythingMe/fix/visualization_api
Fix: opening viz editor resets its options
2015-07-02 13:17:50 +03:00
Arik Fraimovich
26aa199f9c Fix: opening viz editor resets its options 2015-07-02 13:17:32 +03:00
Arik Fraimovich
4c77f3f914 Merge pull request #470 from EverythingMe/fix/visualization_api
Increase limit of tables for showing search.
2015-07-02 11:06:19 +03:00
Arik Fraimovich
d6be792595 Increase limit of tables for showing search. 2015-07-02 11:05:45 +03:00
Arik Fraimovich
59c1ea7f16 Merge pull request #469 from EverythingMe/fix/visualization_api
Fix: map - HTTPS support in tiles/marker
2015-07-02 11:05:25 +03:00
Arik Fraimovich
4d24005eff Fix: map - HTTPS support in tiles/marker 2015-07-02 11:02:44 +03:00
Arik Fraimovich
2dab35b614 Merge pull request #468 from EverythingMe/fix/visualization_api
Fix: visualizations API fixes
2015-07-02 08:51:09 +03:00
Arik Fraimovich
0b61b88f5f Fix: make default options apply to new visualizations 2015-07-02 08:38:08 +03:00
Arik Fraimovich
e5cb58207c Fix: vis title wasn't updating when changing type 2015-07-02 08:26:10 +03:00
Arik Fraimovich
fc17d1af81 Don't cache static assets in debug mode 2015-07-02 08:25:51 +03:00
Arik Fraimovich
e6650e1e2d Merge pull request #467 from BrunoSalerno/leaflet-visualization-marker-path-bug-fixed
leaflet visualization: marker path bug handled
2015-07-01 22:49:12 +03:00
Bruno Salerno
3aa1cd0133 leaflet visualization: marker path bug handled 2015-07-01 16:40:56 -03:00
Arik Fraimovich
e04833c327 Merge pull request #466 from BrunoSalerno/leaflet-visualization
Feature: Map visualization (using Leaflet)
2015-07-01 20:58:43 +03:00
Bruno Salerno
b743cceb60 leaflet visualization: map template margins fixed 2015-07-01 14:53:31 -03:00
Bruno Salerno
a0e134d3b5 leaflet visualization: dinamic height 2015-07-01 14:15:17 -03:00
Bruno Salerno
d7fb2d7458 leaflet-visualization: div size fixed and bounds storting improved 2015-07-01 12:30:48 -03:00
Bruno Salerno
b913ce6022 leaflet visualization: color series named properly 2015-07-01 10:03:43 -03:00
Bruno Salerno
1eb7945d16 leaflet visualization: map bounds are stored and kept 2015-06-30 18:18:34 -03:00
Bruno Salerno
37d0026ee4 leaflet-visualization: point feature 2015-06-30 17:34:31 -03:00
Arik Fraimovich
9cdc2cb2f7 Merge pull request #465 from EverythingMe/fix/time_field_serialize
Feature: ability to control series order in charts.
2015-06-30 09:26:26 +03:00
Arik Fraimovich
a9bff9063e Feature: cli to get status. 2015-06-30 09:25:32 +03:00
Arik Fraimovich
380126ee44 Feature: ability to control series index in charts. 2015-06-30 09:15:00 +03:00
Arik Fraimovich
d8377375b8 Merge pull request #461 from myinsiders/saml
Added SAML authentication support, eg for OneLogin or Okta
2015-06-30 08:25:44 +03:00
Arik Fraimovich
98ff701f9a Merge pull request #464 from EverythingMe/fix/time_field_serialize
Fix #463: support for datetime.time and datetime.timedelta fields
2015-06-29 18:06:25 +03:00
Arik Fraimovich
f5ea3e97d3 Fix: support for datetime.time and datetime.timedelta fields 2015-06-29 18:01:36 +03:00
Mark White
719e96dd2f Added SAML login option to login form 2015-06-28 17:19:57 +01:00
Arik Fraimovich
6c6c0256ba Merge pull request #462 from EverythingMe/fix_codemirror_resize_issue
Fix: refresh CodeMirror size when schema browser appears
2015-06-28 13:46:12 +03:00
Arik Fraimovich
723df51cdd Fix: refresh CodeMirror size when schema browser appears 2015-06-28 13:45:49 +03:00
Arik Fraimovich
a0f4e263b2 Merge pull request #459 from olgakogan/patch-2
Fixed an error in case a query doesn't have last_modified_date
2015-06-28 10:27:04 +03:00
Arik Fraimovich
4706bf8060 Merge pull request #458 from erans/master
Initial and very early support for ElasticSearch query runner
2015-06-28 10:26:26 +03:00
Mark White
f96a9f659a Added Apache license to code taken from Okta 2015-06-26 11:45:24 +01:00
Mark White
63c273f896 Fixed issue in saml login 2015-06-26 11:12:27 +01:00
Mark White
622ac6d781 Fixes to saml callback server name code 2015-06-26 10:26:59 +01:00
Mark White
8dc564a8bc Added configuration of flask server name 2015-06-26 09:06:50 +01:00
Mark White
3ae5baef22 Added OneLogin support 2015-06-25 17:52:00 +01:00
olga
8d819068b5 Fixed an error in case a query doesn't have last_modified_date 2015-06-25 11:31:22 +03:00
Eran Sandler
585e056265 Initial very early release of an ElasticSearch query runner. It only support Lucene style queries (single line, similar to what Kibana uses but without aggregations). 2015-06-24 09:53:09 +03:00
Arik Fraimovich
1914ed7c7c Merge pull request #456 from bells17/master
Changed the README's 'Setting up re:dash instance' url to a new url
2015-06-19 10:25:11 +01:00
bells17
bd216e93e7 Changed the README's 'Setting up re:dash instance' url to a new url 2015-06-19 10:20:41 +09:00
Arik Fraimovich
5e351de896 Merge pull request #455 from erans/master
added Mongo JSON serializer to correctly serialize ObjectId + datetime.datetime serialization
2015-06-17 10:59:42 +03:00
Eran Sandler
de0e534c77 removed the unnecessary check for datetime.datetime in the JSON encoder. 2015-06-17 10:58:12 +03:00
Eran Sandler
5fa1f9440d duh! 2015-06-16 11:50:20 +03:00
Eran Sandler
b3ddc5f8b9 removed old conversion of ObjectId to string since it is now part of the new JSON serializer 2015-06-16 11:34:19 +03:00
Eran Sandler
8cde5f9673 added Mongo JSON serializer to correctly serialize ObjectId 2015-06-16 11:27:23 +03:00
Arik Fraimovich
1bb53ca497 Merge pull request #451 from EverythingMe/fix/unicode_in_annotation
Fix: charts with category X axis were not sorted properly
2015-06-11 21:46:26 +03:00
Arik Fraimovich
0a3cd9267f Fix: charts with category x axis were not sorted properly 2015-06-11 21:45:45 +03:00
Arik Fraimovich
075d843354 Merge pull request #449 from EverythingMe/fix/unicode_in_annotation
Fix: schema browser chokes on large schemas
2015-06-10 13:36:53 +03:00
Arik Fraimovich
b14e5e8c0e Fix: schema browser chokes on large schemas 2015-06-10 13:36:05 +03:00
Arik Fraimovich
c9da4be422 Merge pull request #442 from EverythingMe/fix/timezone
Fix: when the server has non UTC timezone, timestamps were wrong
2015-06-07 22:23:46 +03:00
Arik Fraimovich
276ee7c27a Merge pull request #448 from olgakogan/master
supervisord default config: separate queue for ad-hoc and scheduled queries
2015-06-07 17:38:53 +03:00
olga
334040532a changed default concurrency level to 2 per queue 2015-06-07 17:36:24 +03:00
olga
335a3a98b5 separated the queue for ad-hoc and for scheduled queries (someone who runs an ad-hoc query should not wait because there scheduled queries are being refreshed at that time) 2015-06-07 17:28:57 +03:00
Arik Fraimovich
b17080a7f5 Merge pull request #446 from EverythingMe/fix/unicode_in_annotation
Fix #443: open table when searching & don't hide columns
2015-06-05 18:13:05 +03:00
Arik Fraimovich
8441c12b01 Fix #443: open table when searching & don't hide columns 2015-06-05 18:08:06 +03:00
Arik Fraimovich
3b4af1b6fa Merge pull request #445 from EverythingMe/fix/unicode_in_annotation
Fix #444: unicode characters in username fail query execution
2015-06-05 16:58:00 +03:00
Arik Fraimovich
c3deb8e2fa Fix #444: unicode characters in username fail query execution 2015-06-05 16:49:25 +03:00
Arik Fraimovich
a60b1686da Fix: when the server has non UTC timezone, timestamps were wrong 2015-06-03 07:58:28 +03:00
Arik Fraimovich
b56e87ceb2 Merge pull request #440 from EverythingMe/fix_ui
Fix: python query runner didn't allow iterating lists
2015-05-31 10:20:32 +03:00
Arik Fraimovich
fc89bcdaf3 Fix: python query runner didn't allow accessing dicts 2015-05-31 10:15:48 +03:00
Arik Fraimovich
15ec8321bb Merge pull request #437 from EverythingMe/fix_ui
Feature: ability to disable x axis labels
2015-05-19 22:24:36 +03:00
Arik Fraimovich
e6ba62485c Merge pull request #436 from EverythingMe/fix_ui
Fix: sorting not working for columns with special characters
2015-05-19 22:15:21 +03:00
Arik Fraimovich
9077b01fb9 Feature: ability to disable x axis labels 2015-05-19 22:15:08 +03:00
Arik Fraimovich
f45281be96 Fix: annotation was failing if query had unicode in it 2015-05-19 22:01:02 +03:00
Arik Fraimovich
a1c8ef9037 Merge pull request #435 from EverythingMe/fix_ui
Fix: string columns with date/time values failed to render.
2015-05-19 22:00:37 +03:00
Arik Fraimovich
f46e8af23f Fix: sorting not working for columns with special characters 2015-05-19 22:00:15 +03:00
Arik Fraimovich
30a89bfd2c Fix: string columns with dates failed to render. 2015-05-19 21:43:50 +03:00
Arik Fraimovich
6312f8738d Merge pull request #433 from stanhu/make-query-link-obvious
Make it obvious that the query link is clickable.
2015-05-17 08:18:19 +03:00
Stan Hu
9e3d5c10c5 Make it obvious that the query link is clickable: underline when hovering and add glyphicon 2015-05-16 22:06:04 -07:00
Arik Fraimovich
59b87ec4fd Merge pull request #434 from erans/master
MongoDB aggregation support + mongo documentation (as comments)
2015-05-17 07:49:20 +03:00
Eran Sandler
27ecf5f25c Merged the older MongoDB code into the new mongodb query runner to support aggregation 2015-05-16 22:22:33 +03:00
Arik Fraimovich
105971c4c8 Merge pull request #432 from stanhu/allow-undefined-max-age
Allow undefined max_age parameter in query_results endpoint
2015-05-15 11:25:24 +03:00
Stan Hu
690f8323c3 Allow undefined max_age parameter in query_results endpoint
An Error 500 would be returned by the endpoint if you attempted to
pass a query parameter to the dashboard since maxAge was undefined in JavaScript.
2015-05-14 22:00:08 -07:00
Arik Fraimovich
20eb110ce3 Fix: update_release_commit_sha should return json 2015-05-14 10:09:57 +03:00
Arik Fraimovich
571c9d0aee Update release manager: update tag commit sha on new release 2015-05-14 09:59:21 +03:00
Arik Fraimovich
0ee7292f16 Merge pull request #431 from EverythingMe/feature/additional_refresh_rates
Feature: additional refresh times (5, 10, 15, 30 minutes)
2015-05-14 09:25:35 +03:00
Arik Fraimovich
8c28392dfd Feature: additional refersh times (5,10,15,30 minutes) 2015-05-13 20:59:39 +03:00
Arik Fraimovich
671f1f4478 Merge pull request #428 from olgakogan/master
Feature: support for column types in MySQL query runner
2015-05-12 13:55:56 +03:00
olga
557d3748be added support to column types in mysql 2015-05-12 12:01:47 +03:00
Arik Fraimovich
f00d080ed2 Install optipng in CircleCI. 2015-05-12 10:33:11 +03:00
Arik Fraimovich
4e76c1305f Merge pull request #425 from EverythingMe/new_logo
New logo
2015-05-12 10:27:01 +03:00
Arik Fraimovich
36ef388e92 Bump version 2015-05-12 10:26:16 +03:00
Arik Fraimovich
2e1ee7f76c New logo 2015-05-12 10:25:57 +03:00
Arik Fraimovich
fc1e38772d New logo! 2015-05-11 23:13:15 +03:00
Arik Fraimovich
0e631a5121 Merge pull request #422 from EverythingMe/feature/288_bq_instance_auth
Feature: BigQueryGCE query runner that uses instance auth (fixes #288)
2015-05-10 23:18:45 +03:00
Arik Fraimovich
d74175efca Feature: BigQueryGCE query runner that uses instance auth 2015-05-10 08:46:41 +03:00
Arik Fraimovich
bf5fe7d2c7 Merge pull request #421 from EverythingMe/fix/issue_417
Feature: show visualization name next to query name (#418)
2015-05-08 22:28:12 +03:00
Arik Fraimovich
0f022aba92 Feature: show visualization name next to query name. 2015-05-07 21:58:12 +03:00
Arik Fraimovich
0b6e55e55a Remove unused code 2015-05-07 21:58:08 +03:00
Arik Fraimovich
e1c409366c Merge pull request #420 from EverythingMe/fix/issue_417
Fix: Make query editor auto resize again to prevent scroll issues
2015-05-07 21:52:07 +03:00
Arik Fraimovich
3b942118e9 Make query editor auto resize again to prevent scroll issues 2015-05-07 21:39:25 +03:00
Arik Fraimovich
7f1543db8f Merge pull request #419 from EverythingMe/fix/issue_417
Fix #417: integer columns treated as floats
2015-05-07 21:38:54 +03:00
Arik Fraimovich
74a5121be2 Fix #417: integer columns treated as floats 2015-05-07 21:25:30 +03:00
Arik Fraimovich
26fe136a1a Merge pull request #416 from daamien/patch-1
Upgrade to requests 2.3.0
2015-05-07 09:30:43 +03:00
damien clochard
83fb189b05 Update requirements.txt
The bootstrap.sh script fails on Debian 7.8

I solved the problem with :

$ sudo pip install requests==2.3.0

Check this bug for more details :
https://github.com/kennethreitz/requests/issues/2028
2015-05-06 18:36:24 +02:00
Arik Fraimovich
5e8d0d36c0 Merge pull request #409 from erans/master
Fix: minor fixes for MongoDB, script and Python query runners
2015-04-26 11:07:33 +03:00
Eran Sandler
4ae4cffa04 Removed a copy-paste duplication. Hmpf. 2015-04-26 11:05:40 +03:00
Eran Sandler
bc433e88fe Fix for _getitem_ error when accessing a dictionary directly. 2015-04-26 11:03:53 +03:00
Arik Fraimovich
513ef501a4 Merge pull request #410 from stanhu/sort-by-y-values
Feature: sort by Y values charts that have a single value per series
2015-04-26 10:23:06 +03:00
Stan Hu
f2bdcbedfb Simplify code and remove sortY option to avoid confusion 2015-04-26 00:18:03 -07:00
Stan Hu
fd056edb2a Support sort by y values for charts that have a single value per series 2015-04-21 22:52:14 -07:00
Eran Sandler
0f0acfdd12 Fix which prevented MongoDB connections to execute queries due to a faulty json schema configuration. 2015-04-22 00:18:28 +03:00
Eran Sandler
1e3b507b2b For for the script data source when command line parameters are passed as part of the query. 2015-04-21 09:36:05 +03:00
Arik Fraimovich
84d95272f3 Comment out active tasks cleanup, as it sometimes fails. 2015-04-20 10:05:04 +03:00
Arik Fraimovich
3b08e9e214 Merge pull request #408 from alexanderlz/master
Feature: additional metadata in query annotation (username, query id, queue name)
2015-04-20 08:48:59 +03:00
Arik Fraimovich
f4be83b06f Use query id from UI & annotate scheduled queries 2015-04-20 08:46:01 +03:00
Alexander Leibzon
4918d0430c add redash username/query_id to query for easier backtracking 2015-04-20 02:16:12 +03:00
Arik Fraimovich
e25b86b10d Merge pull request #398 from lenguyenthedat/data_sources_name_unique
Fix: make the data_sources' name unique
2015-04-18 22:51:12 +03:00
Arik Fraimovich
d3d305a843 Make sure data sources have unique names in tests 2015-04-18 22:46:42 +03:00
Arik Fraimovich
825b93bfe9 Fix migration numbering (there is 0007 already) 2015-04-18 22:46:42 +03:00
Arik Fraimovich
8c98282200 Rename only data sources with duplicates 2015-04-18 22:46:42 +03:00
Dat Le
768ac9eb04 Fix: make the data_sources's name unique
Also added migration script.
2015-04-18 22:46:42 +03:00
Arik Fraimovich
71011d2fca Merge pull request #407 from stanhu/add-flask-admin 2015-04-18 22:23:10 +03:00
Arik Fraimovich
9683a8ed82 Dedicated view for data source 2015-04-18 22:21:58 +03:00
Arik Fraimovich
10a6ac9313 Dedicated view for User model 2015-04-18 18:48:44 +03:00
Arik Fraimovich
dba325e9a2 Use ArrayListField for Array fields. 2015-04-18 18:47:54 +03:00
Arik Fraimovich
fcd9ab533c Fix: correctly call CustomModelConverter __init__. 2015-04-18 18:46:32 +03:00
Arik Fraimovich
68e3e8e1c5 Update name in admin screens 2015-04-18 18:00:52 +03:00
Arik Fraimovich
7f8b738b9e Fix requirements.txt (peewee was specified twice) 2015-04-18 16:58:05 +03:00
Arik Fraimovich
8a35dcedfa Merge pull request #406 from stanhu/add-mysql-port
Add support for configuring MySQL port
2015-04-18 16:14:26 +03:00
Stan Hu
ef763b7157 Use Flask-Admin to provide basic Web-based /admin page 2015-04-18 04:11:30 -07:00
Stan Hu
498e1d4474 Add support for configuring MySQL port 2015-04-17 22:57:34 -07:00
Arik Fraimovich
73de936c75 Merge pull request #405 from EverythingMe/feature/syntax_highglight
Feature: use correct syntax highlighting for Python/Mongo data sources
2015-04-14 17:53:46 +03:00
Arik Fraimovich
e32b709a41 Typo fix in the python query runner 2015-04-14 17:50:36 +03:00
Arik Fraimovich
60652f63c4 Use correct syntax highlighting for Python/Mongo sources 2015-04-14 17:48:36 +03:00
Arik Fraimovich
d0d4101f90 Merge pull request #404 from erans/master
Improvement: make Python datasource to use the RestrictedPython sandbox
2015-04-13 16:13:00 +03:00
Eran Sandler
646875794f Per request by Arik - the BDFL :-) 2015-04-13 15:27:28 +03:00
Eran Sandler
cdad4be0d5 Removed the try..catch block in the import of RestrictedPython since we are putting it in the requirements.txt file. 2015-04-13 15:23:49 +03:00
Eran Sandler
8f4285be62 Minor fixes from code review. 2015-04-13 15:21:43 +03:00
Eran Sandler
acfa55e2d0 Python datasource that uses RestrictedPython. Only modules listed in "allowedImportModules" (command separated) will be allowed to be imported and the code assume they are installed on the server running the actual code. 2015-04-13 11:22:22 +03:00
Arik Fraimovich
0b7cd07db0 Merge pull request #403 from EverythingMe/chore/release_process
Fix: schema browser styles
2015-04-08 16:14:30 +03:00
Arik Fraimovich
6297ffd523 Fix: schema browser styles 2015-04-08 16:13:03 +03:00
Arik Fraimovich
368f4fdbef Merge pull request #402 from EverythingMe/chore/release_process
New release process.
2015-04-06 12:51:12 +03:00
Arik Fraimovich
f52044a209 New release process 2015-04-06 12:50:17 +03:00
Arik Fraimovich
9fb33cf746 Merge pull request #399 from EverythingMe/feature/schema
Feature: schema browser and simple autocomplete
2015-04-02 17:10:07 +03:00
Arik Fraimovich
e3c5da5bc5 Fix tests to use correct data 2015-04-02 17:05:16 +03:00
Arik Fraimovich
e675690cc6 Sort schema by name 2015-04-02 16:56:00 +03:00
Arik Fraimovich
edc1622cf5 Schema support for MySQL 2015-04-02 16:55:52 +03:00
Arik Fraimovich
5ab3d4a40d Basic autocomplete functionality 2015-04-02 16:12:33 +03:00
Arik Fraimovich
cb29d87b63 Improve formatting of schema browser 2015-04-02 15:40:43 +03:00
Arik Fraimovich
6ff6bdad9f Use the correct redis connection in tests 2015-04-02 11:25:42 +03:00
Arik Fraimovich
e3cc3ef9a4 Move schema fetching to DataSource + tests 2015-04-02 11:25:42 +03:00
Arik Fraimovich
1fe4f291f2 Flush test redis db after each test 2015-04-02 11:25:22 +03:00
Arik Fraimovich
a54119f4a2 Show schema along side the query 2015-04-02 11:25:22 +03:00
Arik Fraimovich
c5b7fe5321 Use codemirror directly without ui-codemirror 2015-04-02 11:24:47 +03:00
Arik Fraimovich
d487ec9153 Upgrade codemirror to latest version 2015-04-02 11:24:18 +03:00
Arik Fraimovich
fa19b1ddc8 Endpoint to return data source schema 2015-04-02 11:23:52 +03:00
Arik Fraimovich
267c32b390 Merge pull request #401 from EverythingMe/fix/wrong_time_zone
Fix: use correct date when converting to UTC to get correct timezone.
2015-04-02 07:40:27 +03:00
Arik Fraimovich
aeff3f1494 Fix: use correct date when converting to UTC to get correct timezone. 2015-04-02 07:39:37 +03:00
Arik Fraimovich
e80e52f6c9 Add annotations for the injector. 2015-04-01 20:23:18 +03:00
Arik Fraimovich
fe41a70602 Merge pull request #400 from EverythingMe/feature/better_scheduler
Improved query scheduling option
2015-04-01 17:28:02 +03:00
Arik Fraimovich
976d9abe2d Disable UI tests, as they are no longer maintained :-( 2015-04-01 17:23:08 +03:00
Arik Fraimovich
041bc1100a New UI for query schedule setting 2015-04-01 17:07:19 +03:00
Arik Fraimovich
5d095ff6ab Resolve #113: upgrade to latest ui-bootstrap 2015-04-01 12:48:24 +03:00
Arik Fraimovich
ef01b61b29 Fix: refresh selector had empty option 2015-04-01 12:11:14 +03:00
Arik Fraimovich
faad6b656b Change query ttl field to be a string and named schedule.
This to allow other types of scheduling than just repeat every X seconds.
The first supported option will be: repeat every day at hour X.
2015-04-01 11:23:26 +03:00
Arik Fraimovich
0bc775584b Merge pull request #397 from EverythingMe/feature/edit_others_queries
Fix: forking broken
2015-03-22 17:32:13 +02:00
Arik Fraimovich
f2d96d61a1 Fix: forking broken 2015-03-22 17:28:47 +02:00
Arik Fraimovich
09bf2dd608 Merge pull request #396 from EverythingMe/feature/edit_others_queries
Feature: allow editing others' queries
2015-03-22 14:53:02 +02:00
Arik Fraimovich
ad1b9b06cf Fix test. 2015-03-22 14:42:08 +02:00
Arik Fraimovich
a4bceae60b Allow anyone to edit any query & show who edited it 2015-03-22 13:22:11 +02:00
Arik Fraimovich
9385449feb Add updated_at timestamp to visualization, query, dashboard and users models 2015-03-22 12:58:26 +02:00
Arik Fraimovich
562e1bb8c9 Merge pull request #395 from EverythingMe/feature/post_to_create_a_query
Convert additional dates to user's formatting
2015-03-19 08:54:54 +02:00
Arik Fraimovich
082b718303 Convert additional dates to user's formatting 2015-03-19 08:54:04 +02:00
Arik Fraimovich
c0872899e9 Merge pull request #394 from EverythingMe/feature/post_to_create_a_query
Fix: column definitions weren't updated.
2015-03-19 08:43:17 +02:00
Arik Fraimovich
086bbf129d Fix: column definitions weren't udpated 2015-03-19 08:40:21 +02:00
Arik Fraimovich
4b7561e538 Merge pull request #393 from EverythingMe/feature/post_to_create_a_query
Fix: allow Unicode and other special chars in column names
2015-03-19 08:34:06 +02:00
Arik Fraimovich
407c5a839b Fix: allow Unicode and other special chars in column names
Stopped using Angular's $parse and just accessing the property directly.
2015-03-19 08:33:16 +02:00
Arik Fraimovich
b8aefd26b8 Merge pull request #392 from EverythingMe/feature/post_to_create_a_query
Support posting to /queries/new to create a new query.
2015-03-18 13:42:55 +02:00
Arik Fraimovich
85a762bcd2 Support posting to /queries/new to create a new query. 2015-03-18 13:28:23 +02:00
Arik Fraimovich
4f1b3d5beb Merge pull request #391 from EverythingMe/feature/api_key_auth
Fix: allow dots in column name
2015-03-16 15:08:00 +02:00
Arik Fraimovich
9218a7c437 Fix: allow dots in column name 2015-03-16 14:59:51 +02:00
Arik Fraimovich
71a3f066a5 Ignore gh-pages branch in CircleCI. 2015-03-16 09:03:52 +02:00
Arik Fraimovich
89436d779c Merge pull request #390 from fedex1/patch-1
Update bootstrap.sh
2015-03-16 05:37:33 +02:00
Ralph Yozzo
3631e938da Update bootstrap.sh
# modified by @fedex1 3/15/2015 seems to be the latest version at this point in time.
2015-03-15 23:27:45 -04:00
Arik Fraimovich
c0a9db68f0 Merge pull request #389 from EverythingMe/feature/api_key_auth
Fix: show date/time with respect to user's locale
2015-03-15 18:53:31 +02:00
Arik Fraimovich
bec9c9e14e Fix: show date/time in user's locale 2015-03-15 18:53:02 +02:00
Arik Fraimovich
47bbc25277 Merge pull request #388 from EverythingMe/feature/api_key_auth
Make it possible to set enabled query runners from env
2015-03-12 12:00:26 +02:00
Arik Fraimovich
f02c2588d2 Make it possible to set enabled query runners from env 2015-03-12 11:52:31 +02:00
Arik Fraimovich
7db5449dad Merge pull request #387 from EverythingMe/feature/api_key_auth
Record event when accessing query result from API
2015-03-12 11:46:35 +02:00
Arik Fraimovich
7f6c7f0634 Record event when accessing query result from API 2015-03-12 11:43:21 +02:00
Arik Fraimovich
73955c74f7 Merge pull request #386 from EverythingMe/feature/api_key_auth
Code cleanup (remove "worker's status" dead link & unused settings)
2015-03-11 11:30:15 +02:00
Arik Fraimovich
7de85da8ef Remove unused settings 2015-03-11 07:50:49 +02:00
Arik Fraimovich
0aab35252a Remove broken "Worker's Status" page 2015-03-11 07:47:10 +02:00
Arik Fraimovich
141dbc9e70 Merge pull request #385 from EverythingMe/feature/api_key_auth
Feature: optional API Key authentication instead of HMAC
2015-03-10 18:29:01 +02:00
Arik Fraimovich
2e513c347c Cleanup 2015-03-10 18:21:51 +02:00
Arik Fraimovich
335c136ec2 Show API Key button in query view 2015-03-10 18:08:02 +02:00
Arik Fraimovich
df1170eb9b Feature: optional api key only authentication 2015-03-10 17:51:17 +02:00
Arik Fraimovich
69bcaddbe0 Fix: migrations stopped working due to peewee upgrade 2015-03-09 16:55:55 +02:00
Arik Fraimovich
67958cc27b MySQL query runner: make configuration access safer 2015-03-09 10:16:06 +02:00
Arik Fraimovich
6c716f23d9 Fix migration & query runner for mysql 2015-03-09 08:58:03 +02:00
Arik Fraimovich
bea11b0ac2 Merge pull request #384 from EverythingMe/feature/python_query_runner
Experimental Python query runner
2015-03-08 15:03:59 +02:00
Arik Fraimovich
4927386299 Experimental Python query runner 2015-03-08 15:02:57 +02:00
Arik Fraimovich
30a8550f6b Merge pull request #383 from EverythingMe/fix/migration
Fix: make migration work with new peewee
2015-03-08 14:37:42 +02:00
Arik Fraimovich
0389a45be4 Fix: make migration work with new peewee 2015-03-08 13:28:18 +02:00
Arik Fraimovich
707c169867 Merge pull request #382 from EverythingMe/feature/datasources_v2
Fix: import should be global
2015-03-08 12:27:34 +02:00
Arik Fraimovich
fca034ac0d Fix: import should be global 2015-03-08 12:23:51 +02:00
Arik Fraimovich
97691ea5ee Merge pull request #380 from EverythingMe/feature/datasources_v2
Refactor datasources (query runners)
2015-03-08 11:50:09 +02:00
Arik Fraimovich
40335a0e21 Fix: add missing option flags 2015-03-08 11:00:56 +02:00
Arik Fraimovich
9344cbd078 Update bootstrap script to support new format 2015-03-08 10:38:50 +02:00
Arik Fraimovich
9442fd9465 Update logging messages 2015-03-02 09:49:17 +02:00
Arik Fraimovich
c816f1003d Bump version 2015-03-02 09:45:29 +02:00
Arik Fraimovich
2107b79a80 Use validation for data source editing 2015-03-02 09:44:55 +02:00
Arik Fraimovich
8fae6de8c7 Update datasource CLI to use new format 2015-03-02 09:40:15 +02:00
Arik Fraimovich
d798c77574 Support for already valid data source config 2015-03-02 07:34:06 +02:00
Arik Fraimovich
0abce27381 Set configuration in base ctor 2015-02-24 07:50:10 +02:00
Arik Fraimovich
8a171ba39a Use JSON Schema for data source configuration 2015-02-24 07:50:10 +02:00
Arik Fraimovich
20af276772 Updated configuration spec to include friendly name and more 2015-02-24 07:50:10 +02:00
Arik Fraimovich
4058342763 WIP: configuration object 2015-02-24 07:50:10 +02:00
Arik Fraimovich
af64657260 Migration to update all data source options 2015-02-24 07:50:09 +02:00
Arik Fraimovich
b6bd46e59e New query runners implementation 2015-02-24 07:50:09 +02:00
Arik Fraimovich
31fe547e03 Merge pull request #378 from EverythingMe/feature/variables
Fix #263: timestamp fields should be with time zone
2015-02-23 11:10:20 +02:00
Arik Fraimovich
aff324071e Update peewee version 2015-02-23 09:19:39 +02:00
Arik Fraimovich
131266e408 Fix #263: timestamp fields should be with time zone 2015-02-23 09:02:16 +02:00
Arik Fraimovich
b1f97e8c8d Merge pull request #377 from olgakogan/master
'Download Dataset' fix - error in case of big numeric values
2015-02-21 15:21:18 +02:00
Arik Fraimovich
9783d6e839 Merge pull request #374 from akariv/master
Support unicode queries in search API
2015-02-21 14:48:36 +02:00
akariv
8eea2fb367 Support unicode queries in search API
Modify query test case to use unicode strings
2015-02-20 23:49:37 +02:00
olgakogan
b585480c81 removed redundant handling of large numbers when generating a csv file (causes ValueError: timestamp out of range) 2015-02-20 22:33:02 +02:00
Arik Fraimovich
89e307daba Merge pull request #373 from EverythingMe/feature/variables
UI Fixes
2015-02-08 18:18:37 +02:00
Arik Fraimovich
a5eb0e293c Fix: don't lock query editing while executing 2015-02-08 18:17:08 +02:00
Arik Fraimovich
48d1113225 Fix #371: show notification when query fails. 2015-02-08 18:08:24 +02:00
Arik Fraimovich
d82d5c3bdc Merge pull request #372 from EverythingMe/feature/variables
Several UI fixes
2015-02-08 18:05:05 +02:00
Arik Fraimovich
dfe58b3953 Give the user the option to disable sorting of chart data 2015-02-08 18:02:36 +02:00
Arik Fraimovich
44019b8357 Variables: allow nesting variables 2015-02-08 17:07:20 +02:00
Arik Fraimovich
3c15a44faf Fix: keyboard shortcuts were not unbinded 2015-02-08 17:07:06 +02:00
Arik Fraimovich
8d113dadd2 Revert "Fix #242: handle the case there is no connection to the server"
This reverts commit 3960005002.

Conflicts:
	rd_ui/app/index.html
	rd_ui/bower.json
2015-02-02 18:02:42 +02:00
Arik Fraimovich
c1dd26aee7 Merge pull request #370 from alexanderlz/master
add ISO datetime to filename when saving chart as image
2015-02-02 10:52:00 +02:00
Alexander Leibzon
b2228c2a39 replace 'possibly dangerous for some OSs' characters 2015-02-01 15:29:46 +02:00
Alexander Leibzon
d9618cb09c add ISO datetime to filename when saving chart as image 2015-02-01 14:52:58 +02:00
Arik Fraimovich
c8ca683d3a Merge pull request #368 from alexanderlz/master
Issue #168. Visualization: save as image.
2015-02-01 13:22:40 +02:00
Alexander Leibzon
888963ffaa Merge branch 'master' of https://github.com/alexanderlz/redash
Conflicts:
	rd_ui/app/index.html
	rd_ui/bower.json
2015-02-01 13:20:26 +02:00
Alexander Leibzon
ae947a8310 removing unwanted commit 2015-02-01 13:18:57 +02:00
Alexander Leibzon
bee9cde347 removing unwanted commit 2015-02-01 12:00:23 +02:00
Arik Fraimovich
c131dab125 Merge pull request #369 from EverythingMe/fix/dashboard_filters
Fix: filters got linked when they shouldn't have.
2015-02-01 11:57:46 +02:00
Arik Fraimovich
e113642ae4 Fix: filters got linked when they shouldn't have.
- Make a copy of the first filter, to prevent it controlling the other filters.
- If no query string value given or dashboard filters enabled, don't link filters.
2015-02-01 11:51:07 +02:00
Arik Fraimovich
b76906b168 Merge pull request #367 from EverythingMe/feature/offline
Fix #242: handle the case there is no connection to the server
2015-01-29 20:46:32 +02:00
Arik Fraimovich
3960005002 Fix #242: handle the case there is no connection to the server 2015-01-29 20:43:03 +02:00
Arik Fraimovich
3dde578b86 Only try to render params if they are required. 2015-01-29 15:33:08 +02:00
Arik Fraimovich
813f0e74ff Merge pull request #366 from EverythingMe/fix/mget_error
Several chart editor fixes and additions
2015-01-27 22:49:41 +02:00
Arik Fraimovich
1e4e37c2ce Ability to set y axis min/max (closes #257) 2015-01-27 22:45:16 +02:00
Arik Fraimovich
a00c80eab2 Don't change zIndex if it was already set 2015-01-27 22:30:26 +02:00
Arik Fraimovich
496e5ebe8c Fix: if new series was created in result set, it wasn't using the default chart type 2015-01-27 22:23:10 +02:00
Arik Fraimovich
18cc8434a0 Merge pull request #365 from EverythingMe/fix/mget_error
Fix: when no queries are being run, cleanup job fails with error
2015-01-27 18:39:26 +02:00
Arik Fraimovich
5eba318019 Fix: when no queries are being run, cleanup job fails with error 2015-01-27 18:23:21 +02:00
Arik Fraimovich
63274dbb17 Merge pull request #363 from EverythingMe/feature/query_parameters
Feature: support for query parameters
2015-01-27 18:22:29 +02:00
Arik Fraimovich
4c73e788ae Ability to set ttl (max age) from query string 2015-01-27 17:17:58 +02:00
Arik Fraimovich
b71a2b3651 Enable query params in dashboard 2015-01-27 17:00:21 +02:00
Arik Fraimovich
521a32dfff Merge pull request #364 from EverythingMe/fix/missing_text_widgets
Remove unneeded where clause which was preventing from text widgets to show.
2015-01-27 12:59:58 +02:00
Arik Fraimovich
fd6ebe6e12 Remove unneeded where clause which was preventing from text widgets to show. 2015-01-27 12:52:17 +02:00
Arik Fraimovich
6fb97675ad Add mustache to Karma conf 2015-01-27 11:28:51 +02:00
Arik Fraimovich
c0c102207d Initial work on support for query parameters 2015-01-27 10:28:11 +02:00
Arik Fraimovich
3b9d9ac75d Merge pull request #362 from joeysim/ctrl-s-for-save
added support for saving query with ctrl+s
2015-01-26 07:31:53 +02:00
Joey Simhon
2536fd57ed added support for saving query with cmd+s 2015-01-25 22:52:31 +02:00
Arik Fraimovich
d941e5e5b1 Merge pull request #361 from EverythingMe/DAT-825
Fix: fail with 403 when user not allowed to archive query.
2015-01-25 17:44:58 +02:00
Arik Fraimovich
039b0a89bb Merge pull request #359 from alexanderlz/master
add 'autoclose' to notifications (i.e. close after 3 seconds)
2015-01-25 17:35:53 +02:00
Arik Fraimovich
febf9939c8 Fix: fail with 403 when user not allowed to archive query. 2015-01-25 17:30:10 +02:00
Arik Fraimovich
bb84c6dab8 Merge pull request #360 from EverythingMe/DAT-825
Fix Gruntfile.js settings to copy font files.
2015-01-25 17:29:57 +02:00
Arik Fraimovich
cddc00e2cc Fix Gruntfile.js settings to copy font files. 2015-01-25 17:28:41 +02:00
Alexander Leibzon
091e3d41e1 add 'autoclose' to notifications (i.e. close after 3 seconds) 2015-01-25 17:14:06 +02:00
Arik Fraimovich
9dc3a35c1a Merge pull request #357 from alexanderlz/master
Bug #303: 'Show Total' duplicates values on multiple runs
2015-01-25 16:27:24 +02:00
Arik Fraimovich
f8878d3006 Merge pull request #358 from EverythingMe/DAT-825
Feature: archive query
2015-01-25 16:24:32 +02:00
Arik Fraimovich
1c0d596f26 Bump version due to migration. 2015-01-25 16:23:58 +02:00
Arik Fraimovich
1afd2ab388 Refactoring of @christophervalles work on query delete feature:
- Change delete into archive.
- Safely remove widgets.
- Make sure archived queries don't get scheduled, or show up in search.
- If direct link to query used, show notification.
- Tests.
- Some more.
2015-01-25 16:17:52 +02:00
Christopher Valles
4aa9500402 Working on delete query 2015-01-25 16:16:31 +02:00
Alexander Leibzon
4a8a4482fc add {} for readability 2015-01-25 15:29:45 +02:00
Alexander Leibzon
d83849a1b5 fix to apply with the original logic 2015-01-25 14:44:02 +02:00
Alexander Leibzon
44272f5d66 Bug #303: 'Show Total' duplicates values on multiple runs 2015-01-24 22:02:13 +02:00
Arik Fraimovich
83727ae931 Merge pull request #356 from alexanderlz/master
Bug #307 Fix: Notifications stopped working
2015-01-22 12:01:30 +02:00
Alexander Leibzon
0b0b88a255 remove unused line 2015-01-22 00:27:01 +02:00
Alexander Leibzon
f23d709f4e Bug #307 fix. Notifications stopped working 2015-01-22 00:16:31 +02:00
Arik Fraimovich
88abbc7ea6 Merge pull request #355 from EverythingMe/feature/personal_home
Several small fixes
2015-01-20 16:36:32 +02:00
Arik Fraimovich
16f0413af8 Fix: don't show dashboard filters where it's not enabled 2015-01-20 16:35:55 +02:00
Arik Fraimovich
f47020a64d Report personal page as different page 2015-01-20 16:32:31 +02:00
Arik Fraimovich
55e1ef81f7 Add activity_log to list of tables redash_reader can query 2015-01-20 16:32:31 +02:00
Arik Fraimovich
6bb43d0411 Merge pull request #354 from EverythingMe/feature/personal_home
Feature: personal home with recent queries & dashboards
2015-01-19 12:15:13 +02:00
Arik Fraimovich
f51c2328c9 Feature: personal home with recent queries & dashboards 2015-01-19 12:09:06 +02:00
Arik Fraimovich
fd37188ace Merge pull request #353 from EverythingMe/bug/dashboard_auto_refresh
Fix: nulls converted to strings in UI
2015-01-19 10:51:44 +02:00
Arik Fraimovich
758e27ce91 Fix: nulls converted to strings in UI 2015-01-19 10:49:39 +02:00
Arik Fraimovich
9a3b25eb50 Merge pull request #352 from EverythingMe/bug/dashboard_auto_refresh
Fix: dashboard auto refresh stopped working
2015-01-19 09:14:32 +02:00
Arik Fraimovich
6da890dfb8 FIX: dashboard auto refresh stopped working 2015-01-19 08:32:40 +02:00
Arik Fraimovich
0d35ec7139 Merge pull request #349 from erans/master
Minor bug fixes + supprot for limit and skip in simple query
2015-01-18 10:11:27 +02:00
Arik Fraimovich
dc0f9a63cb Merge pull request #351 from joeysim/search_improvements
Search improvements
2015-01-18 09:22:41 +02:00
Arik Fraimovich
21c042996e Merge pull request #350 from joeysim/ctrl_enter_exec
Added support for Cmd+Enter query execution for PCs
2015-01-18 09:21:32 +02:00
Joey Simhon
5f22adadf2 ordering all_queries by created_at desc for better relevancy with big lists 2015-01-17 21:19:22 +02:00
Joey Simhon
4e8888ce2f sort searched queries by creation time, assuming the newer queries are usually more relevant 2015-01-17 21:14:56 +02:00
Joey Simhon
0a69609d38 Added support for Cmd+Enter query execution for PCs 2015-01-17 00:32:21 +02:00
Eran Sandler
2dbcd88313 added support for skip and limit 2015-01-15 17:14:48 +02:00
Eran Sandler
6b0775f7c7 fixed an issue where 'query' element is missing as well as a bad sort order in simple queries 2015-01-15 17:11:32 +02:00
Arik Fraimovich
e85d3c3c9f Merge pull request #348 from EverythingMe/feature/additional_manage_commands
Feature: new data source management commands in manage.py
2015-01-14 12:35:13 +02:00
Arik Fraimovich
e20f57bba8 Added edit & delete commands to data source cli 2015-01-14 12:23:53 +02:00
Arik Fraimovich
933ace2e38 Split CLI commands to several files for easier editing and naming. 2015-01-14 10:52:11 +02:00
Arik Fraimovich
4c1e5aed6b Remove import from settings command (obsolete). 2015-01-14 10:27:53 +02:00
Arik Fraimovich
77d982b4aa Merge pull request #347 from barnash/query-params-for-filters
Query params for filters
2015-01-13 22:35:32 +02:00
barnash
02c8163265 Changed the query param to something more url friendly 2015-01-12 18:56:44 +02:00
Arik Fraimovich
ef868dbb6e Merge pull request #346 from erans/master
Initial support for Mongo's aggregation framework.
2015-01-12 18:17:41 +02:00
Iftach Bar
b2bab33baa added support for deep links to dashboards with saved filters 2015-01-12 09:23:27 +02:00
Iftach Bar
149e0835f8 fixed jshint stuff - semicolon in different places 2015-01-12 09:22:53 +02:00
Eran Sandler
50bed1d8f2 Initial support for Mongo's aggregation framework. 2015-01-11 12:37:37 +02:00
Eran Sandler
d4b5d78743 Perform a JSON.stringify on values who's type is "object" 2015-01-11 12:28:21 +02:00
Arik Fraimovich
7fc82a2562 Merge pull request #345 from EverythingMe/vagrant_dev
Developer Vagrant box for easier contribution
2014-12-30 07:52:07 +02:00
Arik Fraimovich
92fb138c2c Vagrant file to use the redash/dev box 2014-12-30 07:45:30 +02:00
Arik Fraimovich
71b4b45a3c Merge pull request #344 from EverythingMe/feature/query_results_cleanup
Job to cleanup unused query results
2014-12-25 15:58:10 +02:00
Arik Fraimovich
07f4a1b227 Fix: wiredep failing after version upgrade 2014-12-25 15:52:52 +02:00
Arik Fraimovich
e116e88e98 Job to cleanup unused query results 2014-12-25 15:39:49 +02:00
Arik Fraimovich
2278a181ca Merge pull request #339 from EverythingMe/counter-vis
bugfix: Counter visualization font size issues
2014-11-11 18:21:29 +02:00
Amir Nissim
98dc75a404 bugfix: Counter visualization was not watching for filter changes 2014-11-11 13:04:45 +02:00
Amir Nissim
536918aab3 bugfix: Counter visualization font size issues 2014-11-10 15:21:03 +02:00
Arik Fraimovich
c75ac80c7a Merge pull request #333 from EverythingMe/fix/import
Fix: mixed number columns was wrongly detected as integer
2014-11-05 11:33:46 +02:00
Arik Fraimovich
522d8542e9 Fix: mixed number columns was wrongly detected as integer 2014-11-05 11:30:17 +02:00
Arik Fraimovich
562df44c22 Merge pull request #331 from EverythingMe/fix/import
Fixes and improvements to import dashboard command:
2014-11-04 07:34:59 +02:00
Arik Fraimovich
86e6798c96 manage.py: better output for list data sources command 2014-11-04 07:26:32 +02:00
Arik Fraimovich
db7a287e82 manage.py: list all users command 2014-11-04 07:26:16 +02:00
Arik Fraimovich
518206f208 Fixes and imporvements to import dashboard:
- Update it to not expect query result.
- Add support for specifying data source.
- Create mapping file if it doesn't exist yet.
2014-11-04 07:24:51 +02:00
Arik Fraimovich
bcee1e12b4 Merge pull request #325 from EverythingMe/feature/search-by-id
Add support for searching for query by id
2014-10-30 08:23:41 +02:00
Arik Fraimovich
410f4f35e2 Add support for searching for query by id 2014-10-30 07:58:53 +02:00
Arik Fraimovich
84ea9fec43 Merge pull request #323 from EverythingMe/counter-vis
Counter visualization
2014-10-27 13:58:58 +02:00
Amir Nissim
cda82b7adc #27: use <select> for columns names 2014-10-27 11:47:38 +02:00
Amir Nissim
f2d8c2020b #27: counter and target as query params, change UI 2014-10-27 11:34:56 +02:00
Amir Nissim
1b82ecbc46 #27: Counter visualization draft 2014-10-26 15:42:57 +02:00
Arik Fraimovich
e381331c36 Merge pull request #319 from EverythingMe/bug_292
#292: Customizable series colors
2014-10-23 14:44:02 +03:00
Amir Nissim
ff58247987 #292: move color palette to ng_highcharts 2014-10-23 14:36:30 +03:00
Amir Nissim
dcf0d2cbe3 #292: Customizable series colors 2014-10-23 13:46:43 +03:00
Arik Fraimovich
eb99fa5671 Merge pull request #318 from EverythingMe/docs_setup
Packer: make re:dash version configurable
2014-10-22 12:01:32 +03:00
Arik Fraimovich
ce3e19f212 Make redash version configurable 2014-10-22 11:55:17 +03:00
Arik Fraimovich
44dca6da01 Spelling mistakes. 2014-10-21 19:02:17 +03:00
Arik Fraimovich
34c9fee540 Link to new setup instructions. 2014-10-21 19:01:40 +03:00
Arik Fraimovich
e0b13b2ffa Merge pull request #316 from EverythingMe/feature_users_cli
Add commands to change user's password and grant admin
2014-10-21 18:57:40 +03:00
Arik Fraimovich
df362c12b6 Add commands to change user password and grant admin 2014-10-21 18:51:23 +03:00
Arik Fraimovich
0d1f8c948a Merge pull request #309 from EverythingMe/docs_setup
Setup script for Ubuntu/Debian + packer configuration
2014-10-21 18:42:26 +03:00
Arik Fraimovich
f523378326 Setup script for Ubuntu/Debian + packer configuration
This script is intended to work on Ubuntu 12.04, Ubuntu 14.04 and Debian Wheezy (for GCE users).
To make sure we use the same version of Redis across all distributions we install from source,
and to make sure we use the same version of PostgreSQL we install it from PostgreSQL's apt.

Also included Packer configuration to generate GCE & AWS images.
2014-10-21 18:28:39 +03:00
Arik Fraimovich
b0f9e49709 Merge pull request #313 from erans/master
Forced setting a script execution path
2014-10-21 14:32:03 +03:00
Eran Sandler
b6dbb4e3f8 forced setting a script execution path 2014-10-21 11:20:31 +03:00
Arik Fraimovich
3f6a0e8ffa Merge pull request #312 from erans/master
MongoDB ReplicaSet support and a new connection string format.
2014-10-21 10:21:49 +03:00
Eran Sandler
a7bcc6d31e Added support for MongoDB ReplicaSet as well as changed the connection string format to a JSON based one (like BigQuery). Check the wiki for an example. 2014-10-21 10:16:48 +03:00
Arik Fraimovich
8aa2d8e70a landscape.io configuration file 2014-10-19 13:41:29 +03:00
Arik Fraimovich
4720e12be7 add angular-ui-select to list of dependencies 2014-10-15 17:56:32 +03:00
Arik Fraimovich
5463591f0d Merge branch 'feature/dashboard_add_query_by_name' 2014-10-15 17:45:57 +03:00
Arik Fraimovich
2a0198fba8 Make search expect at least 2 characters 2014-10-15 17:45:39 +03:00
Arik Fraimovich
652f214b25 Updated bower dependencies:
- Angular 1.2.7 -> 1.2.18 (to support angular-ui-select).
- angular-resource and angular-route to match Angular version.
- angular-growl to latest version that supports ~1.2.
- Change version of angular-ui-select to specific one.
2014-10-15 17:42:08 +03:00
Arik Fraimovich
aa49780134 Use unminified version of angular-ui-select 2014-10-15 17:41:55 +03:00
Raymond
f483b61cfb add global html sanitizer 2014-10-15 20:55:29 +08:00
Arik Fraimovich
38a189b671 Merge pull request #306 from raymoondtang/fix/clomun_type_ingeter
Client fix, clomun type support ingeter
2014-10-15 15:46:15 +03:00
Raymond
c2331988db use selected_query for ng-show of visualisation form 2014-10-15 20:32:15 +08:00
Raymond
eff5bdb454 Merge branch 'master' of github-yalo:EverythingMe/redash into fix/clomun_type_ingeter 2014-10-15 19:29:01 +08:00
Raymond
bd1babec3a Add query to dashboard based on name not query id, issue #171 2014-10-15 14:46:55 +08:00
Raymond
d43c2bbf62 table column type handle both integer and float 2014-10-13 12:57:42 +08:00
Arik Fraimovich
87db8099d6 Fix: need to group by runtime and retrieved_at 2014-10-06 09:53:02 +03:00
Arik Fraimovich
ebea118c7d Merge pull request #300 from EverythingMe/feature_google_oauth
Remove query stats (runtime, last retrieve) from search as it was too slow
2014-10-06 09:45:03 +03:00
Arik Fraimovich
297ac5c9bd Fix markdown filter (failing for undefined) 2014-10-06 09:41:56 +03:00
Arik Fraimovich
9b23fb4235 Remove query stats from search, as it was too slow 2014-10-06 09:41:40 +03:00
Arik Fraimovich
0a71f5e22d Merge pull request #298 from erans/master
Initial support for MongoDB.
2014-10-06 08:26:03 +03:00
Arik Fraimovich
0a8aaceb85 Merge pull request #299 from EverythingMe/feature_google_oauth
Show last execution time & runtime in search results + event tracking
2014-10-06 08:25:17 +03:00
Arik Fraimovich
00979f3ad7 Event tracking for search 2014-10-06 08:00:56 +03:00
Arik Fraimovich
c7b48837f2 Show last execution time & runtime in search results 2014-10-06 07:55:17 +03:00
Eran Sandler
418c5322c1 added extra error handling for invalid query and invalid database name 2014-10-02 12:42:46 +03:00
Arik Fraimovich
dc5b4c26a3 Updated README: link to new demo instance. 2014-10-02 07:57:52 +03:00
Eran Sandler
9ed0a5ba85 removed a debug message and change to a better error message when collection is not specified. 2014-09-30 18:43:40 +03:00
Eran Sandler
db0770fc17 Initial support for MongoDB.
Support simple queries using the a JSON format:
{
	"collection" : THE NAME OF THE COLLECTION TO QUERY,
	"query" : {
		A DICTIONARY FOR QUERYING FIELDS (similar to what you would find in PyMongo
	},
	"fields" : {
		LIST OF FIELDS TO RETURN IN THE SPECIFIED ORDER
	},
	"sort" : {
		LIST OF FIELDS TO SORT BY (1 - Ascending, -1 - descending)
	}
}

For example:
{
	"collection" : "mycoolcollection",
	"query" : {
		"fieldA" : { "$gte" : 5 },
		"created" : { "$lt" : "ISODate(\"2014-09-01 23:43\")" }
	},
	"fields" : {
		"fieldA" : 1,
		"created" : 2
	},
	"sort" : {
		"created" : -1
	}
}
2014-09-30 18:34:35 +03:00
Arik Fraimovich
9bb58e71d2 Merge pull request #296 from EverythingMe/feature_google_oauth
Feature: basic search page for queries
2014-09-30 08:43:16 +03:00
Arik Fraimovich
560598eaad Search UI. 2014-09-30 08:39:13 +03:00
Arik Fraimovich
f9144fc927 Naive search implementation. 2014-09-30 08:37:59 +03:00
Arik Fraimovich
883bf173c0 Merge pull request #295 from EverythingMe/feature_google_oauth
Feature: support markdown in query description (fixes #293)
2014-09-29 18:15:24 +03:00
Arik Fraimovich
3f2bb65b32 Show markdown in query view too 2014-09-29 18:10:17 +03:00
Arik Fraimovich
3917af019a Feature: support markdown in query description 2014-09-29 17:59:40 +03:00
192 changed files with 11850 additions and 2930 deletions

View File

@@ -1,9 +1,6 @@
REDASH_CONNECTION_ADAPTER=pg
REDASH_CONNECTION_STRING="dbname=data"
REDASH_STATIC_ASSETS_PATH=../rd_ui/app/
REDASH_GOOGLE_APPS_DOMAIN=
REDASH_ADMINS=
REDASH_WORKERS_COUNT=2
REDASH_COOKIE_SECRET=
REDASH_DATABASE_URL='postgresql://rd'
REDASH_LOG_LEVEL = "INFO"
export REDASH_STATIC_ASSETS_PATH="../rd_ui/app/"
export REDASH_LOG_LEVEL="INFO"
export REDASH_REDIS_URL=redis://localhost:6379/1
export REDASH_DATABASE_URL="postgresql://redash"
export REDASH_COOKIE_SECRET=veryverysecret
export REDASH_GOOGLE_APPS_DOMAIN=

1
.gitignore vendored
View File

@@ -8,6 +8,7 @@ celerybeat-schedule*
.#*
\#*#
*~
_build
# Vagrant related
.vagrant

2
.landscape.yaml Normal file
View File

@@ -0,0 +1,2 @@
ignore-paths:
- migrations

View File

@@ -1,6 +1,7 @@
NAME=redash
VERSION=`python ./manage.py version`
FULL_VERSION=$(VERSION)+b$(CIRCLE_BUILD_NUM)
BASE_VERSION=$(shell python ./manage.py version | cut -d + -f 1)
# VERSION gets evaluated every time it's referenced, therefore we need to use VERSION here instead of FULL_VERSION.
FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(VERSION).tar.gz
@@ -12,11 +13,11 @@ deps:
pack:
sed -ri "s/^__version__ = '([0-9.]*)'/__version__ = '$(FULL_VERSION)'/" redash/__init__.py
tar -zcv -f $(FILENAME) --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="rd_ui/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="rd_ui/node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" *
upload:
python bin/upload_version.py $(VERSION) $(FILENAME)
python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME)
test:
nosetests --with-coverage --cover-package=redash tests/*.py
cd rd_ui && grunt test
nosetests --with-coverage --cover-package=redash tests/
#cd rd_ui && grunt test

View File

@@ -1,2 +1,2 @@
web: ./manage.py runserver -p $PORT
web: ./manage.py runserver -p $PORT --host 0.0.0.0
worker: ./bin/run celery worker --app=redash.worker --beat -Qqueries,celery,scheduled_queries

View File

@@ -1,6 +1,5 @@
<p align="center">
<img title="re:dash" src='https://raw.githubusercontent.com/EverythingMe/redash/screenshots/redash_logo.png' />
<img title="re:dash" src='http://redash.io/static/img/redash_logo.png' width="200px"/>
</p>
<p align="center">
<img title="Build Status" src='https://circleci.com/gh/EverythingMe/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
@@ -11,7 +10,8 @@
Prior to **_re:dash_**, we tried to use traditional BI suites and discovered a set of bloated, technically challenged and slow tools/flows. What we were looking for was a more hacker'ish way to look at data, so we built one.
**_re:dash_** was built to allow fast and easy access to billions of records, that we process and collect using Amazon Redshift ("petabyte scale data warehouse" that "speaks" PostgreSQL).
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite and custom scripts.
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite,
Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
**_re:dash_** consists of two parts:
@@ -24,13 +24,11 @@ Today **_re:dash_** has support for querying multiple databases, including: Reds
![Screenshots](https://raw.github.com/EverythingMe/redash/screenshots/screenshots.gif)
You can try out the demo instance: http://rd-demo.herokuapp.com/ (login with any Google account).
Due to Heroku dev plan limits, it has a small database of flights (see schema [here](http://rd-demo.herokuapp.com/dashboard/schema)). Also due to another Heroku limitation, it is running with the regular user, hence you can DELETE or INSERT data/tables. Please be nice and don't do this.
You can try out the demo instance: http://demo.redash.io/ (login with any Google account).
## Getting Started
* [Setting re:dash on your own server (Ubuntu)](https://github.com/EverythingMe/redash/wiki/Setting-re:dash-on-your-own-server-(for-version-0.4))
* [Setting up re:dash instance](http://redash.io/deployment/setup.html) (includes links to ready made AWS/GCE images).
* Additional documentation in the [Wiki](https://github.com/everythingme/redash/wiki).

11
Vagrantfile vendored Normal file
View File

@@ -0,0 +1,11 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "redash/dev"
config.vm.synced_folder "./", "/opt/redash/current"
config.vm.network "forwarded_port", guest: 5000, host: 9001
end

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env python
import sys
import requests
if __name__ == '__main__':
response = requests.get('https://api.github.com/repos/EverythingMe/redash/releases')
if response.status_code != 200:
exit("Failed getting releases (status code: %s)." % response.status_code)
sorted_releases = sorted(response.json(), key=lambda release: release['id'], reverse=True)
latest_release = sorted_releases[0]
asset_url = latest_release['assets'][0]['url']
filename = latest_release['assets'][0]['name']
wget_command = 'wget --header="Accept: application/octet-stream" %s -O %s' % (asset_url, filename)
if '--url-only' in sys.argv:
print asset_url
elif '--wget' in sys.argv:
print wget_command
else:
print "Latest release: %s" % latest_release['tag_name']
print latest_release['body']
print "\nTarball URL: %s" % asset_url
print 'wget: %s' % (wget_command)

147
bin/release_manager.py Normal file
View File

@@ -0,0 +1,147 @@
import os
import sys
import json
import re
import subprocess
import requests
github_token = os.environ['GITHUB_TOKEN']
auth = (github_token, 'x-oauth-basic')
repo = 'EverythingMe/redash'
def _github_request(method, path, params=None, headers={}):
if not path.startswith('https://api.github.com'):
url = "https://api.github.com/{}".format(path)
else:
url = path
if params is not None:
params = json.dumps(params)
response = requests.request(method, url, data=params, auth=auth)
return response
def exception_from_error(message, response):
return Exception("({}) {}: {}".format(response.status_code, message, response.json().get('message', '?')))
def rc_tag_name(version):
return "v{}-rc".format(version)
def get_rc_release(version):
tag = rc_tag_name(version)
response = _github_request('get', 'repos/{}/releases/tags/{}'.format(repo, tag))
if response.status_code == 404:
return None
elif response.status_code == 200:
return response.json()
raise exception_from_error("Unknown error while looking RC release: ", response)
def create_release(version, commit_sha):
tag = rc_tag_name(version)
params = {
'tag_name': tag,
'name': "{} - RC".format(version),
'target_commitish': commit_sha,
'prerelease': True
}
response = _github_request('post', 'repos/{}/releases'.format(repo), params)
if response.status_code != 201:
raise exception_from_error("Failed creating new release", response)
return response.json()
def upload_asset(release, filepath):
upload_url = release['upload_url'].replace('{?name}', '')
filename = filepath.split('/')[-1]
with open(filepath) as file_content:
headers = {'Content-Type': 'application/gzip'}
response = requests.post(upload_url, file_content, params={'name': filename}, headers=headers, auth=auth, verify=False)
if response.status_code != 201: # not 200/201/...
raise exception_from_error('Failed uploading asset', response)
return response
def remove_previous_builds(release):
for asset in release['assets']:
response = _github_request('delete', asset['url'])
if response.status_code != 204:
raise exception_from_error("Failed deleting asset", response)
def get_changelog(commit_sha):
latest_release = _github_request('get', 'repos/{}/releases/latest'.format(repo))
if latest_release.status_code != 200:
raise exception_from_error('Failed getting latest release', latest_release)
latest_release = latest_release.json()
previous_sha = latest_release['target_commitish']
args = ['git', '--no-pager', 'log', '--merges', '--grep', 'Merge pull request', '--pretty=format:"%h|%s|%b|%p"', '{}...{}'.format(previous_sha, commit_sha)]
log = subprocess.check_output(args)
changes = ["Changes since {}:".format(latest_release['name'])]
for line in log.split('\n'):
try:
sha, subject, body, parents = line[1:-1].split('|')
except ValueError:
continue
try:
pull_request = re.match("Merge pull request #(\d+)", subject).groups()[0]
pull_request = " #{}".format(pull_request)
except Exception, ex:
pull_request = ""
author = subprocess.check_output(['git', 'log', '-1', '--pretty=format:"%an"', parents.split(' ')[-1]])[1:-1]
changes.append("{}{}: {} ({})".format(sha, pull_request, body.strip(), author))
return "\n".join(changes)
def update_release_commit_sha(release, commit_sha):
params = {
'target_commitish': commit_sha,
}
response = _github_request('patch', 'repos/{}/releases/{}'.format(repo, release['id']), params)
if response.status_code != 200:
raise exception_from_error("Failed updating commit sha for existing release", response)
return response.json()
def update_release(version, build_filepath, commit_sha):
try:
release = get_rc_release(version)
if release:
release = update_release_commit_sha(release, commit_sha)
else:
release = create_release(version, commit_sha)
print "Using release id: {}".format(release['id'])
remove_previous_builds(release)
response = upload_asset(release, build_filepath)
changelog = get_changelog(commit_sha)
response = _github_request('patch', release['url'], {'body': changelog})
if response.status_code != 200:
raise exception_from_error("Failed updating release description", response)
except Exception, ex:
print ex
if __name__ == '__main__':
commit_sha = sys.argv[1]
version = sys.argv[2]
filepath = sys.argv[3]
# TODO: make sure running from git directory & remote = repo
update_release(version, filepath, commit_sha)

View File

@@ -1,63 +0,0 @@
"""
Script to test concurrency (multithreading/multiprocess) issues with the workers. Use with caution.
"""
import json
import atfork
atfork.monkeypatch_os_fork_functions()
import atfork.stdlib_fixer
atfork.stdlib_fixer.fix_logging_module()
import time
from redash.data import worker
from redash import models, data_manager, redis_connection
if __name__ == '__main__':
models.create_db(True, False)
print "Creating data source..."
data_source = models.DataSource.create(name="Concurrency", type="pg", options="dbname=postgres")
print "Clear jobs/hashes:"
redis_connection.delete("jobs")
query_hashes = redis_connection.keys("query_hash_*")
if query_hashes:
redis_connection.delete(*query_hashes)
starting_query_results_count = models.QueryResult.select().count()
jobs_count = 5000
workers_count = 10
print "Creating jobs..."
for i in xrange(jobs_count):
query = "SELECT {}".format(i)
print "Inserting: {}".format(query)
data_manager.add_job(query=query, priority=worker.Job.LOW_PRIORITY,
data_source=data_source)
print "Starting workers..."
workers = data_manager.start_workers(workers_count)
print "Waiting for jobs to be done..."
keep_waiting = True
while keep_waiting:
results_count = models.QueryResult.select().count() - starting_query_results_count
print "QueryResults: {}".format(results_count)
time.sleep(5)
if results_count == jobs_count:
print "Yay done..."
keep_waiting = False
data_manager.stop_workers()
qr_count = 0
for qr in models.QueryResult.select():
number = int(qr.query.split()[1])
data_number = json.loads(qr.data)['rows'][0].values()[0]
if number != data_number:
print "Oops? {} != {} ({})".format(number, data_number, qr.id)
qr_count += 1
print "Verified {} query results.".format(qr_count)
print "Done."

View File

@@ -1,46 +0,0 @@
#!python
import os
import sys
import json
import requests
import subprocess
def capture_output(command):
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
return proc.stdout.read()
if __name__ == '__main__':
version = sys.argv[1]
filepath = sys.argv[2]
filename = filepath.split('/')[-1]
github_token = os.environ['GITHUB_TOKEN']
auth = (github_token, 'x-oauth-basic')
commit_sha = os.environ['CIRCLE_SHA1']
commit_body = capture_output(["git", "log", "--format=%b", "-n", "1", commit_sha])
file_md5_checksum = capture_output(["md5sum", filepath]).split()[0]
file_sha256_checksum = capture_output(["sha256sum", filepath]).split()[0]
version_body = "%s\n\nMD5: %s\nSHA256: %s" % (commit_body, file_md5_checksum, file_sha256_checksum)
params = json.dumps({
'tag_name': 'v{0}'.format(version),
'name': 're:dash v{0}'.format(version),
'body': version_body,
'target_commitish': commit_sha,
'prerelease': True
})
response = requests.post('https://api.github.com/repos/everythingme/redash/releases',
data=params,
auth=auth)
upload_url = response.json()['upload_url']
upload_url = upload_url.replace('{?name}', '')
with open(filepath) as file_content:
headers = {'Content-Type': 'application/gzip'}
response = requests.post(upload_url, file_content, params={'name': filename}, auth=auth,
headers=headers, verify=False)

View File

@@ -7,8 +7,11 @@ machine:
2.7.3
dependencies:
pre:
- wget http://downloads.sourceforge.net/project/optipng/OptiPNG/optipng-0.7.5/optipng-0.7.5.tar.gz
- tar xvf optipng-0.7.5.tar.gz
- cd optipng-0.7.5; ./configure; make; sudo checkinstall -y;
- make deps
- pip install -r dev_requirements.txt
- pip install -r requirements_dev.txt
- pip install -r requirements.txt
cache_directories:
- rd_ui/node_modules/
@@ -26,3 +29,7 @@ deployment:
notify:
webhooks:
- url: https://webhooks.gitter.im/e/895d09c3165a0913ac2f
general:
branches:
ignore:
- gh-pages

192
docs/Makefile Normal file
View File

@@ -0,0 +1,192 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/redash.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/redash.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/redash"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/redash"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

111
docs/conf.py Normal file
View File

@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
#
# re:dash documentation build configuration file, created by
# sphinx-quickstart on Mon Jul 20 22:40:24 2015.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
import shlex
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u're:dash'
copyright = u'2015, EverythingMe'
author = u'EverythingMe'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
exclude_patterns = ['_build']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If true, links to the reST sources are added to the pages.
html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
html_show_copyright = False
# Output file base name for HTML help builder.
htmlhelp_basename = 'redashdoc'
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'redash', u're:dash Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'redash', u're:dash Documentation',
author, 'redash', 'One line description of project.',
'Miscellaneous'),
]

181
docs/datasources.rst Normal file
View File

@@ -0,0 +1,181 @@
Supported Data Sources
######################
re:dash supports several types of data sources, and if you set it up using the provided images, it should already have
the needed dependencies to use them all. Starting from version 0.7 and newer, you can manage data sources from the UI
by browsing to ``/data_sources`` on your instance.
If one of the listed data source types isn't available when trying to create a new data source, make sure that:
1. You installed required dependencies.
2. If you've set custom value for the ``REDASH_ENABLED_QUERY_RUNNERS`` setting, it's included in the list.
PostgreSQL / Redshift
---------------------
- **Options**:
- Database name (mandatory)
- User
- Password
- Host
- Port
- **Additional requirements**:
- None
MySQL
-----
- **Options**:
- Database name (mandatory)
- User
- Password
- Host
- Port
- **Additional requirements**:
- ``MySQL-python`` python package
Google BigQuery
---------------
- **Options**:
- Project ID (mandatory)
- JSON key file, generated when creating a service account (see `instructions <https://developers.google.com/console/help/new/#serviceaccounts>`__).
- **Additional requirements**:
- ``google-api-python-client``, ``oauth2client`` and ``pyopenssl`` python packages (on Ubuntu it might require installing ``libffi-dev`` and ``libssl-dev`` as well).
Graphite
--------
- **Options**:
- Url (mandatory)
- User
- Password
- Verify SSL certificate
MongoDB
-------
- **Options**:
- Connection String (mandatory)
- Database name
- Replica set name
- **Additional requirements**:
- ``pymongo`` python package.
For information on how to write MongoDB queries, see :doc:`documentation </usage/mongodb_querying>`.
ElasticSearch
-------------
...
InfluxDB
--------
...
Presto
------
- **Options**:
- Host (mandatory)
- Address to a Presto coordinator.
- Port
- Port to a Presto coordinator. `8080` is the default port.
- Schema
- Default schema name of Presto. You can read other schemas by qualified name like `FROM myschema.table1`.
- Catalog
- Catalog (connector) name of Presto such as `hive-cdh4`, `hive-hadoop1`, etc.
- Username
- User name to connect to a Presto.
- **Additional requirements**:
- ``pyhive`` python package.
Hive
----
...
Impala
------
...
URL
---
A URL based data source which requests URLs that return the :doc:`results JSON
format </dev/results_format>`.
Very useful in situations where you want to expose the data without
connecting directly to the database.
The query itself inside re:dash will simply contain the URL to be
executed (i.e. http://myserver/path/myquery)
- **Options**:
- Url - set this if you want to limit queries to certain base path.
Google Spreadsheets
-------------------
- **Options**:
- JSON key file, generated when creating a service account (see `instructions <https://developers.google.com/console/help/new/#serviceaccounts>`__).
- **Additional requirements**:
- ``gspread`` and ``oauth2client`` python packages.
Notes:
1. To be able to load the spreadsheet in re:dash - share your it with
your ServiceAccount's email (it can be found in the credentials json
file, for example
43242343247-fjdfakljr3r2@developer.gserviceaccount.com).
2. The query format is "DOC\_UUID\|SHEET\_NUM" (for example
"kjsdfhkjh4rsEFSDFEWR232jkddsfh\|0")
Python
------
**Execute other queries, manipulate and compute with Python code**
This is a special query runner, that will execute provided Python code as the query. Useful for various scenarios such as
merging data from different data sources, doing data transformation/manipulation that isn't trivial with SQL, merging
with remote data or using data analysis libraries such as Pandas (see `example query <https://gist.github.com/arikfr/be7c2888520c44cf4f0f>`__).
While the Python query runner uses a sandbox (RestrictedPython), it's not 100% secure and the security depends on the
modules you allow to import. We recommend enabling the Python query runner only in a trusted environment (meaning: behind
VPN and with users you trust).
- **Options**:
- Allowed Modules in a comma separated list (optional). **NOTE:**
You MUST make sure these modules are installed on the machine
running the Celery workers.

11
docs/dev.rst Normal file
View File

@@ -0,0 +1,11 @@
Developer Information
=====================
.. toctree::
:maxdepth: 2
:glob:
dev/vagrant
dev/*

View File

@@ -0,0 +1,94 @@
Query Execution Model
#####################
Introduction
============
The first datasource which was used with re:dash was Redshift. Because
we had billions of records in Redshift, and some queries were costly to
re-run, from the get go there was the idea of caching query results in
re:dash.
This was to relieve stress from the Redshift cluster and also to improve
user experience.
How queries get executed and cached in re:dash?
===============================================
Server
------
To make sure each query is executed only once at any giving time, we
translate the query to a ``query hash``, using the following code:
.. code:: python
COMMENTS_REGEX = re.compile("/\*.*?\*/")
def gen_query_hash(sql):
sql = COMMENTS_REGEX.sub("", sql)
sql = "".join(sql.split()).lower()
return hashlib.md5(sql.encode('utf-8')).hexdigest()
When query execution is done, the result gets stored to
``query_results`` table. Also we check for all queries in the
``queries`` table that have the same query hash and update their
reference to the query result we just saved
(`code <https://github.com/EverythingMe/redash/blob/master/redash/models.py#L235>`__).
Client
------
The client (UI) will execute queries in two scenarios:
1. (automatically) When opening a query page of a query that doesn't
have a result yet.
2. (manually) When the user clicks on "Execute".
In each case the client does a POST request to ``/api/query_results``
with the following parameters: ``query`` (the query text),
``data_source_id`` (data source to execute the query with) and ``ttl``.
When loading a cached result, ``ttl`` will be the one set to the query
(if it was set). This is a relic from previous versions, and I'm not
sure if it's really used anymore, as usually we will fetch query result
using its id.
When loading a non cached result, ``ttl`` will be 0 which will "force"
the server to execute the query.
As a response to ``/api/query_results`` the server will send either the
query results (in case of a cached query) or job id of the currently
executing query. When job id received the client will start polling on
this id, until a query result received (this is encapsulated in
``Query`` and ``QueryResult`` services).
Ideas on how to implement query parameters
==========================================
Client side only implementation
-------------------------------
(This was actually implemented in. See pull request `#363 <https://github.com/EverythingMe/redash/pull/363>`__ for details.)
The basic idea of how to implement parametized queries is to treat the
query as a template and merge it with parameters taken from query string
or UI (or both).
When the caching facility isn't required (with queries that return in a
reasonable time frame) the implementation can be completly client side
and the backend can be "blind" to the parameters - it just receives the
final query to execute and returns result.
As one improvement over this, we can let the UI/user specify the TTL
value when making the request to ``/api/query_results``, in which case
caching will be availble too, while not having to make the server aware
of the parameters.
Hybrid
------
Another option, will be to store the list of possible parameters for a
query, with their default/optional values. In such case, the server can
prefetch all the options and cache them to provide faster results to the
client.

View File

@@ -0,0 +1,30 @@
Data Source Results Format
==========================
All data sources in re:dash return the following results in JSON format:
.. code:: javascript
{
"columns" : [
{
// Required: a unique identifier of the column name in this result
"name" : "COLUMN_NAME",
// Required: friendly name of the column that will appear in the results
"friendly_name" : "FRIENDLY_NAME",
// Optional: If not specified sort might not work well.
// Supported types: integer, float, boolean, string (default), datetime (ISO-8601 text format)
"type" : "VALUE_TYPE"
},
...
],
"rows" : [
{
// name is the column name as it appears in the columns above.
// VALUE is a valid JSON value. For dates its an ISO-8601 string.
"name" : VALUE,
"name2" : VALUE2
},
...
]
}

50
docs/dev/vagrant.rst Normal file
View File

@@ -0,0 +1,50 @@
Setting up development environment (using Vagrant)
==================================================
To simplify contribution there is a `Vagrant
box <https://vagrantcloud.com/redash/boxes/dev>`__ available with all
the needed software to run re:dash for development (use it only for
development, for demo purposes there is
`redash/demo <https://vagrantcloud.com/redash/boxes/demo>`__ box and the
AWS/GCE images).
To get started with this box:
1. Make sure you have recent version of
`Vagrant <https://www.vagrantup.com/>`__ installed.
2. Clone the re:dash repository:
``git clone https://github.com/EverythingMe/redash.git``.
3. Change dir into the repository (``cd redash``) and run run
``vagrant up``. This might take some time the first time you run it,
as it downloads the Vagrant virtual box.
4. Once Vagrant is ready, ssh into the instance (``vagrant ssh``), and
change dir to ``/opt/redash/current`` -- this is where your local
repository copy synced to.
5. Copy ``.env`` file into this directory (``cp ../.env ./``).
6. From ``/opt/redash/current/rd_ui`` run ``bower install`` to install
frontend packages. This can be done from your host machine as well,
if you have bower installed.
7. Go back to ``/opt/redash/current`` and install python dependencies
``sudo pip install -r requirements.txt``
8. Apply migrations
::
PYTHONPATH=. bin/run python migrations/0001_allow_delete_query.py
PYTHONPATH=. bin/run python migrations/0002_fix_timestamp_fields.py
PYTHONPATH=. bin/run python migrations/0003_update_data_source_config.py
PYTHONPATH=. bin/run python migrations/0004_allow_null_in_event_user.py
PYTHONPATH=. bin/run python migrations/0005_add_updated_at.py
PYTHONPATH=. bin/run python migrations/0006_queries_last_edit_by.py
PYTHONPATH=. bin/run python migrations/0007_add_schedule_to_queries.py
PYTHONPATH=. bin/run python migrations/0008_make_ds_name_unique.py
PYTHONPATH=. bin/run python migrations/0009_add_api_key_to_user.py
PYTHONPATH=. bin/run python migrations/0010_create_alerts.py
PYTHONPATH=. bin/run python migrations/0010_allow_deleting_datasources.py
PYTHONPATH=. bin/run python migrations/0011_migrate_bigquery_to_json.py
PYTHONPATH=. bin/run python migrations/0012_add_list_users_permission.py
9. Start the server and background workers with
``bin/run honcho start -f Procfile.dev``.
10. Now the server should be available on your host on port 9001 and you
can login with username admin and password admin.

57
docs/index.rst Normal file
View File

@@ -0,0 +1,57 @@
.. image:: http://redash.io/static/img/redash_logo.png
:width: 200px
Open Source Data Collaboration and Visualization Platform
===================================
**re:dash** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
Prior to **re:dash**, we tried to use traditional BI suites and discovered a set of bloated, technically challenged and slow tools/flows. What we were looking for was a more hacker'ish way to look at data, so we built one.
**re:dash** was built to allow fast and easy access to billions of records, that we process and collect using Amazon Redshift ("petabyte scale data warehouse" that "speaks" PostgreSQL).
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery,Google Spreadsheets, PostgreSQL, MySQL, Graphite and custom scripts.
Features
########
1. **Query Editor**: think of `JS Fiddle`_ for SQL queries. It's your way to share data in the organization in an open way, by sharing both the dataset and the query that generated it. This way everyone can peer review not only the resulting dataset but also the process that generated it.
2. **Visualizations**: once you have a dataset, you can create different visualizations out of it. Currently it supports charts, pivot table and cohorts.
3. **Dashboards**: combine several visualizations into a single dashboard.
Demo
####
.. figure:: https://raw.github.com/EverythingMe/redash/screenshots/screenshots.gif
:alt: Screenshots
You can try out the demo instance: `http://demo.redash.io`_ (login with any Google account).
.. _http://demo.redash.io: http://demo.redash.io
.. _JS Fiddle: http://jsfiddle.net
Getting Started
###############
:doc:`Setting up re:dash instance </setup>` (includes links to ready made AWS/GCE images).
Getting Help
############
* Source: https://github.com/everythingme/redash
* Issues: https://github.com/everythingme/redash/issues
* Mailing List: https://groups.google.com/forum/#!forum/redash-users
* Gitter (chat): https://gitter.im/EverythingMe/redash
* Contact Arik, the maintainer directly: arik@everything.me.
TOC
###
.. toctree::
:maxdepth: 2
setup
upgrade
datasources
usage
dev
misc

10
docs/misc.rst Normal file
View File

@@ -0,0 +1,10 @@
Miscellaneous
=============
.. toctree::
:maxdepth: 2
:glob:
misc/*

View File

@@ -0,0 +1,50 @@
How To: Create a Google Developers Project
==========================================
1. Go to the `Google Developers
Console <https://console.developers.google.com/>`__.
2. Select a project, or create a new one by clicking Create Project:
1. In the Project name field, type in a name for your project.
2. In the Project ID field, optionally type in a project ID for your
project or use the one that the console has created for you. This
ID must be unique world-wide.
3. Click the **Create** button and wait for the project to be
created.
4. Click on the new project name in the list to start editing the
project.
3. In the left sidebar, select the **APIs** item below "APIs & auth". A
list of Google web services appears.
4. Find the **Google+ API** service and set its status to **ON**—notice
that this action moves the service to the top of the list.
5. In the sidebar under "APIs & auth", select **Credentials** and in that screen choose the **OAuth consent screen** tab
- Choose an Email Address and specify a Product Name.
6. In the sidebar under "APIs & auth", select **Credentials**.
7. Click **Add Credentials** button and choose **OAuth 20 Client ID**.
- In the **Application type** section of the dialog, select **Web
application**.
- In the **Authorized JavaScript origins** field, enter the origin
for your app. You can enter multiple origins to use with multiple
re:dash instance. Wildcards are not allowed. In the example below,
we assume your re:dash instance address is *redash.example.com*:
::
http://redash.example.com
https://redash.example.com
- In the Authorized redirect URI field, enter the redirect URI
callback:
::
http://redash.example.com/oauth/google_callback
- Click the ``Create`` button.
8. In the resulting **Client ID for web application** section, copy the
**Client ID** and **Client secret** to your ``.env`` file.

59
docs/misc/ssl.rst Normal file
View File

@@ -0,0 +1,59 @@
SSL (HTTPS) Setup
=================
If you used the provided images or the bootstrap script, to start using
SSL with your instance you need to:
1. Update the nginx config file (``/etc/nginx/sites-available/redash``)
with SSL configuration (see below an example). Make sure to upload
the certificate to the server, and set the paths correctly in the new
config.
2. Open port 443 in your security group (if using AWS or GCE).
.. code:: nginx
upstream redash_servers {
server 127.0.0.1:5000;
}
server {
listen 80;
# Allow accessing /ping without https. Useful when placing behind load balancer.
location /ping {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://redash_servers;
}
location / {
# Enforce SSL.
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
# Make sure to set paths to your certificate .pem and .key files.
ssl on;
ssl_certificate /path-to/cert.pem; # or crt
ssl_certificate_key /path-to/cert.key;
access_log /var/log/nginx/redash.access.log;
gzip on;
gzip_types *;
gzip_proxied any;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://redash_servers;
proxy_redirect off;
}
}

3
docs/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
sphinx
sphinx-autobuild
sphinx_rtd_theme

142
docs/setup.rst Normal file
View File

@@ -0,0 +1,142 @@
Setting up re:dash instance
###########################
The `provisioning
script <https://github.com/EverythingMe/redash/blob/master/setup/bootstrap.sh>`__
works on Ubuntu 12.04, Ubuntu 14.04 and Debian Wheezy. This script
installs all needed dependencies and creates basic setup.
To ease the process, there are also images for AWS and Google Compute
Cloud. These images created with the same provision script using Packer.
Create an instance
==================
Google Compute Engine
---------------------
First, you need to add the images to your account:
.. code:: bash
$ gcloud compute images create "redash-071-b1015" --source-uri gs://redash-images/redash.0.7.1.b1015.tar.gz
Next you need to launch an instance using this image (n1-standard-1
instance type is recommended). If you plan using re:dash with BigQuery,
you can use a dedicated image which comes with BigQuery preconfigured
(using instance permissions):
.. code:: bash
$ gcloud compute images create "redash-071-b1015-bq" --source-uri gs://redash-images/redash.0.7.1.b1015-bq.tar.gz
Note that you need to launch this instance with BigQuery access:
.. code:: bash
$ gcloud compute instances create <your_instance_name> --image redash-071-b1015-bq --scopes storage-ro,bigquery
(the same can be done from the web interface, just make sure to enable
BigQuery access)
Now proceed to `"Setup" <#setup>`__.
AWS
---
Launch the instance with from the pre-baked AMI (for small deployments
t2.micro should be enough):
- us-east-1: `ami-95e04efe <https://console.aws.amazon.com/ec2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-95e04efe>`__
- us-west-2: `ami-01d8d331 <https://console.aws.amazon.com/ec2/home?region=us-west-2#LaunchInstanceWizard:ami=ami-01d8d331>`__
- us-west-1: `ami-b35ea1f7 <https://console.aws.amazon.com/ec2/home?region=us-west-1#LaunchInstanceWizard:ami=ami-b35ea1f7>`__
- eu-west-1: `ami-d46734a3 <https://console.aws.amazon.com/ec2/home?region=eu-west-1#LaunchInstanceWizard:ami=ami-d46734a3>`__
- eu-central-1: `ami-7e494e63 <https://console.aws.amazon.com/ec2/home?region=eu-central-1#LaunchInstanceWizard:ami=ami-7e494e63>`__
- ap-southeast-1: `ami-30343b62 <https://console.aws.amazon.com/ec2/home?region=ap-southeast-1#LaunchInstanceWizard:ami=ami-30343b62>`__
- ap-southeast-2: `ami-53357669 <https://console.aws.amazon.com/ec2/home?region=ap-southeast-2#LaunchInstanceWizard:ami=ami-53357669>`__
- ap-northeast-1: `ami-4253ea42 <https://console.aws.amazon.com/ec2/home?region=ap-northeast-1#LaunchInstanceWizard:ami=ami-4253ea42>`__
- sa-east-1: `ami-b170f9ac <https://console.aws.amazon.com/ec2/home?region=sa-east-1#LaunchInstanceWizard:ami=ami-b170f9ac>`__
Now proceed to `"Setup" <#setup>`__.
Other
-----
Download the provision script and run it on your machine. Note that:
1. You need to run the script as root.
2. It was tested only on Ubuntu 12.04, Ubuntu 14.04 and Debian Wheezy.
3. It's designed to run on a "clean" machine. If you're running this script on a machine that is used for other purposes, you might want to tweak it to your needs (like removing the ``apt-get dist-upgrade`` call at the beginning of it).
Setup
=====
Once you created the instance with either the image or the script, you
should have a running re:dash instance with everything you need to get
started. You can now login to it with the user "admin" (password:
"admin"). But to make it useful, there are a few more steps that you
need to manually do to complete the setup:
First ssh to your instance and change directory to ``/opt/redash``. If
you're using the GCE image, switch to root (``sudo su``).
Users & Google Authentication setup
-----------------------------------
Most of the settings you need to edit are in the ``/opt/redash/.env``
file.
1. Update the cookie secret (important! otherwise anyone can sign new
cookies and impersonate users): change "veryverysecret" in the line:
``export REDASH_COOKIE_SECRET=veryverysecret`` to something else (you
can use ``pwgen 32 -1`` to generate random string).
2. By default we create an admin user with the password "admin". You
can change this password at: ``/users/me#password``.
3. If you want to use Google OAuth to authenticate users, you need to
create a Google Developers project (see :doc:`instructions </misc/google_developers_project>`)
and then add the needed configuration in the ``.env`` file:
.. code::
export REDASH_GOOGLE_CLIENT_ID=""
export REDASH_GOOGLE_CLIENT_SECRET=""
export REDASH_GOOGLE_APPS_DOMAIN=""
``REDASH_GOOGLE_CLIENT_ID`` and ``REDASH_GOOGLE_CLIENT_SECRET`` are the values you get after registering with Google. ``READASH_GOOGLE_APPS_DOMAIN`` is used in case you want to limit access to single Google apps domain (*if you leave it empty anyone with a Google account can access your instance*).
4. Restart the web server to apply the configuration changes:
``sudo supervisorctl restart redash_server``.
5. Once you have Google OAuth enabled, you can login using your Google
Apps account. If you want to grant admin permissions to some users,
you can do this by editing the user profile and enabling admin
permission for it.
6. If you don't use Google OAuth or just need username/password logins,
you can create additional users at: ``/users/new``.
Datasources
-----------
To make re:dash truly useful, you need to setup your data sources in it. Browse to ``/data_sources`` on your instance,
to create new data source connection.
See :doc:`documentation </datasources>` for the different options.
Your instance comes ready with dependencies needed to setup supported sources.
How to upgrade?
---------------
It's recommended to upgrade once in a while your re:dash instance to
benefit from bug fixes and new features. See :doc:`here </upgrade>` for full upgrade
instructions (including Fabric script).
Notes
=====
- If this is a production setup, you should enforce HTTPS and make sure
you set the cookie secret (see :doc:`instructions </misc/ssl>`).

36
docs/upgrade.rst Normal file
View File

@@ -0,0 +1,36 @@
How to Upgrade
##############
It's recommended to upgrade your re:dash instance once there are new
releases, to benefit from new features and bug fixes. The upgrade
process is relatively simple, and assuming you used one of the base
images we provide, you can just use the
`Fabric <http://www.fabfile.org/>`__ script provided here:
https://gist.github.com/arikfr/440d1403b4aeb76ebaf8.
How to run the Fabric script
============================
1. Install Fabric: ``pip install fabric requests`` (needed only once)
2. Download the ``fabfile.py`` from the gist.
3. Run the script:
``fab -H{your re:dash host} -u{the ssh user for this host} -i{path to key file for passwordless login} deploy_latest_release``
``-i`` is optional and it is only needed in case you're using private-key based authentication (and didn't add the key file to your authentication agent or set its path in your SSH config).
What the Fabric script does
===========================
Even if you didn't use the image, it's very likely you can reuse most of
this script with small modifications. What this script does is:
1. Find the URL of the latest release tarball (from `GitHub releases
page <github.com/everythingme/redash/releases>`__).
2. Download it.
3. Create new directory for this version (for example:
``/opt/redash/redash.0.5.0.b685``).
4. Unpack that (``tar -C {dir} -xvf {tarball path}``).
5. Link ``/opt/redash/.env`` file into this directory.
6. Apply any new migrations.
7. Link ``/opt/redash/current`` to new version.
8. Restart web server and celery workers.

11
docs/usage.rst Normal file
View File

@@ -0,0 +1,11 @@
Usage
=====
.. toctree::
:maxdepth: 2
:glob:
usage/maintenance.rst
usage/*

View File

@@ -0,0 +1,48 @@
ElasticSearch: Querying
#######################
ElasticSearch currently supports only simple Lucene style queries (like
Kibana but without the aggregation).
Full blown JSON based ElasticSearch queries (including aggregations)
will be added later.
Simple query example:
=====================
- Query the index named "twitter"
- Filter by "user:kimchy"
- Return the fields: "@timestamp", "tweet" and "user"
- Return up to 15 results
- Sort by @timestamp ascending
.. code:: json
{
"index" : "twitter",
"query" : "user:kimchy",
"fields" : ["@timestamp", "tweet", "user"],
"size" : 15,
"sort" : "@timestamp:asc"
}
Simple query on a logstash ElasticSearch instance:
==================================================
- Query the index named "logstash-2015.04.\*" (in this case its all of
April 2015)
- Filter by type:events AND eventName:UserUpgrade AND channel:selfserve
- Return fields: "@timestamp", "userId", "channel", "utm\_source",
"utm\_medium", "utm\_campaign", "utm\_content"
- Return up to 250 results
- Sort by @timestamp ascending
.. code:: json
{
"index" : "logstash-2015.04.*",
"query" : "type:events AND eventName:UserUpgrade AND channel:selfserve",
"fields" : ["@timestamp", "userId", "channel", "utm_source", "utm_medium", "utm_campaign", "utm_content"],
"size" : 250,
"sort" : "@timestamp:asc"
}

View File

@@ -0,0 +1,94 @@
Ongoing Maintanence and Basic Operations
########################################
Configuration and logs
======================
The supervisor config can be found in
``/opt/redash/supervisord/supervisord.conf``.
There you can see the names of its programs (``redash_celery``,
``redash_server``) and the location of their logs.
Restart
=======
Restarting the Web Server
-------------------------
``sudo supervisorctl stop redash_server``
Restarting Celery Workers
-------------------------
``sudo supervisorctl restart redash_celery``
Restarting Celery Workers & the Queries Queue
---------------------------------------------
In case you are handling a problem, and you need to stop the currently
running queries and reset the queue, follow the steps below.
1. Stop celery: ``sudo supervisorctl stop redash_celery`` (celery might
take some time to stop, if it's in the middle of running a query)
2. Flush redis: ``redis-cli flushdb``
3. Start celery: ``sudo supervisorctl start redash_celery``
Changing the Number of Workers
==============================
By default, Celery will start a worker per CPU core. Because most of
re:dash's tasks are IO bound, the real limit for number of workers you
can use depends on the amount of memory your machine has. It's
recommended to increase number of workers, to support more concurrent
queries.
1. Open the supervisord configuration file:
``/opt/redash/supervisord/supervisord.conf``
2. Edit the ``[program:redash_celery]`` section and add to the *command*
value, the param "-c" with the number of concurrent workers you need.
3. Restart supervisord to apply new configuration:
``sudo /etc/init.d/redash_supervisord restart``.
DB
==
Show the Currently Configured Data Source
-----------------------------------------
This varies based on the redash version and personal preferences. You
can do one of the following:
Using the CLI
~~~~~~~~~~~~~
In ``/opt/redash/current``, run:
``sudo -u redash bin/run ./manage.py ds list``
Using the Admin
~~~~~~~~~~~~~~~
(available from version 0.6b797). Browse to ``/admin/datasource``
View the Definition Directly in the DB
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1. Open psql: ``sudo -u redash psql``
2. Run the query: ``SELECT * from data_sources;``
Backup re:dash's DB:
--------------------
``sudo -u redash pg_dump > backup_filename.sql``
Version
=======
See current version:
``bin/run ./manage.py version``

View File

@@ -0,0 +1,74 @@
MongoDB: Querying
#################
Simple query example:
=====================
.. code:: json
{
"collection" : "my_collection",
"query" : {
"date" : {
"$gt" : "ISODate(\"2015-01-15 11:41\")",
},
"type" : 1
},
"fields" : {
"_id" : 1,
"name" : 2
},
"sort" : [
{
"name" : "date",
"direction" : -1
}
]
}
Live example on the demo instance:
http://demo.redash.io/queries/394/source.
Aggregation
===========
Uses a syntax similar to the one used in PyMongo, however to support the
correct order of sorting, it uses a regular list for the "$sort"
operation that converts into a SON (sorted dictionary) object before
execution.
Aggregation query example:
.. code:: json
{
"collection" : "things",
"aggregate" : [
{
"$unwind" : "$tags"
},
{
"$group" : {
"_id" : "$tags",
"count" : { "$sum" : 1 }
}
},
{
"$sort" : [
{
"name" : "count",
"direction" : -1
},
{
"name" : "_id",
"direction" : -1
}
]
}
]
}
Live examples on the demo instance:
1. http://demo.redash.io/queries/393/source
2. http://demo.redash.io/queries/387/source

152
manage.py
View File

@@ -2,27 +2,35 @@
"""
CLI to manage redash.
"""
import datetime
from flask.ext.script import Manager, prompt_pass
import json
from flask.ext.script import Manager
from redash import settings, models, __version__
from redash.wsgi import app
from redash.import_export import import_manager
from redash.cli import users, database, data_sources
from redash.monitor import get_status
manager = Manager(app)
database_manager = Manager(help="Manages the database (create/drop tables).")
users_manager = Manager(help="Users management commands.")
data_sources_manager = Manager(help="Data sources management commands.")
manager.add_command("database", database.manager)
manager.add_command("users", users.manager)
manager.add_command("import", import_manager)
manager.add_command("ds", data_sources.manager)
@manager.command
def version():
"""Displays re:dash version."""
print __version__
@manager.command
def status():
print json.dumps(get_status(), indent=2)
@manager.command
def runworkers():
"""Prints deprecation warning."""
"""Start workers (deprecated)."""
print "** This command is deprecated. Please use Celery's CLI to control the workers. **"
@@ -31,134 +39,20 @@ def make_shell_context():
from redash.models import db
return dict(app=app, db=db, models=models)
@manager.command
def check_settings():
from types import ModuleType
for name in dir(settings):
item = getattr(settings, name)
if not callable(item) and not name.startswith("__") and not isinstance(item, ModuleType):
print "{} = {}".format(name, item)
"""Show the settings as re:dash sees them (useful for debugging)."""
for name, item in settings.all_settings().iteritems():
print "{} = {}".format(name, item)
@manager.command
def import_events(events_file):
# TODO: remove this code past 1/11/2014.
import json
from collections import Counter
def send_test_mail():
from redash import mail
from flask_mail import Message
count = Counter()
mail.send(Message(subject="Test Message from re:dash", recipients=[settings.MAIL_DEFAULT_SENDER], body="Test message."))
with open(events_file) as f:
for line in f:
try:
event = json.loads(line)
object_type = event.get('object_type', None)
object_id = event.get('object_id', None)
if object_id == 'dashboard' and object_type == 'dashboard':
count['bad dashboard id'] += 1
continue
models.Event.record(event)
count['imported'] += 1
except Exception as ex:
print "Failed importing line:"
print line
print ex.message
count[ex.message] += 1
count['failed'] += 1
models.db.close_db(None)
for k, v in count.iteritems():
print k
print v
@database_manager.command
def create_tables():
"""Creates the database tables."""
from redash.models import create_db, init_db
create_db(True, False)
init_db()
@database_manager.command
def drop_tables():
"""Drop the database tables."""
from redash.models import create_db
create_db(False, True)
@users_manager.option('email', help="User's email")
@users_manager.option('name', help="User's full name")
@users_manager.option('--admin', dest='is_admin', action="store_true", default=False, help="set user as admin")
@users_manager.option('--google', dest='google_auth', action="store_true", default=False, help="user uses Google Auth to login")
@users_manager.option('--password', dest='password', default=None, help="Password for users who don't use Google Auth (leave blank for prompt).")
@users_manager.option('--groups', dest='groups', default=models.User.DEFAULT_GROUPS, help="Comma seperated list of groups (leave blank for default).")
def create(email, name, groups, is_admin=False, google_auth=False, password=None):
print "Creating user (%s, %s)..." % (email, name)
print "Admin: %r" % is_admin
print "Login with Google Auth: %r\n" % google_auth
if isinstance(groups, basestring):
groups= groups.split(',')
groups.remove('') # in case it was empty string
if is_admin:
groups += ['admin']
user = models.User(email=email, name=name, groups=groups)
if not google_auth:
password = password or prompt_pass("Password")
user.hash_password(password)
try:
user.save()
except Exception, e:
print "Failed creating user: %s" % e.message
@users_manager.option('email', help="email address of user to delete")
def delete(email):
deleted_count = models.User.delete().where(models.User.email == email).execute()
print "Deleted %d users." % deleted_count
@data_sources_manager.command
def import_from_settings(name=None):
"""Import data source from settings (env variables)."""
name = name or "Default"
data_source = models.DataSource.create(name=name,
type=settings.CONNECTION_ADAPTER,
options=settings.CONNECTION_STRING)
print "Imported data source from settings (id={}).".format(data_source.id)
@data_sources_manager.command
def list():
"""List currently configured data sources"""
for ds in models.DataSource.select():
print "Name: {}\nType: {}\nOptions: {}".format(ds.name, ds.type, ds.options)
@data_sources_manager.command
def new(name, type, options):
"""Create new data source"""
# TODO: validate it's a valid type and in the future, validate the options.
print "Creating {} data source ({}) with options:\n{}".format(type, name, options)
data_source = models.DataSource.create(name=name,
type=type,
options=options)
print "Id: {}".format(data_source.id)
manager.add_command("database", database_manager)
manager.add_command("users", users_manager)
manager.add_command("import", import_manager)
manager.add_command("ds", data_sources_manager)
if __name__ == '__main__':
manager.run()
manager.run()

View File

@@ -0,0 +1,15 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.add_column('queries', 'is_archived', models.Query.is_archived)
)
db.close_db(None)

View File

@@ -0,0 +1,21 @@
from redash.models import db
if __name__ == '__main__':
db.connect_db()
columns = (
('activity_log', 'created_at'),
('dashboards', 'created_at'),
('data_sources', 'created_at'),
('events', 'created_at'),
('groups', 'created_at'),
('queries', 'created_at'),
('widgets', 'created_at'),
('query_results', 'retrieved_at')
)
with db.database.transaction():
for column in columns:
db.database.execute_sql("ALTER TABLE {} ALTER COLUMN {} TYPE timestamp with time zone;".format(*column))
db.close_db(None)

View File

@@ -0,0 +1,73 @@
import json
from redash import query_runner
from redash.models import DataSource
def update(data_source):
print "[%s] Old options: %s" % (data_source.name, data_source.options)
if query_runner.validate_configuration(data_source.type, data_source.options):
print "[%s] configuration already valid. skipping." % data_source.name
return
if data_source.type == 'pg':
values = data_source.options.split(" ")
configuration = {}
for value in values:
k, v = value.split("=", 1)
configuration[k] = v
if k == 'port':
configuration[k] = int(v)
data_source.options = json.dumps(configuration)
elif data_source.type == 'mysql':
mapping = {
'Server': 'host',
'User': 'user',
'Pwd': 'passwd',
'Database': 'db'
}
values = data_source.options.split(";")
configuration = {}
for value in values:
k, v = value.split("=", 1)
configuration[mapping[k]] = v
data_source.options = json.dumps(configuration)
elif data_source.type == 'graphite':
old_config = json.loads(data_source.options)
configuration = {
"url": old_config["url"]
}
if "verify" in old_config:
configuration['verify'] = old_config['verify']
if "auth" in old_config:
configuration['username'], configuration['password'] = old_config["auth"]
data_source.options = json.dumps(configuration)
elif data_source.type == 'url':
data_source.options = json.dumps({"url": data_source.options})
elif data_source.type == 'script':
data_source.options = json.dumps({"path": data_source.options})
elif data_source.type == 'mongo':
data_source.type = 'mongodb'
else:
print "[%s] No need to convert type of: %s" % (data_source.name, data_source.type)
print "[%s] New options: %s" % (data_source.name, data_source.options)
data_source.save()
if __name__ == '__main__':
for data_source in DataSource.all():
update(data_source)

View File

@@ -0,0 +1,12 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.drop_not_null('events', 'user_id')
)

View File

@@ -0,0 +1,26 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.add_column('queries', 'updated_at', models.Query.updated_at),
migrator.add_column('dashboards', 'updated_at', models.Dashboard.updated_at),
migrator.add_column('widgets', 'updated_at', models.Widget.updated_at),
migrator.add_column('users', 'created_at', models.User.created_at),
migrator.add_column('users', 'updated_at', models.User.updated_at),
migrator.add_column('visualizations', 'created_at', models.Visualization.created_at),
migrator.add_column('visualizations', 'updated_at', models.Visualization.updated_at)
)
db.database.execute_sql("UPDATE queries SET updated_at = created_at;")
db.database.execute_sql("UPDATE dashboards SET updated_at = created_at;")
db.database.execute_sql("UPDATE widgets SET updated_at = created_at;")
db.close_db(None)

View File

@@ -0,0 +1,19 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.add_column('queries', 'last_modified_by_id', models.Query.last_modified_by)
)
db.database.execute_sql("UPDATE queries SET last_modified_by_id = user_id;")
db.close_db(None)

View File

@@ -0,0 +1,23 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.add_column('queries', 'schedule', models.Query.schedule),
)
db.database.execute_sql("UPDATE queries SET schedule = ttl WHERE ttl > 0;")
migrate(
migrator.drop_column('queries', 'ttl')
)
db.close_db(None)

View File

@@ -0,0 +1,20 @@
from redash.models import db
if __name__ == '__main__':
db.connect_db()
with db.database.transaction():
# Make sure all data sources names are unique.
db.database.execute_sql("""
UPDATE data_sources
SET name = new_names.name
FROM (
SELECT id, name || ' ' || id as name
FROM (SELECT id, name, rank() OVER (PARTITION BY name ORDER BY created_at ASC) FROM data_sources) ds WHERE rank > 1
) AS new_names
WHERE data_sources.id = new_names.id;
""")
# Add unique constraint on data_sources.name.
db.database.execute_sql("ALTER TABLE data_sources ADD CONSTRAINT unique_name UNIQUE (name);")
db.close_db(None)

View File

@@ -0,0 +1,27 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
from redash import models
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
column = models.User.api_key
column.null = True
migrate(
migrator.add_column('users', 'api_key', models.User.api_key),
)
for user in models.User.select():
user.save()
migrate(
migrator.add_not_null('users', 'api_key')
)
db.close_db(None)

View File

@@ -0,0 +1,18 @@
from playhouse.migrate import PostgresqlMigrator, migrate
from redash.models import db
if __name__ == '__main__':
db.connect_db()
migrator = PostgresqlMigrator(db.database)
with db.database.transaction():
migrate(
migrator.drop_not_null('queries', 'data_source_id'),
)
db.close_db(None)

View File

@@ -0,0 +1,8 @@
from redash.models import db, Alert, AlertSubscription
if __name__ == '__main__':
with db.database.transaction():
Alert.create_table()
AlertSubscription.create_table()
db.close_db(None)

View File

@@ -0,0 +1,44 @@
from base64 import b64encode
import json
from redash.models import DataSource
def convert_p12_to_pem(p12file):
from OpenSSL import crypto
with open(p12file, 'rb') as f:
p12 = crypto.load_pkcs12(f.read(), "notasecret")
return crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey())
if __name__ == '__main__':
for ds in DataSource.all():
if ds.type == 'bigquery':
options = json.loads(ds.options)
if 'jsonKeyFile' in options:
continue
new_options = {
'projectId': options['projectId'],
'jsonKeyFile': b64encode(json.dumps({
'client_email': options['serviceAccount'],
'private_key': convert_p12_to_pem(options['privateKey'])
}))
}
ds.options = json.dumps(new_options)
ds.save()
elif ds.type == 'google_spreadsheets':
options = json.loads(ds.options)
if 'jsonKeyFile' in options:
continue
with open(options['credentialsFilePath']) as f:
new_options = {
'jsonKeyFile': b64encode(f.read())
}
ds.options = json.dumps(new_options)
ds.save()

View File

@@ -0,0 +1,6 @@
from redash import models
if __name__ == '__main__':
default_group = models.Group.get(models.Group.name=='default')
default_group.permissions.append('list_users')
default_group.save()

View File

@@ -19,6 +19,7 @@
"trailing": true,
"smarttabs": true,
"globals": {
"angular": false
"angular": false,
"_": false
}
}

View File

@@ -163,7 +163,6 @@ module.exports = function (grunt) {
// Automatically inject Bower components into the app
wiredep: {
options: {
cwd: '<%= yeoman.app %>'
},
app: {
src: ['<%= yeoman.app %>/index.html'],
@@ -321,7 +320,12 @@ module.exports = function (grunt) {
src: ['generated/*']
}, {
expand: true,
cwd: 'bower_components/bootstrap/dist',
cwd: '<%= yeoman.app %>/bower_components/bootstrap/dist',
src: 'fonts/*',
dest: '<%= yeoman.dist %>'
}, {
expand: true,
cwd: '<%= yeoman.app %>/bower_components/font-awesome',
src: 'fonts/*',
dest: '<%= yeoman.dist %>'
}]

View File

@@ -1,228 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
<font-face units-per-em="1200" ascent="960" descent="-240" />
<missing-glyph horiz-adv-x="500" />
<glyph />
<glyph />
<glyph unicode=" " />
<glyph unicode="*" d="M1100 500h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200z" />
<glyph unicode="+" d="M1100 400h-400v-400h-300v400h-400v300h400v400h300v-400h400v-300z" />
<glyph unicode="&#xa0;" />
<glyph unicode="&#x2000;" horiz-adv-x="652" />
<glyph unicode="&#x2001;" horiz-adv-x="1304" />
<glyph unicode="&#x2002;" horiz-adv-x="652" />
<glyph unicode="&#x2003;" horiz-adv-x="1304" />
<glyph unicode="&#x2004;" horiz-adv-x="434" />
<glyph unicode="&#x2005;" horiz-adv-x="326" />
<glyph unicode="&#x2006;" horiz-adv-x="217" />
<glyph unicode="&#x2007;" horiz-adv-x="217" />
<glyph unicode="&#x2008;" horiz-adv-x="163" />
<glyph unicode="&#x2009;" horiz-adv-x="260" />
<glyph unicode="&#x200a;" horiz-adv-x="72" />
<glyph unicode="&#x202f;" horiz-adv-x="260" />
<glyph unicode="&#x205f;" horiz-adv-x="326" />
<glyph unicode="&#x20ac;" d="M800 500h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257 q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406z" />
<glyph unicode="&#x2212;" d="M1100 700h-900v-300h900v300z" />
<glyph unicode="&#x2601;" d="M178 300h750q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57z" />
<glyph unicode="&#x2709;" d="M1200 1100h-1200l600 -603zM300 600l-300 -300v600zM1200 900v-600l-300 300zM800 500l400 -400h-1200l400 400l200 -200z" />
<glyph unicode="&#x270f;" d="M1101 889l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13l-94 -97zM401 189l614 614l-214 214l-614 -614zM-13 -13l333 112l-223 223z" />
<glyph unicode="&#xe000;" horiz-adv-x="500" d="M0 0z" />
<glyph unicode="&#xe001;" d="M700 100h300v-100h-800v100h300v550l-500 550h1200l-500 -550v-550z" />
<glyph unicode="&#xe002;" d="M1000 934v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q17 -55 85.5 -75.5t147.5 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7q-79 -25 -122.5 -82t-25.5 -112t86 -75.5t147 5.5 q65 21 109 69t44 90v606z" />
<glyph unicode="&#xe003;" d="M913 432l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -142 -78 -261zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
<glyph unicode="&#xe005;" d="M649 949q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5t-94 124.5t-33.5 117.5q0 64 28 123t73 100.5t104.5 64t119 20.5 t120 -38.5t104.5 -104.5z" />
<glyph unicode="&#xe006;" d="M791 522l145 -449l-384 275l-382 -275l146 447l-388 280h479l146 400h2l146 -400h472zM168 71l2 1z" />
<glyph unicode="&#xe007;" d="M791 522l145 -449l-384 275l-382 -275l146 447l-388 280h479l146 400h2l146 -400h472zM747 331l-74 229l193 140h-235l-77 211l-78 -211h-239l196 -142l-73 -226l192 140zM168 71l2 1z" />
<glyph unicode="&#xe008;" d="M1200 143v-143h-1200v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100z" />
<glyph unicode="&#xe009;" d="M1200 1100v-1100h-1200v1100h1200zM200 1000h-100v-100h100v100zM900 1000h-600v-400h600v400zM1100 1000h-100v-100h100v100zM200 800h-100v-100h100v100zM1100 800h-100v-100h100v100zM200 600h-100v-100h100v100zM1100 600h-100v-100h100v100zM900 500h-600v-400h600 v400zM200 400h-100v-100h100v100zM1100 400h-100v-100h100v100zM200 200h-100v-100h100v100zM1100 200h-100v-100h100v100z" />
<glyph unicode="&#xe010;" d="M500 1050v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5zM1100 1050v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400 q21 0 35.5 -14.5t14.5 -35.5zM500 450v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5zM1100 450v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5z" />
<glyph unicode="&#xe011;" d="M300 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5zM1100 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM300 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1100 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM300 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM700 250v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1100 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5 t14.5 -35.5z" />
<glyph unicode="&#xe012;" d="M300 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1200 1050v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h700 q21 0 35.5 -14.5t14.5 -35.5zM300 450v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-200q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM1200 650v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5zM300 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5zM1200 250v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5z" />
<glyph unicode="&#xe013;" d="M448 34l818 820l-212 212l-607 -607l-206 207l-212 -212z" />
<glyph unicode="&#xe014;" d="M882 106l-282 282l-282 -282l-212 212l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282z" />
<glyph unicode="&#xe015;" d="M913 432l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -142 -78 -261zM507 363q137 0 233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5t-234 -97t-97 -233 t97 -233t234 -97zM600 800h100v-200h-100v-100h-200v100h-100v200h100v100h200v-100z" />
<glyph unicode="&#xe016;" d="M913 432l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 299q-120 -77 -261 -77q-200 0 -342 142t-142 342t142 342t342 142t342 -142t142 -342q0 -141 -78 -262zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 801v-200h400v200h-400z" />
<glyph unicode="&#xe017;" d="M700 750v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5zM800 975v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123 t-123 184t-45.5 224.5q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155z" />
<glyph unicode="&#xe018;" d="M1200 1h-200v1200h200v-1200zM900 1h-200v800h200v-800zM600 1h-200v500h200v-500zM300 301h-200v-300h200v300z" />
<glyph unicode="&#xe019;" d="M488 183l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5 q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39zM600 815q89 0 152 -63 t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152q0 88 63 151t152 63z" />
<glyph unicode="&#xe020;" d="M900 1100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100zM800 1100v100h-300v-100h300zM200 900h900v-800q0 -41 -29.5 -71 t-70.5 -30h-700q-41 0 -70.5 30t-29.5 71v800zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
<glyph unicode="&#xe021;" d="M1301 601h-200v-600h-300v400h-300v-400h-300v600h-200l656 644z" />
<glyph unicode="&#xe022;" d="M600 700h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18v1150q0 11 7 18t18 7h475v-500zM1000 800h-300v300z" />
<glyph unicode="&#xe023;" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM600 600h200 v-100h-300v400h100v-300z" />
<glyph unicode="&#xe024;" d="M721 400h-242l-40 -400h-539l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538zM712 500l-27 300h-170l-27 -300h224z" />
<glyph unicode="&#xe025;" d="M1100 400v-400h-1100v400h490l-290 300h200v500h300v-500h200l-290 -300h490zM988 300h-175v-100h175v100z" />
<glyph unicode="&#xe026;" d="M600 1199q122 0 233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233t47.5 233t127.5 191t191 127.5t233 47.5zM600 1012q-170 0 -291 -121t-121 -291t121 -291t291 -121t291 121 t121 291t-121 291t-291 121zM700 600h150l-250 -300l-250 300h150v300h200v-300z" />
<glyph unicode="&#xe027;" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM850 600h-150 v-300h-200v300h-150l250 300z" />
<glyph unicode="&#xe028;" d="M0 500l200 700h800q199 -700 200 -700v-475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18v475zM903 1000h-606l-97 -500h200l50 -200h300l50 200h200z" />
<glyph unicode="&#xe029;" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5zM797 598 l-297 -201v401z" />
<glyph unicode="&#xe030;" d="M1177 600h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123t-123 -184t-45.5 -224.5t45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123 t123 184t45.5 224.5z" />
<glyph unicode="&#xe031;" d="M700 800l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400zM500 400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122l-145 -145v400h400z" />
<glyph unicode="&#xe032;" d="M100 1200v-1200h1100v1200h-1100zM1100 100h-900v900h900v-900zM400 800h-100v100h100v-100zM1000 800h-500v100h500v-100zM400 600h-100v100h100v-100zM1000 600h-500v100h500v-100zM400 400h-100v100h100v-100zM1000 400h-500v100h500v-100zM400 200h-100v100h100v-100 zM1000 300h-500v-100h500v100z" />
<glyph unicode="&#xe034;" d="M200 0h-100v1100h100v-1100zM1100 600v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5z" />
<glyph unicode="&#xe035;" d="M1200 275v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5t-49.5 -227v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50 q11 0 18 7t7 18zM400 480v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14zM1000 480v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14z" />
<glyph unicode="&#xe036;" d="M0 800v-400h300l300 -200v800l-300 -200h-300zM971 600l141 -141l-71 -71l-141 141l-141 -141l-71 71l141 141l-141 141l71 71l141 -141l141 141l71 -71z" />
<glyph unicode="&#xe037;" d="M0 800v-400h300l300 -200v800l-300 -200h-300zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
<glyph unicode="&#xe038;" d="M974 186l6 8q142 178 142 405q0 230 -144 408l-6 8l-83 -64l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8zM300 801l300 200v-800l-300 200h-300v400h300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257z" />
<glyph unicode="&#xe039;" d="M100 700h400v100h100v100h-100v300h-500v-600h100v100zM1200 700v500h-600v-200h100v-300h200v-300h300v200h-200v100h200zM100 1100h300v-300h-300v300zM800 800v300h300v-300h-300zM200 900h100v100h-100v-100zM900 1000h100v-100h-100v100zM300 600h-100v-100h-200 v-500h500v500h-200v100zM900 200v-100h-200v100h-100v100h100v200h-200v100h300v-300h200v-100h-100zM400 400v-300h-300v300h300zM300 200h-100v100h100v-100zM1100 300h100v-100h-100v100zM600 100h100v-100h-100v100zM1200 100v-100h-300v100h300z" />
<glyph unicode="&#xe040;" d="M100 1200h-100v-1000h100v1000zM300 200h-100v1000h100v-1000zM700 200h-200v1000h200v-1000zM900 200h-100v1000h100v-1000zM1200 1200v-1000h-200v1000h200zM400 100v-100h-300v100h300zM500 91h100v-91h-100v91zM700 91h100v-91h-100v91zM1100 91v-91h-200v91h200z " />
<glyph unicode="&#xe041;" d="M1200 500l-500 -500l-699 700v475q0 10 7.5 17.5t17.5 7.5h474zM320 882q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71t29 -71q30 -30 71.5 -30t71.5 30z" />
<glyph unicode="&#xe042;" d="M1201 500l-500 -500l-699 700v475q0 11 7 18t18 7h474zM1501 500l-500 -500l-50 50l450 450l-700 700h100zM320 882q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71t30 -71q29 -30 71 -30t71 30z" />
<glyph unicode="&#xe043;" d="M1200 1200v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900v1025l175 175h925z" />
<glyph unicode="&#xe045;" d="M947 829l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18l-94 -346l40 -124h592zM1200 800v-700h-200v200h-800v-200h-200v700h200l100 -200h600l100 200h200zM881 176l38 -152q2 -10 -3.5 -17t-15.5 -7h-600q-10 0 -15.5 7t-3.5 17l38 152q2 10 11.5 17t19.5 7 h500q10 0 19.5 -7t11.5 -17z" />
<glyph unicode="&#xe047;" d="M1200 0v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417zM416 521l178 457l46 -140l116 -317 h-340z" />
<glyph unicode="&#xe048;" d="M100 1199h471q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111t-162 -38.5h-500v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21 t-29 14t-49 14.5v70zM400 1079v-379h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400z" />
<glyph unicode="&#xe049;" d="M877 1200l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425z" />
<glyph unicode="&#xe050;" d="M1150 1200h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49v300h150h700zM100 1000v-800h75l-125 -167l-125 167h75v800h-75l125 167 l125 -167h-75z" />
<glyph unicode="&#xe051;" d="M950 1201h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50v300h150h700zM200 101h800v75l167 -125l-167 -125v75h-800v-75l-167 125l167 125 v-75z" />
<glyph unicode="&#xe052;" d="M700 950v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35zM1100 650v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h1000 q21 0 35.5 15t14.5 35zM900 350v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35zM1200 50v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35 t35.5 -15h1100q21 0 35.5 15t14.5 35z" />
<glyph unicode="&#xe053;" d="M1000 950v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35zM1200 650v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h1100 q21 0 35.5 15t14.5 35zM1000 350v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35zM1200 50v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35 t35.5 -15h1100q21 0 35.5 15t14.5 35z" />
<glyph unicode="&#xe054;" d="M500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe055;" d="M0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe056;" d="M0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35zM0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
<glyph unicode="&#xe057;" d="M400 1100h-100v-1100h100v1100zM700 950v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35zM1100 650v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15 h500q20 0 35 15t15 35zM100 425v75h-201v100h201v75l166 -125zM900 350v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35zM1200 50v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5 v-100q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35z" />
<glyph unicode="&#xe058;" d="M201 950v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35zM801 1100h100v-1100h-100v1100zM601 650v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15 h500q20 0 35 15t15 35zM1101 425v75h200v100h-200v75l-167 -125zM401 350v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35zM701 50v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5 v-100q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35z" />
<glyph unicode="&#xe059;" d="M900 925v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53zM1200 300l-300 300l300 300v-600z" />
<glyph unicode="&#xe060;" d="M1200 1056v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31zM1100 1000h-1000v-737l247 182l298 -131l-74 156l293 318l236 -288v500zM476 750q0 -56 -39 -95t-95 -39t-95 39t-39 95t39 95t95 39t95 -39 t39 -95z" />
<glyph unicode="&#xe062;" d="M600 1213q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262q0 124 60.5 231.5t165 172t226.5 64.5zM599 514q107 0 182.5 75.5t75.5 182.5t-75.5 182 t-182.5 75t-182 -75.5t-75 -181.5q0 -107 75.5 -182.5t181.5 -75.5z" />
<glyph unicode="&#xe063;" d="M600 1199q122 0 233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233t47.5 233t127.5 191t191 127.5t233 47.5zM600 173v854q-176 0 -301.5 -125t-125.5 -302t125.5 -302t301.5 -125z " />
<glyph unicode="&#xe064;" d="M554 1295q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 138.5t-64 210.5q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5zM455 296q-7 6 -18 17 t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156q14 -82 59.5 -136t136.5 -80z" />
<glyph unicode="&#xe065;" d="M1108 902l113 113l-21 85l-92 28l-113 -113zM1100 625v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125zM436 341l161 50l412 412l-114 113l-405 -405z" />
<glyph unicode="&#xe066;" d="M1100 453v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5z M813 431l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209z" />
<glyph unicode="&#xe067;" d="M1100 569v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5h300q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69z M625 348l566 567l-136 137l-430 -431l-147 147l-136 -136z" />
<glyph unicode="&#xe068;" d="M900 303v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198l-300 300l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296z" />
<glyph unicode="&#xe069;" d="M900 0l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100z" />
<glyph unicode="&#xe070;" d="M1200 0l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100z" />
<glyph unicode="&#xe071;" d="M1200 0l-500 488v-488l-564 550l564 550v-487l500 487v-1100z" />
<glyph unicode="&#xe072;" d="M1100 550l-900 550v-1100z" />
<glyph unicode="&#xe073;" d="M500 150v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM900 150v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -21 14.5 -35.5t35.5 -14.5h200 q21 0 35.5 14.5t14.5 35.5z" />
<glyph unicode="&#xe074;" d="M1100 150v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35z" />
<glyph unicode="&#xe075;" d="M500 0v488l-500 -488v1100l500 -487v487l564 -550z" />
<glyph unicode="&#xe076;" d="M1050 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488l-500 -488v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe077;" d="M850 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe078;" d="M650 1064l-550 -564h1100zM1200 350v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5z" />
<glyph unicode="&#xe079;" d="M777 7l240 240l-353 353l353 353l-240 240l-592 -594z" />
<glyph unicode="&#xe080;" d="M513 -46l-241 240l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1z" />
<glyph unicode="&#xe081;" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 900v-200h-200v-200h200v-200h200v200h200v200h-200v200h-200z" />
<glyph unicode="&#xe082;" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM300 700v-200h600v200h-600z" />
<glyph unicode="&#xe083;" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM247 741l141 -141l-142 -141l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141 l-141 142z" />
<glyph unicode="&#xe084;" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM546 623l-102 102l-174 -174l276 -277l411 411l-175 174z" />
<glyph unicode="&#xe085;" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 500h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3 q-105 0 -172 -56t-67 -183h144q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5zM500 400v-100h200v100h-200z" />
<glyph unicode="&#xe086;" d="M600 1197q162 0 299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5t80 299.5t217.5 217.5t299.5 80zM500 900v-100h200v100h-200zM400 700v-100h100v-200h-100v-100h400v100h-100v300h-300z" />
<glyph unicode="&#xe087;" d="M1200 700v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194v200h194q15 60 36 104.5t55.5 86t88 69t126.5 40.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203zM700 500v-206q149 48 201 206h-201v200h200 q-25 74 -76 127.5t-124 76.5v-204h-200v203q-75 -24 -130 -77.5t-79 -125.5h209v-200h-210q24 -73 79.5 -127.5t130.5 -78.5v206h200z" />
<glyph unicode="&#xe088;" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM844 735 l-135 -135l135 -135l-109 -109l-135 135l-135 -135l-109 109l135 135l-135 135l109 109l135 -135l135 135z" />
<glyph unicode="&#xe089;" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM896 654 l-346 -345l-228 228l141 141l87 -87l204 205z" />
<glyph unicode="&#xe090;" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM248 385l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5q0 -115 62 -215zM955 809l-564 -564q97 -59 209 -59q171 0 292.5 121.5 t121.5 292.5q0 112 -59 209z" />
<glyph unicode="&#xe091;" d="M1200 400h-600v-301l-600 448l600 453v-300h600v-300z" />
<glyph unicode="&#xe092;" d="M600 400h-600v300h600v300l600 -453l-600 -448v301z" />
<glyph unicode="&#xe093;" d="M1098 600h-298v-600h-300v600h-296l450 600z" />
<glyph unicode="&#xe094;" d="M998 600l-449 -600l-445 600h296v600h300v-600h298z" />
<glyph unicode="&#xe095;" d="M600 199v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453z" />
<glyph unicode="&#xe096;" d="M1200 1200h-400l129 -129l-294 -294l142 -142l294 294l129 -129v400zM565 423l-294 -294l129 -129h-400v400l129 -129l294 294z" />
<glyph unicode="&#xe097;" d="M871 730l129 -130h-400v400l129 -129l295 295l142 -141zM200 600h400v-400l-129 130l-295 -295l-142 141l295 295z" />
<glyph unicode="&#xe101;" d="M600 1177q118 0 224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5t45.5 224.5t123 184t184 123t224.5 45.5zM686 549l58 302q4 20 -8 34.5t-33 14.5h-207q-20 0 -32 -14.5t-8 -34.5 l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5zM700 400h-200v-100h200v100z" />
<glyph unicode="&#xe102;" d="M1200 900h-111v6t-1 15t-3 18l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6h-111v-100h100v-200h400v300h200v-300h400v200h100v100z M731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269zM481 900h-281q-3 0 14 48t35 96l18 47zM100 0h400v400h-400v-400zM700 400h400v-400h-400v400z" />
<glyph unicode="&#xe103;" d="M0 121l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55l-201 -202 v143zM692 611q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5t86.5 76.5q55 66 367 234z" />
<glyph unicode="&#xe105;" d="M1261 600l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30l-26 40l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5 t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30zM600 240q64 0 123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212 q0 85 46 158q-102 -87 -226 -258q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5zM484 762l-107 -106q49 -124 154 -191l105 105q-37 24 -75 72t-57 84z" />
<glyph unicode="&#xe106;" d="M906 1200l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43l-26 40l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148zM1261 600l-26 -40q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5 t-124 -100t-146.5 -79l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52zM513 264l37 141q-107 18 -178.5 101.5t-71.5 193.5q0 85 46 158q-102 -87 -226 -258q210 -282 393 -336z M484 762l-107 -106q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68z" />
<glyph unicode="&#xe107;" d="M-47 0h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 33 -48 36t-48 -29l-642 -1066q-21 -32 -7.5 -66t50.5 -34zM700 200v100h-200v-100h-345l445 723l445 -723h-345zM700 700h-200v-100l100 -300l100 300v100z" />
<glyph unicode="&#xe108;" d="M800 711l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -21 -13 -29t-32 1l-94 78h-222l-94 -78q-19 -9 -32 -1t-13 29v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5v41q0 20 11 44.5t26 38.5 l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339z" />
<glyph unicode="&#xe110;" d="M941 800l-600 -600h-341v200h259l600 600h241v198l300 -295l-300 -300v197h-159zM381 678l141 142l-181 180h-341v-200h259zM1100 598l300 -295l-300 -300v197h-241l-181 181l141 142l122 -123h159v198z" />
<glyph unicode="&#xe111;" d="M100 1100h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5z" />
<glyph unicode="&#xe112;" d="M400 900h-300v300h300v-300zM1100 900h-300v300h300v-300zM1100 800v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5t-58 109.5t-31.5 116t-15 104t-3 83v200h300v-250q0 -113 6 -145 q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300z" />
<glyph unicode="&#xe113;" d="M902 184l226 227l-578 579l-580 -579l227 -227l352 353z" />
<glyph unicode="&#xe114;" d="M650 218l578 579l-226 227l-353 -353l-352 353l-227 -227z" />
<glyph unicode="&#xe115;" d="M1198 400v600h-796l215 -200h381v-400h-198l299 -283l299 283h-200zM-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196z" />
<glyph unicode="&#xe116;" d="M1050 1200h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35 q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43l-100 475q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5z" />
<glyph unicode="&#xe117;" d="M1200 1000v-100h-1200v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500zM0 800h1200v-800h-1200v800z" />
<glyph unicode="&#xe118;" d="M201 800l-200 -400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000zM1501 700l-300 -700h-1200l300 700h1200z" />
<glyph unicode="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
<glyph unicode="&#xe120;" d="M900 303v197h-600v-197l-300 297l300 298v-198h600v198l300 -298z" />
<glyph unicode="&#xe121;" d="M31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM100 300h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM900 200h-100v-100h100v100z M1100 200h-100v-100h100v100z" />
<glyph unicode="&#xe122;" d="M1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35zM325 800l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35q-56 337 -56 351v250v5 q0 13 0.5 18.5t2.5 13t8 10.5t15 3h200zM-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5z" />
<glyph unicode="&#xe124;" d="M445 1180l-45 -233l-224 78l78 -225l-233 -44l179 -156l-179 -155l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180z" />
<glyph unicode="&#xe125;" d="M700 1200h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400q0 -75 100 -75h61q123 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5zM700 925l-50 -225h450 v-125l-250 -375h-214l-136 100h-100v375l150 212l100 213h50v-175zM0 800v-600h200v600h-200z" />
<glyph unicode="&#xe126;" d="M700 0h-50q-27 0 -51 20t-38 48l-96 198l-145 196q-20 26 -20 63v400q0 75 100 75h61q123 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5zM200 400h-200v600h200 v-600zM700 275l-50 225h450v125l-250 375h-214l-136 -100h-100v-375l150 -212l100 -213h50v175z" />
<glyph unicode="&#xe127;" d="M364 873l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341q-7 0 -90 81t-83 94v525q0 17 14 35.5t28 28.5zM408 792v-503 l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83zM208 200h-200v600h200v-600z" />
<glyph unicode="&#xe128;" d="M475 1104l365 -230q7 -4 16.5 -10.5t26 -26t16.5 -36.5v-526q0 -13 -85.5 -93.5t-93.5 -80.5h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-84 0 -139 39t-55 111t54 110t139 37h302l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6zM370 946 l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100h222q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l106 89v502l-342 237zM1199 201h-200v600h200v-600z" />
<glyph unicode="&#xe129;" d="M1100 473v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90zM911 400h-503l-236 339 l83 86l183 -146q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6v7.5v7v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294zM1000 200v-200h-600v200h600z" />
<glyph unicode="&#xe130;" d="M305 1104v200h600v-200h-600zM605 310l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15l-230 -362q-15 -31 7 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85l-1 -302q0 -84 38.5 -138t110.5 -54t111 55t39 139v106z M905 804v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146l-83 86l237 339h503z" />
<glyph unicode="&#xe131;" d="M603 1195q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM598 701h-298v-201h300l-2 -194l402 294l-402 298v-197z" />
<glyph unicode="&#xe132;" d="M597 1195q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5zM200 600l400 -294v194h302v201h-300v197z" />
<glyph unicode="&#xe133;" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM300 600h200v-300h200v300h200l-300 400z" />
<glyph unicode="&#xe134;" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM500 900v-300h-200l300 -400l300 400h-200v300h-200z" />
<glyph unicode="&#xe135;" d="M603 1195q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5zM627 1101q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6 q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55 t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q102 -2 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7 q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5 t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 41 1 44q31 -13 58.5 -14.5t39.5 3.5l11 4q6 36 -17 53.5t-64 28.5t-56 23q-19 -3 -37 0zM613 994q0 -18 8 -42.5t16.5 -44t9.5 -23.5q-9 2 -31 5t-36 5t-32 8t-30 14q3 12 16 30t16 25q10 -10 18.5 -10 t14 6t14.5 14.5t16 12.5z" />
<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M100 1196h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 1096h-200v-100h200v100zM100 796h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 696h-500v-100h500v100zM100 396h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 296h-300v-100h300v100z " />
<glyph unicode="&#xe138;" d="M1100 1200v-100h-1000v100h1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
<glyph unicode="&#xe140;" d="M329 729l142 142l-200 200l129 129h-400v-400l129 129zM1200 1200v-400l-129 129l-200 -200l-142 142l200 200l-129 129h400zM271 129l129 -129h-400v400l129 -129l200 200l142 -142zM1071 271l129 129v-400h-400l129 129l-200 200l142 142z" />
<glyph unicode="&#xe141;" d="M596 1192q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM596 1010q-171 0 -292.5 -121.5t-121.5 -292.5q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5zM455 905 q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5t16 38.5t39 16.5zM708 821l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5 q0 32 20.5 56.5t51.5 29.5zM855 709q23 0 38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39q0 22 16 38t39 16zM345 709q23 0 39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39t15.5 38.5t38.5 15.5z" />
<glyph unicode="&#xe143;" d="M649 54l-16 22q-90 125 -293 323q-71 70 -104.5 105.5t-77 89.5t-61 99t-17.5 91q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-203 -198 -293 -323zM844 524l12 12 q64 62 97.5 97t64.5 79t31 72q0 71 -48 119t-105 48q-74 0 -132 -82l-118 -171l-114 174q-51 79 -123 79q-60 0 -109.5 -49t-49.5 -118q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203z" />
<glyph unicode="&#xe144;" d="M476 406l19 -17l105 105l-212 212l389 389l247 -247l-95 -96l18 -18q46 -46 77 -99l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159q0 -93 66 -159zM123 193l141 -141q66 -66 159 -66q95 0 159 66 l283 283q66 66 66 159t-66 159l-141 141q-12 12 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159q0 -94 66 -160z" />
<glyph unicode="&#xe145;" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM900 1000h-600v-700h600v700zM600 46q43 0 73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5t-73.5 -30.5t-30.5 -73.5 t30.5 -73.5t73.5 -30.5z" />
<glyph unicode="&#xe148;" d="M700 1029v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5h139q5 -77 48.5 -126.5t117.5 -64.5v335l-27 7q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5 t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5zM600 755v274q-61 -8 -97.5 -37.5t-36.5 -102.5q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3zM700 548 v-311q170 18 170 151q0 64 -44 99.5t-126 60.5z" />
<glyph unicode="&#xe149;" d="M866 300l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5t-30 142.5h-221v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5 t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -11 2.5 -24.5t5.5 -24t9.5 -26.5t10.5 -25t14 -27.5t14 -25.5t15.5 -27t13.5 -24h242v-100h-197q8 -50 -2.5 -115t-31.5 -94 q-41 -59 -99 -113q35 11 84 18t70 7q32 1 102 -16t104 -17q76 0 136 30z" />
<glyph unicode="&#xe150;" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1200l298 -300h-198v-900h-200v900h-198z" />
<glyph unicode="&#xe151;" d="M400 300h198l-298 -300l-298 300h198v900h200v-900zM1000 1200v-500h-100v100h-100v-100h-100v500h300zM901 1100h-100v-200h100v200zM700 500h300v-200h-99v-100h-100v100h99v100h-200v100zM800 100h200v-100h-300v200h100v-100z" />
<glyph unicode="&#xe152;" d="M400 300h198l-298 -300l-298 300h198v900h200v-900zM1000 1200v-200h-99v-100h-100v100h99v100h-200v100h300zM800 800h200v-100h-300v200h100v-100zM700 500h300v-500h-100v100h-100v-100h-100v500zM801 200h100v200h-100v-200z" />
<glyph unicode="&#xe153;" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1100h-100v100h200v-500h-100v400zM1100 500v-500h-100v100h-200v400h300zM1001 400h-100v-200h100v200z" />
<glyph unicode="&#xe154;" d="M300 0l298 300h-198v900h-200v-900h-198zM1100 1200v-500h-100v100h-200v400h300zM1001 1100h-100v-200h100v200zM900 400h-100v100h200v-500h-100v400z" />
<glyph unicode="&#xe155;" d="M300 0l298 300h-198v900h-200v-900h-198zM900 1000h-200v200h200v-200zM1000 700h-300v200h300v-200zM1100 400h-400v200h400v-200zM1200 100h-500v200h500v-200z" />
<glyph unicode="&#xe156;" d="M300 0l298 300h-198v900h-200v-900h-198zM1200 1000h-500v200h500v-200zM1100 700h-400v200h400v-200zM1000 400h-300v200h300v-200zM900 100h-200v200h200v-200z" />
<glyph unicode="&#xe157;" d="M400 1100h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5v300q0 165 117.5 282.5t282.5 117.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5 t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5z" />
<glyph unicode="&#xe158;" d="M700 0h-300q-163 0 -281.5 117.5t-118.5 282.5v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5 t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5zM400 800v-500l333 250z" />
<glyph unicode="&#xe159;" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM900 300v500q0 41 -29.5 70.5t-70.5 29.5h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5zM800 700h-500l250 -333z" />
<glyph unicode="&#xe160;" d="M1100 700v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5zM900 300v500q0 41 -29.5 70.5t-70.5 29.5h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5 t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5zM550 733l-250 -333h500z" />
<glyph unicode="&#xe161;" d="M500 1100h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200zM700 550l-400 -350v200h-300v300h300v200z" />
<glyph unicode="&#xe162;" d="M403 2l9 -1q13 0 26 16l538 630q15 19 6 36q-8 18 -32 16h-300q1 4 78 219.5t79 227.5q2 17 -6 27l-8 8h-9q-16 0 -25 -15q-4 -5 -98.5 -111.5t-228 -257t-209.5 -238.5q-17 -19 -7 -40q10 -19 32 -19h302q-155 -438 -160 -458q-5 -21 4 -32z" />
<glyph unicode="&#xe163;" d="M800 200h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185zM900 200v200h-300v300h300v200l400 -350z" />
<glyph unicode="&#xe164;" d="M1200 700l-149 149l-342 -353l-213 213l353 342l-149 149h500v-500zM1022 571l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5v-300 q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98z" />
<glyph unicode="&#xe165;" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM600 794 q80 0 137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137t57 137t137 57z" />
<glyph unicode="&#xe166;" d="M700 800v400h-300v-400h-300l445 -500l450 500h-295zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
<glyph unicode="&#xe167;" d="M400 700v-300h300v300h295l-445 500l-450 -500h300zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
<glyph unicode="&#xe168;" d="M405 400l596 596l-154 155l-442 -442l-150 151l-155 -155zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
<glyph unicode="&#xe169;" d="M409 1103l-97 97l-212 -212l97 -98zM650 861l-149 149l-212 -212l149 -149l-238 -248h700v699zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
<glyph unicode="&#xe170;" d="M539 950l-149 -149l212 -212l149 148l248 -237v700h-699zM297 709l-97 -97l212 -212l98 97zM25 300h1048q11 0 19 -7.5t8 -17.5v-275h-1100v275q0 11 7 18t18 7zM1000 200h-100v-50h100v50z" />
<glyph unicode="&#xe171;" d="M1200 1199v-1079l-475 272l-310 -393v416h-392zM1166 1148l-672 -712v-226z" />
<glyph unicode="&#xe172;" d="M1100 1000v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1200h-100v-200h100v200z" />
<glyph unicode="&#xe173;" d="M578 500h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120zM700 1200h-100v-200h100v200zM1300 538l-475 -476l-244 244l123 123l120 -120l353 352z" />
<glyph unicode="&#xe174;" d="M529 500h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170zM700 1200h-100v-200h100v200zM1167 6l-170 170l-170 -170l-127 127l170 170l-170 170l127 127l170 -170l170 170l127 -128 l-170 -169l170 -170z" />
<glyph unicode="&#xe175;" d="M700 500h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200zM700 1000h-100v200h100v-200zM1000 600h-200v-300h-200l300 -300l300 300h-200v300z" />
<glyph unicode="&#xe176;" d="M602 500h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200zM700 1000h-100v200h100v-200zM1000 300h200l-300 300l-300 -300h200v-300h200v300z" />
<glyph unicode="&#xe177;" d="M1200 900v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150h1200zM0 800v-550q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200zM100 500h400v-200h-400v200z" />
<glyph unicode="&#xe178;" d="M500 1000h400v198l300 -298l-300 -298v198h-400v200zM100 800v200h100v-200h-100zM400 800h-100v200h100v-200zM700 300h-400v-198l-300 298l300 298v-198h400v-200zM800 500h100v-200h-100v200zM1000 500v-200h100v200h-100z" />
<glyph unicode="&#xe179;" d="M1200 50v1106q0 31 -18 40.5t-44 -7.5l-276 -117q-25 -16 -43.5 -50.5t-18.5 -65.5v-359q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5zM550 1200l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447l-100 203v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300z" />
<glyph unicode="&#xe180;" d="M1100 106v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394 q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5z" />
<glyph unicode="&#xe181;" d="M675 1000l-100 100h-375l-100 -100h400l200 -200v-98l295 98h105v200h-425zM500 300v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5zM100 800h300v-200h-300v200zM700 565l400 133 v-163l-400 -133v163zM100 500h300v-200h-300v200zM805 300l295 98v-298h-425l-100 -100h-375l-100 100h400l200 200h105z" />
<glyph unicode="&#xe182;" d="M179 1169l-162 -162q-1 -11 -0.5 -32.5t16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q16 17 13 40.5t-22 37.5l-192 136q-19 14 -45 12t-42 -19l-119 -118q-143 103 -267 227q-126 126 -227 268l118 118 q17 17 20 41.5t-11 44.5l-139 194q-14 19 -36.5 22t-40.5 -14z" />
<glyph unicode="&#xe183;" d="M1200 712v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40t-53.5 -36.5t-31 -27.5l-9 -10v-200q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38 t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5zM800 650l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -15 -35.5t-35 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5 t30 -27.5t12 -24l1 -10v-50z" />
<glyph unicode="&#xe184;" d="M175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250zM1200 100v-100h-1100v100h1100z" />
<glyph unicode="&#xe185;" d="M600 1100h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300v1000q0 41 29.5 70.5t70.5 29.5zM1000 800h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300v700q0 41 29.5 70.5t70.5 29.5zM400 0v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400h300z" />
<glyph unicode="&#xe186;" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-300h200v-100h-200v-100h300v300h-200v100h200v100h-300zM800 800h-200v-500h200v100h100v300h-100 v100zM800 700v-300h-100v300h100z" />
<glyph unicode="&#xe187;" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM400 600h-100v200h-100v-500h100v200h100v-200h100v500h-100v-200zM800 800h-200v-500h200v100h100v300h-100 v100zM800 700v-300h-100v300h100z" />
<glyph unicode="&#xe188;" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-500h300v100h-200v300h200v100h-300zM600 800v-500h300v100h-200v300h200v100h-300z" />
<glyph unicode="&#xe189;" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM500 700l-300 -150l300 -150v300zM600 400l300 150l-300 150v-300z" />
<glyph unicode="&#xe190;" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM900 800v-500h-700v500h700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM800 700h-130 q-38 0 -66.5 -43t-28.5 -108t27 -107t68 -42h130v300z" />
<glyph unicode="&#xe191;" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM200 800v-300h200v-100h-200v-100h300v300h-200v100h200v100h-300zM800 300h100v500h-200v-100h100v-400z M601 300h100v100h-100v-100z" />
<glyph unicode="&#xe192;" d="M1200 800v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212zM1000 900h-900v-700h900v700zM300 700v100h-100v-500h300v400h-200zM800 300h100v500h-200v-100h100v-400zM401 400h-100v200h100v-200z M601 300h100v100h-100v-100z" />
<glyph unicode="&#xe193;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM1000 900h-900v-700h900v700zM400 700h-200v100h300v-300h-99v-100h-100v100h99v200zM800 700h-100v100h200v-500h-100v400zM201 400h100v-100 h-100v100zM701 300h-100v100h100v-100z" />
<glyph unicode="&#xe194;" d="M600 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM600 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM800 700h-300 v-200h300v-100h-300l-100 100v200l100 100h300v-100z" />
<glyph unicode="&#xe195;" d="M596 1196q162 0 299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299t80 299t217 217t299 80zM596 1014q-171 0 -292.5 -121.5t-121.5 -292.5t121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5zM800 700v-100 h-100v100h-200v-100h200v-100h-200v-100h-100v400h300zM800 400h-100v100h100v-100z" />
<glyph unicode="&#xe197;" d="M800 300h128q120 0 205 86t85 208q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57h222v300h400v-300zM700 200h200l-300 -300 l-300 300h200v300h200v-300z" />
<glyph unicode="&#xe198;" d="M600 714l403 -403q94 26 154.5 104t60.5 178q0 121 -85 207.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5q0 -80 56.5 -137t135.5 -57h8zM700 -100h-200v300h-200l300 300 l300 -300h-200v-300z" />
<glyph unicode="&#xe199;" d="M700 200h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170l-270 -300h400v-155l-75 -45h350l-75 45v155z" />
<glyph unicode="&#xe200;" d="M700 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -12t1 -11q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5 q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350z" />
<glyph unicode="&#x1f4bc;" d="M800 1000h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100zM500 1000h200v100h-200v-100zM1200 400v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v200h1200z" />
<glyph unicode="&#x1f4c5;" d="M1100 900v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150h1100zM0 800v-750q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100zM100 600h100v-100h-100v100zM300 600h100v-100h-100v100z M500 600h100v-100h-100v100zM700 600h100v-100h-100v100zM900 600h100v-100h-100v100zM100 400h100v-100h-100v100zM300 400h100v-100h-100v100zM500 400h100v-100h-100v100zM700 400h100v-100h-100v100zM900 400h100v-100h-100v100zM100 200h100v-100h-100v100zM300 200 h100v-100h-100v100zM500 200h100v-100h-100v100zM700 200h100v-100h-100v100zM900 200h100v-100h-100v100z" />
<glyph unicode="&#x1f4cc;" d="M902 1185l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207l-380 -303l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15z" />
<glyph unicode="&#x1f4ce;" d="M518 119l69 -60l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163t35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84 t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -79.5 -17t-67.5 -51l-388 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348 q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256z" />
<glyph unicode="&#x1f4f7;" d="M1200 200v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5z M1000 700h-100v100h100v-100zM844 500q0 -100 -72 -172t-172 -72t-172 72t-72 172t72 172t172 72t172 -72t72 -172zM706 500q0 44 -31 75t-75 31t-75 -31t-31 -75t31 -75t75 -31t75 31t31 75z" />
<glyph unicode="&#x1f512;" d="M900 800h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
<glyph unicode="&#x1f514;" d="M1062 400h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-22 -9 -63 -23t-167.5 -37t-251.5 -23t-245.5 20.5t-178.5 41.5l-58 20q-18 7 -31 27.5t-13 40.5q0 21 13.5 35.5t33.5 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94 q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327zM600 104q-54 0 -103 6q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6z" />
<glyph unicode="&#x1f516;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
<glyph unicode="&#x1f525;" d="M400 755q2 -12 8 -41.5t8 -43t6 -39.5t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85t5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5 q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129 q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5z" />
<glyph unicode="&#x1f527;" d="M948 778l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5t15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138z" />
</font>
</defs></svg>

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -14,10 +14,18 @@
<link rel="stylesheet" href="/bower_components/gridster/dist/jquery.gridster.css">
<link rel="stylesheet" href="/bower_components/pivottable/dist/pivot.css">
<link rel="stylesheet" href="/bower_components/cornelius/src/cornelius.css">
<link rel="stylesheet" href="/bower_components/select2/select2.css">
<link rel="stylesheet" href="/bower_components/angular-ui-select/dist/select.css">
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
<link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/hint/show-hint.css">
<link rel="stylesheet" href="/bower_components/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="/styles/redash.css">
<!-- endbuild -->
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
</head>
<body>
<div growl></div>
@@ -31,15 +39,15 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><strong>{{name}}</strong></a>
<a class="navbar-brand" href="/"><img src="/images/redash_icon_small.png"/></a>
</div>
{% raw %}
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li class="active" ng-show="pageTitle"><a class="page-title" ng-bind="pageTitle"></a></li>
<li class="dropdown" ng-show="groupedDashboards.length > 0 || otherDashboards.length > 0 || currentUser.hasPermission('create_dashboard')">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-th-large"></span> <b class="caret"></b></a>
<ul class="dropdown-menu">
<li class="dropdown" ng-show="groupedDashboards.length > 0 || otherDashboards.length > 0 || currentUser.hasPermission('create_dashboard')" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle><span class="fa fa-tachometer"></span> <b class="caret"></b></a>
<ul class="dropdown-menu" dropdown-menu>
<span ng-repeat="(name, group) in groupedDashboards">
<li class="dropdown-submenu">
<a href="#" ng-bind="name"></a>
@@ -57,21 +65,52 @@
<li><a data-toggle="modal" href="#new_dashboard_dialog" ng-show="currentUser.hasPermission('create_dashboard')">New Dashboard</a></li>
</ul>
</li>
<li class="dropdown" ng-show="currentUser.hasPermission('view_query')">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Queries <b class="caret"></b></a>
<ul class="dropdown-menu">
<li class="dropdown" ng-show="currentUser.hasPermission('view_query')" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle>Queries <b class="caret"></b></a>
<ul class="dropdown-menu" dropdown-menu>
<li ng-show="currentUser.hasPermission('create_query')"><a href="/queries/new">New Query</a></li>
<li><a href="/queries">Queries</a></li>
</ul>
</li>
<li>
<a href="/alerts">Alerts</a>
</li>
</ul>
<form class="navbar-form navbar-left" role="search" ng-submit="searchQueries()">
<div class="form-group">
<input type="text" ng-model="term" class="form-control" placeholder="Search queries...">
</div>
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
</form>
<ul class="nav navbar-nav navbar-right">
<p class="navbar-text avatar" ng-show="currentUser.id" ng-cloak>
<img ng-src="{{currentUser.gravatar_url}}" class="img-circle" alt="{{currentUser.name}}"/>
<a target="_self" href="/logout" id="logout" title="Logout">
<span class="glyphicon glyphicon-log-out"></span>
</a>
</p>
<li ng-show="currentUser.hasPermission('admin')">
<a href="/data_sources" title="Data Sources"><i class="fa fa-database"></i></a>
</li>
<li ng-show="currentUser.hasPermission('list_users')">
<a href="/users" title="Users"><i class="fa fa-users"></i></a>
</li>
<li class="dropdown" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle><span ng-bind="currentUser.name"></span> <span class="caret"></span></a>
<ul class="dropdown-menu" dropdown-menu>
<li style="width:300px">
<a ng-href="/users/{{currentUser.id}}">
<div class="row">
<div class="col-sm-2">
<img src="{{currentUser.gravatar_url}}" size="40px" class="img-circle"/>
</div>
<div class="col-sm-10">
<p><strong>{{currentUser.name}}</strong></p>
</div>
</div>
</a>
</li>
<li class="divider">
</li>
<li>
<a href="/logout">Log out</a>
</li>
</ul>
</li>
</ul>
</div>
{% endraw %}
@@ -97,9 +136,11 @@
<script src="/bower_components/codemirror/lib/codemirror.js"></script>
<script src="/bower_components/codemirror/addon/edit/matchbrackets.js"></script>
<script src="/bower_components/codemirror/addon/edit/closebrackets.js"></script>
<script src="/bower_components/codemirror/addon/hint/show-hint.js"></script>
<script src="/bower_components/codemirror/addon/hint/anyword-hint.js"></script>
<script src="/bower_components/codemirror/mode/sql/sql.js"></script>
<script src="/bower_components/codemirror/mode/python/python.js"></script>
<script src="/bower_components/codemirror/mode/javascript/javascript.js"></script>
<script src="/bower_components/angular-ui-codemirror/ui-codemirror.js"></script>
<script src="/bower_components/highcharts/highcharts.js"></script>
<script src="/bower_components/highcharts/modules/exporting.js"></script>
<script src="/bower_components/gridster/dist/jquery.gridster.js"></script>
@@ -108,15 +149,21 @@
<script src="/bower_components/cornelius/src/cornelius.js"></script>
<script src="/bower_components/mousetrap/mousetrap.js"></script>
<script src="/bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.js"></script>
<script src="/bower_components/select2/select2.js"></script>
<script src="/bower_components/angular-ui-select2/src/select2.js"></script>
<script src="/bower_components/angular-ui-select/dist/select.js"></script>
<script src="/bower_components/underscore.string/lib/underscore.string.js"></script>
<script src="/bower_components/marked/lib/marked.js"></script>
<script src="/bower_components/angular-base64-upload/dist/angular-base64-upload.js"></script>
<script src="/scripts/ng_highchart.js"></script>
<script src="/scripts/ng_smart_table.js"></script>
<script src="/scripts/ui-bootstrap-tpls-0.5.0.min.js"></script>
<script src="/bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.js"></script>
<script src="/bower_components/bucky/bucky.js"></script>
<script src="/bower_components/pace/pace.js"></script>
<script src="/bower_components/mustache/mustache.js"></script>
<script src="/bower_components/canvg/rgbcolor.js"></script>
<script src="/bower_components/canvg/StackBlur.js"></script>
<script src="/bower_components/canvg/canvg.js"></script>
<script src="/bower_components/leaflet/dist/leaflet.js"></script>
<script src="/bower_components/angular-bootstrap-show-errors/src/showErrors.js"></script>
<!-- endbuild -->
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
@@ -128,17 +175,23 @@
<script src="/scripts/controllers/controllers.js"></script>
<script src="/scripts/controllers/dashboard.js"></script>
<script src="/scripts/controllers/admin_controllers.js"></script>
<script src="/scripts/controllers/data_sources.js"></script>
<script src="/scripts/controllers/query_view.js"></script>
<script src="/scripts/controllers/query_source.js"></script>
<script src="/scripts/controllers/users.js"></script>
<script src="/scripts/visualizations/base.js"></script>
<script src="/scripts/visualizations/chart.js"></script>
<script src="/scripts/visualizations/cohort.js"></script>
<script src="/scripts/visualizations/map.js"></script>
<script src="/scripts/visualizations/counter.js"></script>
<script src="/scripts/visualizations/table.js"></script>
<script src="/scripts/visualizations/pivot.js"></script>
<script src="/scripts/directives/directives.js"></script>
<script src="/scripts/directives/query_directives.js"></script>
<script src="/scripts/directives/data_source_directives.js"></script>
<script src="/scripts/directives/dashboard_directives.js"></script>
<script src="/scripts/filters.js"></script>
<script src="/scripts/controllers/alerts.js"></script>
<!-- endbuild -->
<script>
@@ -153,7 +206,7 @@
currentUser.hasPermission = function(permission) {
return this.permissions.indexOf(permission) != -1;
}
};
{{ analytics|safe }}
</script>

View File

@@ -13,6 +13,10 @@
<link rel="stylesheet" href="/styles/redash.css">
<link rel="stylesheet" href="/styles/login.css">
<!-- endbuild -->
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
</head>
<body>
@@ -26,13 +30,20 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><strong>{{name}}</strong></a>
<a class="navbar-brand" href="/"><img src="/images/redash_icon_small.png"/></a>
</div>
</div>
</nav>
<div class="container">
<div class="row">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-warning" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="main">
{% if show_google_openid %}
@@ -48,10 +59,23 @@
{% endif %}
{% if show_saml_login %}
<div class="row">
<a href="/saml/login">SAML Login</a>
</div>
<div class="login-or">
<hr class="hr-or">
<span class="span-or">or</span>
</div>
{% endif %}
<form role="form" method="post" name="login">
<div class="form-group">
<label for="inputUsernameEmail">Username or email</label>
<input type="text" class="form-control" id="inputUsernameEmail" name="username" value="{{username}}">
<label for="inputEmail">Email</label>
<input type="text" class="form-control" id="inputEmail" name="email" value="{{email}}">
</div>
<div class="form-group">
<!--<a class="pull-right" href="#">Forgot password?</a>-->

View File

@@ -6,17 +6,18 @@ angular.module('redash', [
'redash.services',
'redash.renderers',
'redash.visualization',
'ui.codemirror',
'highchart',
'ui.select2',
'angular-growl',
'angularMoment',
'ui.bootstrap',
'smartTable.table',
'ngResource',
'ngRoute'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider) {
'ngRoute',
'ui.select',
'naif.base64',
'ui.bootstrap.showErrors'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider', 'uiSelectConfig',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider, uiSelectConfig) {
if (featureFlags.clientSideMetrics) {
Bucky.setOptions({
host: '/api/metrics'
@@ -31,6 +32,8 @@ angular.module('redash', [
return query.$promise;
};
uiSelectConfig.theme = "bootstrap";
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);
@@ -55,6 +58,11 @@ angular.module('redash', [
}]
}
});
$routeProvider.when('/queries/search', {
templateUrl: '/views/queries_search_results.html',
controller: 'QuerySearchCtrl',
reloadOnSearch: true,
});
$routeProvider.when('/queries/:queryId', {
templateUrl: '/views/query.html',
controller: 'QueryViewCtrl',
@@ -75,14 +83,47 @@ angular.module('redash', [
templateUrl: '/views/admin_status.html',
controller: 'AdminStatusCtrl'
});
$routeProvider.when('/admin/workers', {
templateUrl: '/views/admin_workers.html',
controller: 'AdminWorkersCtrl'
$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('/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('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
templateUrl: '/views/personal.html',
controller: 'PersonalIndexCtrl'
});
$routeProvider.when('/personal', {
templateUrl: '/views/personal.html',
controller: 'PersonalIndexCtrl'
});
$routeProvider.otherwise({
redirectTo: '/'

View File

@@ -16,16 +16,9 @@
$timeout(refresh, 59 * 1000);
};
$scope.flowerUrl = featureFlags.flowerUrl;
refresh();
}
var AdminWorkersCtrl = function ($scope, $sce) {
$scope.flowerUrl = $sce.trustAsResourceUrl(featureFlags.flowerUrl);
};
angular.module('redash.admin_controllers', [])
.controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl])
.controller('AdminWorkersCtrl', ['$scope', '$sce', AdminWorkersCtrl])
})();

View File

@@ -0,0 +1,174 @@
(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": '<a href="/alerts/{{dataRow.id}}">{{dataRow.name}}</a> (<a href="/queries/{{dataRow.query.id}}">query</a>)'
},
{
'label': 'Created By',
'map': 'user.name'
},
{
'label': 'State',
'cellTemplate': '<span ng-class="dataRow.class">{{dataRow.state | uppercase}}</span> since <span am-time-ago="dataRow.updated_at"></span>'
},
{
'label': 'Created At',
'cellTemplate': '<span am-time-ago="dataRow.created_at"></span>'
}
];
};
var AlertCtrl = function($scope, $routeParams, $location, growl, Query, Events, Alert) {
$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: {}});
} else {
$scope.alert = Alert.get({id: $scope.alertId}, function(alert) {
$scope.onQuerySelected(new Query($scope.alert.query));
});
}
$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();
}
$scope.alert.$save(function(alert) {
growl.addSuccessMessage("Saved.");
if ($scope.alertId === "new") {
$location.path('/alerts/' + alert.id).replace();
}
}, function() {
growl.addErrorMessage("Failed saving alert.");
});
};
};
angular.module('redash.directives').directive('alertSubscribers', ['AlertSubscription', function (AlertSubscription) {
return {
restrict: 'E',
replace: true,
templateUrl: '/views/alerts/subscribers.html',
scope: {
'alertId': '='
},
controller: function ($scope) {
$scope.subscribers = AlertSubscription.query({alertId: $scope.alertId});
}
}
}]);
angular.module('redash.directives').directive('subscribeButton', ['AlertSubscription', 'growl', function (AlertSubscription, growl) {
return {
restrict: 'E',
replace: true,
template: '<button class="btn btn-default btn-xs" ng-click="toggleSubscription()"><i ng-class="class"></i></button>',
controller: function ($scope) {
var updateClass = function() {
if ($scope.subscription) {
$scope.class = "fa fa-eye-slash";
} else {
$scope.class = "fa fa-eye";
}
}
$scope.subscribers.$promise.then(function() {
$scope.subscription = _.find($scope.subscribers, function(subscription) {
return (subscription.user.email == currentUser.email);
});
updateClass();
});
$scope.toggleSubscription = function() {
if ($scope.subscription) {
$scope.subscription.$delete(function() {
$scope.subscribers = _.without($scope.subscribers, $scope.subscription);
$scope.subscription = undefined;
updateClass();
}, function() {
growl.addErrorMessage("Failed saving subscription.");
});
} else {
$scope.subscription = new AlertSubscription({alert_id: $scope.alertId});
$scope.subscription.$save(function() {
$scope.subscribers.push($scope.subscription);
updateClass();
}, function() {
growl.addErrorMessage("Unsubscription failed.");
});
}
}
}
}
}]);
angular.module('redash.controllers')
.controller('AlertsCtrl', ['$scope', 'Events', 'Alert', AlertsCtrl])
.controller('AlertCtrl', ['$scope', '$routeParams', '$location', 'growl', 'Query', 'Events', 'Alert', AlertCtrl])
})();

View File

@@ -1,21 +1,77 @@
(function () {
var dateFormatter = function (value) {
if (!value) {
return "-";
}
return value.toDate().toLocaleString();
};
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) {
$scope.$parent.pageTitle = "All Queries";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
isGlobalSearchActivated: true
}
isGlobalSearchActivated: true};
$scope.allQueries = [];
$scope.queries = [];
var dateFormatter = function (value) {
if (!value) return "-";
return value.format("DD/MM/YY HH:mm");
}
var filterQueries = function () {
$scope.queries = _.filter($scope.allQueries, function (query) {
if (!$scope.selectedTab) {
@@ -35,7 +91,7 @@
Query.query(function (queries) {
$scope.allQueries = _.map(queries, function (query) {
query.created_at = moment(query.created_at);
query.last_retrieved_at = moment(query.last_retrieved_at);
query.retrieved_at = moment(query.retrieved_at);
return query;
});
@@ -58,43 +114,26 @@
'formatFunction': dateFormatter
},
{
'label': 'Runtime (avg)',
'map': 'avg_runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (min)',
'map': 'min_runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (max)',
'map': 'max_runtime',
'label': 'Runtime',
'map': 'runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Last Executed At',
'map': 'last_retrieved_at',
'map': 'retrieved_at',
'formatFunction': dateFormatter
},
{
'label': 'Times Executed',
'map': 'times_retrieved'
},
{
'label': 'Update Schedule',
'map': 'ttl',
'map': 'schedule',
'formatFunction': function (value) {
return $filter('refreshRateHumanize')(value);
return $filter('scheduleHumanize')(value);
}
}
]
$scope.tabs = [
{"name": "My Queries", "key": "my"},
{"key": "all", "name": "All Queries"},
@@ -110,7 +149,7 @@
});
}
var MainCtrl = function ($scope, Dashboard, notifications) {
var MainCtrl = function ($scope, $location, Dashboard, notifications) {
if (featureFlags.clientSideMetrics) {
$scope.$on('$locationChangeSuccess', function(event, newLocation, oldLocation) {
// This will be called once per actual page load.
@@ -133,7 +172,11 @@
$scope.otherDashboards = $scope.allDashboards['Other'] || [];
$scope.groupedDashboards = _.omit($scope.allDashboards, 'Other');
});
}
};
$scope.searchQueries = function() {
$location.path('/queries/search').search({q: $scope.term});
};
$scope.reloadDashboards();
@@ -146,24 +189,25 @@
$(window).click(function () {
notifications.getPermissions();
});
}
};
var IndexCtrl = function ($scope, Events, Dashboard) {
Events.record(currentUser, "view", "page", "homepage");
$scope.$parent.pageTitle = "Home";
};
$scope.archiveDashboard = function (dashboard) {
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
Events.record(currentUser, "archive", "dashboard", dashboard.id);
dashboard.$delete(function () {
$scope.$parent.reloadDashboards();
});
}
}
}
var PersonalIndexCtrl = function ($scope, Events, Dashboard, Query) {
Events.record(currentUser, "view", "page", "personal_homepage");
$scope.$parent.pageTitle = "Home";
$scope.recentQueries = Query.recent();
$scope.recentDashboards = Dashboard.recent();
};
angular.module('redash.controllers', [])
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', IndexCtrl])
.controller('MainCtrl', ['$scope', 'Dashboard', 'notifications', MainCtrl]);
.controller('PersonalIndexCtrl', ['$scope', 'Events', 'Dashboard', 'Query', PersonalIndexCtrl])
.controller('MainCtrl', ['$scope', '$location', 'Dashboard', 'notifications', MainCtrl])
.controller('QuerySearchCtrl', ['$scope', '$location', '$filter', 'Events', 'Query', QuerySearchCtrl]);
})();

View File

@@ -1,5 +1,5 @@
(function() {
var DashboardCtrl = function($scope, Events, Widget, $routeParams, $http, $timeout, $q, Dashboard) {
var DashboardCtrl = function($scope, Events, Widget, $routeParams, $location, $http, $timeout, $q, Dashboard) {
$scope.refreshEnabled = false;
$scope.refreshRate = 60;
@@ -15,8 +15,8 @@
return _.map(row, function (widget) {
var w = new Widget(widget);
if (w.visualization && dashboard.dashboard_filters_enabled) {
promises.push(w.getQuery().getQueryResultPromise());
if (w.visualization) {
promises.push(w.getQuery().getQueryResult().toPromise());
}
return w;
@@ -27,27 +27,35 @@
var filters = {};
_.each(queryResults, function(queryResult) {
var queryFilters = queryResult.getFilters();
_.each(queryFilters, function (filter) {
if (!_.has(filters, filter.name)) {
// TODO: first object should be a copy, otherwise one of the chart filters behaves different than the others.
_.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[filter.name].originFilters.push(filter);
filters[queryFilter.name].originFilters.push(queryFilter);
});
});
if (dashboard.dashboard_filters_enabled) {
$scope.filters = _.values(filters);
}
$scope.filters = _.values(filters);
});
@@ -74,7 +82,7 @@
_.each(row, function(widget, i) {
var newWidget = newWidgets[widget.id];
if (newWidget && newWidget[0].visualization.query.latest_query_data_id != widget.visualization.query.latest_query_data_id) {
row[i] = newWidget[0];
row[i] = new Widget(newWidget[0]);
}
});
});
@@ -83,7 +91,16 @@
});
}, $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.triggerRefresh = function() {
@@ -92,9 +109,13 @@
Events.record(currentUser, "autorefresh", "dashboard", dashboard.id, {'enable': $scope.refreshEnabled});
if ($scope.refreshEnabled) {
var refreshRate = _.min(_.flatten($scope.dashboard.widgets), function(widget) {
return widget.visualization.query.ttl;
}).visualization.query.ttl;
var refreshRate = _.min(_.map(_.flatten($scope.dashboard.widgets), function(widget) {
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 = _.max([120, refreshRate * 2]) * 1000;
@@ -103,7 +124,7 @@
};
};
var WidgetCtrl = function($scope, Events, Query) {
var WidgetCtrl = function($scope, $location, Events, Query) {
$scope.deleteWidget = function() {
if (!confirm('Are you sure you want to remove "' + $scope.widget.getName() + '" from the dashboard?')) {
return;
@@ -127,8 +148,9 @@
Events.record(currentUser, "view", "visualization", $scope.widget.visualization.id);
$scope.query = $scope.widget.getQuery();
$scope.queryResult = $scope.query.getQueryResult();
$scope.nextUpdateTime = moment(new Date(($scope.query.updated_at + $scope.query.ttl + $scope.query.runtime + 300) * 1000)).fromNow();
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
var maxAge = $location.search()['maxAge'];
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
$scope.type = 'visualization';
} else {
@@ -137,7 +159,7 @@
};
angular.module('redash.controllers')
.controller('DashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$http', '$timeout', '$q', 'Dashboard', DashboardCtrl])
.controller('WidgetCtrl', ['$scope', 'Events', 'Query', WidgetCtrl])
.controller('DashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$location', '$http', '$timeout', '$q', 'Dashboard', DashboardCtrl])
.controller('WidgetCtrl', ['$scope', '$location', 'Events', 'Query', WidgetCtrl])
})();
})();

View File

@@ -0,0 +1,47 @@
(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();
$scope.openDataSource = function(datasource) {
$location.path('/data_sources/' + datasource.id);
};
$scope.deleteDataSource = function(event, datasource) {
event.stopPropagation();
Events.record(currentUser, "delete", "datasource", datasource.id);
datasource.$delete(function(resource) {
growl.addSuccessMessage("Data source deleted succesfully.");
this.$parent.dataSources = _.without(this.dataSources, resource);
}.bind(this), function(httpResponse) {
console.log("Failed to delete data source: ", httpResponse.status, httpResponse.statusText, httpResponse.data);
growl.addErrorMessage("Failed to delete data source.");
});
}
};
var DataSourceCtrl = function ($scope, $routeParams, $http, $location, 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();
}
});
};
angular.module('redash.controllers')
.controller('DataSourcesCtrl', ['$scope', '$location', 'growl', 'Events', 'DataSource', DataSourcesCtrl])
.controller('DataSourceCtrl', ['$scope', '$routeParams', '$http', '$location', 'Events', 'DataSource', DataSourceCtrl])
})();

View File

@@ -14,20 +14,10 @@
var isNewQuery = !$scope.query.id,
queryText = $scope.query.query,
// ref to QueryViewCtrl.saveQuery
saveQuery = $scope.saveQuery,
shortcuts = {
'meta+s': function () {
if ($scope.canEdit) {
$scope.saveQuery();
}
},
'meta+enter': function () {
$scope.executeQuery();
}
};
saveQuery = $scope.saveQuery;
$scope.sourceMode = true;
$scope.canEdit = currentUser.canEdit($scope.query);
$scope.canEdit = currentUser.canEdit($scope.query) || featureFlags.allowAllToEditQueries;
$scope.isDirty = false;
$scope.newVisualization = undefined;
@@ -39,6 +29,22 @@
}
});
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);
@@ -62,7 +68,7 @@
$scope.duplicateQuery = function() {
Events.record(currentUser, 'fork', 'query', $scope.query.id);
$scope.query.id = null;
$scope.query.ttl = -1;
$scope.query.schedule = null;
$scope.saveQuery({
successMessage: 'Query forked',

View File

@@ -1,29 +1,76 @@
(function() {
'use strict';
function QueryViewCtrl($scope, Events, $route, $location, notifications, growl, Query, DataSource) {
function QueryViewCtrl($scope, Events, $route, $location, notifications, growl, $modal, Query, DataSource) {
var DEFAULT_TAB = 'table';
var getQueryResult = function(maxAge) {
// Collect params, and getQueryResult with params; getQueryResult merges it into the query
var parameters = Query.collectParamsFromQueryString($location, $scope.query);
if (maxAge == undefined) {
maxAge = $location.search()['maxAge'];
}
if (maxAge == undefined) {
maxAge = -1;
}
$scope.showLog = false;
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
}
$scope.dataSource = {};
$scope.query = $route.current.locals.query;
var updateSchema = function() {
$scope.hasSchema = false;
$scope.editorSize = "col-md-12";
var dataSourceId = $scope.query.data_source_id || $scope.dataSources[0].id;
DataSource.getSchema({id: dataSourceId}, 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.hasSchema = false;
$scope.editorSize = "col-md-12";
}
});
}
Events.record(currentUser, 'view', 'query', $scope.query.id);
$scope.queryResult = $scope.query.getQueryResult();
getQueryResult();
$scope.queryExecuting = false;
$scope.isQueryOwner = currentUser.id === $scope.query.user.id;
$scope.isQueryOwner = (currentUser.id === $scope.query.user.id) || currentUser.hasPermission('admin');
$scope.canViewSource = currentUser.hasPermission('view_source');
$scope.dataSources = DataSource.get(function(dataSources) {
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id;
$scope.dataSources = DataSource.query(function(dataSources) {
updateSchema();
if ($scope.query.isNew()) {
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id;
$scope.dataSource = _.find(dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
}
});
// 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) {
data.id = $scope.query.id;
@@ -57,7 +104,10 @@
};
$scope.executeQuery = function() {
$scope.queryResult = $scope.query.getQueryResult(0);
if (!$scope.query.query) {
return;
}
getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
Events.record(currentUser, 'execute', 'query', $scope.query.id);
@@ -69,6 +119,31 @@
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);
@@ -83,6 +158,8 @@
});
}
updateSchema();
$scope.dataSource = _.find($scope.dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
$scope.executeQuery();
};
@@ -121,13 +198,41 @@
$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.openScheduleForm = function() {
if (!$scope.isQueryOwner) {
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.$watch(function() {
return $location.hash()
}, function(hash) {
@@ -140,5 +245,5 @@
angular.module('redash.controllers')
.controller('QueryViewCtrl',
['$scope', 'Events', '$route', '$location', 'notifications', 'growl', 'Query', 'DataSource', QueryViewCtrl]);
['$scope', 'Events', '$route', '$location', 'notifications', 'growl', '$modal', 'Query', 'DataSource', QueryViewCtrl]);
})();

View File

@@ -0,0 +1,160 @@
(function () {
var UsersCtrl = function ($scope, $location, growl, Events, User) {
Events.record(currentUser, "view", "page", "users");
$scope.$parent.pageTitle = "Users";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
};
$scope.gridColumns = [
{
"label": "",
"map": "gravatar_url",
"cellTemplate": '<img src="{{dataRow.gravatar_url}}" height="40px"/>'
},
{
"label": "Name",
"map": "name",
"cellTemplate": '<a href="/users/{{dataRow.id}}">{{dataRow.name}}</a>'
},
{
'label': 'Joined',
'cellTemplate': '<span am-time-ago="dataRow.created_at"></span>'
}
];
$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) {
_.each($scope.tabs, function(v, k) {
$scope.tabs[k] = (k === tab);
});
};
$scope.setTab = function(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
};
if ($scope.user.admin === true && $scope.user.groups.indexOf("admin") === -1) {
data.groups = $scope.user.groups.concat("admin");
} else if ($scope.user.admin === false && $scope.user.groups.indexOf("admin") !== -1) {
data.groups = _.without($scope.user.groups, "admin");
}
User.save(data, function(user) {
growl.addSuccessMessage("Saved.")
$scope.user = user;
}, function(error) {
var message = error.data.message || "Failed saving.";
growl.addErrorMessage(message);
});
};
};
var NewUserCtrl = function ($scope, $location, growl, Events, User) {
Events.record(currentUser, "view", "page", "users/new");
$scope.user = new User({});
$scope.saveUser = function() {
$scope.$broadcast('show-errors-check-validity');
if (!$scope.userForm.$valid) {
return;
}
$scope.user.$save(function(user) {
growl.addSuccessMessage("Saved.")
$location.path('/users/' + user.id).replace();
}, function(error) {
var message = error.data.message || "Failed saving.";
growl.addErrorMessage(message);
});
}
};
angular.module('redash.controllers')
.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])
})();

View File

@@ -147,22 +147,22 @@
var reset = function() {
$scope.saveInProgress = false;
$scope.widgetSize = 1;
$scope.queryId = null;
$scope.selectedVis = null;
$scope.query = null;
$scope.query = {};
$scope.selected_query = undefined;
$scope.text = "";
};
reset();
$scope.loadVisualizations = function () {
if (!$scope.queryId) {
if (!$scope.query.selected) {
return;
}
Query.get({ id: $scope.queryId }, function(query) {
Query.get({ id: $scope.query.selected.id }, function(query) {
if (query) {
$scope.query = query;
$scope.selected_query = query;
if (query.visualizations.length) {
$scope.selectedVis = query.visualizations[0];
}
@@ -170,6 +170,20 @@
});
};
$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;

View File

@@ -0,0 +1,76 @@
(function () {
'use strict';
var directives = angular.module('redash.directives');
// Angular strips data- from the directive, so data-source-form becomes sourceForm...
directives.directive('sourceForm', ['$http', 'growl', function ($http, growl) {
return {
restrict: 'E',
replace: true,
templateUrl: '/views/data_sources/form.html',
scope: {
'dataSource': '='
},
link: function ($scope) {
var setType = function(types) {
if ($scope.dataSource.type === undefined) {
$scope.dataSource.type = types[0].type;
return types[0];
}
$scope.type = _.find(types, function (t) {
return t.type == $scope.dataSource.type;
});
};
$scope.files = {};
$scope.$watchCollection('files', function() {
_.each($scope.files, function(v, k) {
if (v) {
$scope.dataSource.options[k] = v.base64;
}
});
});
$http.get('/api/data_sources/types').success(function (types) {
setType(types);
$scope.dataSourceTypes = 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';
}
prop.required = _.contains(type.configuration_schema.required, name);
});
});
});
$scope.$watch('dataSource.type', function(current, prev) {
if (prev !== current) {
if (prev !== undefined) {
$scope.dataSource.options = {};
}
setType($scope.dataSourceTypes);
}
});
$scope.saveChanges = function() {
$scope.dataSource.$save(function() {
growl.addSuccessMessage("Saved.");
}, function() {
growl.addErrorMessage("Failed saving.");
});
}
}
}
}]);
})();

View File

@@ -97,14 +97,24 @@
value: '=',
ignoreBlanks: '=',
editable: '=',
done: '='
done: '=',
},
template: function (tElement, tAttrs) {
var elType = tAttrs.editor || 'input';
var placeholder = tAttrs.placeholder || 'Click to edit';
return '<span ng-click="editable && edit()" ng-bind="value" ng-class="{editable: editable}"></span>' +
'<span ng-click="editable && edit()" ng-show="editable && !value" ng-class="{editable: editable}">' + placeholder + '</span>' +
'<{elType} ng-model="value" class="rd-form-control"></{elType}>'.replace('{elType}', elType);
var viewMode = '';
if (tAttrs.markdown == "true") {
viewMode = '<span ng-click="editable && edit()" ng-bind-html="value|markdown" ng-class="{editable: editable}"></span>';
} else {
viewMode = '<span ng-click="editable && edit()" ng-bind="value" ng-class="{editable: editable}"></span>';
}
var placeholderSpan = '<span ng-click="editable && edit()" ng-show="editable && !value" ng-class="{editable: editable}">' + placeholder + '</span>';
var editor = '<{elType} ng-model="value" class="rd-form-control"></{elType}>'.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.
@@ -224,4 +234,51 @@
'</span>'
}
});
// 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: "="
}
};
});
})();

View File

@@ -8,7 +8,7 @@
'query': '=',
'visualization': '=?'
},
template: '<a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
template: '<small><span class="glyphicon glyphicon-link"></span></small> <a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
link: function(scope, element) {
scope.link = '/queries/' + scope.query.id;
if (scope.visualization) {
@@ -29,7 +29,7 @@
restrict: 'E',
template: '<span ng-show="query.id && canViewSource">\
<a ng-show="!sourceMode"\
ng-href="{{query.id}}/source#{{selectedTab}}">Show Source\
ng-href="/queries/{{query.id}}/source#{{selectedTab}}">Show Source\
</a>\
<a ng-show="sourceMode"\
ng-href="/queries/{{query.id}}#{{selectedTab}}">Hide Source\
@@ -63,26 +63,97 @@
restrict: 'E',
scope: {
'query': '=',
'lock': '='
'lock': '=',
'schema': '=',
'syntax': '='
},
template: '<textarea\
ui-codemirror="editorOptions"\
ng-model="query.query">',
link: function($scope) {
$scope.editorOptions = {
mode: 'text/x-sql',
template: '<textarea></textarea>',
link: {
pre: function ($scope, element) {
$scope.syntax = $scope.syntax || 'sql';
var modes = {
'sql': 'text/x-sql',
'python': 'text/x-python',
'json': 'application/json'
};
var textarea = element.children()[0];
var editorOptions = {
mode: modes[$scope.syntax],
lineWrapping: true,
lineNumbers: true,
readOnly: false,
matchBrackets: true,
autoCloseBrackets: true
};
autoCloseBrackets: true,
extraKeys: {"Ctrl-Space": "autocomplete"}
};
$scope.$watch('lock', function(locked) {
$scope.editorOptions.readOnly = locked ? 'nocursor' : false;
});
var additionalHints = [];
CodeMirror.commands.autocomplete = function(cm) {
var hinter = function(editor, options) {
var hints = CodeMirror.hint.anyword(editor, options);
var cur = editor.getCursor(), token = editor.getTokenAt(cur).string;
hints.list = _.union(hints.list, _.filter(additionalHints, function (h) {
return h.search(token) === 0;
}));
return hints;
};
// CodeMirror.showHint(cm, CodeMirror.hint.anyword);
CodeMirror.showHint(cm, hinter);
};
var codemirror = CodeMirror.fromTextArea(textarea, editorOptions);
codemirror.on('change', function(instance) {
var newValue = instance.getValue();
if (newValue !== $scope.query.query) {
$scope.$evalAsync(function() {
$scope.query.query = newValue;
});
}
$('.schema-container').css('height', $('.CodeMirror').css('height'));
});
$scope.$watch('query.query', function () {
if ($scope.query.query !== codemirror.getValue()) {
codemirror.setValue($scope.query.query);
}
});
$scope.$watch('schema', function (schema) {
if (schema) {
var keywords = [];
_.each(schema, function (table) {
keywords.push(table.name);
_.each(table.columns, function (c) {
keywords.push(c);
});
});
additionalHints = _.unique(keywords);
}
codemirror.refresh();
});
$scope.$watch('syntax', function(syntax) {
codemirror.setOption('mode', modes[syntax]);
});
$scope.$watch('lock', function (locked) {
var readOnly = locked ? 'nocursor' : false;
codemirror.setOption('readOnly', readOnly);
});
}
}
}
};
}
function queryFormatter($http) {
@@ -98,55 +169,111 @@
</button>',
link: function($scope) {
$scope.formatQuery = function formatQuery() {
$scope.queryExecuting = true;
$scope.queryFormatting = true;
$http.post('/api/queries/format', {
'query': $scope.query.query
}).success(function (response) {
$scope.query.query = response;
}).finally(function () {
$scope.queryExecuting = false;
$scope.queryFormatting = false;
});
};
}
}
}
function queryTimePicker() {
return {
restrict: 'E',
template: '<select ng-disabled="refreshType != \'daily\'" ng-model="hour" ng-change="updateSchedule()" ng-options="c as c for c in hourOptions"></select> :\
<select ng-disabled="refreshType != \'daily\'" ng-model="minute" ng-change="updateSchedule()" ng-options="c as c for c in minuteOptions"></select>',
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: '<select\
ng-disabled="!isQueryOwner"\
ng-model="query.ttl"\
ng-disabled="refreshType != \'periodic\'"\
ng-model="query.schedule"\
ng-change="saveQuery()"\
ng-options="c.value as c.name for c in refreshOptions">\
<option value="">No Refresh</option>\
</select>',
link: function($scope) {
$scope.refreshOptions = [
{
value: -1,
name: 'No Refresh'
},
{
value: 60,
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: i * 3600,
value: String(i * 3600),
name: 'Every ' + i + 'h'
});
})
$scope.refreshOptions.push({
value: 24 * 3600,
value: String(24 * 3600),
name: 'Every 24h'
});
$scope.refreshOptions.push({
value: 7 * 24 * 3600,
value: String(7 * 24 * 3600),
name: 'Once a week'
});
$scope.$watch('refreshType', function() {
if ($scope.refreshType == 'periodic') {
if ($scope.query.hasDailySchedule()) {
$scope.query.schedule = null;
$scope.saveQuery();
}
}
});
}
}
@@ -158,5 +285,6 @@
.directive('queryResultLink', queryResultCSVLink)
.directive('queryEditor', queryEditor)
.directive('queryRefreshSelect', queryRefreshSelect)
.directive('queryTimePicker', queryTimePicker)
.directive('queryFormatter', ['$http', queryFormatter]);
})();

View File

@@ -24,13 +24,17 @@ angular.module('redash.filters', []).
return durationHumanize;
})
.filter('refreshRateHumanize', function () {
return function (ttl) {
if (ttl == -1) {
.filter('scheduleHumanize', function() {
return function (schedule) {
if (schedule === null) {
return "Never";
} else {
return "Every " + durationHumanize(ttl);
} 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));
}
})
@@ -70,6 +74,18 @@ angular.module('redash.filters', []).
.filter('markdown', ['$sce', function ($sce) {
return function (text) {
if (!text) {
return "";
}
return $sce.trustAsHtml(marked(text));
}
}]);
}])
.filter('trustAsHtml', ['$sce', function ($sce) {
return function (text) {
if (!text) {
return "";
}
return $sce.trustAsHtml(text);
}
}]);

View File

@@ -1,9 +1,20 @@
(function () {
'use strict';
var ColorPalette = {
'Blue':'#4572A7',
'Red':'#AA4643',
'Green': '#89A54E',
'Purple': '#80699B',
'Cyan': '#3D96AE',
'Orange': '#DB843D',
'Light Blue': '#92A8CD',
'Lilac': '#A47D7C',
'Light Green': '#B5CA92',
};
Highcharts.setOptions({
colors: ["#4572A7", "#AA4643", "#89A54E", "#80699B", "#3D96AE",
"#DB843D", "#92A8CD", "#A47D7C", "#B5CA92"]
colors: _.values(ColorPalette)
});
var defaultOptions = {
@@ -39,7 +50,7 @@
;
if (moment.isMoment(this.x)) {
var s = '<b>' + moment(this.x).format("DD/MM/YY HH:mm") + '</b>',
var s = '<b>' + this.x.toDate().toLocaleString() + '</b>',
pointsCount = this.points.length;
$.each(this.points, function (i, point) {
@@ -115,23 +126,51 @@
{
text: 'Show Total',
onclick: function () {
var hasTotalsAlready = _.some(this.series, function (s) {
var res = (s.name == 'Total');
//if 'Total' already exists - just make it visible
if (res) s.setVisible(true, false);
return res;
})
var data = {};
_.each(this.series, function (s) {
s.setVisible(false, false);
_.each(s.data, function (p) {
data[p.x] = data[p.x] || {'x': p.x, 'y': 0};
data[p.x].y = data[p.x].y + p.y;
});
if (s.name != 'Total') s.setVisible(false, false);
if (!hasTotalsAlready) {
_.each(s.data, function (p) {
data[p.x] = data[p.x] || {'x': p.x, 'y': 0};
data[p.x].y = data[p.x].y + p.y;
});
}
});
this.addSeries({
data: _.values(data),
type: 'line',
name: 'Total'
}, false)
if (!hasTotalsAlready) {
this.addSeries({
data: _.sortBy(_.values(data), 'x'),
type: 'line',
name: 'Total'
}, false)
}
this.redraw();
}
},
{
text: 'Save Image',
onclick: function () {
var canvas = document.createElement('canvas');
window.canvg(canvas, this.getSVG());
var href = canvas.toDataURL('image/png');
var a = document.createElement('a');
a.href = href;
var filenameSuffix = new Date().toISOString().replace(/:/g,'_').replace('Z', '');
if (this.title) {
filenameSuffix = this.title.text;
}
a.download = 'redash_charts_'+filenameSuffix+'.png';
document.body.appendChild(a);
a.click();
a.remove();
}
}
]
}
@@ -204,6 +243,7 @@
};
angular.module('highchart', [])
.constant('ColorPalette', ColorPalette)
.directive('chart', ['$timeout', function ($timeout) {
return {
restrict: 'E',
@@ -265,8 +305,29 @@
scope.chart.series[0].remove(false);
};
// We check either for true or undefined for backward compatibility.
var series = scope.series;
// If this is a chart that has just one row for multiple columns, sort
// by the Y values. For example:
//
// A | B | C
// 20 | 30 | 15
//
// Will be sorted:
// C | A | B
// 15 | 20 | 30
var sortable = _.every(series, function(s) { return s.data.length == 1 });
if (sortable) {
series = _.sortBy(series, function (s) {
return s.data[0].y
});
}
if (!('xAxis' in chartOptions && 'type' in chartOptions['xAxis'])) {
if (scope.series.length > 0 && _.some(scope.series[0].data, function (p) {
if (series.length > 0 && _.some(series[0].data, function (p) {
return (angular.isString(p.x) || angular.isDefined(p.name));
})) {
chartOptions['xAxis'] = chartOptions['xAxis'] || {};
@@ -278,13 +339,13 @@
}
if (chartOptions['xAxis']['type'] == 'category' || chartOptions['series']['type']=='pie') {
if (!angular.isDefined(scope.series[0].data[0].name)) {
if (!angular.isDefined(series[0].data[0].name)) {
// We need to make sure that for each category, each series has a value.
var categories = _.union.apply(this, _.map(scope.series, function (s) {
var categories = _.union.apply(this, _.map(series, function (s) {
return _.pluck(s.data, 'x')
}));
_.each(scope.series, function (s) {
_.each(series, function (s) {
// TODO: move this logic to Query#getChartData
var yValues = _.groupBy(s.data, 'x');
@@ -295,19 +356,31 @@
}
});
if (categories.length == 1) {
newData = _.sortBy(newData, 'y').reverse();
}
;
s.data = newData;
});
}
}
if (chartOptions['sortX'] === true || chartOptions['sortX'] === undefined) {
var seriesCopy = [];
_.each(series, function (s) {
// make a copy of series data, so we don't override original.
var fieldName = 'x';
if (s.data.length > 0 && _.has(s.data[0], 'name')) {
fieldName = 'name';
};
var sorted = _.extend({}, s, {data: _.sortBy(s.data, fieldName)});
seriesCopy.push(sorted);
});
series = seriesCopy;
}
scope.chart.counters.color = 0;
_.each(scope.series, function (s) {
_.each(series, function (s) {
// here we override the series with the visualization config
s = _.extend(s, chartOptions['series']);
@@ -338,4 +411,4 @@
};
}]);
})();
})();

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,12 @@
(function () {
var Dashboard = function($resource) {
var resource = $resource('/api/dashboards/:slug', {slug: '@slug'});
var resource = $resource('/api/dashboards/:slug', {slug: '@slug'}, {
recent: {
method: 'get',
isArray: true,
url: "/api/dashboards/recent"
}});
resource.prototype.canEdit = function() {
return currentUser.hasPermission('admin') || currentUser.canEdit(this);
}

View File

@@ -1,10 +1,9 @@
(function () {
var notifications = function (Events) {
var notificationService = {};
var lastNotification = null;
notificationService.isSupported = function () {
if (window.webkitNotifications) {
if ("Notification" in window) {
return true;
} else {
console.log("HTML5 notifications are not supported.");
@@ -17,8 +16,12 @@
return;
}
if (!window.webkitNotifications.checkPermission() == 0) { // 0 is PERMISSION_ALLOWED
window.webkitNotifications.requestPermission();
if (Notification.permission !== "granted") {
Notification.requestPermission(function (status) {
if (Notification.permission !== status) {
Notification.permission = status;
}
});
}
}
@@ -27,23 +30,16 @@
return;
}
if (document.webkitVisibilityState && document.webkitVisibilityState == 'visible') {
return;
}
if (lastNotification) {
lastNotification.cancel();
}
var notification = window.webkitNotifications.createNotification('', title, content);
lastNotification = notification;
//using the 'tag' to avoid showing duplicate notifications
var notification = new Notification(title, {'tag': title+content, 'body': content});
setTimeout(function(){
notification.close();
},3000);
notification.onclick = function () {
window.focus();
this.cancel();
Events.record(currentUser, 'click', 'notification');
};
notification.show()
}
return notificationService;

View File

@@ -1,4 +1,28 @@
(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'});
@@ -12,6 +36,8 @@
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)) {
@@ -22,13 +48,17 @@
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}/)) {
row[k] = moment(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]) {
column.type = columnTypes[column.name];
if (column.type == null || column.type == 'string') {
column.type = columnTypes[column.name];
}
}
});
@@ -38,7 +68,7 @@
} else {
this.status = undefined;
}
}
};
function QueryResult(props) {
this.deferred = $q.defer();
@@ -89,6 +119,14 @@
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;
}
@@ -160,8 +198,8 @@
var yValues = {};
_.each(row, function (value, definition) {
var name = definition.split("::")[0];
var type = definition.split("::")[1];
var name = definition.split("::")[0] || definition.split("__")[0];
var type = definition.split("::")[1] || definition.split("__")[0];
if (mapping) {
type = mapping[definition];
}
@@ -186,7 +224,7 @@
seriesName = String(value);
}
if (type == 'multi-filter') {
if (type == 'multiFilter' || type == 'multi-filter') {
seriesName = String(value);
}
});
@@ -212,10 +250,6 @@
}
});
_.each(series, function (series) {
series.data = _.sortBy(series.data, 'x');
});
return _.values(series);
};
@@ -238,33 +272,25 @@
}
QueryResult.prototype.getColumnNameWithoutType = function (column) {
var parts = column.split('::');
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];
};
var charConversionMap = {
'__pct': /%/g,
'_': / /g,
'__qm': /\?/g,
'__brkt': /[\(\)\[\]]/g,
'__dash': /-/g,
'__amp': /&/g,
'__sl': /\//g,
'__fsl': /\\/g,
};
QueryResult.prototype.getColumnCleanName = function (column) {
var name = this.getColumnNameWithoutType(column);
if (name != '') {
_.each(charConversionMap, function(regex, replacement) {
name = name.replace(regex, replacement);
});
}
return name;
}
@@ -296,16 +322,16 @@
QueryResult.prototype.prepareFilters = function () {
var filters = [];
var filterTypes = ['filter', 'multi-filter'];
var filterTypes = ['filter', 'multi-filter', 'multiFilter'];
_.each(this.getColumnNames(), function (col) {
var type = col.split('::')[1]
var type = col.split('::')[1] || col.split('__')[1];
if (_.contains(filterTypes, type)) {
// filter found
var filter = {
name: col,
friendlyName: this.getColumnFriendlyName(col),
values: [],
multiple: (type=='multi-filter')
multiple: (type=='multiFilter') || (type=='multi-filter')
}
filters.push(filter);
}
@@ -327,7 +353,7 @@
this.filters = filters;
}
var refreshStatus = function (queryResult, query, ttl) {
var refreshStatus = function (queryResult, query) {
Job.get({'id': queryResult.job.id}, function (response) {
queryResult.update(response);
@@ -337,7 +363,7 @@
});
} else if (queryResult.getStatus() != "failed") {
$timeout(function () {
refreshStatus(queryResult, query, ttl);
refreshStatus(queryResult, query);
}, 3000);
}
})
@@ -357,14 +383,19 @@
return this.deferred.promise;
}
QueryResult.get = function (data_source_id, query, ttl) {
QueryResult.get = function (data_source_id, query, maxAge, queryId) {
var queryResult = new QueryResult();
QueryResultResource.post({'data_source_id': data_source_id, 'query': query, 'ttl': ttl}, function (response) {
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, ttl);
refreshStatus(queryResult, query);
}
});
@@ -375,36 +406,100 @@
};
var Query = function ($resource, QueryResult, DataSource) {
var Query = $resource('/api/queries/:id', {id: '@id'});
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.newQuery = function () {
return new Query({
query: "",
name: "New Query",
ttl: -1,
schedule: null,
user: currentUser
});
};
Query.collectParamsFromQueryString = function($location, query) {
var parameterNames = query.getParameters();
var parameters = {};
var queryString = $location.search();
_.each(parameterNames, function(param, i) {
var qsName = "p_" + param;
if (qsName in queryString) {
parameters[param] = queryString[qsName];
}
});
return parameters;
};
Query.prototype.getSourceLink = function () {
return '/queries/' + this.id + '/source';
};
Query.prototype.getQueryResult = function (ttl) {
if (ttl == undefined) {
ttl = this.ttl;
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.getQueryResult = function (maxAge, parameters) {
if (!this.query) {
return;
}
var queryText = this.query;
var queryParameters = this.getParameters();
var paramsRequired = !_.isEmpty(queryParameters);
var missingParams = parameters === undefined ? queryParameters : _.difference(queryParameters, _.keys(parameters));
if (paramsRequired && missingParams.length > 0) {
var paramsWord = "parameter";
if (missingParams.length > 1) {
paramsWord = "parameters";
}
return new QueryResult({job: {error: "Missing values for " + missingParams.join(', ') + " "+paramsWord+".", status: 4}});
}
if (this.latest_query_data && ttl != 0) {
if (paramsRequired) {
queryText = Mustache.render(queryText, parameters);
// Need to clear latest results, to make sure we don't used 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 && ttl != 0) {
} 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, this.query, ttl);
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;
@@ -412,6 +507,26 @@
Query.prototype.getQueryResultPromise = function() {
return this.getQueryResult().toPromise();
};
Query.prototype.getParameters = function() {
var parts = Mustache.parse(this.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 = collectParams(parts);
return parameters;
}
return Query;
@@ -420,10 +535,69 @@
var DataSource = function ($resource) {
var DataSourceResource = $resource('/api/data_sources/:id', {id: '@id'}, {'get': {'method': 'GET', 'cache': true, 'isArray': true}});
var actions = {
'get': {'method': 'GET', 'cache': false, 'isArray': false},
'query': {'method': 'GET', 'cache': false, 'isArray': true},
'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 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 AlertSubscription = function ($resource) {
var resource = $resource('/api/alerts/:alertId/subscriptions/:userId', {alertId: '@alert_id', userId: '@user.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;
delete newData.query;
}
return newData;
}].concat($http.defaults.transformRequest)
}
};
var resource = $resource('/api/alerts/:id', {id: '@id'}, actions);
return resource;
};
var Widget = function ($resource, Query) {
var WidgetResource = $resource('/api/widgets/:id', {id: '@id'});
@@ -450,5 +624,8 @@
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
.factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
.factory('DataSource', ['$resource', DataSource])
.factory('Widget', ['$resource', 'Query', Widget]);
.factory('Alert', ['$resource', '$http', Alert])
.factory('AlertSubscription', ['$resource', AlertSubscription])
.factory('Widget', ['$resource', 'Query', Widget])
.factory('User', ['$resource', '$http', User]);
})();

File diff suppressed because one or more lines are too long

View File

@@ -55,6 +55,22 @@
}];
};
var VisualizationName = function(Visualization) {
return {
restrict: 'E',
scope: {
visualization: '='
},
template: '<small>{{name}}</small>',
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',
@@ -68,46 +84,9 @@
template: '<filters></filters>\n' + Visualization.renderVisualizationsTemplate,
replace: false,
link: function (scope) {
scope.select2Options = {
width: '50%'
};
function readURL() {
var searchFilters = angular.fromJson($location.search().filters);
if (searchFilters) {
_.forEach(scope.filters, function(filter) {
var value = searchFilters[filter.friendlyName];
if (value) {
filter.current = value;
}
});
}
}
function updateURL(filters) {
var current = {};
_.each(filters, function(filter) {
if (filter.current) {
current[filter.friendlyName] = filter.current;
}
});
var newSearch = angular.extend($location.search(), {
filters: angular.toJson(current)
});
$location.search(newSearch);
}
scope.$watch('queryResult && queryResult.getFilters()', function (filters) {
if (filters) {
scope.filters = filters;
if (filters.length && false) {
readURL();
// start watching for changes and update URL
scope.$watch('filters', updateURL, true);
}
}
});
}
@@ -138,7 +117,7 @@
query: '=',
queryResult: '=',
visualization: '=?',
openEditor: '=?',
openEditor: '@',
onNewSuccess: '=?'
},
link: function (scope, element, attrs) {
@@ -167,9 +146,13 @@
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) {
// poor man's titlecase
scope.visualization.name = scope.visualization.type[0] + scope.visualization.type.slice(1).toLowerCase();
scope.visualization.name = _.string.titleize(scope.visualization.type);
}
if (type && oldType != type && scope.visualization) {
scope.visualization.options = Visualization.visualizations[scope.visualization.type].defaultOptions;
}
});
scope.submit = function () {
@@ -208,6 +191,7 @@
.provider('Visualization', VisualizationProvider)
.directive('visualizationRenderer', ['$location', 'Visualization', VisualizationRenderer])
.directive('visualizationOptionsEditor', ['Visualization', VisualizationOptionsEditor])
.directive('visualizationName', ['Visualization', VisualizationName])
.directive('filters', Filters)
.directive('editVisulatizationForm', ['Events', 'Visualization', 'growl', EditVisualizationForm])
})();

View File

@@ -41,6 +41,9 @@
_.each($scope.queryResult.getChartData($scope.options.columnMapping), function (s) {
var additional = {'stacking': 'normal'};
if ('globalSeriesType' in $scope.options) {
additional['type'] = $scope.options.globalSeriesType;
}
if ($scope.options.seriesOptions && $scope.options.seriesOptions[s.name]) {
additional = $scope.options.seriesOptions[s.name];
if (!additional.name || additional.name == "") {
@@ -74,11 +77,13 @@
};
});
chartVisualization.directive('chartEditor', function () {
chartVisualization.directive('chartEditor', function (ColorPalette) {
return {
restrict: 'E',
templateUrl: '/views/visualizations/chart_editor.html',
link: function (scope, element, attrs) {
scope.palette = ColorPalette;
scope.seriesTypes = {
'Line': 'line',
'Column': 'column',
@@ -87,7 +92,7 @@
'Pie': 'pie'
};
scope.globalSeriesType = 'column';
scope.globalSeriesType = scope.visualization.options.globalSeriesType || 'column';
scope.stackingOptions = {
"None": "none",
@@ -107,9 +112,6 @@
scope.columnTypes = {
"X": "x",
// "X (Date time)": "x",
// "X (Linear)": "x-linear",
// "X (Category)": "x-category",
"Y": "y",
"Series": "series",
"Unused": "unused"
@@ -123,12 +125,15 @@
columnsWatch = null;
scope.$watch('globalSeriesType', function(type, old) {
scope.visualization.options.globalSeriesType = type;
if (type && old && type !== old && scope.visualization.options.seriesOptions) {
_.each(scope.visualization.options.seriesOptions, function(sOptions) {
sOptions.type = type;
});
}
});
scope.$watch('visualization.type', function (visualizationType) {
if (visualizationType == 'CHART') {
if (scope.visualization.options.series.stacking === null) {
@@ -139,6 +144,10 @@
scope.stacking = scope.visualization.options.series.stacking;
}
if (scope.visualization.options.sortX === undefined) {
scope.visualization.options.sortX = true;
}
var refreshSeries = function() {
scope.series = _.map(scope.queryResult.getChartData(scope.visualization.options.columnMapping), function (s) { return s.name; });
@@ -151,10 +160,10 @@
_.each(scope.series, function(s, i) {
if (scope.visualization.options.seriesOptions[s] == undefined) {
scope.visualization.options.seriesOptions[s] = {'type': 'column', 'yAxis': 0};
scope.visualization.options.seriesOptions[s] = {'type': scope.visualization.options.globalSeriesType, 'yAxis': 0};
}
scope.visualization.options.seriesOptions[s].zIndex = i;
scope.visualization.options.seriesOptions[s].zIndex = scope.visualization.options.seriesOptions[s].zIndex === undefined ? i : scope.visualization.options.seriesOptions[s].zIndex;
scope.visualization.options.seriesOptions[s].index = scope.visualization.options.seriesOptions[s].index === undefined ? i : scope.visualization.options.seriesOptions[s].index;
});
scope.zIndexes = _.range(scope.series.length);
scope.yAxes = [[0, 'left'], [1, 'right']];
@@ -215,6 +224,12 @@
}
});
scope.visualization.options.xAxis = scope.visualization.options.xAxis || {};
scope.visualization.options.xAxis.labels = scope.visualization.options.xAxis.labels || {};
if (scope.visualization.options.xAxis.labels.enabled === undefined) {
scope.visualization.options.xAxis.labels.enabled = true;
}
scope.xAxisType = (scope.visualization.options.xAxis && scope.visualization.options.xAxis.type) || scope.xAxisType;
xAxisUnwatch = scope.$watch("xAxisType", function (xAxisType) {

View File

@@ -26,7 +26,10 @@
if ($scope.queryResult.getData() == null) {
} else {
var sortedData = _.sortBy($scope.queryResult.getData(), "date");
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;

View File

@@ -0,0 +1,61 @@
'use strict';
(function() {
var module = angular.module('redash.visualization');
module.config(['VisualizationProvider', function(VisualizationProvider) {
var renderTemplate =
'<counter-renderer ' +
'options="visualization.options" query-result="queryResult">' +
'</counter-renderer>';
var editTemplate = '<counter-editor></counter-editor>';
var defaultOptions = {};
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) {
$scope.visualization.options.rowNumber =
$scope.visualization.options.rowNumber || 0;
$scope.$watch('[queryResult && queryResult.getData(), visualization.options]',
function() {
var queryData = $scope.queryResult.getData();
if (queryData) {
var rowNumber = $scope.visualization.options.rowNumber || 0;
var counterColName = $scope.visualization.options.counterColName || 'counter';
var targetColName = $scope.visualization.options.targetColName || 'target';
$scope.counterValue = queryData[rowNumber][counterColName];
$scope.targetValue = queryData[rowNumber][targetColName];
if ($scope.targetValue) {
$scope.delta = $scope.counterValue - $scope.targetValue;
$scope.trendPositive = $scope.delta >= 0;
}
}
}, true);
}
}
});
module.directive('counterEditor', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/counter_editor.html'
}
});
})();

View File

@@ -0,0 +1,238 @@
'use strict';
(function() {
var module = angular.module('redash.visualization');
module.config(['VisualizationProvider', function(VisualizationProvider) {
var renderTemplate =
'<map-renderer ' +
'options="visualization.options" query-result="queryResult">' +
'</map-renderer>';
var editTemplate = '<map-editor></map-editor>';
var defaultOptions = {
'height': 500,
'draw': 'Marker',
'classify':'none'
};
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) {
var setBounds = function(){
var b = $scope.visualization.options.bounds;
if(b){
$scope.map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]);
} else if ($scope.features.length > 0){
var group= new L.featureGroup($scope.features);
$scope.map.fitBounds(group.getBounds());
}
};
$scope.$watch('[queryResult && queryResult.getData(), visualization.options.draw,visualization.options.latColName,'+
'visualization.options.lonColName,visualization.options.classify,visualization.options.classify]',
function() {
var marker = function(lat,lon){
if (lat == null || lon == null) return;
return L.marker([lat, lon]);
};
var heatpoint = function(lat,lon,obj){
if (lat == null || lon == null) return;
var color = 'red';
if (obj &&
obj[$scope.visualization.options.classify] &&
$scope.visualization.options.classification){
var v = $.grep($scope.visualization.options.classification,function(e){
return e.value == obj[$scope.visualization.options.classify];
});
if (v.length >0) color = v[0].color;
}
var style = {
fillColor:color,
fillOpacity:0.5,
stroke:false
};
return L.circleMarker([lat,lon],style)
};
var color = function(val){
// taken from http://jsfiddle.net/xgJ2e/2/
var h= Math.floor((100 - val) * 120 / 100);
var s = Math.abs(val - 50)/50;
var v = 1;
var rgb, i, data = [];
if (s === 0) {
rgb = [v,v,v];
} else {
h = h / 60;
i = Math.floor(h);
data = [v*(1-s), v*(1-s*(h-i)), v*(1-s*(1-(h-i)))];
switch(i) {
case 0:
rgb = [v, data[2], data[0]];
break;
case 1:
rgb = [data[1], v, data[0]];
break;
case 2:
rgb = [data[0], v, data[2]];
break;
case 3:
rgb = [data[0], data[1], v];
break;
case 4:
rgb = [data[2], data[0], v];
break;
default:
rgb = [v, data[0], data[1]];
break;
}
}
return '#' + rgb.map(function(x){
return ("0" + Math.round(x*255).toString(16)).slice(-2);
}).join('');
};
// 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 getBounds(e) {
$scope.visualization.options.bounds = $scope.map.getBounds();
}
var queryData = $scope.queryResult.getData();
var classify = $scope.visualization.options.classify;
if (queryData) {
$scope.visualization.options.classification = [];
for (var row in queryData) {
if (queryData[row][classify] &&
$.grep($scope.visualization.options.classification, function (e) {
return e.value == queryData[row][classify]
}).length == 0) {
$scope.visualization.options.classification.push({value: queryData[row][classify], color: null});
}
}
$.each($scope.visualization.options.classification, function (i, c) {
c.color = color(parseInt((i / $scope.visualization.options.classification.length) * 100));
});
if (!$scope.map) {
$scope.map = L.map(elm[0].children[0].children[0])
}
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo($scope.map);
$scope.features = $scope.features || [];
var tmp_features = [];
var lat_col = $scope.visualization.options.latColName || 'lat';
var lon_col = $scope.visualization.options.lonColName || 'lon';
for (var row in queryData) {
var feature;
if ($scope.visualization.options.draw == 'Marker') {
feature = marker(queryData[row][lat_col], queryData[row][lon_col])
} else if ($scope.visualization.options.draw == 'Color') {
feature = heatpoint(queryData[row][lat_col], queryData[row][lon_col], queryData[row])
}
if (!feature) continue;
var obj_description = '<ul style="list-style-type: none;padding-left: 0">';
for (var k in queryData[row]){
obj_description += "<li>" + k + ": " + queryData[row][k] + "</li>";
}
obj_description += '</ul>';
feature.bindPopup(obj_description);
tmp_features.push(feature);
}
$.each($scope.features, function (i, f) {
$scope.map.removeLayer(f);
});
$scope.features = tmp_features;
$.each($scope.features, function (i, f) {
f.addTo($scope.map)
});
setBounds();
$scope.map.on('focus',function(){
$scope.map.on('moveend', getBounds);
});
$scope.map.on('blur',function(){
$scope.map.off('moveend', getBounds);
});
// We redraw the map if it was loaded in a hidden tab
if ($('a[href="#'+$scope.visualization.id+'"]').length > 0) {
$('a[href="#'+$scope.visualization.id+'"]').on('click', function () {
setTimeout(function() {
$scope.map.invalidateSize(false);
setBounds();
},500);
});
}
}
}, true);
$scope.$watch('visualization.options.height', function() {
if (!$scope.map) return;
$scope.map.invalidateSize(false);
setBounds();
});
}
}
});
module.directive('mapEditor', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/map_editor.html',
link: function($scope, elm, attrs) {
$scope.draw_options = ['Marker','Color'];
$scope.classify_columns = $scope.queryResult.columnNames.concat('none');
}
}
});
})();

View File

@@ -78,15 +78,15 @@
};
} else if (columnType === 'date') {
columnDefinition.formatFunction = function (value) {
if (value) {
return value.format("DD/MM/YY");
if (value && moment.isMoment(value)) {
return value.toDate().toLocaleDateString();
}
return value;
};
} else if (columnType === 'datetime') {
columnDefinition.formatFunction = function (value) {
if (value) {
return value.format("DD/MM/YY HH:mm");
if (value && moment.isMoment(value)) {
return value.toDate().toLocaleString();
}
return value;
};

View File

@@ -14,7 +14,16 @@ a.page-title {
}
a.navbar-brand {
font-style: italic;
padding: 5px 5px 0px 0px;
margin-left: 0px !important;
}
.navbar .fa {
font-size: 18px;
}
a.navbar-brand img {
height: 40px;
}
.graph {
@@ -92,7 +101,29 @@ a.navbar-brand {
}
.panel-heading .query-link:hover {
text-decoration: none;
text-decoration: underline;
}
.list-group-item.clickable {
cursor: pointer;
}
.list-group-item.clickable:focus,
.list-group-item.clickable:hover {
background-color: #f5f5f5;
}
.form-group.required .control-label:after {
content:"*";
color:red;
}
.form-group .help-block.error {
display: none;
}
.form-group.has-error .help-block.error {
display: block;
}
/* angular-growl */
@@ -123,6 +154,23 @@ a.navbar-brand {
}
/* Visualization Filters */
.filters-container {
display: flex;
flex-wrap: wrap;
}
.filter {
width: 33%;
padding-left: 5px;
padding-bottom: 5px;
}
.filter > div {
width: 100%;
}
/* Gridster */
.gridster ul {
@@ -165,6 +213,14 @@ li.widget:hover {
overflow-x: auto;
}
/* Support for Font-Awesome in btn-xs */
.btn-xs > .fa {
font-size: 14px;
top: 1px;
position: relative;
}
/* Because of ng-repeat we add span between the .dropdown-menu element and the li element, so we had
to add those CSS styles here. */
@@ -270,6 +326,35 @@ to add those CSS styles here. */
pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div {
overflow: auto;
}
counter-renderer {
display: block;
text-align: center;
}
counter-renderer counter {
margin: 0 auto;
background: #f9f9f9;
padding: 15px 50px;
display: block;;
}
counter-renderer value,
counter-renderer counter-target {
font-size: 80px;
display: block;
}
counter-renderer counter-target {
color: #ccc;
}
counter-renderer counter.positive value {
color: #5cb85c;
}
counter-renderer counter.negative value {
color: #d9534f;
margin-right: 15px;
}
counter-renderer counter-name {
font-size: 40px;
display: block;
}
.rd-widget-textbox p {
margin-bottom: 0;
@@ -279,6 +364,28 @@ pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div
height: 100%;
}
.schema-container {
height: 300px;
}
.schema-browser {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
div.table-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.blankslate {
text-align: center;
padding: 30px;
}
/*
bootstrap's hidden-xs class adds display:block when not hidden
use this class when you need to keep the original display value
@@ -288,3 +395,7 @@ use this class when you need to keep the original display value
display: none !important;
}
}
.log-container {
margin-bottom: 50px;
}

View File

@@ -24,10 +24,6 @@
<span class="badge">{{manager.outdated_queries_count}}</span>
Outdated Queries Count
</li>
<li class="list-group-item" ng-if="flowerUrl">
<a href="/admin/workers">Workers' Status</a>
</li>
</ul>
<ul class="list-group col-lg-4">
<li class="list-group-item active">Queues</li>

View File

@@ -1,3 +0,0 @@
<div class="container-fluid iframe-container">
<iframe src="{{flowerUrl}}" style="width:100%; height:100%; background-color:transparent;"></iframe>
</div>

View File

@@ -0,0 +1,58 @@
<div class="container">
<ol class="breadcrumb">
<li><a href="/alerts">Alerts</a></li>
<li class="active">{{alert.name || getDefaultName() || "New"}}</li>
</ol>
<div class="row">
<div class="col-md-8">
<form name="alertForm" ng-submit="saveChanges()" class="form">
<div class="form-group">
<label>Query</label>
<ui-select ng-model="alert.query" reset-search-input="false" on-select="onQuerySelected($item)">
<ui-select-match placeholder="Search a query by name">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="q in queries"
refresh="searchQueries($select.search)"
refresh-delay="0">
<div ng-bind-html="q.name | highlight: $select.search | trustAsHtml"></div>
</ui-select-choices>
</ui-select>
</div>
<div class="form-group" ng-show="selectedQuery">
<label>Name</label>
<input type="string" placeholder="{{getDefaultName()}}" class="form-control" ng-model="alert.name">
</div>
<div ng-show="queryResult" class="form-horizontal">
<div class="form-group">
<label class="control-label col-md-2">Value column</label>
<div class="col-md-4">
<select ng-options="name for name in queryResult.getColumnNames()" ng-model="alert.options.column" class="form-control"></select>
</div>
<label class="control-label col-md-2">Value</label>
<div class="col-md-4">
<p class="form-control-static">{{queryResult.getData()[0][alert.options.column]}}</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2">Op</label>
<div class="col-md-4">
<select ng-options="name for name in ops" ng-model="alert.options.op" class="form-control"></select>
</div>
<label class="control-label col-md-2">Reference</label>
<div class="col-md-4">
<input type="number" class="form-control" ng-model="alert.options.value" placeholder="reference value" required/>
</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" ng-disabled="!alertForm.$valid">Save</button>
</div>
</form>
</div>
<div class="col-md-4" ng-if="alert.id">
<alert-subscribers alert-id="alert.id"></alert-subscribers>
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="container">
<ol class="breadcrumb">
<li class="active">Alerts</li>
</ol>
<div class="row">
<div class="col-md-12">
<p>
<a href="/alerts/new" class="btn btn-default"><i class="fa fa-plus"></i> New Alert</a>
</p>
<smart-table rows="alerts" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>
</div>
</div>
</div>

View File

@@ -0,0 +1,4 @@
<div>
<strong>Subscribers</strong> <subscribe-button alert-id="alertId" subscribers="subscribers"></subscribe-button><br/>
<img ng-src="{{s.user.gravatar_url}}" class="img-circle" alt="{{s.user.name}}" ng-repeat="s in subscribers"/>
</div>

View File

@@ -2,19 +2,23 @@
<edit-dashboard-form dashboard="dashboard" id="edit_dashboard_dialog"></edit-dashboard-form>
<div class="container">
<p class="alert alert-warning" ng-if="dashboard.is_archived">This dashboard is archived and won't appear in the dashboards list or search results.</p>
<h2 id="dashboard_title">
{{dashboard.name}}
<span ng-if="!dashboard.is_archived">
<button type="button" class="btn btn-default btn-xs" ng-class="{active: refreshEnabled}" tooltip="Enable/Disable Auto Refresh" ng-click="triggerRefresh()"><span class="glyphicon glyphicon-refresh"></span></button>
<span ng-show="dashboard.canEdit()">
<div class="btn-group" role="group" ng-show="dashboard.canEdit()">
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" href="#edit_dashboard_dialog" tooltip="Edit Dashboard (Name/Layout)"><span
class="glyphicon glyphicon-cog"></span></button>
<button type="button" class="btn btn-default btn-xs" data-toggle="modal"
href="#add_query_dialog" tooltip="Add Widget (Chart/Table)"><span class="glyphicon glyphicon-plus"></span>
</button>
</span>
<butotn class="btn btn-danger btn-xs" ng-click="archiveDashboard()" ng-if="!dashboard.is_archived" tooltip="Archive"><i class="fa fa-archive"></i></butotn>
</div>
</span>
</h2>
<filters></filters>
<filters ng-if="dashboard.dashboard_filters_enabled"></filters>
</div>
<div class="container" id="dashboard">
@@ -28,8 +32,9 @@
<p>
<span ng-hide="currentUser.hasPermission('view_query')">{{query.name}}</span>
<query-link query="query" visualization="widget.visualization" ng-show="currentUser.hasPermission('view_query')"></query-link>
<visualization-name visualization="widget.visualization"/>
</p>
<div class="text-muted" ng-bind="query.description"></div>
<div class="text-muted" ng-bind-html="query.description | markdown"></div>
</h3>
</div>
@@ -37,7 +42,7 @@
<div class="panel-footer">
<span class="label label-default"
tooltip="next update {{nextUpdateTime}} (query runtime: {{queryResult.getRuntime() | durationHumanize}})"
tooltip="(query runtime: {{queryResult.getRuntime() | durationHumanize}})"
tooltip-placement="bottom">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
<span class="pull-right">

View File

@@ -0,0 +1,11 @@
<div class="container">
<ol class="breadcrumb">
<li><a href="/data_sources">Data Sources</a></li>
<li class="active">{{dataSource.name || "New"}}</li>
</ol>
<div class="row">
<div class="col-md-8">
<data-source-form data-data-source="dataSource" />
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<form name="dataSourceForm" ng-submit="saveChanges()">
<div class="form-group">
<label for="dataSourceName">Name</label>
<input type="string" class="form-control" name="dataSourceName" ng-model="dataSource.name" required>
</div>
<div class="form-group">
<label for="type">Type</label>
<select name="type" class="form-control" ng-options="type.type as type.name for type in dataSourceTypes" ng-model="dataSource.type"></select>
</div>
<div class="form-group" ng-class='{"has-error": !inner.input.$valid}' ng-form="inner" ng-repeat="(name, input) in type.configuration_schema.properties">
<label>{{input.title || name | capitalize}}</label>
<input name="input" type="{{input.type}}" class="form-control" ng-model="dataSource.options[name]" ng-required="input.required"
ng-if="input.type !== 'file'" accesskey="tab">
<input name="input" type="file" class="form-control" ng-model="files[name]" ng-required="input.required"
base-sixty-four-input
ng-if="input.type === 'file'">
</div>
<button class="btn btn-primary" ng-disabled="!dataSourceForm.$valid">Save</button>
</form>

View File

@@ -0,0 +1,18 @@
<div class="container">
<ol class="breadcrumb">
<li class="active">Data Sources</li>
</ol>
<div class="row">
<div class="col-md-4">
<div class="list-group">
<div class="list-group-item clickable" ng-repeat="dataSource in dataSources" ng-click="openDataSource(dataSource)">
<i class="fa fa-database"></i> {{dataSource.name}}
<button class="btn btn-xs btn-danger pull-right" ng-click="deleteDataSource($event, dataSource)">Delete</button>
</div>
<a ng-href="/data_sources/new" class="list-group-item">
<i class="fa fa-plus"></i> Add Data Source
</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
<div>
<span class="help-block error" ng-if="errors.required">This field is required.</span>
<span class="help-block error" ng-if="errors.minlength">This field is too short.</span>
<span class="help-block error" ng-if="errors.email">This needs to be a valid email.</span>
</div>

View File

@@ -22,22 +22,22 @@
</div>
<div ng-show="isVisualization()">
<p>
<form class="form-inline" role="form" ng-submit="loadVisualizations()">
<div class="form-group">
<input class="form-control" placeholder="Query Id" ng-model="queryId">
</div>
<button type="submit" class="btn btn-primary" ng-disabled="!queryId">
Load visualizations
</button>
</form>
</p>
<div class="form-group">
<ui-select ng-model="query.selected" theme="bootstrap" reset-search-input="false">
<ui-select-match placeholder="Search a query by name">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="q in queries"
refresh="searchQueries($select.search)"
refresh-delay="0">
<div ng-bind-html="q.name | highlight: $select.search | trustAsHtml"></div>
</ui-select-choices>
</ui-select>
</div>
<div ng-show="query">
<div class="form-group">
<label for="">Choose Visualization</label>
<select ng-model="selectedVis" ng-options="vis as vis.name group by vis.type for vis in query.visualizations" class="form-control"></select>
</div>
<div ng-show="selected_query">
<div class="form-group">
<label for="">Choose Visualization</label>
<select ng-model="selectedVis" ng-options="vis as vis.name group by vis.type for vis in selected_query.visualizations" class="form-control"></select>
</div>
</div>
</div>

View File

@@ -0,0 +1,34 @@
<div class="container">
<div class="row">
<p>
<a href="/queries/new" class="btn btn-default">New Query</a>
<button ng-show="currentUser.hasPermission('create_dashboard')" type="button" class="btn btn-default" data-toggle="modal" href="#new_dashboard_dialog">New Dashboard</button>
<a href="/alerts/new" class="btn btn-default">New Alert</a>
</p>
</div>
<div class="row">
<div class="list-group col-md-6">
<div class="list-group-item active">
Recent Dashboards
</div>
<a ng-href="/dashboard/{{dashboard.slug}}" class="list-group-item" ng-repeat="dashboard in recentDashboards">
{{dashboard.name}}
</a>
</div>
<div class="list-group col-md-6">
<div class="list-group-item active">
Recent Queries
</div>
<a ng-href="/queries/{{query.id}}" class="list-group-item" ng-repeat="query in recentQueries">{{query.name}}</a>
</div>
</div>
<div ng-show="currentUser.hasPermission('admin')" class="row">
<div class="list-group">
<div class="list-group-item active">Admin</div>
<a href="/admin/status" class="list-group-item">Status</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
<div class="container">
<div class="row">
<p>
<form class="form-inline" role="form" ng-submit="search()">
<div class="form-group">
<input class="form-control" placeholder="Search..." ng-model="term" autofocus>
</div>
<button type="submit" class="btn btn-primary">
<span class="glyphicon glyphicon-search"></span>
</button>
</form>
</p>
<smart-table rows="queries" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>
</div>
</div>

View File

@@ -1,6 +1,7 @@
<div class="container">
<p class="alert alert-warning" ng-if="query.is_archived">This query is archived and can't be used in dashboards, and won't appear in search results.</p>
<alert-unsaved-changes ng-if="canEdit" is-dirty="isDirty"></alert-unsaved-changes>
<div class="row">
@@ -12,7 +13,14 @@
</h2>
<p>
<em>
<edit-in-place editable="isQueryOwner" done="saveDescription" editor="textarea" placeholder="No description" ignore-blanks='false' value="query.description"></edit-in-place>
<edit-in-place editable="isQueryOwner"
done="saveDescription"
editor="textarea"
placeholder="No description"
ignore-blanks='false'
value="query.description"
markdown="true">
</edit-in-place>
</em>
</p>
</div>
@@ -51,9 +59,9 @@
<hr>
<div class="row">
<div class="col-lg-12">
<div ng-show="sourceMode">
<div class="row" ng-if="sourceMode">
<div ng-class="editorSize">
<div>
<p>
<button type="button" class="btn btn-primary btn-xs" ng-disabled="queryExecuting" ng-click="executeQuery()">
<span class="glyphicon glyphicon-play"></span> Execute
@@ -69,21 +77,43 @@
</button>
</span>
</p>
</div>
<!-- code editor -->
<div ng-show="sourceMode">
<p>
<query-editor query="query" lock="queryExecuting"></query-editor>
<query-editor query="query" schema="schema" syntax="dataSource.syntax" lock="queryFormatting"></query-editor>
</p>
<hr>
</div>
</div>
<div class="col-md-3 schema-container" ng-show="hasSchema">
<div ng-show="schema.length < 200">
<input type="text" placeholder="Search schema..." class="form-control" ng-model="schemaFilter">
</div>
<div class="schema-browser">
<div ng-repeat="table in schema | filter:schemaFilter track by table.name">
<div class="table-name" ng-click="table.collapsed = !table.collapsed">
<i class="fa fa-table"></i> <strong><span title="{{table.name}}">{{table.name}}</span></strong>
</div>
<div collapse="table.collapsed && !schemaFilter">
<div ng-repeat="column in table.columns track by column" style="padding-left:16px;">{{column}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<hr ng-if="sourceMode">
<div class="row">
<div class="col-lg-3 rd-hidden-xs">
<p>
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Created By </span>
<strong>{{query.user.name}}</strong>
</p>
<p ng-if="query.last_modified_by && query.user.id != query.last_modified_by.id">
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Last Modified By </span>
<strong>{{query.last_modified_by.name}}</strong>
</p>
<p>
<span class="glyphicon glyphicon-time"></span>
<span class="text-muted">Last update </span>
@@ -91,12 +121,6 @@
<rd-time-ago value="queryResult.query_result.retrieved_at"></rd-time-ago>
</strong>
</p>
<p>
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Created By </span>
<strong ng-hide="isQueryOwner">{{query.user.name}}</strong>
<strong ng-show="isQueryOwner">You</strong>
</p>
<p>
<span class="glyphicon glyphicon-play"></span>
<span class="text-muted">Runtime </span>
@@ -109,12 +133,12 @@
</p>
<p>
<span class="glyphicon glyphicon-refresh"></span>
<span class="text-muted">Refresh Interval</span>
<query-refresh-select></query-refresh-select>
<span class="text-muted">Refresh Schedule</span>
<a href="" ng-click="openScheduleForm()">{{query.schedule | scheduleHumanize}}</a>
</p>
<p>
<span class="glyphicon glyphicon-hdd"></span>
<i class="fa fa-database"></i>
<span class="text-muted">Data Source</span>
<select ng-disabled="!isQueryOwner" ng-model="query.data_source_id" ng-change="updateDataSource()" ng-options="ds.id as ds.name for ds in dataSources"></select>
</p>
@@ -126,6 +150,33 @@
<span class="glyphicon glyphicon-cloud-download"></span>
<span class="rd-hidden-xs">Download Dataset</span>
</a>
<a class="btn btn-warning btn-sm" ng-disabled="queryExecuting" data-toggle="modal" data-target="#archive-confirmation-modal"
ng-show="!query.is_archived && query.id != undefined && (isQueryOwner || currentUser.hasPermission('admin'))">
<i class="fa fa-archive" title="Archive Query"></i>
</a>
<button class="btn btn-default btn-sm" ng-show="query.id != undefined" ng-click="showApiKey()">
<i class="fa fa-key" title="Show API Key"></i>
</button>
<div class="modal fade" id="archive-confirmation-modal" tabindex="-1" role="dialog" aria-labelledby="archiveConfirmationModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Query Archive</h4>
</div>
<div class="modal-body">
Are you sure you want to archive this query? <br/>
All dashboard widgets created with its visualizations will be deleted.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">No</button>
<button type="button" class="btn btn-primary" ng-click="archiveQuery()">Yes, archive.</button>
</div>
</div>
</div>
</div>
</p>
</div>
@@ -141,6 +192,16 @@
</div>
<div class="alert alert-danger" ng-show="queryResult.getError()">Error running query: <strong>{{queryResult.getError()}}</strong></div>
<div class="row log-container" ng-show="showLog">
<span ng-show="showLog">Log Information:</span>
<table>
<tbody>
<tr ng-repeat="l in queryResult.getLog()">
<td>{{l}}</td>
</tr>
</tbody>
</table>
</div>
<!-- tabs and data -->
<div ng-show="showDataset">
<div class="row">
@@ -151,7 +212,7 @@
<rd-tab tab-id="{{vis.id}}" name="{{vis.name}}" ng-if="vis.type!='TABLE'" ng-repeat="vis in query.visualizations">
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-show="canEdit"> &times;</span>
</rd-tab>
<rd-tab tab-id="add" name="&plus; New" removeable="true" ng-show="canEdit"></rd-tab>
<rd-tab tab-id="add" name="&plus; New Visualization" removeable="true" ng-show="canEdit"></rd-tab>
<li ng-if="!sourceMode" class="rd-tab-btn"><button class="btn btn-sm btn-default" ng-click="executeQuery()" ng-disabled="queryExecuting" title="Refresh Dataset"><span class="glyphicon glyphicon-refresh"></span></button></li>
</ul>
</div>

View File

@@ -0,0 +1,18 @@
<div class="modal-header">
<button type="button" class="close" aria-label="Close" ng-click="close()"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Refresh Schedule</h4>
</div>
<div class="modal-body">
<div class="radio">
<label>
<input type="radio" value="periodic" ng-model="refreshType">
<query-refresh-select ng-disabled="refreshType != 'periodic'"></query-refresh-select>
</label>
</div>
<div class="radio">
<label>
<input type="radio" value="daily" ng-model="refreshType">
<query-time-picker ng-disabled="refreshType != 'daily'"></query-time-picker>
</label>
</div>
</div>

View File

@@ -0,0 +1,16 @@
<div class="container">
<ol class="breadcrumb">
<li class="active">Users</li>
</ol>
<div class="row">
<div class="col-md-12">
<p ng-if="currentUser.hasPermission('admin')">
<a href="/users/new" class="btn btn-default"><i class="fa fa-plus"></i> New User</a>
</p>
<smart-table rows="users" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>
</div>
</div>
</div>

View File

@@ -0,0 +1,32 @@
<div class="container">
<ol class="breadcrumb">
<li><a href="/users">Users</a></li>
<li class="active">New</li>
</ol>
<form class="form" name="userForm" ng-submit="saveUser()" novalidate>
<div class="form-group required" show-errors>
<label class="control-label">Name</label>
<input type="text" name="name" class="form-control" ng-model="user.name" required/>
<input-errors name="Name" errors="userForm.name.$error"/>
</div>
<div class="form-group required" show-errors>
<label class="control-label">Email</label>
<input name="email" type="email" class="form-control" ng-model="user.email" required/>
<input-errors name="Email" errors="userForm.email.$error"/>
</div>
<div class="form-group required" show-errors>
<label class="control-label">Password</label>
<input class="form-control" type="password" name="password" ng-model="user.password" ng-minlength="6" required/>
<input-errors name="Password" errors="userForm.password.$error"/>
</div>
<div class="form-group required" show-errors>
<label class="control-label">Repeat Password</label>
<input class="form-control" type="password" name="passwordRepeat" ng-model="user.passwordRepeat" compare-to="user.password"/>
<span class="help-block error" ng-if="userForm.passwordRepeat.$error.compareTo">Passwords don't match.</span>
</div>
<div class="form-group">
<button class="btn btn-primary">Save</button>
</div>
</form>
</div>

Some files were not shown because too many files have changed in this diff Show More