Compare commits

..

3 Commits

Author SHA1 Message Date
Arik Fraimovich
150fc6dbf0 Fix repo name 2015-04-06 12:39:30 +03:00
Arik Fraimovich
ccff9614d4 New release process 2015-04-06 12:17:47 +03:00
Arik Fraimovich
a7b881874f Remove unused script 2015-04-06 09:38:01 +03:00
199 changed files with 2567 additions and 11209 deletions

View File

@@ -1,4 +0,0 @@
rd_ui/.tmp/
rd_ui/node_modules/
.git/
.vagrant/

View File

@@ -1,6 +1,9 @@
REDASH_STATIC_ASSETS_PATH="../rd_ui/app/"
REDASH_LOG_LEVEL="INFO"
REDASH_REDIS_URL=redis://localhost:6379/1
REDASH_DATABASE_URL="postgresql://redash"
REDASH_COOKIE_SECRET=veryverysecret
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"

4
.gitignore vendored
View File

@@ -8,7 +8,6 @@ celerybeat-schedule*
.#*
\#*#
*~
_build
# Vagrant related
.vagrant
@@ -19,6 +18,3 @@ redash/dump.rdb
venv
dump.rdb
# Docker related
docker-compose.yml

View File

@@ -1,41 +0,0 @@
FROM ubuntu:trusty
MAINTAINER Di Wu <diwu@yelp.com>
# Ubuntu packages
RUN apt-get update && \
apt-get install -y python-pip python-dev curl build-essential pwgen libffi-dev sudo git-core wget \
# Postgres client
libpq-dev \
# Additional packages required for data sources:
libssl-dev libmysqlclient-dev
# Users creation
RUN useradd --system --comment " " --create-home redash
# Pip requirements for all data source types
RUN pip install -U setuptools && \
pip install supervisor==3.1.2
COPY . /opt/redash/current
# Setting working directory
WORKDIR /opt/redash/current
# Install project specific dependencies
RUN pip install -r requirements_all_ds.txt && \
pip install -r requirements.txt
# Setup supervisord
RUN mkdir -p /opt/redash/supervisord && \
mkdir -p /opt/redash/logs && \
cp /opt/redash/current/setup/docker/supervisord/supervisord.conf /opt/redash/supervisord/supervisord.conf
# Fix permissions
RUN chown -R redash /opt/redash
# Expose ports
EXPOSE 5000
EXPOSE 9001
# Startup script
CMD ["supervisord", "-c", "/opt/redash/supervisord/supervisord.conf"]

View File

@@ -13,11 +13,11 @@ deps:
pack:
sed -ri "s/^__version__ = '([0-9.]*)'/__version__ = '$(FULL_VERSION)'/" redash/__init__.py
tar -zcv -f $(FILENAME) --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="rd_ui/node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" *
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" *
upload:
python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME)
test:
nosetests --with-coverage --cover-package=redash tests/
nosetests --with-coverage --cover-package=redash tests/*.py
#cd rd_ui && grunt test

View File

@@ -1,16 +1,9 @@
Some of you read the news about EverythingMe closing down. While more detailed announcement will come later (once more details are clear), **I just wanted to reassure you that you shouldn't worry -- this won't affect the future of re:dash.** I will keep maintaining re:dash, and might even be able to work more on it.
If you still have concerns, you're welcome to reach out to me directly -- arik@arikfr.com.
Arik.
---
<p align="center">
<img title="re:dash" src='http://redash.io/static/old_img/redash_logo.png' width="200px"/>
<img title="re:dash" src='https://raw.githubusercontent.com/EverythingMe/redash/screenshots/redash_logo.png' />
</p>
<p align="center">
<img title="Build Status" src='https://circleci.com/gh/getredash/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
<img title="Build Status" src='https://circleci.com/gh/EverythingMe/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
</p>
**_re:dash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
@@ -18,8 +11,7 @@ Arik.
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,
Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
Today **_re:dash_** has support for querying multiple databases, including: Redshift, Google BigQuery, PostgreSQL, MySQL, Graphite and custom scripts.
**_re:dash_** consists of two parts:
@@ -30,27 +22,31 @@ Presto, Google Spreadsheets, Cloudera Impala, Hive and custom scripts.
## Demo
![Screenshots](https://raw.github.com/getredash/redash/screenshots/screenshots.gif)
![Screenshots](https://raw.github.com/EverythingMe/redash/screenshots/screenshots.gif)
You can try out the demo instance: http://demo.redash.io/ (login with any Google account).
## Getting Started
* [Setting up re:dash instance](http://redash.io/deployment/setup.html) (includes links to ready made AWS/GCE images).
* [Documentation](http://docs.redash.io).
* [Setting up re:dash instance](https://github.com/EverythingMe/redash/wiki/Setting-up-re:dash-instance) (includes links to ready made AWS/GCE images).
* Additional documentation in the [Wiki](https://github.com/everythingme/redash/wiki).
## Getting help
* [Google Group (mailing list)](https://groups.google.com/forum/#!forum/redash-users): the best place to get updates about new releases or ask general questions.
* Find us [on gitter](https://gitter.im/getredash/redash#) (chat).
* Contact Arik, the maintainer directly: arik@redash.io.
* Find us [on gitter](https://gitter.im/EverythingMe/redash#) (chat).
* Contact Arik, the maintainer directly: arik@everything.me.
## Roadmap
TBD.
## Reporting Bugs and Contributing Code
* Want to report a bug or request a feature? Please open [an issue](https://github.com/getredash/redash/issues/new).
* Want to report a bug or request a feature? Please open [an issue](https://github.com/everythingme/redash/issues/new).
* Want to help us build **_re:dash_**? Fork the project and make a pull request. We need all the help we can get!
## License
See [LICENSE](https://github.com/getredash/redash/blob/master/LICENSE) file.
See [LICENSE](https://github.com/EverythingMe/redash/blob/master/LICENSE) file.

View File

@@ -7,7 +7,7 @@ import requests
github_token = os.environ['GITHUB_TOKEN']
auth = (github_token, 'x-oauth-basic')
repo = 'getredash/redash'
repo = 'EverythingMe/redash'
def _github_request(method, path, params=None, headers={}):
if not path.startswith('https://api.github.com'):
@@ -56,7 +56,7 @@ def create_release(version, commit_sha):
return response.json()
def upload_asset(release, filepath):
upload_url = release['upload_url'].replace('{?name,label}', '')
upload_url = release['upload_url'].replace('{?name}', '')
filename = filepath.split('/')[-1]
with open(filepath) as file_content:
@@ -104,26 +104,9 @@ def get_changelog(commit_sha):
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)
release = get_rc_release(version) or create_release(version, commit_sha)
print "Using release id: {}".format(release['id'])
remove_previous_builds(release)
@@ -132,8 +115,8 @@ def update_release(version, build_filepath, commit_sha):
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)
print response.status_code
print response.text
except Exception, ex:
print ex

View File

@@ -0,0 +1,63 @@
"""
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,16 +1,14 @@
machine:
services:
- docker
node:
version:
0.12.4
0.10.24
python:
version:
2.7.3
dependencies:
pre:
- make deps
- pip install -r requirements_dev.txt
- pip install -r dev_requirements.txt
- pip install -r requirements.txt
cache_directories:
- rd_ui/node_modules/
@@ -21,17 +19,14 @@ test:
post:
- make pack
deployment:
test:
branch: chore/release_process
commands:
- make upload
github:
branch: master
commands:
- make upload
docker:
branch: [master, docker]
commands:
- echo "rd_ui/app" >> .dockerignore
- docker build -t redash/redash:$(./manage.py version | sed -e "s/\+/./") .
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- docker push redash/redash:$(./manage.py version | sed -e "s/\+/./")
notify:
webhooks:
- url: https://webhooks.gitter.im/e/895d09c3165a0913ac2f

View File

@@ -1,28 +0,0 @@
redash:
image: redash
ports:
- "5000:5000"
links:
- redis
- postgres
environment:
REDASH_STATIC_ASSETS_PATH:"../rd_ui/app/"
REDASH_LOG_LEVEL:"INFO"
REDASH_REDIS_URL:redis://localhost:6379/0
REDASH_DATABASE_URL:"postgresql://redash"
REDASH_COOKIE_SECRET:veryverysecret
REDASH_GOOGLE_APPS_DOMAIN:
redis:
image: redis:2.8
postgres:
image: postgres:9.3
volumes:
- /opt/postgres-data:/var/lib/postgresql/data
redash-nginx:
image: redash-nginx:1.0
ports:
- "80:80"
volumes:
- "../redash-nginx/nginx.conf:/etc/nginx/nginx.conf"
links:
- redash

View File

@@ -1,192 +0,0 @@
# 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."

View File

@@ -1,111 +0,0 @@
# -*- 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'),
]

View File

@@ -1,218 +0,0 @@
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.
Notes:
- For security, the python query runner is disabled by default.
To enable, add ``redash.query_runner.python`` to the ``REDASH_ADDITIONAL_QUERY_RUNNERS`` environmental variable. If you used
the bootstrap script, or one of the provided images, add to ``/opt/redash/.env`` file the line: ``export REDASH_ADDITIONAL_QUERY_RUNNERS=redash.query_runner.python``.
Vertica
-----
- **Options**:
- Database (mandatory)
- User
- Password
- Host
- Port
- **Additional requirements**:
- ``vertica-python`` python package
Oracle
------
- **Options**
- DSN Service name
- User
- Password
- Host
- Port
- **Additional requirements**
- ``cx_Oracle`` python package. This requires the installation of the Oracle `instant client <http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html>`__.

View File

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

View File

@@ -1,94 +0,0 @@
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/getredash/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/getredash/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

@@ -1,30 +0,0 @@
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
},
...
]
}

View File

@@ -1,38 +0,0 @@
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/getredash/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
::
export PYTHONPATH=. && find migrations/ -type f | grep 00 --null | xargs -I file bin/run python file
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.

View File

@@ -1,57 +0,0 @@
.. image:: http://redash.io/static/old_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/getredash/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/getredash/redash
* Issues: https://github.com/getredash/redash/issues
* Mailing List: https://groups.google.com/forum/#!forum/redash-users
* Gitter (chat): https://gitter.im/getredash/redash
* Contact Arik, the maintainer directly: arik@redash.io.
TOC
###
.. toctree::
:maxdepth: 2
setup
upgrade
datasources
usage
dev
misc

View File

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

View File

@@ -1,50 +0,0 @@
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.

View File

@@ -1,59 +0,0 @@
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;
}
}

View File

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

View File

@@ -1,170 +0,0 @@
Setting up re:dash instance
###########################
The `provisioning
script <https://github.com/getredash/redash/blob/master/setup/ubuntu/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
==================
AWS
---
Launch the instance with from the pre-baked AMI (for small deployments
t2.micro should be enough):
- us-east-1: `ami-752c7f10 <https://console.aws.amazon.com/ec2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-752c7f10>`__
- us-west-1: `ami-b36babf7 <https://console.aws.amazon.com/ec2/home?region=us-west-1#LaunchInstanceWizard:ami=ami-b36babf7>`__
- us-west-2: `ami-a0a04393 <https://console.aws.amazon.com/ec2/home?region=us-west-2#LaunchInstanceWizard:ami=ami-a0a04393>`__
- eu-west-1: `ami-198cb16e <https://console.aws.amazon.com/ec2/home?region=eu-west-1#LaunchInstanceWizard:ami=ami-198cb16e>`__
- eu-central-1: `ami-a81418b5 <https://console.aws.amazon.com/ec2/home?region=eu-central-1#LaunchInstanceWizard:ami=ami-a81418b5>`__
- sa-east-1: `ami-2b52c336 <https://console.aws.amazon.com/ec2/home?region=sa-east-1#LaunchInstanceWizard:ami=ami-2b52c336>`__
- ap-northeast-1: `ami-4898fb48 <https://console.aws.amazon.com/ec2/home?region=ap-northeast-1#LaunchInstanceWizard:ami=ami-4898fb48>`__
- ap-southeast-2: `ami-7559134f <https://console.aws.amazon.com/ec2/home?region=ap-southeast-2#LaunchInstanceWizard:ami=ami-7559134f>`__
- ap-southeast-1: `ami-a0786bf2 <https://console.aws.amazon.com/ec2/home?region=ap-southeast-1#LaunchInstanceWizard:ami=ami-a0786bf2>`__
Now proceed to `"Setup" <#setup>`__.
Google Compute Engine
---------------------
First, you need to add the images to your account:
.. code:: bash
$ gcloud compute images create "redash-081-b1110" --source-uri gs://redash-images/redash.0.8.1.b1110.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-081-b1110-bq" --source-uri gs://redash-images/redash.0.8.1.b1110-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-081-b1110-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>`__.
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.
Mail Configuration
------------------
For the system to be able to send emails (for example when alerts trigger), you need to set the mail server to use and the
host name of your re:dash server. If you're using one of our images, you can do this by editing the `.env` file:
.. code::
# Note that not all values are required, as they have default values.
export REDASH_MAIL_SERVER="" # default: localhost
export REDASH_MAIL_PORT="" # default: 25
export REDASH_MAIL_USE_TLS="" # default: False
export REDASH_MAIL_USE_SSL="" # default: False
export REDASH_MAIL_USERNAME="" # default: None
export REDASH_MAIL_PASSWORD="" # default: None
export REDASH_MAIL_DEFAULT_SENDER="" # Email address to send from
export REDASH_HOST="" # base address of your re:dash instance, for example: "https://demo.redash.io"
- Note that not all values are required, as there are default values.
- It's recommended to use some mail service, like `Amazon SES <https://aws.amazon.com/ses/>`__, `Mailgun <http://www.mailgun.com/>`__
or `Mandrill <http://mandrillapp.com>`__ to send emails to ensure deliverability.
To test email configuration, you can run `bin/run ./manage.py send_test_mail` (from `/opt/redash/current`).
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>`).

View File

@@ -1,36 +0,0 @@
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/getredash/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.

View File

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

View File

@@ -1,72 +0,0 @@
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"
}
Simple query on a ElasticSearch instance:
==================================================
- Query the index named "twitter"
- Filter by user equal "kimchy"
- Return the fields: "@timestamp", "tweet" and "user"
- Return up to 15 results
- Sort by @timestamp ascending
.. code:: json
{
"index" : "twitter",
"query" : {
"match": {
"user" : "kimchy"
}
},
"fields" : ["@timestamp", "tweet", "user"],
"size" : 15,
"sort" : "@timestamp:asc"
}

View File

@@ -1,70 +0,0 @@
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 flushall``.
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
==
Backup re:dash's DB:
--------------------
``sudo -u redash pg_dump > backup_filename.sql``
Version
=======
See current version:
``bin/run ./manage.py version``

View File

@@ -1,74 +0,0 @@
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

View File

@@ -2,15 +2,12 @@
"""
CLI to manage redash.
"""
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)
manager.add_command("database", database.manager)
@@ -24,9 +21,6 @@ def version():
"""Displays re:dash version."""
print __version__
@manager.command
def status():
print json.dumps(get_status(), indent=2)
@manager.command
def runworkers():
@@ -43,15 +37,12 @@ def make_shell_context():
@manager.command
def check_settings():
"""Show the settings as re:dash sees them (useful for debugging)."""
for name, item in settings.all_settings().iteritems():
print "{} = {}".format(name, item)
from types import ModuleType
@manager.command
def send_test_mail():
from redash import mail
from flask_mail import Message
mail.send(Message(subject="Test Message from re:dash", recipients=[settings.MAIL_DEFAULT_SENDER], body="Test message."))
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)
if __name__ == '__main__':

View File

@@ -1,20 +0,0 @@
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

@@ -1,27 +0,0 @@
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

@@ -1,18 +0,0 @@
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

@@ -1,8 +0,0 @@
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

@@ -1,44 +0,0 @@
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

@@ -1,6 +0,0 @@
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

@@ -1,24 +0,0 @@
import json
from redash import models
if __name__ == '__main__':
for vis in models.Visualization.select():
if vis.type == 'COUNTER':
options = json.loads(vis.options)
print "Before: ", options
if 'rowNumber' in options and options['rowNumber'] is not None:
options['rowNumber'] += 1
else:
options['rowNumber'] = 1
if 'counterColName' not in options:
options['counterColName'] = 'counter'
if 'targetColName' not in options:
options['targetColName'] = 'target'
options['targetRowNumber'] = options['rowNumber']
print "After: ", options
vis.options = json.dumps(options)
vis.save()

View File

@@ -1,10 +0,0 @@
__author__ = 'lior'
from redash.models import DataSource
if __name__ == '__main__':
for ds in DataSource.all():
if ds.type == 'elasticsearch':
ds.type = 'kibana'
ds.save()

View File

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

View File

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

View File

@@ -236,6 +236,17 @@ module.exports = function (grunt) {
// dist: {}
// },
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.{png,jpg,jpeg,gif}',
dest: '<%= yeoman.dist %>/images'
}]
}
},
svgmin: {
dist: {
files: [{
@@ -302,11 +313,6 @@ module.exports = function (grunt) {
'images/{,*/}*.{webp}',
'fonts/*'
]
}, {
expand: true,
cwd: '<%= yeoman.app %>/images',
dest: '<%= yeoman.dist %>/images',
src: ['*']
}, {
expand: true,
cwd: '.tmp/images',
@@ -342,6 +348,7 @@ module.exports = function (grunt) {
],
dist: [
'copy:styles',
'imagemin',
'svgmin'
]
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -14,18 +14,13 @@
<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>
@@ -39,13 +34,14 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><img src="/images/redash_icon_small.png"/></a>
<a class="navbar-brand" href="/"><strong>{{name}}</strong></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')" dropdown>
<a href="#" class="dropdown-toggle" dropdown-toggle><span class="fa fa-tachometer"></span> <b class="caret"></b></a>
<a href="#" class="dropdown-toggle" dropdown-toggle><span class="glyphicon glyphicon-th-large"></span> <b class="caret"></b></a>
<ul class="dropdown-menu" dropdown-menu>
<span ng-repeat="(name, group) in groupedDashboards">
<li class="dropdown-submenu">
@@ -71,9 +67,6 @@
<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">
@@ -82,34 +75,12 @@
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-search"></span></button>
</form>
<ul class="nav navbar-nav navbar-right">
<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 ng-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" target="_self">Log out</a>
</li>
</ul>
</li>
<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>
</ul>
</div>
{% endraw %}
@@ -120,27 +91,10 @@
<edit-dashboard-form dashboard="newDashboard" id="new_dashboard_dialog"></edit-dashboard-form>
<div ng-view></div>
{% raw %}
<div class="container-fluid footer">
<hr/>
<div class="container">
<div class="row">
<a href="http://redash.io">re:dash</a> <span ng-bind="version"></span>
<small ng-if="newVersionAvailable" ng-cloak class="ng-cloak"><a href="http://version.redash.io/">(new re:dash version available)</a></small>
<div class="pull-right">
<a href="http://docs.redash.io/">Docs</a>
<a href="http://github.com/getredash/redash">Contribute</a>
</div>
</div>
</div>
</div>
{% endraw %}
<script src="/bower_components/jquery/jquery.js"></script>
<!-- build:js /scripts/plugins.js -->
<script src="/bower_components/angular/angular.js"></script>
<script src="/bower_components/angular-sanitize/angular-sanitize.js"></script>
<script src="/bower_components/jquery-ui/ui/jquery-ui.js"></script>
<script src="/bower_components/bootstrap/js/collapse.js"></script>
<script src="/bower_components/bootstrap/js/modal.js"></script>
@@ -155,7 +109,6 @@
<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/highcharts/highcharts.js"></script>
<script src="/bower_components/highcharts/modules/exporting.js"></script>
@@ -165,23 +118,20 @@
<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="/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/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>
<script src="/bower_components/d3/d3.min.js"></script>
<script src="/bower_components/angular-ui-sortable/sortable.js"></script>
<script src="/bower_components/canvg/canvg.js"></script>
<!-- endbuild -->
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
@@ -193,41 +143,33 @@
<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/boxplot.js"></script>
<script src="/scripts/visualizations/box.js"></script>
<script src="/scripts/visualizations/table.js"></script>
<script src="/scripts/visualizations/pivot.js"></script>
<script src="/scripts/visualizations/date_range_selector.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>
// TODO: move currentUser & features to be an Angular service
var clientConfig = {{ client_config|safe }};
var featureFlags = {{ features|safe }};
var currentUser = {{ user|safe }};
currentUser.canEdit = function(object) {
var user_id = object.user_id || (object.user && object.user.id);
return this.hasPermission('admin') || (user_id && (user_id == currentUser.id));
return user_id && (user_id == currentUser.id);
};
currentUser.hasPermission = function(permission) {
return this.permissions.indexOf(permission) != -1;
};
}
{{ analytics|safe }}
</script>

View File

@@ -13,10 +13,6 @@
<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>
@@ -30,20 +26,13 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><img src="/images/redash_icon_small.png"/></a>
<a class="navbar-brand" href="/"><strong>{{name}}</strong></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 %}
@@ -59,23 +48,10 @@
{% 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="inputEmail">Email</label>
<input type="text" class="form-control" id="inputEmail" name="email" value="{{email}}">
<label for="inputUsernameEmail">Username or email</label>
<input type="text" class="form-control" id="inputUsernameEmail" name="username" value="{{username}}">
</div>
<div class="form-group">
<!--<a class="pull-right" href="#">Forgot password?</a>-->

View File

@@ -7,20 +7,17 @@ angular.module('redash', [
'redash.renderers',
'redash.visualization',
'highchart',
'ui.select2',
'angular-growl',
'angularMoment',
'ui.bootstrap',
'ui.sortable',
'smartTable.table',
'ngResource',
'ngRoute',
'ui.select',
'naif.base64',
'ui.bootstrap.showErrors',
'ngSanitize'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider', 'uiSelectConfig',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider, uiSelectConfig) {
if (clientConfig.clientSideMetrics) {
'ui.select'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider) {
if (featureFlags.clientSideMetrics) {
Bucky.setOptions({
host: '/api/metrics'
});
@@ -34,8 +31,6 @@ angular.module('redash', [
return query.$promise;
};
uiSelectConfig.theme = "bootstrap";
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);
@@ -85,44 +80,15 @@ angular.module('redash', [
templateUrl: '/views/admin_status.html',
controller: 'AdminStatusCtrl'
});
$routeProvider.when('/alerts', {
templateUrl: '/views/alerts/list.html',
controller: 'AlertsCtrl'
});
$routeProvider.when('/alerts/:alertId', {
templateUrl: '/views/alerts/edit.html',
controller: 'AlertCtrl'
});
$routeProvider.when('/data_sources/:dataSourceId', {
templateUrl: '/views/data_sources/edit.html',
controller: 'DataSourceCtrl'
});
$routeProvider.when('/data_sources', {
templateUrl: '/views/data_sources/list.html',
controller: 'DataSourcesCtrl'
});
$routeProvider.when('/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('/admin/workers', {
templateUrl: '/views/admin_workers.html',
controller: 'AdminWorkersCtrl'
});
$routeProvider.when('/', {
templateUrl: '/views/personal.html',
controller: 'PersonalIndexCtrl'
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
});
$routeProvider.when('/personal', {
templateUrl: '/views/personal.html',
controller: 'PersonalIndexCtrl'

View File

@@ -17,7 +17,7 @@
};
refresh();
};
}
angular.module('redash.admin_controllers', [])
.controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl])

View File

@@ -1,174 +0,0 @@
(function() {
var AlertsCtrl = function($scope, Events, Alert) {
Events.record(currentUser, "view", "page", "alerts");
$scope.$parent.pageTitle = "Alerts";
$scope.alerts = []
Alert.query(function(alerts) {
var stateClass = {
'ok': 'label label-success',
'triggered': 'label label-danger',
'unknown': 'label label-warning'
};
_.each(alerts, function(alert) {
alert.class = stateClass[alert.state];
})
$scope.alerts = alerts;
});
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
};
$scope.gridColumns = [
{
"label": "Name",
"map": "name",
"cellTemplate": '<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

@@ -3,8 +3,7 @@
if (!value) {
return "-";
}
return value.format(clientConfig.dateTimeFormat);
return value.toDate().toLocaleString();
};
var QuerySearchCtrl = function($scope, $location, $filter, Events, Query) {
@@ -24,7 +23,7 @@
},
{
'label': 'Created By',
'map': 'user.name'
'map': 'user_name'
},
{
'label': 'Created At',
@@ -46,6 +45,7 @@
Query.search({q: $scope.term }, function(results) {
$scope.queries = _.map(results, function(query) {
query.created_at = moment(query.created_at);
query.user_name = query.user.name;
return query;
});
});
@@ -93,6 +93,7 @@
$scope.allQueries = _.map(queries, function (query) {
query.created_at = moment(query.created_at);
query.retrieved_at = moment(query.retrieved_at);
query.user_name = query.user.name;
return query;
});
@@ -107,7 +108,7 @@
},
{
'label': 'Created By',
'map': 'user.name'
'map': 'user_name'
},
{
'label': 'Created At',
@@ -151,16 +152,14 @@
}
var MainCtrl = function ($scope, $location, Dashboard, notifications) {
$scope.version = clientConfig.version;
$scope.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.hasPermission("admin");
if (clientConfig.clientSideMetrics) {
if (featureFlags.clientSideMetrics) {
$scope.$on('$locationChangeSuccess', function(event, newLocation, oldLocation) {
// This will be called once per actual page load.
Bucky.sendPagePerformance();
});
}
$scope.dashboards = [];
$scope.reloadDashboards = function () {
Dashboard.query(function (dashboards) {
@@ -197,6 +196,15 @@
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) {
@@ -205,6 +213,15 @@
$scope.recentQueries = Query.recent();
$scope.recentDashboards = Dashboard.recent();
$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();
});
}
}
};
angular.module('redash.controllers', [])

View File

@@ -16,9 +16,7 @@
var w = new Widget(widget);
if (w.visualization) {
var queryResult = w.getQuery().getQueryResult();
if (angular.isDefined(queryResult))
promises.push(queryResult.toPromise());
promises.push(w.getQuery().getQueryResult().toPromise());
}
return w;
@@ -96,15 +94,6 @@
}
};
$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() {
$scope.refreshEnabled = !$scope.refreshEnabled;

View File

@@ -1,47 +0,0 @@
(function () {
var DataSourcesCtrl = function ($scope, $location, growl, Events, DataSource) {
Events.record(currentUser, "view", "page", "admin/data_sources");
$scope.$parent.pageTitle = "Data Sources";
$scope.dataSources = DataSource.query();
$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

@@ -17,7 +17,7 @@
saveQuery = $scope.saveQuery;
$scope.sourceMode = true;
$scope.canEdit = currentUser.canEdit($scope.query) || clientConfig.allowAllToEditQueries;
$scope.canEdit = true;
$scope.isDirty = false;
$scope.newVisualization = undefined;

View File

@@ -15,39 +15,16 @@
maxAge = -1;
}
$scope.showLog = false;
$scope.queryResult = $scope.query.getQueryResult(maxAge, parameters);
}
var getDataSourceId = function() {
// Try to get the query's data source id
var dataSourceId = $scope.query.data_source_id;
// If there is no source yet, then parse what we have in localStorage
// e.g. `null` -> `NaN`, malformed data -> `NaN`, "1" -> 1
if (dataSourceId === undefined) {
dataSourceId = parseInt(localStorage.lastSelectedDataSourceId, 10);
}
// If we had an invalid value in localStorage (e.g. nothing, deleted source), then use the first data source
var isValidDataSourceId = !isNaN(dataSourceId) && _.some($scope.dataSources, function(ds) {
return ds.id == dataSourceId;
});
if (!isValidDataSourceId) {
dataSourceId = $scope.dataSources[0].id;
}
// Return our data source id
return dataSourceId;
}
$scope.dataSource = {};
$scope.query = $route.current.locals.query;
var updateSchema = function() {
$scope.hasSchema = false;
$scope.editorSize = "col-md-12";
DataSource.getSchema({id: getDataSourceId()}, function(data) {
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) {
@@ -69,22 +46,15 @@
$scope.isQueryOwner = (currentUser.id === $scope.query.user.id) || currentUser.hasPermission('admin');
$scope.canViewSource = currentUser.hasPermission('view_source');
$scope.canExecuteQuery = currentUser.hasPermission('execute_query');
$scope.canScheduleQuery = currentUser.hasPermission('schedule_query');
$scope.dataSources = DataSource.query(function(dataSources) {
$scope.dataSources = DataSource.get(function(dataSources) {
updateSchema();
if ($scope.query.isNew()) {
$scope.query.data_source_id = getDataSourceId();
$scope.dataSource = _.find(dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
}
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].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;
@@ -127,14 +97,6 @@
};
$scope.executeQuery = function() {
if (!$scope.canExecuteQuery) {
return;
}
if (!$scope.query.query) {
return;
}
getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
@@ -146,21 +108,21 @@
$scope.queryResult.cancelExecution();
Events.record(currentUser, 'cancel_execute', 'query', $scope.query.id);
};
$scope.archiveQuery = function(options, data) {
if (data) {
data.id = $scope.query.id;
} else {
data = $scope.query;
}
$scope.isDirty = false;
options = _.extend({}, {
successMessage: 'Query archived',
errorMessage: 'Query could not be archived'
}, options);
return Query.delete({id: data.id}, function() {
$scope.query.is_archived = true;
$scope.query.schedule = null;
@@ -174,7 +136,6 @@
$scope.updateDataSource = function() {
Events.record(currentUser, 'update_data_source', 'query', $scope.query.id);
localStorage.lastSelectedDataSourceId = $scope.query.data_source_id;
$scope.query.latest_query_data = null;
$scope.query.latest_query_data_id = null;
@@ -188,7 +149,6 @@
}
updateSchema();
$scope.dataSource = _.find($scope.dataSources, function(ds) { return ds.id == $scope.query.data_source_id; });
$scope.executeQuery();
};
@@ -234,14 +194,10 @@
if (status === 'done' || status === 'failed') {
$scope.lockButton(false);
}
if ($scope.queryResult.getLog() != null) {
$scope.showLog = true;
}
});
$scope.openScheduleForm = function() {
if (!$scope.isQueryOwner || !$scope.canScheduleQuery) {
if (!$scope.isQueryOwner) {
return;
};

View File

@@ -1,160 +0,0 @@
(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

@@ -142,11 +142,6 @@
$scope.setType = function (type) {
$scope.type = type;
if (type == 'textbox') {
$scope.widgetSizes.push({name: 'Hidden', value: 0});
} else if ($scope.widgetSizes.length > 2) {
$scope.widgetSizes.pop();
}
};
var reset = function() {
@@ -191,6 +186,7 @@
$scope.saveWidget = function() {
$scope.saveInProgress = true;
var widget = new Widget({
'visualization_id': $scope.selectedVis && $scope.selectedVis.id,
'dashboard_id': $scope.dashboard.id,

View File

@@ -1,80 +0,0 @@
(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';
}
if (prop.type == 'boolean') {
prop.type = 'checkbox';
}
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

@@ -247,68 +247,4 @@
};
}]
);
directives.directive('compareTo', function () {
return {
require: "ngModel",
scope: {
otherModelValue: "=compareTo"
},
link: function (scope, element, attributes, ngModel) {
var validate = function(value) {
ngModel.$setValidity("compareTo", value === scope.otherModelValue);
};
scope.$watch("otherModelValue", function() {
validate(ngModel.$modelValue);
});
ngModel.$parsers.push(function(value) {
validate(value);
return value;
});
}
};
});
directives.directive('inputErrors', function () {
return {
restrict: "E",
templateUrl: "/views/directives/input_errors.html",
replace: true,
scope: {
errors: "="
}
};
});
directives.directive('onDestroy', function () {
/* This directive can be used to invoke a callback when an element is destroyed,
A useful example is the following:
<div ng-if="includeText" on-destroy="form.text = null;">
<input type="text" ng-model="form.text">
</div>
*/
return {
restrict: "A",
scope: {
onDestroy: "&",
},
link: function(scope, elem, attrs) {
console.log(scope.onDestroy);
scope.$on('$destroy', function() {
scope.onDestroy();
});
}
};
});
directives.directive('colorBox', function () {
return {
restrict: "E",
scope: {color: "="},
template: "<span style='width: 12px; height: 12px; background-color: {{color}}; display: inline-block; margin-right: 5px;'></span>"
};
});
})();

View File

@@ -8,7 +8,7 @@
'query': '=',
'visualization': '=?'
},
template: '<small><span class="glyphicon glyphicon-link"></span></small> <a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
template: '<a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
link: function(scope, element) {
scope.link = '/queries/' + scope.query.id;
if (scope.visualization) {
@@ -64,23 +64,14 @@
scope: {
'query': '=',
'lock': '=',
'schema': '=',
'syntax': '='
'schema': '='
},
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],
mode: 'text/x-sql',
lineWrapping: true,
lineNumbers: true,
readOnly: false,
@@ -117,8 +108,6 @@
$scope.query.query = newValue;
});
}
$('.schema-container').css('height', $('.CodeMirror').css('height'));
});
$scope.$watch('query.query', function () {
@@ -139,12 +128,6 @@
additionalHints = _.unique(keywords);
}
codemirror.refresh();
});
$scope.$watch('syntax', function(syntax) {
codemirror.setOption('mode', modes[syntax]);
});
$scope.$watch('lock', function (locked) {
@@ -241,14 +224,7 @@
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({

View File

@@ -48,9 +48,6 @@ angular.module('redash.filters', []).
.filter('colWidth', function () {
return function (widgetWidth) {
if (widgetWidth == 0) {
return 0;
}
if (widgetWidth == 1) {
return 6;
}
@@ -80,13 +77,7 @@ angular.module('redash.filters', []).
if (!text) {
return "";
}
var html = marked(text);
if (clientConfig.allowScriptsInUserInput) {
html = $sce.trustAsHtml(html);
}
return html;
return $sce.trustAsHtml(marked(text));
}
}])
@@ -97,21 +88,4 @@ angular.module('redash.filters', []).
}
return $sce.trustAsHtml(text);
}
}])
.filter('remove', function() {
return function(items, item) {
if (items == undefined)
return items;
if (item instanceof Array) {
var notEquals = function(other) { return item.indexOf(other) == -1; }
} else {
var notEquals = function(other) { return item != other; }
}
var filtered = [];
for (var i = 0; i < items.length; i++)
if (notEquals(items[i]))
filtered.push(items[i])
return filtered;
};
});
}]);

View File

@@ -11,11 +11,6 @@
'Light Blue': '#92A8CD',
'Lilac': '#A47D7C',
'Light Green': '#B5CA92',
'Brown':'#A52A2A',
'Black':'#000000',
'Gray':'#808080',
'Pink':'#FFC0CB',
'Dark Blue':'#00008b'
};
Highcharts.setOptions({
@@ -55,7 +50,7 @@
;
if (moment.isMoment(this.x)) {
var s = '<b>' + this.x.format(clientConfig.dateTimeFormat) + '</b>',
var s = '<b>' + this.x.toDate().toLocaleString() + '</b>',
pointsCount = this.points.length;
$.each(this.points, function (i, point) {
@@ -97,6 +92,19 @@
buttons: {
contextButton: {
menuItems: [
{
text: 'Toggle % Stacking',
onclick: function () {
var newStacking = "normal";
if (this.series[0].options.stacking == "normal") {
newStacking = "percent";
}
_.each(this.series, function (series) {
series.update({stacking: newStacking}, true);
});
}
},
{
text: 'Select All',
onclick: function () {
@@ -137,7 +145,7 @@
if (!hasTotalsAlready) {
this.addSeries({
data: _.sortBy(_.values(data), 'x'),
data: _.values(data),
type: 'line',
name: 'Total'
}, false)
@@ -258,9 +266,6 @@
};
var chartOptions = $.extend(true, {}, defaultOptions, chartsDefaults);
chartOptions.plotOptions.series = {
turboThreshold: clientConfig.highChartsTurboThreshold
}
// $timeout makes sure that this function invoked after the DOM ready. When draw/init
// invoked after the DOM is ready, we see first an empty HighCharts objects and later
@@ -303,22 +308,21 @@
// We check either for true or undefined for backward compatibility.
var series = scope.series;
if (chartOptions['sortX'] === true || chartOptions['sortX'] === undefined) {
var seriesCopy = [];
// 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 });
_.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';
};
if (sortable) {
series = _.sortBy(series, function (s) {
return s.data[0].y
var sorted = _.extend({}, s, {data: _.sortBy(s.data, fieldName)});
seriesCopy.push(sorted);
});
series = seriesCopy;
}
if (!('xAxis' in chartOptions && 'type' in chartOptions['xAxis'])) {
@@ -355,23 +359,6 @@
});
}
}
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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,4 @@
(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'});
@@ -36,17 +12,15 @@
var columnTypes = {};
// TODO: we should stop manipulating incoming data, and switch to relaying on the column type set by the backend.
// This logic is prone to errors, and better be removed. Kept for now, for backward compatability.
_.each(this.query_result.data.rows, function (row) {
_.each(row, function (v, k) {
if (angular.isNumber(v)) {
columnTypes[k] = 'float';
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}T/)) {
row[k] = moment.utc(v);
row[k] = moment(v);
columnTypes[k] = 'datetime';
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}$/)) {
row[k] = moment.utc(v);
} 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);
@@ -56,9 +30,7 @@
_.each(this.query_result.data.columns, function(column) {
if (columnTypes[column.name]) {
if (column.type == null || column.type == 'string') {
column.type = columnTypes[column.name];
}
column.type = columnTypes[column.name];
}
});
@@ -68,7 +40,7 @@
} else {
this.status = undefined;
}
};
}
function QueryResult(props) {
this.deferred = $q.defer();
@@ -119,14 +91,6 @@
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;
}
@@ -186,38 +150,9 @@
}
return this.filteredData;
};
/**
* Helper function to add a point into a series, also checks whether the point is within dateRange
*/
QueryResult.prototype._addPointToSeriesIfInDateRange = function (point, seriesCollection, seriesName, dateRange) {
if (dateRange && moment.isMoment(point.x)) {
// if dateRange is provided and x Axis is of type datetime
if (point.x.isBefore(dateRange.min) || point.x.isAfter(dateRange.max)) {
// if the point's date isn't within dateRange, then we will not add this point to series
return;
}
}
this._addPointToSeries(point, seriesCollection, seriesName);
}
/**
* Helper function to add a point into a series
*/
QueryResult.prototype._addPointToSeries = function (point, seriesCollection, seriesName) {
if (seriesCollection[seriesName] == undefined) {
seriesCollection[seriesName] = {
name: seriesName,
type: 'column',
data: []
};
}
seriesCollection[seriesName]['data'].push(point);
};
QueryResult.prototype.getChartData = function (mapping, dateRange) {
QueryResult.prototype.getChartData = function (mapping) {
var series = {};
_.each(this.getData(), function (row) {
@@ -227,8 +162,8 @@
var yValues = {};
_.each(row, function (value, definition) {
var name = definition.split("::")[0] || definition.split("__")[0];
var type = definition.split("::")[1] || definition.split("__")[1];
var name = definition.split("::")[0];
var type = definition.split("::")[1];
if (mapping) {
type = mapping[definition];
}
@@ -253,20 +188,31 @@
seriesName = String(value);
}
if (type == 'multiFilter' || type == 'multi-filter') {
if (type == 'multi-filter') {
seriesName = String(value);
}
});
var addPointToSeries = function (seriesName, point) {
if (series[seriesName] == undefined) {
series[seriesName] = {
name: seriesName,
type: 'column',
data: []
}
}
series[seriesName]['data'].push(point);
}
if (seriesName === undefined) {
_.each(yValues, function (yValue, seriesName) {
this._addPointToSeriesIfInDateRange({'x': xValue, 'y': yValue}, series, seriesName, dateRange);
}.bind(this));
addPointToSeries(seriesName, {'x': xValue, 'y': yValue});
});
} else {
addPointToSeries(seriesName, point);
}
else {
this._addPointToSeriesIfInDateRange(point, series, seriesName, dateRange);
}
}.bind(this));
});
return _.values(series);
};
@@ -290,16 +236,7 @@
}
QueryResult.prototype.getColumnNameWithoutType = function (column) {
var typeSplit;
if (column.indexOf("::") != -1) {
typeSplit = "::";
} else if (column.indexOf("__" != -1)) {
typeSplit = "__";
} else {
return column;
}
var parts = column.split(typeSplit);
var parts = column.split('::');
if (parts[0] == "" && parts.length == 2) {
return parts[1];
}
@@ -340,16 +277,16 @@
QueryResult.prototype.prepareFilters = function () {
var filters = [];
var filterTypes = ['filter', 'multi-filter', 'multiFilter'];
var filterTypes = ['filter', 'multi-filter'];
_.each(this.getColumnNames(), function (col) {
var type = col.split('::')[1] || col.split('__')[1];
var type = col.split('::')[1]
if (_.contains(filterTypes, type)) {
// filter found
var filter = {
name: col,
friendlyName: this.getColumnFriendlyName(col),
values: [],
multiple: (type=='multiFilter') || (type=='multi-filter')
multiple: (type=='multi-filter')
}
filters.push(filter);
}
@@ -401,15 +338,10 @@
return this.deferred.promise;
}
QueryResult.get = function (data_source_id, query, maxAge, queryId) {
QueryResult.get = function (data_source_id, query, maxAge) {
var queryResult = new QueryResult();
var params = {'data_source_id': data_source_id, 'query': query, 'max_age': maxAge};
if (queryId !== undefined) {
params['query_id'] = queryId;
};
QueryResultResource.post(params, function (response) {
QueryResultResource.post({'data_source_id': data_source_id, 'query': query, 'max_age': maxAge}, function (response) {
queryResult.update(response);
if ('job' in response) {
@@ -465,23 +397,20 @@
return '/queries/' + this.id + '/source';
};
Query.prototype.isNew = function() {
return this.id === undefined;
};
Query.prototype.hasDailySchedule = function() {
return (this.schedule && this.schedule.match(/\d\d:\d\d/) !== null);
};
}
Query.prototype.scheduleInLocalTime = function() {
var parts = this.schedule.split(':');
return moment.utc().hour(parts[0]).minute(parts[1]).local().format('HH:mm');
};
}
Query.prototype.getQueryResult = function (maxAge, parameters) {
if (!this.query) {
return;
}
// if (ttl == undefined) {
// ttl = this.ttl;
// }
var queryText = this.query;
var queryParameters = this.getParameters();
@@ -515,9 +444,7 @@
this.queryResult = QueryResult.getById(this.latest_query_data_id);
}
} else if (this.data_source_id) {
this.queryResult = QueryResult.get(this.data_source_id, queryText, maxAge, this.id);
} else {
return new QueryResultError("Please select data source to run this query.");
this.queryResult = QueryResult.get(this.data_source_id, queryText, maxAge);
}
return this.queryResult;
@@ -554,68 +481,14 @@
var DataSource = function ($resource) {
var actions = {
'get': {'method': 'GET', 'cache': false, 'isArray': false},
'query': {'method': 'GET', 'cache': false, 'isArray': true},
'get': {'method': 'GET', 'cache': true, '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'});
@@ -642,8 +515,5 @@
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
.factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
.factory('DataSource', ['$resource', DataSource])
.factory('Alert', ['$resource', '$http', Alert])
.factory('AlertSubscription', ['$resource', AlertSubscription])
.factory('Widget', ['$resource', 'Query', Widget])
.factory('User', ['$resource', '$http', User]);
.factory('Widget', ['$resource', 'Query', Widget]);
})();

File diff suppressed because one or more lines are too long

View File

@@ -55,22 +55,6 @@
}];
};
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',
@@ -84,9 +68,46 @@
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);
}
}
});
}
@@ -117,7 +138,7 @@
query: '=',
queryResult: '=',
visualization: '=?',
openEditor: '@',
openEditor: '=?',
onNewSuccess: '=?'
},
link: function (scope, element, attrs) {
@@ -146,13 +167,9 @@
scope.$watch('visualization.type', function (type, oldType) {
// if not edited by user, set name to match type
if (type && oldType != type && scope.visualization && !scope.visForm.name.$dirty) {
scope.visualization.name = _.string.titleize(scope.visualization.type);
// poor man's titlecase
scope.visualization.name = scope.visualization.type[0] + scope.visualization.type.slice(1).toLowerCase();
}
if (type && oldType != type && scope.visualization) {
scope.visualization.options = Visualization.visualizations[scope.visualization.type].defaultOptions;
}
});
scope.submit = function () {
@@ -191,7 +208,6 @@
.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

@@ -1,307 +0,0 @@
(function() {
// Inspired by http://informationandvisualization.de/blog/box-plot
d3.box = function() {
var width = 1,
height = 1,
duration = 0,
domain = null,
value = Number,
whiskers = boxWhiskers,
quartiles = boxQuartiles,
tickFormat = null;
// For each small multiple…
function box(g) {
g.each(function(d, i) {
d = d.map(value).sort(d3.ascending);
var g = d3.select(this),
n = d.length,
min = d[0],
max = d[n - 1];
// Compute quartiles. Must return exactly 3 elements.
var quartileData = d.quartiles = quartiles(d);
// Compute whiskers. Must return exactly 2 elements, or null.
var whiskerIndices = whiskers && whiskers.call(this, d, i),
whiskerData = whiskerIndices && whiskerIndices.map(function(i) { return d[i]; });
// Compute outliers. If no whiskers are specified, all data are "outliers".
// We compute the outliers as indices, so that we can join across transitions!
var outlierIndices = whiskerIndices
? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n))
: d3.range(n);
// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain(domain && domain.call(this, d, i) || [min, max])
.range([height, 0]);
// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
.range(x1.range());
// Stash the new scale.
this.__chart__ = x1;
// Note: the box, median, and box tick elements are fixed in number,
// so we only have to handle enter and update. In contrast, the outliers
// and other elements are variable, so we need to exit them! Variable
// elements also fade in and out.
// Update center line: the vertical line spanning the whiskers.
var center = g.selectAll("line.center")
.data(whiskerData ? [whiskerData] : []);
center.enter().insert("line", "rect")
.attr("class", "center")
.attr("x1", width / 2)
.attr("y1", function(d) { return x0(d[0]); })
.attr("x2", width / 2)
.attr("y2", function(d) { return x0(d[1]); })
.style("opacity", 1e-6)
.transition()
.duration(duration)
.style("opacity", 1)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); });
center.transition()
.duration(duration)
.style("opacity", 1)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); });
center.exit().transition()
.duration(duration)
.style("opacity", 1e-6)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); })
.remove();
// Update innerquartile box.
var box = g.selectAll("rect.box")
.data([quartileData]);
box.enter().append("rect")
.attr("class", "box")
.attr("x", 0)
.attr("y", function(d) { return x0(d[2]); })
.attr("width", width)
.attr("height", function(d) { return x0(d[0]) - x0(d[2]); })
.transition()
.duration(duration)
.attr("y", function(d) { return x1(d[2]); })
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); });
box.transition()
.duration(duration)
.attr("y", function(d) { return x1(d[2]); })
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); });
box.exit().remove()
// Update median line.
var medianLine = g.selectAll("line.median")
.data([quartileData[1]]);
medianLine.enter().append("line")
.attr("class", "median")
.attr("x1", 0)
.attr("y1", x0)
.attr("x2", width)
.attr("y2", x0)
.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1);
medianLine.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1);
medianLine.exit().remove()
// Update whiskers.
var whisker = g.selectAll("line.whisker")
.data(whiskerData || []);
whisker.enter().insert("line", "circle, text")
.attr("class", "whisker")
.attr("x1", 0)
.attr("y1", x0)
.attr("x2", width)
.attr("y2", x0)
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1);
whisker.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1);
whisker.exit().transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1e-6)
.remove();
// Update outliers.
var outlier = g.selectAll("circle.outlier")
.data(outlierIndices, Number);
outlier.enter().insert("circle", "text")
.attr("class", "outlier")
.attr("r", 5)
.attr("cx", width / 2)
.attr("cy", function(i) { return x0(d[i]); })
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[i]); })
.style("opacity", 1);
outlier.transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[i]); })
.style("opacity", 1);
outlier.exit().transition()
.duration(duration)
.attr("cy", function(i) { return x1(d[i]); })
.style("opacity", 1e-6)
.remove();
// Compute the tick format.
var format = tickFormat || x1.tickFormat(8);
// Update box ticks.
var boxTick = g.selectAll("text.box")
.data(quartileData);
boxTick.enter().append("text")
.attr("class", "box")
.attr("dy", ".3em")
.attr("dx", function(d, i) { return i & 1 ? 6 : -6 })
.attr("x", function(d, i) { return i & 1 ? width : 0 })
.attr("y", x0)
.attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; })
.text(format)
.transition()
.duration(duration)
.attr("y", x1);
boxTick.transition()
.duration(duration)
.text(format)
.attr("y", x1);
boxTick.exit().remove()
// Update whisker ticks. These are handled separately from the box
// ticks because they may or may not exist, and we want don't want
// to join box ticks pre-transition with whisker ticks post-.
var whiskerTick = g.selectAll("text.whisker")
.data(whiskerData || []);
whiskerTick.enter().append("text")
.attr("class", "whisker")
.attr("dy", ".3em")
.attr("dx", 6)
.attr("x", width)
.attr("y", x0)
.text(format)
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("y", x1)
.style("opacity", 1);
whiskerTick.transition()
.duration(duration)
.text(format)
.attr("y", x1)
.style("opacity", 1);
whiskerTick.exit().transition()
.duration(duration)
.attr("y", x1)
.style("opacity", 1e-6)
.remove();
});
d3.timer.flush();
}
box.width = function(x) {
if (!arguments.length) return width;
width = x;
return box;
};
box.height = function(x) {
if (!arguments.length) return height;
height = x;
return box;
};
box.tickFormat = function(x) {
if (!arguments.length) return tickFormat;
tickFormat = x;
return box;
};
box.duration = function(x) {
if (!arguments.length) return duration;
duration = x;
return box;
};
box.domain = function(x) {
if (!arguments.length) return domain;
domain = x == null ? x : d3.functor(x);
return box;
};
box.value = function(x) {
if (!arguments.length) return value;
value = x;
return box;
};
box.whiskers = function(x) {
if (!arguments.length) return whiskers;
whiskers = x;
return box;
};
box.quartiles = function(x) {
if (!arguments.length) return quartiles;
quartiles = x;
return box;
};
return box;
};
function boxWhiskers(d) {
return [0, d.length - 1];
}
function boxQuartiles(d) {
return [
d3.quantile(d, .25),
d3.quantile(d, .5),
d3.quantile(d, .75)
];
}
})();

View File

@@ -1,173 +0,0 @@
(function() {
var module = angular.module('redash.visualization');
module.config(['VisualizationProvider', function(VisualizationProvider) {
var renderTemplate =
'<boxplot-renderer ' +
'options="visualization.options" query-result="queryResult">' +
'</boxplot-renderer>';
var editTemplate = '<boxplot-editor></boxplot-editor>';
VisualizationProvider.registerVisualization({
type: 'BOXPLOT',
name: 'Boxplot',
renderTemplate: renderTemplate,
editorTemplate: editTemplate
});
}
]);
module.directive('boxplotRenderer', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/boxplot.html',
link: function($scope, elm, attrs) {
function iqr(k) {
return function(d, i) {
var q1 = d.quartiles[0],
q3 = d.quartiles[2],
iqr = (q3 - q1) * k,
i = -1,
j = d.length;
while (d[++i] < q1 - iqr);
while (d[--j] > q3 + iqr);
return [i, j];
};
};
$scope.$watch('[queryResult && queryResult.getData(), visualization.options]', function () {
var data = $scope.queryResult.getData();
var parentWidth = d3.select(elm[0].parentNode).node().getBoundingClientRect().width;
var margin = {top: 10, right: 50, bottom: 40, left: 50, inner: 25},
width = parentWidth - margin.right - margin.left
height = 500 - margin.top - margin.bottom;
var min = Infinity,
max = -Infinity;
var mydata = [];
var value = 0;
var d = [];
var xAxisLabel = $scope.visualization.options.xAxisLabel;
var yAxisLabel = $scope.visualization.options.yAxisLabel;
var columns = $scope.queryResult.columnNames;
var xscale = d3.scale.ordinal()
.domain(columns)
.rangeBands([0, parentWidth-margin.left-margin.right]);
if (columns.length > 1){
boxWidth = Math.min(xscale(columns[1]),120.0);
} else {
boxWidth=120.0;
};
margin.inner = boxWidth/3.0;
_.each(columns, function(column, i){
d = mydata[i] = [];
_.each(data, function (row) {
value = row[column];
d.push(value);
if (value > max) max = Math.ceil(value);
if (value < min) min = Math.floor(value);
});
});
var yscale = d3.scale.linear()
.domain([min*0.99,max*1.01])
.range([height, 0]);
var chart = d3.box()
.whiskers(iqr(1.5))
.width(boxWidth-2*margin.inner)
.height(height)
.domain([min*0.99,max*1.01]);
var xAxis = d3.svg.axis()
.scale(xscale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yscale)
.orient("left");
var xLines = d3.svg.axis()
.scale(xscale)
.tickSize(height)
.orient("bottom");
var yLines = d3.svg.axis()
.scale(yscale)
.tickSize(width)
.orient("right");
var barOffset = function(i){
return xscale(columns[i]) + (xscale(columns[1]) - margin.inner)/2.0;
};
d3.select(elm[0]).selectAll("svg").remove();
var plot = d3.select(elm[0])
.append("svg")
.attr("width",parentWidth)
.attr("height",height + margin.bottom + margin.top)
.append("g")
.attr("width",parentWidth-margin.left-margin.right)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
d3.select("svg").append("text")
.attr("class", "box")
.attr("x", parentWidth/2.0)
.attr("text-anchor", "middle")
.attr("y", height+margin.bottom)
.text(xAxisLabel)
d3.select("svg").append("text")
.attr("class", "box")
.attr("transform","translate(10,"+(height+margin.top+margin.bottom)/2.0+")rotate(-90)")
.attr("text-anchor", "middle")
.text(yAxisLabel)
plot.append("rect")
.attr("class", "grid-background")
.attr("width", width)
.attr("height", height);
plot.append("g")
.attr("class","grid")
.call(yLines)
plot.append("g")
.attr("class","grid")
.call(xLines)
plot.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
plot.append("g")
.attr("class", "y axis")
.call(yAxis);
plot.selectAll(".box").data(mydata)
.enter().append("g")
.attr("class", "box")
.attr("width", boxWidth)
.attr("height", height)
.attr("transform", function(d,i) { return "translate(" + barOffset(i) + "," + 0 + ")"; } )
.call(chart);
}, true);
}
}
});
module.directive('boxplotEditor', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/boxplot_editor.html'
};
});
})();

View File

@@ -3,16 +3,12 @@
chartVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
var renderTemplate = '<chart-renderer options="visualization.options" query-result="queryResult"></chart-renderer>';
var editTemplate = '<chart-editor options="visualization.options" query-result="queryResult"></chart-editor>';
var editTemplate = '<chart-editor></chart-editor>';
var defaultOptions = {
globalSeriesType: 'column',
sortX: true,
yAxis: [{type: 'linear'}, {type: 'linear', opposite: true}],
xAxis: {type: 'datetime', labels: {enabled: true}},
series: {stacking: null},
seriesOptions: {},
columnMapping: {}
'series': {
// 'type': 'column',
'stacking': null
}
};
VisualizationProvider.registerVisualization({
@@ -31,62 +27,30 @@
queryResult: '=',
options: '=?'
},
templateUrl: '/views/visualizations/chart.html',
template: "<chart options='chartOptions' series='chartSeries' class='graph'></chart>",
replace: false,
controller: ['$scope', function ($scope) {
$scope.chartSeries = [];
$scope.chartOptions = {};
$scope.dateRangeEnabled = function() {
return $scope.options.xAxis && $scope.options.xAxis.type === 'datetime';
}
$scope.dateRange = { min: moment('1970-01-01'), max: moment() };
/**
* Update date range by finding date extremes
*
* ISSUE: chart.getExtreme() does not support getting Moment object out of box
* TODO: Find a faster way to do this
*/
var setDateRangeToExtreme = function (allSeries) {
if ($scope.dateRangeEnabled() && allSeries && allSeries.length > 0) {
$scope.dateRange = {
min: moment.min.apply(undefined, _.map(allSeries, function (series) {
return moment.min(_.pluck(series.data, 'x'));
})),
max: moment.max.apply(undefined, _.map(allSeries, function (series) {
return moment.max(_.pluck(series.data, 'x'));
}))
};
}
};
var reloadData = function(data, options) {
options = options || {};
var reloadData = function(data) {
if (!data || ($scope.queryResult && $scope.queryResult.getData()) == null) {
$scope.chartSeries.splice(0, $scope.chartSeries.length);
} else {
$scope.chartSeries.splice(0, $scope.chartSeries.length);
var allSeries = $scope.queryResult.getChartData($scope.options.columnMapping);
if (!options.preventSetExtreme) {
setDateRangeToExtreme(allSeries);
}
var allSeries = $scope.queryResult.getChartData(
$scope.options.columnMapping,
$scope.dateRangeEnabled() ? $scope.dateRange : null
);
_.each(allSeries, function (series) {
_.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[series.name]) {
additional = $scope.options.seriesOptions[series.name];
if ($scope.options.seriesOptions && $scope.options.seriesOptions[s.name]) {
additional = $scope.options.seriesOptions[s.name];
if (!additional.name || additional.name == "") {
additional.name = series.name;
additional.name = s.name;
}
}
$scope.chartSeries.push(_.extend(series, additional));
$scope.chartSeries.push(_.extend(s, additional));
});
};
};
@@ -109,22 +73,6 @@
$scope.$watch('queryResult && queryResult.getData()', function (data) {
reloadData(data);
});
$scope.$watch('dateRange.min', function(minDateRange, oldMinDateRange) {
if (!minDateRange.isSame(oldMinDateRange)) {
reloadData(true, {
preventSetExtreme: true
});
}
});
$scope.$watch('dateRange.max', function (maxDateRange, oldMaxDateRange) {
if (!maxDateRange.isSame(oldMaxDateRange)) {
reloadData(true, {
preventSetExtreme: true
});
}
});
}]
};
});
@@ -133,135 +81,175 @@
return {
restrict: 'E',
templateUrl: '/views/visualizations/chart_editor.html',
scope: {
queryResult: '=',
options: '=?'
},
link: function (scope, element, attrs) {
scope.colors = _.extend({'Automatic': null}, ColorPalette);
scope.palette = ColorPalette;
scope.seriesTypes = {
'Line': 'line',
'Column': 'column',
'Area': 'area',
'Scatter': 'scatter',
'Pie': 'pie'
};
scope.globalSeriesType = scope.visualization.options.globalSeriesType || 'column';
scope.stackingOptions = {
'Disabled': null,
'Enabled': 'normal',
'Percent': 'percent'
"None": "none",
"Normal": "normal",
"Percent": "percent"
};
scope.chartTypes = {
'line': {name: 'Line', icon: 'line-chart'},
'column': {name: 'Bar', icon: 'bar-chart'},
'area': {name: 'Area', icon: 'area-chart'},
'pie': {name: 'Pie', icon: 'pie-chart'},
'scatter': {name: 'Scatter', icon: 'circle-o'}
scope.xAxisOptions = {
"Date/Time": "datetime",
"Linear": "linear",
"Category": "category"
};
scope.chartTypeChanged = function() {
_.each(scope.options.seriesOptions, function(options) {
options.type = scope.options.globalSeriesType;
});
}
scope.xAxisType = "datetime";
scope.stacking = "none";
scope.xAxisScales = ['datetime', 'linear', 'logarithmic', 'category'];
scope.yAxisScales = ['linear', 'logarithmic'];
var refreshColumns = function() {
scope.columns = scope.queryResult.getColumns();
scope.columnNames = _.pluck(scope.columns, 'name');
if (scope.columnNames.length > 0)
_.each(_.difference(_.keys(scope.options.columnMapping), scope.columnNames), function(column) {
delete scope.options.columnMapping[column];
scope.columnTypes = {
"X": "x",
// "X (Date time)": "x",
// "X (Linear)": "x-linear",
// "X (Category)": "x-category",
"Y": "y",
"Series": "series",
"Unused": "unused"
};
scope.series = [];
scope.columnTypeSelection = {};
var chartOptionsUnwatch = null,
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;
});
};
refreshColumns();
}
});
var refreshColumnsAndForm = function() {
refreshColumns();
if (!scope.queryResult.getData() || scope.queryResult.getData().length == 0 || scope.columns.length == 0)
return;
scope.form.yAxisColumns = _.intersection(scope.form.yAxisColumns, scope.columnNames);
if (!_.contains(scope.columnNames, scope.form.xAxisColumn))
scope.form.xAxisColumn = undefined;
if (!_.contains(scope.columnNames, scope.form.groupby))
scope.form.groupby = undefined;
}
scope.$watch('visualization.type', function (visualizationType) {
if (visualizationType == 'CHART') {
if (scope.visualization.options.series.stacking === null) {
scope.stacking = "none";
} else if (scope.visualization.options.series.stacking === undefined) {
scope.stacking = "normal";
} else {
scope.stacking = scope.visualization.options.series.stacking;
}
var refreshSeries = function() {
var seriesNames = _.pluck(scope.queryResult.getChartData(scope.options.columnMapping), 'name');
var existing = _.keys(scope.options.seriesOptions);
_.each(_.difference(seriesNames, existing), function(name) {
scope.options.seriesOptions[name] = {
'type': scope.options.globalSeriesType,
'yAxis': 0,
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; });
// TODO: remove uneeded ones?
if (scope.visualization.options.seriesOptions == undefined) {
scope.visualization.options.seriesOptions = {
type: scope.globalSeriesType
};
};
_.each(scope.series, function(s, i) {
if (scope.visualization.options.seriesOptions[s] == undefined) {
scope.visualization.options.seriesOptions[s] = {'type': scope.visualization.options.globalSeriesType, 'yAxis': 0};
}
scope.visualization.options.seriesOptions[s].zIndex = scope.visualization.options.seriesOptions[s].zIndex === undefined ? i : scope.visualization.options.seriesOptions[s].zIndex;
});
scope.zIndexes = _.range(scope.series.length);
scope.yAxes = [[0, 'left'], [1, 'right']];
};
scope.form.seriesList.push(name);
});
_.each(_.difference(existing, seriesNames), function(name) {
scope.form.seriesList = _.without(scope.form.seriesList, name)
delete scope.options.seriesOptions[name];
});
};
scope.$watch('options.columnMapping', refreshSeries, true);
var initColumnMapping = function() {
scope.columns = scope.queryResult.getColumns();
scope.$watch(function() {return [scope.queryResult.getId(), scope.queryResult.status]}, function(changed) {
if (!changed[0]) {
return;
}
refreshColumnsAndForm();
refreshSeries();
}, true);
if (scope.visualization.options.columnMapping == undefined) {
scope.visualization.options.columnMapping = {};
}
scope.form = {
yAxisColumns: [],
seriesList: _.sortBy(_.keys(scope.options.seriesOptions), function(name) {
return scope.options.seriesOptions[name].zIndex;
})
};
scope.columnTypeSelection = scope.visualization.options.columnMapping;
scope.$watchCollection('form.seriesList', function(value, old) {
_.each(value, function(name, index) {
scope.options.seriesOptions[name].zIndex = index;
scope.options.seriesOptions[name].index = 0; // is this needed?
});
});
_.each(scope.columns, function(column) {
var definition = column.name.split("::"),
definedColumns = _.keys(scope.visualization.options.columnMapping);
var setColumnRole = function(role, column) {
scope.options.columnMapping[column] = role;
}
var unsetColumn = function(column) {
setColumnRole('unused', column);
}
if (_.indexOf(definedColumns, column.name) != -1) {
// Skip already defined columns.
return;
};
scope.$watchCollection('form.yAxisColumns', function(value, old) {
_.each(old, unsetColumn);
_.each(value, _.partial(setColumnRole, 'y'));
});
if (definition.length == 1) {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'unused';
} else if (definition == 'multi-filter') {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'series';
} else if (_.indexOf(_.values(scope.columnTypes), definition[1]) != -1) {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = definition[1];
} else {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'unused';
}
});
};
scope.$watch('form.xAxisColumn', function(value, old) {
if (old !== undefined)
unsetColumn(old);
if (value !== undefined)
setColumnRole('x', value);
});
columnsWatch = scope.$watch('queryResult.getId()', function(id) {
if (!id) {
return;
}
scope.$watch('form.groupby', function(value, old) {
if (old !== undefined)
unsetColumn(old)
if (value !== undefined) {
setColumnRole('series', value);
initColumnMapping();
refreshSeries();
});
scope.$watchCollection('columnTypeSelection', function(selections) {
_.each(scope.columnTypeSelection, function(type, name) {
scope.visualization.options.columnMapping[name] = type;
});
refreshSeries();
});
chartOptionsUnwatch = scope.$watch("stacking", function (stacking) {
if (stacking == "none") {
scope.visualization.options.series.stacking = null;
} else {
scope.visualization.options.series.stacking = stacking;
}
});
scope.xAxisType = (scope.visualization.options.xAxis && scope.visualization.options.xAxis.type) || scope.xAxisType;
xAxisUnwatch = scope.$watch("xAxisType", function (xAxisType) {
scope.visualization.options.xAxis = scope.visualization.options.xAxis || {};
scope.visualization.options.xAxis.type = xAxisType;
});
} else {
if (chartOptionsUnwatch) {
chartOptionsUnwatch();
chartOptionsUnwatch = null;
}
if (columnsWatch) {
columnWatch();
columnWatch = null;
}
if (xAxisUnwatch) {
xAxisUnwatch();
xAxisUnwatch = null;
}
}
});
if (scope.columnNames)
_.each(scope.options.columnMapping, function(value, key) {
if (scope.columnNames.length > 0 && !_.contains(scope.columnNames, key))
return;
if (value == 'x')
scope.form.xAxisColumn = key;
else if (value == 'y')
scope.form.yAxisColumns.push(key);
else if (value == 'series')
scope.form.groupby = key;
});
}
}
});

View File

@@ -1,87 +1,64 @@
(function () {
var cohortVisualization = angular.module('redash.visualization');
var cohortVisualization = angular.module('redash.visualization');
cohortVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
cohortVisualization.config(['VisualizationProvider', function(VisualizationProvider) {
VisualizationProvider.registerVisualization({
type: 'COHORT',
name: 'Cohort',
renderTemplate: '<cohort-renderer options="visualization.options" query-result="queryResult"></cohort-renderer>'
});
}]);
var editTemplate = '<cohort-editor></cohort-editor>';
var defaultOptions = {
'timeInterval': 'daily'
};
cohortVisualization.directive('cohortRenderer', function() {
return {
restrict: 'E',
scope: {
queryResult: '='
},
template: "",
replace: false,
link: function($scope, element, attrs) {
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
VisualizationProvider.registerVisualization({
type: 'COHORT',
name: 'Cohort',
renderTemplate: '<cohort-renderer options="visualization.options" query-result="queryResult"></cohort-renderer>',
editorTemplate: editTemplate,
defaultOptions: defaultOptions
});
}]);
if ($scope.queryResult.getData() == null) {
cohortVisualization.directive('cohortRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '=',
options: '='
},
template: "",
replace: false,
link: function ($scope, element, attrs) {
$scope.options.timeInterval = $scope.options.timeInterval || 'daily';
} else {
var sortedData = _.sortBy($scope.queryResult.getData(), "date");
var grouped = _.groupBy(sortedData, "date");
var maxColumns = _.reduce(grouped, function(memo, data){
return (data.length > memo)? data.length : memo;
}, 0);
var data = _.map(grouped, function(values, date) {
var row = [values[0].total];
_.each(values, function(value) { row.push(value.value); });
_.each(_.range(values.length, maxColumns), function() { row.push(null); });
return row;
});
var updateCohort = function () {
if ($scope.queryResult.getData() === null) {
return;
}
var initialDate = moment(sortedData[0].date).toDate(),
container = angular.element(element)[0];
var sortedData = _.sortBy($scope.queryResult.getData(), function (r) {
return r['date'] + r['day_number'];
});
var grouped = _.groupBy(sortedData, "date");
var maxColumns = _.reduce(grouped, function (memo, data) {
return (data.length > memo) ? data.length : memo;
}, 0);
var data = _.map(grouped, function (values, date) {
var row = [values[0].total];
_.each(values, function (value) {
row.push(value.value);
});
_.each(_.range(values.length, maxColumns), function () {
row.push(null);
});
return row;
});
var initialDate = moment(sortedData[0].date).toDate(),
container = angular.element(element)[0];
Cornelius.draw({
initialDate: initialDate,
container: container,
cohort: data,
title: null,
timeInterval: $scope.options.timeInterval,
labels: {
time: 'Time',
people: 'Users',
weekOf: 'Week of'
Cornelius.draw({
initialDate: initialDate,
container: container,
cohort: data,
title: null,
timeInterval: 'daily',
labels: {
time: 'Activation Day',
people: 'Users'
},
formatHeaderLabel: function (i) {
return "Day " + (i - 1);
}
});
}
});
}
});
}
});
$scope.$watch('queryResult && queryResult.getData()', updateCohort);
$scope.$watch('options.timeInterval', updateCohort);
}
}
});
cohortVisualization.directive('cohortEditor', function () {
return {
restrict: 'E',
templateUrl: '/views/visualizations/cohort_editor.html'
}
});
}());
}());

View File

@@ -10,11 +10,7 @@
'</counter-renderer>';
var editTemplate = '<counter-editor></counter-editor>';
var defaultOptions = {
counterColName: 'counter',
rowNumber: 1,
targetRowNumber: 1
};
var defaultOptions = {};
VisualizationProvider.registerVisualization({
type: 'COUNTER',
@@ -31,28 +27,23 @@
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 - 1;
var targetRowNumber = $scope.visualization.options.targetRowNumber - 1;
var counterColName = $scope.visualization.options.counterColName;
var targetColName = $scope.visualization.options.targetColName;
var rowNumber = $scope.visualization.options.rowNumber || 0;
var counterColName = $scope.visualization.options.counterColName || 'counter';
var targetColName = $scope.visualization.options.targetColName || 'target';
if (counterColName) {
$scope.counterValue = queryData[rowNumber][counterColName];
}
$scope.counterValue = queryData[rowNumber][counterColName];
$scope.targetValue = queryData[rowNumber][targetColName];
if (targetColName) {
$scope.targetValue = queryData[targetRowNumber][targetColName];
if ($scope.targetValue) {
$scope.delta = $scope.counterValue - $scope.targetValue;
$scope.trendPositive = $scope.delta >= 0;
}
} else {
$scope.targetValue = null;
if ($scope.targetValue) {
$scope.delta = $scope.counterValue - $scope.targetValue;
$scope.trendPositive = $scope.delta >= 0;
}
}
}, true);

View File

@@ -1,43 +0,0 @@
(function (window) {
var module = angular.module('redash.visualization');
module.directive('dateRangeSelector', [function () {
return {
restrict: 'E',
scope: {
dateRange: "="
},
templateUrl: '/views/visualizations/date_range_selector.html',
replace: true,
controller: ['$scope', function ($scope) {
$scope.dateRangeHuman = {
min: null,
max: null
};
$scope.$watch('dateRange', function (dateRange, oldDateRange, scope) {
scope.dateRangeHuman.min = dateRange.min.format('YYYY-MM-DD');
scope.dateRangeHuman.max = dateRange.max.format('YYYY-MM-DD');
});
$scope.$watch('dateRangeHuman', function (dateRangeHuman, oldDateRangeHuman, scope) {
var newDateRangeMin = moment.utc(dateRangeHuman.min);
var newDateRangeMax = moment.utc(dateRangeHuman.max);
if (!newDateRangeMin ||
!newDateRangeMax ||
!newDateRangeMin.isValid() ||
!newDateRangeMax.isValid() ||
newDateRangeMin.isAfter(newDateRangeMax)) {
// Prevent invalid date input
// No need to show up a notification to user here, it will be too noisy.
// Instead, simply preventing changes to the scope silently.
scope.dateRangeHuman = oldDateRangeHuman;
return;
}
scope.dateRange.min = newDateRangeMin;
scope.dateRange.max = newDateRangeMax;
}, true);
}]
}
}]);
})(window);

View File

@@ -1,238 +0,0 @@
'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

@@ -79,14 +79,14 @@
} else if (columnType === 'date') {
columnDefinition.formatFunction = function (value) {
if (value && moment.isMoment(value)) {
return value.format(clientConfig.dateFormat);
return value.toDate().toLocaleDateString();
}
return value;
};
} else if (columnType === 'datetime') {
columnDefinition.formatFunction = function (value) {
if (value && moment.isMoment(value)) {
return value.format(clientConfig.dateTimeFormat);
return value.toDate().toLocaleString();
}
return value;
};

View File

@@ -14,16 +14,7 @@ a.page-title {
}
a.navbar-brand {
padding: 5px 5px 0px 0px;
margin-left: 0px !important;
}
.navbar .fa {
font-size: 18px;
}
a.navbar-brand img {
height: 40px;
font-style: italic;
}
.graph {
@@ -101,29 +92,7 @@ a.navbar-brand img {
}
.panel-heading .query-link:hover {
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;
text-decoration: none;
}
/* angular-growl */
@@ -154,23 +123,6 @@ a.navbar-brand img {
}
/* 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 {
@@ -204,7 +156,7 @@ li.widget:hover {
/* CodeMirror */
.CodeMirror {
border: 1px solid #eee;
height: auto;
/*height: auto;*/
min-height: 300px;
margin-bottom: 10px;
}
@@ -213,14 +165,6 @@ 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. */
@@ -356,130 +300,32 @@ counter-renderer counter-name {
display: block;
}
.box {
font: 10px sans-serif;
}
.box line,
.box rect,
.box circle {
fill: #fff;
stroke: #000;
stroke-width: 1.5px;
}
.box .center {
stroke-dasharray: 3,3;
}
.box .outlier {
fill: none;
stroke: #000;
}
.axis text {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.grid-background {
fill: #ddd;
}
.grid path,
.grid line {
fill: none;
stroke: #fff;
shape-rendering: crispEdges;
}
.grid .minor line {
stroke-opacity: .5;
}
.grid text {
display: none;
}
.rd-widget-textbox p {
margin-bottom: 0;
}
.iframe-container {
height: 100%;
}
.schema-container {
height: 300px;
}
.schema-browser {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
height: 300px;
overflow: scroll;
}
div.table-name {
overflow: hidden;
overflow: scroll;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.blankslate {
text-align: center;
padding: 30px;
}
.log-container {
margin-bottom: 50px;
}
/* Footer */
.footer {
color: #818d9f;
padding-bottom: 30px;
}
.footer a {
color: #818d9f;
margin-left: 20px;
}
.col-table .missing-value {
color: #b94a48;
}
.col-table .super-small-input {
padding-left: 3px;
height: 24px;
}
.col-table .ui-select-toggle, .col-table .ui-select-search {
padding: 2px;
padding-left: 5px;
height: 24px;
}
.clearable button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
/* Immediately apply ng-cloak, instead of waiting for angular.js to load: */
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
/* Smart Table */
.smart-table {
margin-bottom: 0px;
}
.smart-table .pagination {
margin-bottom: 5px;
margin-top: 10px;
/*
bootstrap's hidden-xs class adds display:block when not hidden
use this class when you need to keep the original display value
*/
@media (max-width: 767px) {
.rd-hidden-xs {
display: none !important;
}
}

View File

@@ -1,58 +0,0 @@
<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

@@ -1,16 +0,0 @@
<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

@@ -1,4 +0,0 @@
<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,21 +2,17 @@
<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>
<div class="btn-group" role="group" ng-show="dashboard.canEdit()">
<span 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>
<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>
</span>
</h2>
<filters ng-if="dashboard.dashboard_filters_enabled"></filters>
</div>
@@ -32,7 +28,6 @@
<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-html="query.description | markdown"></div>
</h3>
@@ -58,7 +53,7 @@
</div>
</div>
<div class="panel panel-default rd-widget-textbox" ng-hide="widget.width == 0" ng-if="type=='textbox'" ng-mouseenter="showControls = true" ng-mouseleave="showControls = false">
<div class="panel panel-default rd-widget-textbox" ng-if="type=='textbox'" ng-mouseenter="showControls = true" ng-mouseleave="showControls = false">
<div class="panel-body">
<div class="row">
<div class="col-lg-11">

View File

@@ -1,11 +0,0 @@
<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

@@ -1,20 +0,0 @@
<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" placeholder="{{input.default}}">
<input name="input" type="file" class="form-control" ng-model="files[name]" ng-required="input.required && !dataSource.options[name]"
base-sixty-four-input
ng-if="input.type === 'file'">
</div>
<button class="btn btn-primary" ng-disabled="!dataSourceForm.$valid">Save</button>
</form>

View File

@@ -1,18 +0,0 @@
<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

@@ -1,5 +0,0 @@
<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

@@ -17,5 +17,4 @@
<a href="/admin/status" class="list-group-item">Status</a>
</div>
</div>
</div>
</div>

View File

@@ -1,20 +1,14 @@
<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
<button ng-show="currentUser.hasPermission('create_dashboard')" type="button" class="btn btn-sm btn-link" data-toggle="modal" href="#new_dashboard_dialog" tooltip="New Dashboard"><span class="glyphicon glyphicon-plus-sign"></span></button>
</div>
<div class="list-group-item" ng-repeat="dashboard in recentDashboards" >
<button type="button" class="close delete-button" aria-hidden="true" ng-show="dashboard.canEdit()" ng-click="archiveDashboard(dashboard)" tooltip="Delete Dashboard">&times;</button>
<a ng-href="/dashboard/{{dashboard.slug}}">{{dashboard.name}}</a>
</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">

View File

@@ -26,7 +26,7 @@
</div>
<div class="col-lg-2">
<div class="pull-right">
<div class="rd-hidden-xs pull-right">
<query-source-link></query-source-link>
</div>
</div>
@@ -63,12 +63,12 @@
<div ng-class="editorSize">
<div>
<p>
<button type="button" class="btn btn-primary btn-xs" ng-disabled="queryExecuting" ng-click="executeQuery()" ng-if="canExecuteQuery">
<button type="button" class="btn btn-primary btn-xs" ng-disabled="queryExecuting" ng-click="executeQuery()">
<span class="glyphicon glyphicon-play"></span> Execute
</button>
<query-formatter></query-formatter>
<span class="pull-right">
<button class="btn btn-xs btn-default" ng-click="duplicateQuery()">
<button class="btn btn-xs btn-default rd-hidden-xs" ng-click="duplicateQuery()">
<span class="glyphicon glyphicon-share-alt"></span> Fork
</button>
@@ -79,21 +79,21 @@
</p>
<p>
<query-editor query="query" schema="schema" syntax="dataSource.syntax" lock="queryFormatting"></query-editor>
<query-editor query="query" schema="schema" lock="queryFormatting"></query-editor>
</p>
</div>
</div>
<div class="col-md-3 schema-container" ng-show="hasSchema">
<div ng-show="schema.length < 200">
<div class="col-md-3" ng-show="hasSchema">
<div>
<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 ng-repeat="table in schema | filter:schemaFilter">
<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 collapse="table.collapsed">
<div ng-repeat="column in table.columns | filter:schemaFilter" style="padding-left:16px;">{{column}}</div>
</div>
</div>
</div>
@@ -103,7 +103,7 @@
</div>
<hr ng-if="sourceMode">
<div class="row">
<div class="col-lg-3">
<div class="col-lg-3 rd-hidden-xs">
<p>
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Created By </span>
@@ -138,7 +138,7 @@
</p>
<p>
<i class="fa fa-database"></i>
<span class="glyphicon glyphicon-hdd"></span>
<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>
@@ -148,7 +148,7 @@
<p>
<a class="btn btn-primary btn-sm" ng-disabled="queryExecuting || !queryResult.getData()" query-result-link target="_self">
<span class="glyphicon glyphicon-cloud-download"></span>
<span>Download Dataset</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"
@@ -192,16 +192,6 @@
</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">
@@ -213,7 +203,7 @@
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-show="canEdit"> &times;</span>
</rd-tab>
<rd-tab tab-id="add" name="&plus; New Visualization" removeable="true" ng-show="canEdit"></rd-tab>
<li ng-if="!sourceMode && canExecuteQuery" 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>
<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>
</div>

View File

@@ -1,16 +0,0 @@
<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

@@ -1,32 +0,0 @@
<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">Create</button>
</div>
</form>
</div>

View File

@@ -1,76 +0,0 @@
<div class="container">
<ol class="breadcrumb">
<li ng-if="currentUser.hasPermission('list_users')"><a href="/users">Users</a></li>
<li ng-if="!currentUser.hasPermission('list_users')">Users</li>
<li class="active">{{user.name}}</li>
</ol>
<tabset>
<tab heading="Profile" active="tabs['profile']" select="setTab('profile')">
<p>
<img src="{{user.gravatar_url}}"/>
</p>
<p>
<strong>{{user.name}}</strong>
</p>
<p>
{{user.email}}
</p>
</tab>
<tab heading="API Key" ng-if="user.api_key" active="tabs['apiKey']" select="setTab('apiKey')">
API Key:
<input type="text" value="{{user.api_key}}" size="44" readonly/>
</tab>
<tab heading="Settings" ng-if="showSettings || currentUser.hasPermission('admin')" active="tabs['settings']" select="setTab('settings')">
<div class="col-md-6">
<form class="form" name="userSettingsForm" ng-submit="updateUser(userSettingsForm)" novalidate>
<div class="form-group required" ng-if="showSettings" show-errors>
<label class="control-label">Name</label>
<input name="name" type="text" class="form-control" ng-model="user.name" required/>
<input-errors errors="userSettingsForm.name.$error"/>
</div>
<div class="form-group required" ng-if="showSettings" show-errors>
<label class="control-label">Email</label>
<input name="email" type="email" class="form-control" ng-model="user.email" required/>
<input-errors errors="userSettingsForm.email.$error"/>
</div>
<div class="checkbox" ng-if="currentUser.hasPermission('admin')">
<label>
<input type="checkbox" ng-model="user.admin"> Admin
</label>
</div>
<div class="form-gruup">
<button class="btn btn-primary">Save</button>
</div>
</form>
</div>
</tab>
<tab heading="Password" ng-if="showPasswordSettings" active="tabs['password']" select="setTab('password')">
<div class="col-md-6">
<form class="form" name="userPasswordForm" ng-submit="savePassword(userPasswordForm)" novalidate>
<div class="form-group required" show-errors>
<label class="control-label">Current Password</label>
<input name="currentPassword" class="form-control" type="password" ng-model="password.current" required/>
<input-errors name="Password" errors="userPasswordForm.currentPassword.$error"/>
</div>
<div class="form-group required" show-errors>
<label class="control-label">New Password</label>
<input name="newPassword" class="form-control" type="password" ng-model="password.new" ng-minlength="6" required/>
<input-errors name="Password" errors="userPasswordForm.newPassword.$error"/>
</div>
<div class="form-group required" show-errors>
<label class="control-label">Repeat New Password</label>
<input name="passwordRepeat" class="form-control" type="password" ng-model="password.newRepeat" compare-to="password.new"/>
<span class="help-block error" ng-if="userPasswordForm.passwordRepeat.$error.compareTo">Passwords don't match.</span>
</div>
<div class="form-group">
<button class="btn btn-primary">Save</button>
</div>
</form>
</div>
</tab>
</tabset>
</div>

View File

@@ -1,2 +0,0 @@
<boxplot>
</boxplot>

View File

@@ -1,15 +0,0 @@
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-6">X Axis Label</label>
<div class="col-lg-6">
<input type="text" ng-model="visualization.options.xAxisLabel" class="form-control"></input>
</div>
</div>
<div class="form-group">
<label class="col-lg-6">Y Axis Label</label>
<div class="col-lg-6">
<input type="text" ng-model="visualization.options.yAxisLabel" class="form-control"></input>
</div>
</div>
</div>

View File

@@ -1,8 +0,0 @@
<div>
<section class="clearfix">
<date-range-selector ng-if="dateRangeEnabled()" date-range='dateRange' class='pull-right'></date-range-selector>
</section>
<section>
<chart options='chartOptions' series='chartSeries' class='graph'></chart>
</section>
</div>

View File

@@ -1,236 +1,134 @@
<form class="form-horizontal" name="chartEditor">
<div class="row">
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Chart Type</label>
<div class="col-sm-7" ng-if="chartTypes"><!--the if is a weird workaround-->
<ui-select ng-model="options.globalSeriesType" on-select="chartTypeChanged()">
<ui-select-match placeholder="Choose chart type..."><i class="fa fa-{{$select.selected.value.icon}}"></i> {{$select.selected.value.name}}</ui-select-match>
<ui-select-choices repeat="info.chartType as (chartType, info) in chartTypes">
<div><i class="fa fa-{{info.value.icon}}"></i><span> </span><span ng-bind-html="info.value.name | highlight: $select.search"></span></div>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Stacking</label>
<div class="col-sm-7" ng-if="stackingOptions"><!--the if is a weird workaround-->
<ui-select ng-model="options.series.stacking" ng-disabled="['line', 'area', 'column'].indexOf(options.globalSeriesType) == -1">
<ui-select-match placeholder="Choose Stacking...">{{$select.selected.key | capitalize}}</ui-select-match>
<ui-select-choices repeat="value.value as (key, value) in stackingOptions">
<div ng-bind-html="value.key | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group row" ng-class="{'has-error': chartEditor.xAxisColumn.$invalid}">
<label class="control-label col-sm-5">X Column</label>
<div class="col-sm-7">
<ui-select name="xAxisColumn" required ng-model="form.xAxisColumn">
<ui-select-match placeholder="Choose column...">{{$select.selected}}</ui-select-match>
<ui-select-choices repeat="column in columnNames | remove:form.yAxisColumns | remove:form.groupby">
<span ng-bind-html="column | highlight: $select.search"></span><span> </span><small class="text-muted" ng-bind="columns[column].type"></small>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Group by</label>
<div class="col-sm-7">
<ui-select name="groupby" ng-model="form.groupby" class="clearable">
<ui-select-match allow-clear="true" placeholder="Choose column...">{{$select.selected}}</ui-select-match>
<ui-select-choices repeat="column in columnNames | remove:form.yAxisColumns | remove:form.xAxisColumn">
<span ng-bind-html="column | highlight: $select.search"></span><span> </span><small class="text-muted" ng-bind="columns[column].type"></small>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<!-- not using regular validation (chartEditor.yAxisColumns.$invalid) due to a bug in ui-select with multiple choices-->
<div class="form-group row" ng-class="{'has-error': !form.yAxisColumns || form.yAxisColumns.length == 0}">
<label class="control-label col-sm-5">Y Columns</label>
<div class="col-sm-7">
<ui-select multiple name="yAxisColumns" required ng-model="form.yAxisColumns">
<ui-select-match placeholder="Choose columns...">{{$item}}</ui-select-match>
<ui-select-choices repeat="column in columnNames | remove:form.groupby | remove:form.xAxisColumn">
<span ng-bind-html="column | highlight: $select.search"></span><span> </span><small class="text-muted" ng-bind="columns[column].type"></small>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">X Axis</h3>
</div>
<div class="form-horizontal">
<div class="panel panel-default">
<div class="panel-body">
<div class="row">
<div class="form-group">
<label class="control-label col-sm-2">Stacking</label>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-3">Scale</label>
<div class="col-sm-9">
<ui-select ng-model="options.xAxis.type">
<ui-select-match placeholder="Choose Scale...">{{$select.selected | capitalize}}</ui-select-match>
<ui-select-choices repeat="scaleType in xAxisScales">
<div ng-bind-html="scaleType | capitalize | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<div class="col-sm-10">
<select required ng-model="stacking"
ng-options="value as key for (key, value) in stackingOptions"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">X Axis Type</label>
<div class="col-sm-10">
<select required ng-model="xAxisType" ng-options="value as key for (key, value) in xAxisOptions"
class="form-control"></select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-8">Sort Values</label>
<div class="col-sm-4">
<input type="checkbox" ng-model="options.sortX">
<div class="form-group">
<label class="control-label col-sm-2">Series Type</label>
<div class="col-sm-10">
<select required ng-options="value as key for (key, value) in seriesTypes"
ng-model="globalSeriesType" class="form-control"></select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="form-group">
<label class="control-label col-sm-2">y Axis min</label>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-3">Name</label>
<div class="col-sm-9">
<input ng-model="options.xAxis.title.text" type="text" class="form-control"></input>
<div class="col-sm-10">
<input name="yAxisMin" type="number" class="form-control"
ng-model="visualization.options.yAxis.min"
placeholder="Auto">
</div>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">y Axis max</label>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-8">Show Labels</label>
<div class="col-sm-4">
<input type="checkbox" ng-model="options.xAxis.labels.enabled">
<div class="col-sm-10">
<input name="yAxisMin" type="number" class="form-control"
ng-model="visualization.options.yAxis.max"
placeholder="Auto">
</div>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Sort X Values</label>
</div>
<div class="col-sm-10">
<input name="sortX" type="checkbox" class="form-control"
ng-model="visualization.options.sortX">
</div>
</div>
</div>
</div>
</div>
<div ng-repeat="yAxis in options.yAxis" class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{$index == 0 ? 'Left' : 'Right'}} Y Axis</h3>
<div class="row">
<div class="col-lg-6">
<div class="list-group">
<div class="list-group-item active">
Columns Mapping
</div>
<div class="list-group-item">
<div class="form-group" ng-repeat="column in columns">
<label class="control-label col-sm-4">{{column.name}}</label>
<div class="col-sm-8">
<select ng-options="value as key for (key, value) in columnTypes" class="form-control"
ng-model="columnTypeSelection[column.name]"></select>
</div>
</div>
</div>
</div>
</div>
<div class="panel-body">
<div class="form-group row">
<label class="control-label col-sm-3">Scale</label>
<div class="col-sm-9">
<ui-select ng-model="yAxis.type">
<ui-select-match placeholder="Choose Scale...">{{$select.selected | capitalize}}</ui-select-match>
<ui-select-choices repeat="scaleType in yAxisScales">
<div ng-bind-html="scaleType | capitalize | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<div class="col-lg-6" ng-if="series.length > 0">
<div class="list-group" ng-repeat="seriesName in series">
<div class="list-group-item active">
{{seriesName}}
</div>
<div class="list-group-item">
<div class="form-group">
<label class="control-label col-sm-3">Type</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].type"
ng-options="value as key for (key, value) in seriesTypes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">zIndex</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].zIndex"
ng-options="o as o for o in zIndexes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">y Axis</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].yAxis"
ng-options="o[0] as o[1] for o in yAxes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">Name</label>
<div class="col-sm-9">
<input name="seriesName" type="text" class="form-control"
ng-model="visualization.options.seriesOptions[seriesName].name"
placeholder="{{seriesName}}">
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">Color</label>
<div class="col-sm-9">
<select class="form-control" ng-model="visualization.options.seriesOptions[seriesName].color" ng-options="val as key for (key,val) in palette"></select>
</div>
</div>
</div>
</div>
</div>
<div class="form-group row">
<label class="control-label col-sm-3">Name</label>
<div class="col-sm-9">
<input ng-model="yAxis.title.text" type="text" class="form-control"></input>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading"><h3 class="panel-title">Series Options</h3></div>
<div>
<table class="table table-condensed col-table">
<thead>
<th>zIndex</th>
<th>Column</th>
<th>Left Y Axis</th>
<th>Right Y Axis</th>
<th>Label</th>
<th>Color</th>
<th>Type</th>
</thead>
<tbody ui-sortable ng-model="form.seriesList">
<tr ng-repeat="name in form.seriesList">
<td style="cursor: move;"><i class="fa fa-arrows-v"></i> <span ng-bind="options.seriesOptions[name].zIndex + 1"></span></td>
<td>{{name}}</td>
<td>
<input type="radio" ng-value="0" ng-model="options.seriesOptions[name].yAxis">
</td>
<td>
<input type="radio" ng-value="1" ng-model="options.seriesOptions[name].yAxis">
</td>
<td style="padding: 3px; width: 140px;">
<input placeholder="{{name}}" class="form-control input-sm super-small-input" type="text" ng-model="options.seriesOptions[name].name">
</td>
<td style="padding: 3px; width: 35px;">
<ui-select ng-model="options.seriesOptions[name].color">
<ui-select-match><color-box color="$select.selected.value"></color-box></ui-select-match>
<ui-select-choices repeat="color.value as (key, color) in colors">
<color-box color="color.value"></color-box><span ng-bind-html="color.key | capitalize | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
</td>
<td style="padding: 3px; width: 105px;">
<ui-select ng-model="options.seriesOptions[name].type">
<ui-select-match placeholder="Chart Type"><i class="fa fa-{{$select.selected.value.icon}}"></i> {{$select.selected.value.name}}</ui-select-match>
<ui-select-choices repeat="info.chartType as (chartType, info) in chartTypes">
<div><i class="fa fa-{{info.value.icon}}"></i><span> </span><span ng-bind-html="info.value.name | highlight: $select.search"></span></div>
</ui-select-choices>
</ui-select>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</form>

View File

@@ -1,8 +1,15 @@
<div class="form-group">
<label class="control-label">Time Interval</label>
<select class="form-control" ng-model="visualization.options.timeInterval">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<label class="control-label">Time Label</label>
<input type="text" class="form-control" ng-model="cohortOptions.timeLabel">
<label class="control-label">People Label</label>
<input type="text" class="form-control" ng-model="cohortOptions.peopleLabel">
<label class="control-label">Bucket Column</label>
<select ng-model="bucket_column" ng-options="value as key for (key, value) in columns" class="form-control"></select>
<label class="control-label">Bucket Total Value Column</label>
<select ng-model="total_column" ng-options="value as key for (key, value) in columns" class="form-control"></select>
<label class="control-label">Day Number Column</label>
<select ng-model="value_column" ng-options="value as key for (key, value) in columns" class="form-control"></select>
<label class="control-label">Day Value Column</label>
<select ng-model="day_column" ng-options="value as key for (key, value) in columns" class="form-control"></select>
</div>

View File

@@ -1,28 +1,20 @@
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-6">Counter Value Column Name</label>
<label class="col-lg-6">Row Number</label>
<div class="col-lg-6">
<input type="number" ng-model="visualization.options.rowNumber" class="form-control">
</div>
</div>
<div class="form-group">
<label class="col-lg-6">Counter Column Name</label>
<div class="col-lg-6">
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.counterColName" class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="col-lg-6">Counter Value Row Number</label>
<label class="col-lg-6">Target Column Name</label>
<div class="col-lg-6">
<input type="number" ng-model="visualization.options.rowNumber" min="1" class="form-control">
</div>
</div>
<div class="form-group">
<label class="col-lg-6">Target Value Column Name</label>
<div class="col-lg-6">
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.targetColName" class="form-control">
<option value="">No target value</option>
</select>
</div>
</div>
<div class="form-group" ng-if="visualization.options.targetColName">
<label class="col-lg-6">Target Value Row Number</label>
<div class="col-lg-6">
<input type="number" ng-model="visualization.options.targetRowNumber" min="1" class="form-control">
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.targetColName" class="form-control"></select>
</div>
</div>
</div>

View File

@@ -1,8 +0,0 @@
<div>
<span>
From <input type="date" ng-model="dateRangeHuman.min">
</span>
<span>
To <input type="date" ng-model="dateRangeHuman.max">
</span>
</div>

View File

@@ -1,42 +1,25 @@
<div>
<span ng-click="openEditor=!openEditor" class="details-toggle" ng-class="{open: openEditor}">Edit</span>
<form ng-show="openEditor" role="form" name="visForm" ng-submit="submit()" class="form-horizontal">
<div class="row">
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Visualization Type</label>
<div class="col-sm-7">
<select required ng-model="visualization.type" ng-options="value as key for (key, value) in visTypes" class="form-control" ng-change="typeChanged()"></select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group row">
<label class="control-label col-sm-5">Name</label>
<div class="col-sm-7">
<input name="name" type="text" class="form-control" ng-model="visualization.name" placeholder="{{visualization.type | capitalize}}">
</div>
</div>
</div>
<form ng-if="openEditor" role="form" name="visForm" ng-submit="submit()">
<div class="form-group">
<label class="control-label">Name</label>
<input name="name" type="text" class="form-control" ng-model="visualization.name" placeholder="{{visualization.type | capitalize}}">
</div>
<div class="row">
<div class="col-md-12">
<visualization-options-editor></visualization-options-editor>
</div>
<div class="form-group">
<label class="control-label">Visualization Type</label>
<select required ng-model="visualization.type" ng-options="value as key for (key, value) in visTypes" class="form-control" ng-change="typeChanged()"></select>
</div>
<visualization-options-editor></visualization-options-editor>
<div class="form-group" ng-if="editRawOptions">
<label class="control-label">Advanced</label>
<textarea json-text ng-model="visualization.options" class="form-control" rows="10"></textarea>
</div>
<div class="form-group text-center">
<div class="form-group">
<button type="submit" class="btn btn-primary">Save</button>
</div>

View File

@@ -1,17 +1,8 @@
<div class="well well-sm filters-container" ng-show="filters">
<div class="filter" ng-repeat="filter in filters">
<ui-select ng-model="filter.current" ng-if="!filter.multiple">
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected}}</ui-select-match>
<ui-select-choices repeat="value in filter.values | filter: $select.search track by $index">
{{value}}
</ui-select-choices>
</ui-select>
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple">
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$item}}</ui-select-match>
<ui-select-choices repeat="value in filter.values | filter: $select.search track by $index">
{{value}}
</ui-select-choices>
</ui-select>
<div class="well well-sm" ng-show="filters">
<div ng-repeat="filter in filters">
{{filter.friendlyName}}:
<select ui-select2='select2Options' ng-model="filter.current" ng-multiple="{{filter.multiple}}">
<option ng-repeat="value in filter.values" value="{{value}}">{{value}}</option>
</select>
</div>
</div>

View File

@@ -1,3 +0,0 @@
<div style='margin:1%;width:98%;height:{{visualization.options.height}}px'>
<div style="width:100%; height:100%;"></div>
</div>

View File

@@ -1,55 +0,0 @@
<div class="form-horizontal">
<div class="form-group">
<label class="col-lg-2">Map height (px)</label>
<div class="col-sm-4">
<input class="form-control" type="number" ng-model = "visualization.options.height" />
</div>
</div>
<div class="form-group">
<label class="col-lg-2">Draw option</label>
<div class="col-sm-4">
<select ng-options="opt for opt in draw_options" ng-model="visualization.options.draw" class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="col-lg-2">Latitude column name</label>
<div class="col-sm-4">
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.latColName" class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="col-lg-2">Longitude column name</label>
<div class="col-sm-4">
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.lonColName" class="form-control"></select>
</div>
</div>
<div ng-show = "visualization.options.draw == 'Color'">
<div class="form-group">
<label class="col-lg-2">Classify by column</label>
<div class="col-sm-4">
<select ng-options="name for name in classify_columns" ng-model="visualization.options.classify" class="form-control"></select>
</div>
</div>
<div class="row" >
<div class="col-lg-6">
<div ng-repeat="element in visualization.options.classification" class="list-group">
<div class="list-group-item active">
{{element.value}}
</div>
<div class="list-group-item">
<div class="form-group">
<label class="col-lg-4">Color</label>
<div class="col-sm-4">
<input class="form-control" style="background-color:{{element.color}};" type="text" ng-model = "element.color" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

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