diff --git a/.pyup.yml b/.pyup.yml
new file mode 100644
index 0000000000..3694ef8f58
--- /dev/null
+++ b/.pyup.yml
@@ -0,0 +1,8 @@
+schedule: "every day"
+search: False
+update: insecure
+requirements:
+ - requirements.txt:
+ update: insecure
+ - requirements-newrelic.txt:
+ update: security
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index d0282d4181..3a84db223e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,6 +6,15 @@ COPY requirements.txt requirements_dev.txt requirements_all_ds.txt ./
RUN pip install -r requirements.txt -r requirements_dev.txt -r requirements_all_ds.txt
COPY . ./
+
+# Upgrade node to LTS 6.11.2
+RUN cd ~
+RUN wget https://nodejs.org/download/release/v6.11.2/node-v6.11.2-linux-x64.tar.gz
+RUN sudo tar --strip-components 1 -xzvf node-v* -C /usr/local
+
+# Upgrade npm
+RUN npm upgrade npm
+
RUN npm install && npm run build && rm -rf node_modules
RUN chown -R redash /app
USER redash
diff --git a/bin/deploy b/bin/deploy
new file mode 100755
index 0000000000..bccc6cb999
--- /dev/null
+++ b/bin/deploy
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -eo pipefail
+
+[ ! -z $DOCKERHUB_REPO ] && [ $# -eq 1 ]
+
+VERSION="$1"
+
+printf '{"commit":"%s","version":"%s","source":"https://github.com/%s/%s","build":"%s"}\n' \
+ "$CIRCLE_SHA1" \
+ "$VERSION" \
+ "$CIRCLE_PROJECT_USERNAME" \
+ "$CIRCLE_PROJECT_REPONAME" \
+ "$CIRCLE_BUILD_URL" \
+> version.json
+
+docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
+docker build -t $DOCKERHUB_REPO:$VERSION .
+docker push $DOCKERHUB_REPO:$VERSION
diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint
index 5121103ef2..cb4a8000fb 100755
--- a/bin/docker-entrypoint
+++ b/bin/docker-entrypoint
@@ -2,6 +2,7 @@
set -e
worker() {
+ /app/manage.py db upgrade
WORKERS_COUNT=${WORKERS_COUNT:-2}
QUEUES=${QUEUES:-queries,scheduled_queries,celery}
@@ -10,6 +11,7 @@ worker() {
}
scheduler() {
+ /app/manage.py db upgrade
WORKERS_COUNT=${WORKERS_COUNT:-1}
QUEUES=${QUEUES:-celery}
@@ -19,6 +21,7 @@ scheduler() {
}
server() {
+ /app/manage.py db upgrade
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app
}
diff --git a/circle.yml b/circle.yml
index 45c1301ac6..db19810c8b 100644
--- a/circle.yml
+++ b/circle.yml
@@ -17,20 +17,16 @@ test:
override:
- pytest --junitxml=$CIRCLE_TEST_REPORTS/junit.xml tests/
deployment:
- github_and_docker:
- branch: [master, /release.*/]
+ latest:
+ branch: master
+ owner: mozilla
commands:
- - make pack
- # Skipping uploads for now, until master is stable.
- # - make upload
- #- echo "client/app" >> .dockerignore
- #- docker pull redash/redash:latest
- - docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
- - docker build -t redash/redash:$(./manage.py version | sed -e "s/\+/./") .
- - docker push redash/redash:$(./manage.py version | sed -e "s/\+/./")
-notify:
- webhooks:
- - url: https://webhooks.gitter.im/e/895d09c3165a0913ac2f
+ - ./bin/deploy "latest"
+ hub_releases:
+ tag: /^m[0-9]+(\.[0-9]+)?$/
+ owner: mozilla
+ commands:
+ - ./bin/deploy "$CIRCLE_TAG"
general:
branches:
ignore:
diff --git a/client/app/components/dashboards/edit-dashboard-dialog.html b/client/app/components/dashboards/edit-dashboard-dialog.html
index edac266bfc..02855f7dab 100644
--- a/client/app/components/dashboards/edit-dashboard-dialog.html
+++ b/client/app/components/dashboards/edit-dashboard-dialog.html
@@ -4,7 +4,7 @@
Edit: {{$ctrl.dashboard.name}}
-
+
diff --git a/client/app/components/dashboards/edit-dashboard-dialog.js b/client/app/components/dashboards/edit-dashboard-dialog.js
index 40a31efaa2..9727353658 100644
--- a/client/app/components/dashboards/edit-dashboard-dialog.js
+++ b/client/app/components/dashboards/edit-dashboard-dialog.js
@@ -85,7 +85,6 @@ const EditDashboardDialog = {
'Please copy/backup your changes and reload this page.', { autoDismiss: false });
}
});
- Events.record('edit', 'dashboard', this.dashboard.id);
} else {
$http.post('api/dashboards', {
name: this.dashboard.name,
@@ -96,6 +95,16 @@ const EditDashboardDialog = {
Events.record('create', 'dashboard');
}
};
+ this.saveDashboardOnEnter = ($event) => {
+ // keyCode 13 is the Enter key
+ if ($event.keyCode === 13) {
+ this.saveDashboard();
+ }
+ };
+ this.closeWithoutSave = () => {
+ this.dashboard.name = this.dashboard.existing_name;
+ this.dismiss();
+ };
},
};
diff --git a/client/app/components/dashboards/widget.js b/client/app/components/dashboards/widget.js
index 82dca750f2..47c04dcdd5 100644
--- a/client/app/components/dashboards/widget.js
+++ b/client/app/components/dashboards/widget.js
@@ -42,6 +42,8 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
resolve: {
widget: this.widget,
},
+ backdrop: 'static',
+ keyboard: false,
});
};
@@ -57,8 +59,6 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
return;
}
- Events.record('delete', 'widget', this.widget.id);
-
this.widget.$delete((response) => {
this.dashboard.widgets =
this.dashboard.widgets.map(row => row.filter(widget => widget.id !== undefined));
diff --git a/client/app/components/dynamic-form.html b/client/app/components/dynamic-form.html
index 066a6a2ecc..519882de1e 100644
--- a/client/app/components/dynamic-form.html
+++ b/client/app/components/dynamic-form.html
@@ -11,6 +11,7 @@
{{field.property.title || field.name | capitalize}}
+
{{ field.property.info }}
{
+ keywords[table.name] = 'Table';
+
+ table.columns.forEach((c) => { // autoCompleteColumns
+ if (c.charAt(c.length - 1) === ')') {
+ let parensStartAt = c.indexOf('(') - 1;
+ c = c.substring(0, parensStartAt);
+ parensStartAt = 1; // linter complains without this line
+ }
+ // remove '[P] ' for partition keys
+ if (c.charAt(0) === '[') {
+ c = c.substring(4, c.length);
+ }
+ // keywords[c] = 'Column'; // dups columns
+ keywords[`${table.name}.${c}`] = 'Column';
+ });
+ });
+
+ $scope.autoCompleteSchema.keywords = map(keywords, (v, k) =>
+ ({
+ name: k,
+ value: k,
+ score: 0,
+ meta: v,
+ }));
+ }
+ callback(null, $scope.autoCompleteSchema.keywords);
+ },
+ };
+
$scope.editorOptions = {
mode: 'json',
// require: ['ace/ext/language_tools'],
@@ -70,16 +113,18 @@ function queryEditor(QuerySnippet) {
editor.getSession().setMode(newMode);
});
- $scope.$watch('schema', (newSchema, oldSchema) => {
+ $scope.$watch('autoCompleteSchema', (newSchema, oldSchema) => {
if (newSchema !== oldSchema) {
const tokensCount =
newSchema.reduce((totalLength, table) => totalLength + table.columns.length, 0);
- // If there are too many tokens we disable live autocomplete,
- // as it makes typing slower.
- if (tokensCount > 5000) {
+ // If there are too many tokens or if it's requested via the UI
+ // we disable live autocomplete, as it makes typing slower.
+ if (tokensCount > 5000 || !$scope.$parent.autocompleteQuery) {
editor.setOption('enableLiveAutocompletion', false);
+ editor.setOption('enableBasicAutocompletion', false);
} else {
editor.setOption('enableLiveAutocompletion', true);
+ editor.setOption('enableBasicAutocompletion', true);
}
}
});
@@ -87,44 +132,15 @@ function queryEditor(QuerySnippet) {
$scope.$parent.$on('angular-resizable.resizing', () => {
editor.resize();
});
+ $scope.$parent.$watch('autocompleteQuery', () => {
+ editor.setOption('enableLiveAutocompletion', $scope.$parent.autocompleteQuery);
+ editor.setOption('enableBasicAutocompletion', $scope.$parent.autocompleteQuery);
+ });
editor.focus();
},
};
-
- const schemaCompleter = {
- getCompletions(state, session, pos, prefix, callback) {
- if (prefix.length === 0 || !$scope.schema) {
- callback(null, []);
- return;
- }
-
- if (!$scope.schema.keywords) {
- const keywords = {};
-
- $scope.schema.forEach((table) => {
- keywords[table.name] = 'Table';
-
- table.columns.forEach((c) => {
- keywords[c] = 'Column';
- keywords[`${table.name}.${c}`] = 'Column';
- });
- });
-
- $scope.schema.keywords = map(keywords, (v, k) =>
- ({
- name: k,
- value: k,
- score: 0,
- meta: v,
- }));
- }
- callback(null, $scope.schema.keywords);
- },
- };
-
-
window.ace.acequire(['ace/ext/language_tools'], (langTools) => {
langTools.addCompleter(schemaCompleter);
});
diff --git a/client/app/components/queries/schedule-dialog.html b/client/app/components/queries/schedule-dialog.html
index 8f1ab21541..f9344238a1 100644
--- a/client/app/components/queries/schedule-dialog.html
+++ b/client/app/components/queries/schedule-dialog.html
@@ -15,4 +15,8 @@
Refresh Schedule
+
+ Stop scheduling at date/time (format yyyy-MM-ddTHH:mm:ss, like 2016-12-28T14:57:00):
+
+
diff --git a/client/app/components/queries/schedule-dialog.js b/client/app/components/queries/schedule-dialog.js
index 3561bba31c..fdfa2dd259 100644
--- a/client/app/components/queries/schedule-dialog.js
+++ b/client/app/components/queries/schedule-dialog.js
@@ -124,6 +124,17 @@ function queryRefreshSelect() {
};
}
+function scheduleUntil() {
+ return {
+ restrict: 'E',
+ scope: {
+ query: '=',
+ saveQuery: '=',
+ },
+ template: ' ',
+ };
+}
+
const ScheduleForm = {
controller() {
this.query = this.resolve.query;
@@ -146,5 +157,6 @@ const ScheduleForm = {
export default function init(ngModule) {
ngModule.directive('queryTimePicker', queryTimePicker);
ngModule.directive('queryRefreshSelect', queryRefreshSelect);
+ ngModule.directive('scheduleUntil', scheduleUntil);
ngModule.component('scheduleDialog', ScheduleForm);
}
diff --git a/client/app/components/queries/schema-browser.html b/client/app/components/queries/schema-browser.html
index 65905cd9ae..7d0a950f2c 100644
--- a/client/app/components/queries/schema-browser.html
+++ b/client/app/components/queries/schema-browser.html
@@ -1,15 +1,25 @@
-
+
+
+
+
+
+
+
-
+
diff --git a/client/app/components/queries/schema-browser.js b/client/app/components/queries/schema-browser.js
index 7826a86e2b..b6140ef9eb 100644
--- a/client/app/components/queries/schema-browser.js
+++ b/client/app/components/queries/schema-browser.js
@@ -3,6 +3,9 @@ import template from './schema-browser.html';
function SchemaBrowserCtrl($scope) {
'ngInject';
+ this.versionToggle = false;
+ this.versionFilter = 'abcdefghijklmnop';
+
this.showTable = (table) => {
table.collapsed = !table.collapsed;
$scope.$broadcast('vsRepeatTrigger');
@@ -17,12 +20,24 @@ function SchemaBrowserCtrl($scope) {
return size;
};
+
+ this.flipToggleVersionedTables = (versionToggle, toggleString) => {
+ if (versionToggle === false) {
+ this.versionToggle = true;
+ this.versionFilter = toggleString;
+ } else {
+ this.versionToggle = false;
+ this.versionFilter = 'abcdefghijklmnop';
+ }
+ };
}
const SchemaBrowser = {
bindings: {
schema: '<',
+ tabletogglestring: '<',
onRefresh: '&',
+ flipToggleVersionedTables: '&',
},
controller: SchemaBrowserCtrl,
template,
diff --git a/client/app/pages/admin/outdated-queries/index.js b/client/app/pages/admin/outdated-queries/index.js
index aa5a54ad5d..e1dfd1e280 100644
--- a/client/app/pages/admin/outdated-queries/index.js
+++ b/client/app/pages/admin/outdated-queries/index.js
@@ -3,8 +3,7 @@ import moment from 'moment';
import { Paginator } from '@/lib/pagination';
import template from './outdated-queries.html';
-function OutdatedQueriesCtrl($scope, Events, $http, $timeout) {
- Events.record('view', 'page', 'admin/outdated_queries');
+function OutdatedQueriesCtrl($scope, $http, $timeout) {
$scope.autoUpdate = true;
this.queries = new Paginator([], { itemsPerPage: 50 });
diff --git a/client/app/pages/admin/status/index.js b/client/app/pages/admin/status/index.js
index bafe413a41..1cc092ffc6 100644
--- a/client/app/pages/admin/status/index.js
+++ b/client/app/pages/admin/status/index.js
@@ -10,6 +10,8 @@ function AdminStatusCtrl($scope, $http, $timeout, currentUser, Events) {
delete data.workers;
$scope.manager = data.manager;
delete data.manager;
+ $scope.database_metrics = data.database_metrics;
+ delete data.database_metrics;
$scope.status = data;
});
diff --git a/client/app/pages/admin/status/status.html b/client/app/pages/admin/status/status.html
index 1aad8a94d6..85259a00f4 100644
--- a/client/app/pages/admin/status/status.html
+++ b/client/app/pages/admin/status/status.html
@@ -43,5 +43,15 @@
+
+
+
+ Redash Database
+
+ {{size[1]}}
+ {{size[0]}}
+
+
+
diff --git a/client/app/pages/admin/tasks/index.js b/client/app/pages/admin/tasks/index.js
index 53d9007ea9..bceb11e53c 100644
--- a/client/app/pages/admin/tasks/index.js
+++ b/client/app/pages/admin/tasks/index.js
@@ -3,8 +3,7 @@ import moment from 'moment';
import { Paginator } from '@/lib/pagination';
import template from './tasks.html';
-function TasksCtrl($scope, $location, $http, $timeout, Events) {
- Events.record('view', 'page', 'admin/tasks');
+function TasksCtrl($scope, $location, $http, $timeout) {
$scope.autoUpdate = true;
$scope.selectedTab = 'in_progress';
diff --git a/client/app/pages/alert/index.js b/client/app/pages/alert/index.js
index 805ff24da4..1b79cc93b1 100644
--- a/client/app/pages/alert/index.js
+++ b/client/app/pages/alert/index.js
@@ -6,8 +6,6 @@ function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Ev
if (this.alertId === 'new') {
Events.record('view', 'page', 'alerts/new');
- } else {
- Events.record('view', 'alert', this.alertId);
}
this.trustAsHtml = html => $sce.trustAsHtml(html);
@@ -47,7 +45,7 @@ function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Ev
return;
}
- Query.search({ q: term }, (results) => {
+ Query.search({ q: term, include_drafts: true }, (results) => {
this.queries = results;
});
};
diff --git a/client/app/pages/alerts-list/index.js b/client/app/pages/alerts-list/index.js
index 8aa44533c7..4301464cf3 100644
--- a/client/app/pages/alerts-list/index.js
+++ b/client/app/pages/alerts-list/index.js
@@ -8,9 +8,7 @@ const stateClass = {
};
class AlertsListCtrl {
- constructor(Events, Alert) {
- Events.record('view', 'page', 'alerts');
-
+ constructor(Alert) {
this.alerts = new Paginator([], { itemsPerPage: 20 });
Alert.query((alerts) => {
this.alerts.updateRows(alerts.map(alert => ({
diff --git a/client/app/pages/dashboards/dashboard-list.js b/client/app/pages/dashboards/dashboard-list.js
index 2d0e9d83cb..b89b52ccd0 100644
--- a/client/app/pages/dashboards/dashboard-list.js
+++ b/client/app/pages/dashboards/dashboard-list.js
@@ -6,7 +6,7 @@ import './dashboard-list.css';
function DashboardListCtrl(Dashboard, $location, clientConfig) {
- const TAGS_REGEX = /(^([\w\s]|[^\u0000-\u007F])+):|(#([\w-]|[^\u0000-\u007F])+)/ig;
+ const TAGS_REGEX = /(^([\w\s/]|[^\u0000-\u007F])+):|(#([\w-]|[^\u0000-\u007F])+)/ig;
this.logoUrl = clientConfig.logoUrl;
const page = parseInt($location.search().page || 1, 10);
diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js
index 6e76fdbe22..3a79f80acb 100644
--- a/client/app/pages/dashboards/dashboard.js
+++ b/client/app/pages/dashboards/dashboard.js
@@ -112,7 +112,6 @@ function DashboardCtrl(
this.loadDashboard = _.throttle((force) => {
this.dashboard = Dashboard.get({ slug: $routeParams.dashboardSlug }, (dashboard) => {
- Events.record('view', 'dashboard', dashboard.id);
renderDashboard(dashboard, force);
}, () => {
// error...
@@ -133,7 +132,6 @@ function DashboardCtrl(
this.archiveDashboard = () => {
const archive = () => {
- Events.record('archive', 'dashboard', this.dashboard.id);
this.dashboard.$delete(() => {
$rootScope.$broadcast('reloadDashboards');
});
diff --git a/client/app/pages/data-sources/list.html b/client/app/pages/data-sources/list.html
index 0ccf480eef..3a40c10343 100644
--- a/client/app/pages/data-sources/list.html
+++ b/client/app/pages/data-sources/list.html
@@ -5,7 +5,7 @@
New Data Source
diff --git a/client/app/pages/data-sources/list.js b/client/app/pages/data-sources/list.js
index 497da8a560..a09ce0eb59 100644
--- a/client/app/pages/data-sources/list.js
+++ b/client/app/pages/data-sources/list.js
@@ -1,8 +1,6 @@
import template from './list.html';
-function DataSourcesCtrl($scope, $location, currentUser, Events, DataSource) {
- Events.record('view', 'page', 'admin/data_sources');
-
+function DataSourcesCtrl($scope, $location, currentUser, DataSource) {
$scope.dataSources = DataSource.query();
}
diff --git a/client/app/pages/data-sources/show.js b/client/app/pages/data-sources/show.js
index bd7e9106fd..ed85aa4882 100644
--- a/client/app/pages/data-sources/show.js
+++ b/client/app/pages/data-sources/show.js
@@ -5,10 +5,8 @@ const logger = debug('redash:http');
function DataSourceCtrl(
$scope, $routeParams, $http, $location, toastr,
- currentUser, Events, DataSource,
+ currentUser, DataSource,
) {
- Events.record('view', 'page', 'admin/data_source');
-
$scope.dataSourceId = $routeParams.dataSourceId;
if ($scope.dataSourceId === 'new') {
@@ -24,8 +22,6 @@ function DataSourceCtrl(
});
function deleteDataSource() {
- Events.record('delete', 'datasource', $scope.dataSource.id);
-
$scope.dataSource.$delete(() => {
toastr.success('Data source deleted successfully.');
$location.path('/data_sources/');
@@ -36,8 +32,6 @@ function DataSourceCtrl(
}
function testConnection(callback) {
- Events.record('test', 'datasource', $scope.dataSource.id);
-
DataSource.test({ id: $scope.dataSource.id }, (httpResponse) => {
if (httpResponse.ok) {
toastr.success('Success');
@@ -52,11 +46,30 @@ function DataSourceCtrl(
});
}
+ function getDataSourceVersion(callback) {
+ DataSource.version({ id: $scope.dataSource.id }, (httpResponse) => {
+ if (httpResponse.ok) {
+ const versionNumber = httpResponse.message;
+ toastr.success(`Success. Version: ${versionNumber}`);
+ } else {
+ toastr.error(httpResponse.message, 'Version Test Failed:', { timeOut: 10000 });
+ }
+ callback();
+ }, (httpResponse) => {
+ logger('Failed to get data source version: ', httpResponse.status, httpResponse.statusText, httpResponse);
+ toastr.error('Unknown error occurred while performing data source version test. Please try again later.', 'Data Source Version Test Failed:', { timeOut: 10000 });
+ callback();
+ });
+ }
+
$scope.actions = [
{ name: 'Delete', class: 'btn-danger', callback: deleteDataSource },
{
name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true,
},
+ {
+ name: 'Test Data Source Version', class: 'btn-default', callback: getDataSourceVersion, disableWhenDirty: true,
+ },
];
}
diff --git a/client/app/pages/destinations/list.js b/client/app/pages/destinations/list.js
index 8bc73e085f..c6599c2791 100644
--- a/client/app/pages/destinations/list.js
+++ b/client/app/pages/destinations/list.js
@@ -1,8 +1,6 @@
import template from './list.html';
-function DestinationsCtrl($scope, $location, toastr, currentUser, Events, Destination) {
- Events.record('view', 'page', 'admin/destinations');
-
+function DestinationsCtrl($scope, $location, toastr, currentUser, Destination) {
$scope.destinations = Destination.query();
}
diff --git a/client/app/pages/destinations/show.js b/client/app/pages/destinations/show.js
index 689240ba8c..277d1501c8 100644
--- a/client/app/pages/destinations/show.js
+++ b/client/app/pages/destinations/show.js
@@ -6,10 +6,8 @@ const logger = debug('redash:http');
function DestinationCtrl(
$scope, $routeParams, $http, $location, toastr,
- currentUser, Events, Destination,
+ currentUser, Destination,
) {
- Events.record('view', 'page', 'admin/destination');
-
$scope.destinationId = $routeParams.destinationId;
if ($scope.destinationId === 'new') {
@@ -25,8 +23,6 @@ function DestinationCtrl(
});
$scope.delete = () => {
- Events.record('delete', 'destination', $scope.destination.id);
-
$scope.destination.$delete(() => {
toastr.success('Destination deleted successfully.');
$location.path('/destinations/');
diff --git a/client/app/pages/groups/data-sources.js b/client/app/pages/groups/data-sources.js
index 4c4ccd4791..6cbfe9249f 100644
--- a/client/app/pages/groups/data-sources.js
+++ b/client/app/pages/groups/data-sources.js
@@ -1,8 +1,7 @@
import { contains } from 'underscore';
import template from './data-sources.html';
-function GroupDataSourcesCtrl($scope, $routeParams, $http, Events, Group, DataSource) {
- Events.record('view', 'group_data_sources', $scope.groupId);
+function GroupDataSourcesCtrl($scope, $routeParams, $http, Group, DataSource) {
$scope.group = Group.get({ id: $routeParams.groupId });
$scope.dataSources = Group.dataSources({ id: $routeParams.groupId });
$scope.newDataSource = {};
diff --git a/client/app/pages/groups/list.js b/client/app/pages/groups/list.js
index 94bc83aa0f..1ee987f251 100644
--- a/client/app/pages/groups/list.js
+++ b/client/app/pages/groups/list.js
@@ -1,8 +1,7 @@
import { Paginator } from '@/lib/pagination';
import template from './list.html';
-function GroupsCtrl($scope, $uibModal, currentUser, Events, Group) {
- Events.record('view', 'page', 'groups');
+function GroupsCtrl($scope, $uibModal, currentUser, Group) {
$scope.currentUser = currentUser;
$scope.groups = new Paginator([], { itemsPerPage: 20 });
Group.query((groups) => {
diff --git a/client/app/pages/groups/show.js b/client/app/pages/groups/show.js
index 2bb532d4f6..7f8b3e331a 100644
--- a/client/app/pages/groups/show.js
+++ b/client/app/pages/groups/show.js
@@ -1,9 +1,7 @@
import { contains } from 'underscore';
import template from './show.html';
-function GroupCtrl($scope, $routeParams, $http, currentUser, Events, Group, User) {
- Events.record('view', 'group', $scope.groupId);
-
+function GroupCtrl($scope, $routeParams, $http, currentUser, Group, User) {
$scope.currentUser = currentUser;
$scope.group = Group.get({ id: $routeParams.groupId });
$scope.members = Group.members({ id: $routeParams.groupId });
diff --git a/client/app/pages/queries/add-to-dashboard.html b/client/app/pages/queries/add-to-dashboard.html
new file mode 100644
index 0000000000..1f5e6f027a
--- /dev/null
+++ b/client/app/pages/queries/add-to-dashboard.html
@@ -0,0 +1,23 @@
+
+
diff --git a/client/app/pages/queries/add-to-dashboard.js b/client/app/pages/queries/add-to-dashboard.js
new file mode 100644
index 0000000000..5eca59975c
--- /dev/null
+++ b/client/app/pages/queries/add-to-dashboard.js
@@ -0,0 +1,72 @@
+import template from './add-to-dashboard.html';
+
+const AddToDashboardForm = {
+ controller($sce, Dashboard, currentUser, toastr, Query, Widget) {
+ 'ngInject';
+
+ this.query = this.resolve.query;
+ this.vis = this.resolve.vis;
+ this.saveAddToDashbosard = this.resolve.saveAddToDashboard;
+ this.saveInProgress = false;
+
+ this.trustAsHtml = html => $sce.trustAsHtml(html);
+
+ this.onDashboardSelected = (dash) => {
+ // add widget to dashboard
+ this.saveInProgress = true;
+ this.widgetSize = 1;
+ this.selectedVis = null;
+ this.query = {};
+ this.selected_query = this.query.id;
+ this.type = 'visualization';
+ this.isVisualization = () => this.type === 'visualization';
+
+ const widget = new Widget({
+ visualization_id: this.vis && this.vis.id,
+ dashboard_id: dash.id,
+ options: {},
+ width: this.widgetSize,
+ type: this.type,
+ });
+
+ // (response)
+ widget.$save().then(() => {
+ // (dashboard)
+ this.selectedDashboard = Dashboard.get({ slug: dash.slug }, () => {});
+ this.close();
+ }).catch(() => {
+ toastr.error('Widget can not be added');
+ }).finally(() => {
+ this.saveInProgress = false;
+ });
+ };
+
+ this.selectedDashboard = null;
+
+ this.searchDashboards = (term) => { // , limitToUsersDashboards
+ if (!term || term.length < 3) {
+ return;
+ }
+
+ Dashboard.search({
+ q: term,
+ user_id: currentUser.id,
+ // limit_to_users_dashboards: limitToUsersDashboards,
+ include_drafts: true,
+ }, (results) => {
+ this.dashboards = results;
+ });
+ };
+ },
+ bindings: {
+ resolve: '<',
+ close: '&',
+ dismiss: '&',
+ vis: '<',
+ },
+ template,
+};
+
+export default function (ngModule) {
+ ngModule.component('addToDashboardDialog', AddToDashboardForm);
+}
diff --git a/client/app/pages/queries/compare-query-dialog.css b/client/app/pages/queries/compare-query-dialog.css
new file mode 100644
index 0000000000..ce2d01370e
--- /dev/null
+++ b/client/app/pages/queries/compare-query-dialog.css
@@ -0,0 +1,54 @@
+/* Compare Query Version container */
+/* Offers slight visual improvement (alignment) to modern UAs */
+.compare-query-version {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.diff-removed {
+ background-color: rgba(208, 2, 27, 0.3);
+}
+
+.diff-added {
+ background-color: rgba(65, 117, 5, 0.3);
+}
+
+.query-diff-container span {
+ display: inline-block;
+ border-radius: 3px;
+ line-height: 20px;
+ vertical-align: middle;
+ margin: 0 5px 0 0;
+}
+
+.query-diff-container > div:not(.compare-query-version-controls) {
+ float: left;
+ width: calc(50% - 5px);
+ margin: 0 10px 0 0;
+}
+
+.compare-query-version {
+ background-color: #f5f5f5;
+ padding: 5px;
+ border: 1px solid #ccc;
+ margin-right: 15px;
+ border-radius: 3px;
+}
+
+.diff-content {
+ border: 1px solid #ccc;
+ background-color: #f5f5f5;
+ border-radius: 3px;
+ padding: 15px;
+}
+
+.query-diff-container > div:last-child {
+ margin: 0;
+}
+
+.compare-query-version-controls {
+ display: flex;
+ align-items: center;
+ margin-bottom: 25px;
+}
diff --git a/client/app/pages/queries/compare-query-dialog.html b/client/app/pages/queries/compare-query-dialog.html
new file mode 100644
index 0000000000..5214046055
--- /dev/null
+++ b/client/app/pages/queries/compare-query-dialog.html
@@ -0,0 +1,33 @@
+
+
+
+
+ Compare current version to
+
+ Version {{version.object_version}}
+
+
+
+
+
+
Current Version {{$ctrl.currentQuery.version}}
+
+
+
+ {{part.value}}
+
+
+
+
+
+
Previous Version {{$ctrl.previousQueryVersion + 1}}
+
+
{{$ctrl.previousQuery}}
+
+
+
diff --git a/client/app/pages/queries/compare-query-dialog.js b/client/app/pages/queries/compare-query-dialog.js
new file mode 100644
index 0000000000..fb4338971a
--- /dev/null
+++ b/client/app/pages/queries/compare-query-dialog.js
@@ -0,0 +1,63 @@
+import * as jsDiff from 'diff';
+import template from './compare-query-dialog.html';
+import './compare-query-dialog.css';
+
+const CompareQueryDialog = {
+ controller: ['clientConfig', '$http', function doCompare(clientConfig, $http) {
+ this.currentQuery = this.resolve.query;
+
+ this.previousQuery = '';
+ this.currentDiff = [];
+ this.previousDiff = [];
+ this.versions = [];
+ this.previousQueryVersion = this.currentQuery.version - 2; // due to 0-indexed versions[]
+
+ this.compareQueries = (isInitialLoad) => {
+ if (!isInitialLoad) {
+ this.previousQueryVersion = document.getElementById('version-choice').value - 1; // due to 0-indexed versions[]
+ }
+
+ this.previousQuery = this.versions[this.previousQueryVersion].change.query.current;
+ this.currentDiff = jsDiff.diffChars(this.previousQuery, this.currentQuery.query);
+ document.querySelector('.compare-query-revert-wrapper').classList.remove('hidden');
+ };
+
+ this.revertQuery = () => {
+ this.resolve.query.query = this.previousQuery;
+ this.resolve.saveQuery();
+
+ // Close modal.
+ this.dismiss();
+ };
+
+ $http.get(`/api/queries/${this.currentQuery.id}/version`).then((response) => {
+ this.versions = response.data;
+
+ const compare = (a, b) => {
+ if (a.object_version < b.object_version) {
+ return -1;
+ } else if (a.object_version > b.object_version) {
+ return 1;
+ }
+ return 0;
+ };
+
+ this.versions.sort(compare);
+ this.compareQueries(true);
+ });
+ }],
+ scope: {
+ query: '=',
+ saveQuery: '<',
+ },
+ bindings: {
+ resolve: '<',
+ close: '&',
+ dismiss: '&',
+ },
+ template,
+};
+
+export default function (ngModule) {
+ ngModule.component('compareQueryDialog', CompareQueryDialog);
+}
diff --git a/client/app/pages/queries/get-data-source-version.js b/client/app/pages/queries/get-data-source-version.js
new file mode 100644
index 0000000000..90df5cb61c
--- /dev/null
+++ b/client/app/pages/queries/get-data-source-version.js
@@ -0,0 +1,19 @@
+function GetDataSourceVersionCtrl(Events, toastr, $scope, DataSource, $route) {
+ 'ngInject';
+
+ this.getDataSourceVersion = DataSource.version({
+ id: $route.current.locals.query.data_source_id,
+ });
+}
+
+const GetDataSourceVersionInfo = {
+ bindings: {
+ onRefresh: '&',
+ },
+ controller: GetDataSourceVersionCtrl,
+ template: '{{ $ctrl.getDataSourceVersion.message }} ',
+};
+
+export default function (ngModule) {
+ ngModule.component('getDataSourceVersion', GetDataSourceVersionInfo);
+}
diff --git a/client/app/pages/queries/queries-search-results-page.js b/client/app/pages/queries/queries-search-results-page.js
index 0518399f31..e45550d7de 100644
--- a/client/app/pages/queries/queries-search-results-page.js
+++ b/client/app/pages/queries/queries-search-results-page.js
@@ -24,8 +24,6 @@ function QuerySearchCtrl($location, $filter, currentUser, Events, Query) {
$location.search({ q: this.term });
}
};
-
- Events.record('search', 'query', '', { term: this.term });
}
export default function init(ngModule) {
diff --git a/client/app/pages/queries/query.html b/client/app/pages/queries/query.html
index 5a8b19f54c..5bf328bfca 100644
--- a/client/app/pages/queries/query.html
+++ b/client/app/pages/queries/query.html
@@ -85,6 +85,7 @@
@@ -197,6 +213,14 @@
+
+
+
+ Data Scanned
+
+ {{ queryResult.query_result.data_scanned }}
+
+
@@ -238,7 +262,7 @@
×
+ ng-show="canEdit"> × +
+ New Visualization
diff --git a/client/app/pages/queries/source-view.js b/client/app/pages/queries/source-view.js
index b703fa358c..7e30a902ee 100644
--- a/client/app/pages/queries/source-view.js
+++ b/client/app/pages/queries/source-view.js
@@ -71,9 +71,12 @@ function QuerySourceCtrl(
.catch(error => toastr.error(error));
};
- $scope.duplicateQuery = () => {
- Events.record('fork', 'query', $scope.query.id);
+ $scope.autocompleteQuery = true;
+ $scope.toggleAutocompleteQuery = () => {
+ $scope.autocompleteQuery = !$scope.autocompleteQuery;
+ };
+ $scope.duplicateQuery = () => {
Query.fork({ id: $scope.query.id }, (newQuery) => {
$location.url(newQuery.getSourceLink()).replace();
});
@@ -87,8 +90,6 @@ function QuerySourceCtrl(
const confirm = { class: 'btn-danger', title: 'Delete' };
AlertDialog.open(title, message, confirm).then(() => {
- Events.record('delete', 'visualization', vis.id);
-
Visualization.delete({ id: vis.id }, () => {
if ($scope.selectedTab === String(vis.id)) {
$scope.selectedTab = DEFAULT_TAB;
diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js
index f7351fafea..40e45e193b 100644
--- a/client/app/pages/queries/view.js
+++ b/client/app/pages/queries/view.js
@@ -128,7 +128,6 @@ function QueryViewCtrl(
KeyboardShortcuts.unbind(shortcuts);
});
- Events.record('view', 'query', $scope.query.id);
if ($scope.query.hasResult() || $scope.query.paramsRequired()) {
getQueryResult();
}
@@ -260,7 +259,7 @@ function QueryViewCtrl(
updateSchema();
$scope.dataSource = find($scope.dataSources, ds => ds.id === $scope.query.data_source_id);
- $scope.executeQuery();
+ document.getElementById('data-source-version').innerHTML = ' ';
};
$scope.setVisualizationTab = (visualization) => {
@@ -268,6 +267,21 @@ function QueryViewCtrl(
$location.hash(visualization.id);
};
+ $scope.compareQueryVersion = () => {
+ if (!$scope.query.query) {
+ return;
+ }
+
+ $uibModal.open({
+ windowClass: 'modal-xl',
+ component: 'compareQueryDialog',
+ resolve: {
+ query: $scope.query,
+ saveQuery: () => $scope.saveQuery,
+ },
+ });
+ };
+
$scope.$watch('query.name', () => {
Title.set($scope.query.name);
});
@@ -348,6 +362,18 @@ function QueryViewCtrl(
});
};
+ $scope.openAddToDashboardForm = (vis) => {
+ $uibModal.open({
+ component: 'addToDashboardDialog',
+ size: 'sm',
+ resolve: {
+ query: $scope.query,
+ vis,
+ saveAddToDashboard: () => $scope.saveAddToDashboard,
+ },
+ });
+ };
+
$scope.showEmbedDialog = (query, visualization) => {
$uibModal.open({
component: 'embedCodeDialog',
@@ -371,6 +397,17 @@ function QueryViewCtrl(
},
});
};
+
+ $scope.moreMenuIsPopulated = () => {
+ const menuParent = document.getElementById('query-more-menu');
+
+ if (menuParent) {
+ if (menuParent.querySelectorAll('.dropdown-menu li').length) {
+ return true;
+ }
+ }
+ return false;
+ };
}
export default function init(ngModule) {
diff --git a/client/app/pages/query-snippets/edit.js b/client/app/pages/query-snippets/edit.js
index 22bd85014e..8e870b5c67 100644
--- a/client/app/pages/query-snippets/edit.js
+++ b/client/app/pages/query-snippets/edit.js
@@ -3,7 +3,6 @@ import template from './edit.html';
function SnippetCtrl($routeParams, $http, $location, toastr, currentUser, Events, QuerySnippet) {
this.snippetId = $routeParams.snippetId;
- Events.record('view', 'query_snippet', this.snippetId);
this.editorOptions = {
mode: 'snippets',
diff --git a/client/app/pages/query-snippets/list.js b/client/app/pages/query-snippets/list.js
index df38ae2d5e..5097293625 100644
--- a/client/app/pages/query-snippets/list.js
+++ b/client/app/pages/query-snippets/list.js
@@ -1,9 +1,7 @@
import { Paginator } from '@/lib/pagination';
import template from './list.html';
-function SnippetsCtrl($location, currentUser, Events, QuerySnippet) {
- Events.record('view', 'page', 'query_snippets');
-
+function SnippetsCtrl($location, currentUser, QuerySnippet) {
this.snippets = new Paginator([], { itemsPerPage: 20 });
QuerySnippet.query((snippets) => {
this.snippets.updateRows(snippets);
diff --git a/client/app/pages/users/list.html b/client/app/pages/users/list.html
index 1f27c4a6d0..817d305c4c 100644
--- a/client/app/pages/users/list.html
+++ b/client/app/pages/users/list.html
@@ -10,6 +10,7 @@
Name
Joined
+ Last Active At
@@ -20,6 +21,9 @@
+
+ {{ row.last_active_at[0] }}
+
diff --git a/client/app/pages/users/list.js b/client/app/pages/users/list.js
index 4e65311db3..2fdbde17ba 100644
--- a/client/app/pages/users/list.js
+++ b/client/app/pages/users/list.js
@@ -1,9 +1,7 @@
import { Paginator } from '@/lib/pagination';
import template from './list.html';
-function UsersCtrl(currentUser, Events, User) {
- Events.record('view', 'page', 'users');
-
+function UsersCtrl(currentUser, User) {
this.currentUser = currentUser;
this.users = new Paginator([], { itemsPerPage: 20 });
User.query((users) => {
diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js
index e324f62a1a..eeea35b2fb 100644
--- a/client/app/pages/users/show.js
+++ b/client/app/pages/users/show.js
@@ -3,7 +3,7 @@ import template from './show.html';
function UserCtrl(
$scope, $routeParams, $http, $location, toastr,
- clientConfig, currentUser, Events, User,
+ clientConfig, currentUser, User,
) {
$scope.userId = $routeParams.userId;
$scope.currentUser = currentUser;
@@ -13,7 +13,6 @@ function UserCtrl(
$scope.userId = currentUser.id;
}
- Events.record('view', 'user', $scope.userId);
$scope.canEdit = currentUser.hasPermission('admin') || currentUser.id === parseInt($scope.userId, 10);
$scope.showSettings = false;
$scope.showPasswordSettings = false;
diff --git a/client/app/services/dashboard.js b/client/app/services/dashboard.js
index b6fcbe62fa..4eb2f7a507 100644
--- a/client/app/services/dashboard.js
+++ b/client/app/services/dashboard.js
@@ -19,6 +19,7 @@ function Dashboard($resource, $http, currentUser, Widget) {
get: { method: 'GET', transformResponse: transform },
save: { method: 'POST', transformResponse: transform },
query: { method: 'GET', isArray: true, transformResponse: transform },
+ search: { method: 'GET', isArray: true, url: 'api/dashboards/search' },
recent: {
method: 'get',
isArray: true,
diff --git a/client/app/services/data-source.js b/client/app/services/data-source.js
index 281e919fb0..6a081f7d6c 100644
--- a/client/app/services/data-source.js
+++ b/client/app/services/data-source.js
@@ -8,6 +8,9 @@ function DataSource($resource) {
getSchema: {
method: 'GET', cache: false, isArray: true, url: 'api/data_sources/:id/schema',
},
+ version: {
+ method: 'GET', cache: false, isArray: false, url: 'api/data_sources/:id/version',
+ },
};
const DataSourceResource = $resource('api/data_sources/:id', { id: '@id' }, actions);
diff --git a/client/app/services/query-result.js b/client/app/services/query-result.js
index 1ebe00b3f2..f91db0a9d1 100644
--- a/client/app/services/query-result.js
+++ b/client/app/services/query-result.js
@@ -465,8 +465,16 @@ function QueryResultService($resource, $timeout, $q) {
}
}, (error) => {
logger('Connection error', error);
- // TODO: use QueryResultError, or better yet: exception/reject of promise.
- this.update({ job: { error: 'failed communicating with server. Please check your Internet connection and try again.', status: 4 } });
+ this.update({
+ job: {
+ error: 'Failed communicating with server. Retrying...',
+ status: 4,
+ id: this.job.id,
+ },
+ });
+ $timeout(() => {
+ this.refreshStatus(query);
+ }, 3000);
});
}
diff --git a/client/app/services/query.js b/client/app/services/query.js
index 81ad534fa4..95f2c8cb8f 100644
--- a/client/app/services/query.js
+++ b/client/app/services/query.js
@@ -251,6 +251,10 @@ function QueryResource($resource, $http, $q, $location, currentUser, QueryResult
.format('HH:mm');
};
+ Query.prototype.hasScheduleExpiry = function hasScheduleExpiry() {
+ return (this.schedule && this.schedule_until);
+ };
+
Query.prototype.hasResult = function hasResult() {
return !!(this.latest_query_data || this.latest_query_data_id);
};
diff --git a/client/app/visualizations/chart/chart-editor.html b/client/app/visualizations/chart/chart-editor.html
index f29f35ac5f..7a4dc4477c 100644
--- a/client/app/visualizations/chart/chart-editor.html
+++ b/client/app/visualizations/chart/chart-editor.html
@@ -9,9 +9,14 @@
Y Axis
-
+
Series
+
+ Colors
+
-
X Column
@@ -138,18 +142,18 @@
-
-
-
- Show errors in the console
-
-
+
+ Custom code
+
+
-
@@ -186,6 +190,12 @@
Height
+
+
+ Label Length
+
+ How many characters should X Axis Labels be truncated at in the legend?
+
@@ -216,7 +226,7 @@
{{$index == 0 ? 'Left' : 'Right'}} Y Axis
-
+
+
+
+
+
+ zIndex
+ Label
+ Color
+
+
+
+
+
+
+ {{name}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/app/visualizations/chart/index.js b/client/app/visualizations/chart/index.js
index b67c8cd61d..d753561f1d 100644
--- a/client/app/visualizations/chart/index.js
+++ b/client/app/visualizations/chart/index.js
@@ -120,19 +120,48 @@ function ChartEditor(ColorPalette, clientConfig) {
}
function refreshSeries() {
- const seriesNames = pluck(scope.queryResult.getChartData(scope.options.columnMapping), 'name');
- const existing = keys(scope.options.seriesOptions);
- each(difference(seriesNames, existing), (name) => {
- scope.options.seriesOptions[name] = {
- type: scope.options.globalSeriesType,
- yAxis: 0,
- };
- scope.form.seriesList.push(name);
- });
- each(difference(existing, seriesNames), (name) => {
- scope.form.seriesList = without(scope.form.seriesList, name);
- delete scope.options.seriesOptions[name];
- });
+ // for pie charts only get color list (x row) instead of series list (y column)
+ if (scope.options.globalSeriesType === 'pie') {
+ const seriesData = scope.queryResult.getData();
+ scope.form.colorsList = [];
+ const xColumnName = scope.form.xAxisColumn;
+ seriesData.forEach((rowOfData) => {
+ scope.form.colorsList.push(rowOfData[xColumnName]);
+ });
+
+ const colorNames = scope.form.colorsList;
+ let existing = [];
+ if (scope.options.colorOptions === undefined) {
+ existing = colorNames;
+ } else {
+ existing = keys(scope.options.colorOptions);
+ }
+ each(difference(colorNames, existing), (name) => {
+ scope.options.colorOptions[name] = {
+ type: scope.options.globalSeriesType,
+ yAxis: 0,
+ };
+ scope.form.colorsList.push(name);
+ });
+ each(difference(existing, colorNames), (name) => {
+ scope.form.colorsList = without(scope.form.colorsList, name);
+ delete scope.options.colorOptions[name];
+ });
+ } else {
+ const seriesNames = pluck(scope.queryResult.getChartData(scope.options.columnMapping), 'name');
+ const existing = keys(scope.options.seriesOptions);
+ each(difference(seriesNames, existing), (name) => {
+ scope.options.seriesOptions[name] = {
+ type: scope.options.globalSeriesType,
+ yAxis: 0,
+ };
+ scope.form.seriesList.push(name);
+ });
+ each(difference(existing, seriesNames), (name) => {
+ scope.form.seriesList = without(scope.form.seriesList, name);
+ delete scope.options.seriesOptions[name];
+ });
+ }
}
function setColumnRole(role, column) {
@@ -164,6 +193,8 @@ function ChartEditor(ColorPalette, clientConfig) {
yAxisColumns: [],
seriesList: sortBy(keys(scope.options.seriesOptions), name =>
scope.options.seriesOptions[name].zIndex),
+ colorsList: sortBy(keys(scope.options.colorOptions), name =>
+ scope.options.colorOptions[name].zIndex),
};
scope.$watchCollection('form.seriesList', (value) => {
@@ -173,7 +204,6 @@ function ChartEditor(ColorPalette, clientConfig) {
});
});
-
scope.$watchCollection('form.yAxisColumns', (value, old) => {
each(old, unsetColumn);
each(value, partial(setColumnRole, 'y'));
@@ -222,6 +252,10 @@ function ChartEditor(ColorPalette, clientConfig) {
scope.options.bottomMargin = 50;
}
+ if (!has(scope.options, 'xAxisLabelLength')) {
+ scope.options.xAxisLabelLength = 300;
+ }
+
if (scope.columnNames) {
each(scope.options.columnMapping, (value, key) => {
if (scope.columnNames.length > 0 && !contains(scope.columnNames, key)) {
diff --git a/client/app/visualizations/chart/plotly.js b/client/app/visualizations/chart/plotly.js
index a411a35e9f..1c4f631e34 100644
--- a/client/app/visualizations/chart/plotly.js
+++ b/client/app/visualizations/chart/plotly.js
@@ -13,6 +13,8 @@ Plotly.setPlotConfig({
modeBarButtonsToRemove: ['sendDataToCloud'],
});
+const DEFAULT_XAXIS_LABEL_LENGTH = 300;
+
// The following colors will be used if you pick "Automatic" color.
const BaseColors = {
Blue: '#4572A7',
@@ -236,10 +238,15 @@ const PlotlyChart = () => {
function recalculateOptions() {
scope.data.length = 0;
scope.layout.showlegend = has(scope.options, 'legend') ? scope.options.legend.enabled : true;
+ scope.layout.legend = {
+ bgcolor: '#cccccc',
+ wordWrap: 'normal',
+ };
if (has(scope.options, 'bottomMargin')) {
bottomMargin = parseInt(scope.options.bottomMargin, 10);
scope.layout.margin.b = bottomMargin;
}
+ const xAxisLabelLength = has(scope.options, 'xAxisLabelLength') ? parseInt(scope.options.xAxisLabelLength, 10) : DEFAULT_XAXIS_LABEL_LENGTH;
delete scope.layout.barmode;
delete scope.layout.xaxis;
delete scope.layout.yaxis;
@@ -262,7 +269,7 @@ const PlotlyChart = () => {
labels: [],
type: 'pie',
hole: 0.4,
- marker: { colors: ColorPaletteArray },
+ marker: { colors: ColorPaletteArray.slice() },
text: series.name,
textposition: 'inside',
name: series.name,
@@ -272,9 +279,11 @@ const PlotlyChart = () => {
},
};
- series.data.forEach((row) => {
+ each(series.data, (row, rowIdx) => {
plotlySeries.values.push(row.y);
- plotlySeries.labels.push(hasX ? row.x : `Slice ${index}`);
+ plotlySeries.labels.push(hasX ? row.x.toString().substr(0, xAxisLabelLength) : `Slice ${index}`);
+ const rowOpts = scope.options.seriesOptions[hasX ? row.x.toString().substr(0, xAxisLabelLength) : `Slice ${index}`];
+ plotlySeries.marker.colors[rowIdx] = rowOpts ? rowOpts.color : getColor(rowIdx);
});
scope.data.push(plotlySeries);
diff --git a/client/app/visualizations/edit-visualization-dialog.css b/client/app/visualizations/edit-visualization-dialog.css
new file mode 100644
index 0000000000..3e84b755b2
--- /dev/null
+++ b/client/app/visualizations/edit-visualization-dialog.css
@@ -0,0 +1,5 @@
+/* Edit Visualization Dialog specific CSS */
+
+.slight-padding {
+ padding: 5px;
+}
\ No newline at end of file
diff --git a/client/app/visualizations/edit-visualization-dialog.html b/client/app/visualizations/edit-visualization-dialog.html
index 3a984c3680..666ad18db3 100644
--- a/client/app/visualizations/edit-visualization-dialog.html
+++ b/client/app/visualizations/edit-visualization-dialog.html
@@ -33,10 +33,18 @@
Visualization Editor
+
+
diff --git a/client/app/visualizations/edit-visualization-dialog.js b/client/app/visualizations/edit-visualization-dialog.js
index c021be54ca..fb19350cd1 100644
--- a/client/app/visualizations/edit-visualization-dialog.js
+++ b/client/app/visualizations/edit-visualization-dialog.js
@@ -1,6 +1,7 @@
import { pluck } from 'underscore';
import { copy } from 'angular';
import template from './edit-visualization-dialog.html';
+import './edit-visualization-dialog.css';
const EditVisualizationDialog = {
template,
@@ -19,6 +20,9 @@ const EditVisualizationDialog = {
this.visualization = copy(this.originalVisualization);
this.visTypes = Visualization.visualizationTypes;
+ this.warning_three_column_groupby = 'You have more than 2 columns in your result set. To ensure the chart is accurate, please do one of the following: Change the SQL query to give 2 result columns. You can CONCAT() columns together if you wish. Select column(s) to group by. ';
+ this.warning_three_column_stacking = 'You have more than 2 columns in your result set. You may wish to make the Stacking option equal to `Enabled` or `Percent`.';
+
this.newVisualization = () =>
({
type: Visualization.defaultVisualization.type,
@@ -45,6 +49,24 @@ const EditVisualizationDialog = {
}
};
+ this.has3plusColumnsFunction = () => {
+ let has3plusColumns = false;
+ if ((JSON.stringify(this.visualization.options.columnMapping).match(/,/g) || []).length > 2) {
+ has3plusColumns = true;
+ }
+ return has3plusColumns;
+ };
+
+ this.disableSubmit = () => {
+ if (this.visualization.options.globalSeriesType === 'column'
+ && this.has3plusColumnsFunction()
+ && !JSON.stringify(this.visualization.options.columnMapping).includes('"":')
+ && JSON.stringify(this.visualization.options.columnMapping).includes('unused')) {
+ return true;
+ }
+ return false;
+ };
+
this.submit = () => {
if (this.visualization.id) {
Events.record('update', 'visualization', this.visualization.id, { type: this.visualization.type });
diff --git a/migrations/versions/58f810489c47_.py b/migrations/versions/58f810489c47_.py
new file mode 100644
index 0000000000..37c5075285
--- /dev/null
+++ b/migrations/versions/58f810489c47_.py
@@ -0,0 +1,28 @@
+"""empty message
+
+Revision ID: 58f810489c47
+Revises: eb2f788f997e
+Create Date: 2017-06-25 21:24:54.942119
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '58f810489c47'
+down_revision = 'eb2f788f997e'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('query_results', sa.Column('data_scanned', sa.String(length=255), nullable=True))
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('query_results', 'data_scanned')
+ # ### end Alembic commands ###
diff --git a/migrations/versions/5ec5c84ba61e_.py b/migrations/versions/5ec5c84ba61e_.py
new file mode 100644
index 0000000000..7a8bdeb3ce
--- /dev/null
+++ b/migrations/versions/5ec5c84ba61e_.py
@@ -0,0 +1,35 @@
+"""empty message
+
+Revision ID: 5ec5c84ba61e
+Revises: d1eae8b9893e
+Create Date: 2017-10-17 18:21:00.174015
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import sqlalchemy_utils as su
+import sqlalchemy_searchable as ss
+
+
+# revision identifiers, used by Alembic.
+revision = '5ec5c84ba61e'
+down_revision = '58f810489c47'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ conn = op.get_bind()
+ op.add_column('queries', sa.Column('search_vector', su.TSVectorType()))
+ op.create_index('ix_queries_search_vector', 'queries', ['search_vector'],
+ unique=False, postgresql_using='gin')
+ ss.sync_trigger(conn, 'queries', 'search_vector',
+ ['name', 'description', 'query'])
+
+
+def downgrade():
+ conn = op.get_bind()
+
+ ss.drop_trigger(conn, 'queries', 'search_vector')
+ op.drop_index('ix_queries_search_vector', table_name='queries')
+ op.drop_column('queries', 'search_vector')
diff --git a/migrations/versions/6b5be7e0a0ef_.py b/migrations/versions/6b5be7e0a0ef_.py
new file mode 100644
index 0000000000..4765f07095
--- /dev/null
+++ b/migrations/versions/6b5be7e0a0ef_.py
@@ -0,0 +1,48 @@
+"""empty message
+
+Revision ID: 6b5be7e0a0ef
+Revises: 5ec5c84ba61e
+Create Date: 2017-11-02 20:42:13.356360
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import sqlalchemy_searchable as ss
+
+
+# revision identifiers, used by Alembic.
+revision = '6b5be7e0a0ef'
+down_revision = '5ec5c84ba61e'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ ss.vectorizer.clear()
+
+ conn = op.get_bind()
+
+ metadata = sa.MetaData(bind=conn)
+ queries = sa.Table('queries', metadata, autoload=True)
+
+ @ss.vectorizer(queries.c.id)
+ def integer_vectorizer(column):
+ return sa.func.cast(column, sa.Text)
+
+ ss.sync_trigger(
+ conn,
+ 'queries',
+ 'search_vector',
+ ['id', 'name', 'description', 'query'],
+ metadata=metadata
+ )
+
+
+def downgrade():
+ conn = op.get_bind()
+ ss.drop_trigger(conn, 'queries', 'search_vector')
+ op.drop_index('ix_queries_search_vector', table_name='queries')
+ op.create_index('ix_queries_search_vector', 'queries', ['search_vector'],
+ unique=False, postgresql_using='gin')
+ ss.sync_trigger(conn, 'queries', 'search_vector',
+ ['name', 'description', 'query'])
diff --git a/migrations/versions/eb2f788f997e_.py b/migrations/versions/eb2f788f997e_.py
new file mode 100644
index 0000000000..71fd2bd5b3
--- /dev/null
+++ b/migrations/versions/eb2f788f997e_.py
@@ -0,0 +1,27 @@
+"""Add 'schedule_until' column to queries.
+
+Revision ID: eb2f788f997e
+Revises: d1eae8b9893e
+Create Date: 2017-03-02 12:20:00.029066
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'eb2f788f997e'
+down_revision = 'd1eae8b9893e'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.add_column(
+ 'queries',
+ sa.Column('schedule_until', sa.DateTime(timezone=True), nullable=True))
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('queries', 'schedule_until')
diff --git a/migrations/versions/fbc0849e2674_.py b/migrations/versions/fbc0849e2674_.py
new file mode 100644
index 0000000000..1d1ec26649
--- /dev/null
+++ b/migrations/versions/fbc0849e2674_.py
@@ -0,0 +1,24 @@
+"""merge mozilla updates with schema from master
+
+Revision ID: fbc0849e2674
+Revises: 6b5be7e0a0ef, 7671dca4e604
+Create Date: 2017-12-12 04:45:34.360587
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'fbc0849e2674'
+down_revision = ('6b5be7e0a0ef', '7671dca4e604')
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ pass
+
+
+def downgrade():
+ pass
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
new file mode 100644
index 0000000000..3e2d5e1fbc
--- /dev/null
+++ b/npm-shrinkwrap.json
@@ -0,0 +1,3095 @@
+{
+ "name": "redash-client",
+ "version": "1.0.0",
+ "dependencies": {
+ "3d-view": {
+ "version": "2.0.0",
+ "from": "3d-view@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/3d-view/-/3d-view-2.0.0.tgz"
+ },
+ "3d-view-controls": {
+ "version": "2.1.1",
+ "from": "3d-view-controls@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/3d-view-controls/-/3d-view-controls-2.1.1.tgz"
+ },
+ "a-big-triangle": {
+ "version": "1.0.3",
+ "from": "a-big-triangle@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/a-big-triangle/-/a-big-triangle-1.0.3.tgz"
+ },
+ "add-line-numbers": {
+ "version": "1.0.1",
+ "from": "add-line-numbers@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/add-line-numbers/-/add-line-numbers-1.0.1.tgz"
+ },
+ "affine-hull": {
+ "version": "1.0.0",
+ "from": "affine-hull@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/affine-hull/-/affine-hull-1.0.0.tgz"
+ },
+ "align-text": {
+ "version": "0.1.4",
+ "from": "align-text@>=0.1.3 <0.2.0",
+ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz"
+ },
+ "alpha-complex": {
+ "version": "1.0.0",
+ "from": "alpha-complex@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/alpha-complex/-/alpha-complex-1.0.0.tgz"
+ },
+ "alpha-shape": {
+ "version": "1.0.0",
+ "from": "alpha-shape@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/alpha-shape/-/alpha-shape-1.0.0.tgz"
+ },
+ "alter": {
+ "version": "0.2.0",
+ "from": "alter@>=0.2.0 <0.3.0",
+ "resolved": "https://registry.npmjs.org/alter/-/alter-0.2.0.tgz"
+ },
+ "amdefine": {
+ "version": "1.0.0",
+ "from": "amdefine@>=0.0.4",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz"
+ },
+ "angular": {
+ "version": "1.5.8",
+ "from": "angular@latest",
+ "resolved": "https://registry.npmjs.org/angular/-/angular-1.5.8.tgz"
+ },
+ "angular-base64-upload": {
+ "version": "0.1.19",
+ "from": "angular-base64-upload@latest",
+ "resolved": "https://registry.npmjs.org/angular-base64-upload/-/angular-base64-upload-0.1.19.tgz"
+ },
+ "angular-gridster": {
+ "version": "0.13.14",
+ "from": "angular-gridster@latest",
+ "resolved": "https://registry.npmjs.org/angular-gridster/-/angular-gridster-0.13.14.tgz"
+ },
+ "angular-messages": {
+ "version": "1.5.8",
+ "from": "angular-messages@latest",
+ "resolved": "https://registry.npmjs.org/angular-messages/-/angular-messages-1.5.8.tgz"
+ },
+ "angular-moment": {
+ "version": "1.0.0",
+ "from": "angular-moment@latest",
+ "resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.0.0.tgz"
+ },
+ "angular-resizable": {
+ "version": "1.2.0",
+ "from": "angular-resizable@latest",
+ "resolved": "https://registry.npmjs.org/angular-resizable/-/angular-resizable-1.2.0.tgz"
+ },
+ "angular-resource": {
+ "version": "1.5.8",
+ "from": "angular-resource@latest",
+ "resolved": "https://registry.npmjs.org/angular-resource/-/angular-resource-1.5.8.tgz"
+ },
+ "angular-route": {
+ "version": "1.5.8",
+ "from": "angular-route@latest",
+ "resolved": "https://registry.npmjs.org/angular-route/-/angular-route-1.5.8.tgz"
+ },
+ "angular-sanitize": {
+ "version": "1.5.8",
+ "from": "angular-sanitize@latest",
+ "resolved": "https://registry.npmjs.org/angular-sanitize/-/angular-sanitize-1.5.8.tgz"
+ },
+ "angular-toastr": {
+ "version": "2.1.1",
+ "from": "angular-toastr@latest",
+ "resolved": "https://registry.npmjs.org/angular-toastr/-/angular-toastr-2.1.1.tgz"
+ },
+ "angular-ui-ace": {
+ "version": "0.2.3",
+ "from": "angular-ui-ace@latest",
+ "resolved": "https://registry.npmjs.org/angular-ui-ace/-/angular-ui-ace-0.2.3.tgz"
+ },
+ "angular-ui-bootstrap": {
+ "version": "2.2.0",
+ "from": "angular-ui-bootstrap@latest",
+ "resolved": "https://registry.npmjs.org/angular-ui-bootstrap/-/angular-ui-bootstrap-2.2.0.tgz"
+ },
+ "angular-vs-repeat": {
+ "version": "1.1.7",
+ "from": "angular-vs-repeat@latest",
+ "resolved": "https://registry.npmjs.org/angular-vs-repeat/-/angular-vs-repeat-1.1.7.tgz"
+ },
+ "ansi-regex": {
+ "version": "2.0.0",
+ "from": "ansi-regex@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "from": "ansi-styles@>=2.2.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
+ },
+ "arraytools": {
+ "version": "1.1.2",
+ "from": "arraytools@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/arraytools/-/arraytools-1.1.2.tgz"
+ },
+ "asn1": {
+ "version": "0.2.3",
+ "from": "asn1@>=0.2.3 <0.3.0",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz"
+ },
+ "assert-plus": {
+ "version": "0.2.0",
+ "from": "assert-plus@>=0.2.0 <0.3.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz"
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "from": "asynckit@>=0.4.0 <0.5.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
+ },
+ "atob-lite": {
+ "version": "1.0.0",
+ "from": "atob-lite@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/atob-lite/-/atob-lite-1.0.0.tgz"
+ },
+ "aws-sign2": {
+ "version": "0.6.0",
+ "from": "aws-sign2@>=0.6.0 <0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz"
+ },
+ "aws4": {
+ "version": "1.5.0",
+ "from": "aws4@>=1.2.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.5.0.tgz"
+ },
+ "balanced-match": {
+ "version": "0.4.2",
+ "from": "balanced-match@>=0.4.1 <0.5.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz"
+ },
+ "barycentric": {
+ "version": "1.0.1",
+ "from": "barycentric@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/barycentric/-/barycentric-1.0.1.tgz"
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.0",
+ "from": "bcrypt-pbkdf@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz",
+ "optional": true
+ },
+ "big-rat": {
+ "version": "1.0.2",
+ "from": "big-rat@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/big-rat/-/big-rat-1.0.2.tgz"
+ },
+ "big.js": {
+ "version": "3.1.3",
+ "from": "big.js@>=3.1.3 <4.0.0",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz"
+ },
+ "binary-search-bounds": {
+ "version": "1.0.0",
+ "from": "binary-search-bounds@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-1.0.0.tgz"
+ },
+ "bit-twiddle": {
+ "version": "1.0.2",
+ "from": "bit-twiddle@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-1.0.2.tgz"
+ },
+ "bl": {
+ "version": "1.2.0",
+ "from": "bl@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.0.tgz"
+ },
+ "bn.js": {
+ "version": "2.2.0",
+ "from": "bn.js@>=2.0.5 <3.0.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-2.2.0.tgz"
+ },
+ "boom": {
+ "version": "2.10.1",
+ "from": "boom@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz"
+ },
+ "bops": {
+ "version": "0.0.6",
+ "from": "bops@0.0.6",
+ "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz",
+ "dependencies": {
+ "base64-js": {
+ "version": "0.0.2",
+ "from": "base64-js@0.0.2",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz"
+ }
+ }
+ },
+ "boundary-cells": {
+ "version": "2.0.1",
+ "from": "boundary-cells@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/boundary-cells/-/boundary-cells-2.0.1.tgz"
+ },
+ "box-intersect": {
+ "version": "1.0.1",
+ "from": "box-intersect@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/box-intersect/-/box-intersect-1.0.1.tgz"
+ },
+ "brace": {
+ "version": "0.9.0",
+ "from": "brace@latest",
+ "resolved": "https://registry.npmjs.org/brace/-/brace-0.9.0.tgz"
+ },
+ "brace-expansion": {
+ "version": "1.1.6",
+ "from": "brace-expansion@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz"
+ },
+ "brfs": {
+ "version": "1.4.3",
+ "from": "brfs@>=1.4.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.4.3.tgz",
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.3",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "quote-stream": {
+ "version": "1.0.2",
+ "from": "quote-stream@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz"
+ },
+ "through2": {
+ "version": "2.0.3",
+ "from": "through2@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz"
+ }
+ }
+ },
+ "buffer-equal": {
+ "version": "0.0.1",
+ "from": "buffer-equal@0.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz"
+ },
+ "buffer-shims": {
+ "version": "1.0.0",
+ "from": "buffer-shims@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
+ },
+ "call-matcher": {
+ "version": "1.0.1",
+ "from": "call-matcher@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/call-matcher/-/call-matcher-1.0.1.tgz",
+ "dependencies": {
+ "estraverse": {
+ "version": "4.2.0",
+ "from": "estraverse@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz"
+ }
+ }
+ },
+ "camelcase": {
+ "version": "1.2.1",
+ "from": "camelcase@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz"
+ },
+ "caseless": {
+ "version": "0.11.0",
+ "from": "caseless@>=0.11.0 <0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz"
+ },
+ "cdt2d": {
+ "version": "1.0.0",
+ "from": "cdt2d@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/cdt2d/-/cdt2d-1.0.0.tgz",
+ "dependencies": {
+ "binary-search-bounds": {
+ "version": "2.0.3",
+ "from": "binary-search-bounds@>=2.0.3 <3.0.0",
+ "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz"
+ }
+ }
+ },
+ "cell-orientation": {
+ "version": "1.0.1",
+ "from": "cell-orientation@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/cell-orientation/-/cell-orientation-1.0.1.tgz"
+ },
+ "center-align": {
+ "version": "0.1.3",
+ "from": "center-align@>=0.1.1 <0.2.0",
+ "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz"
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "from": "chalk@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "dependencies": {
+ "supports-color": {
+ "version": "2.0.0",
+ "from": "supports-color@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
+ }
+ }
+ },
+ "circumcenter": {
+ "version": "1.0.0",
+ "from": "circumcenter@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/circumcenter/-/circumcenter-1.0.0.tgz"
+ },
+ "circumradius": {
+ "version": "1.0.0",
+ "from": "circumradius@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/circumradius/-/circumradius-1.0.0.tgz"
+ },
+ "clean-pslg": {
+ "version": "1.1.0",
+ "from": "clean-pslg@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/clean-pslg/-/clean-pslg-1.1.0.tgz"
+ },
+ "cliui": {
+ "version": "2.1.0",
+ "from": "cliui@>=2.1.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+ "dependencies": {
+ "wordwrap": {
+ "version": "0.0.2",
+ "from": "wordwrap@0.0.2",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz"
+ }
+ }
+ },
+ "clone": {
+ "version": "1.0.2",
+ "from": "clone@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz"
+ },
+ "colormap": {
+ "version": "2.2.0",
+ "from": "colormap@>=2.1.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/colormap/-/colormap-2.2.0.tgz"
+ },
+ "combined-stream": {
+ "version": "1.0.5",
+ "from": "combined-stream@>=1.0.5 <1.1.0",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz"
+ },
+ "commander": {
+ "version": "2.9.0",
+ "from": "commander@>=2.9.0 <2.10.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz"
+ },
+ "compare-angle": {
+ "version": "1.0.1",
+ "from": "compare-angle@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/compare-angle/-/compare-angle-1.0.1.tgz"
+ },
+ "compare-cell": {
+ "version": "1.0.0",
+ "from": "compare-cell@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/compare-cell/-/compare-cell-1.0.0.tgz"
+ },
+ "compare-oriented-cell": {
+ "version": "1.0.1",
+ "from": "compare-oriented-cell@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/compare-oriented-cell/-/compare-oriented-cell-1.0.1.tgz"
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "from": "concat-map@0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+ },
+ "concat-stream": {
+ "version": "1.4.10",
+ "from": "concat-stream@>=1.4.5 <1.5.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.4.10.tgz",
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "from": "readable-stream@>=1.1.9 <1.2.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz"
+ }
+ }
+ },
+ "convert-source-map": {
+ "version": "1.3.0",
+ "from": "convert-source-map@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.3.0.tgz"
+ },
+ "convex-hull": {
+ "version": "1.0.3",
+ "from": "convex-hull@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/convex-hull/-/convex-hull-1.0.3.tgz"
+ },
+ "core-js": {
+ "version": "2.4.1",
+ "from": "core-js@>=2.4.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz"
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "from": "core-util-is@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+ },
+ "cornelius": {
+ "version": "0.1.0",
+ "from": "git+https://github.com/restorando/cornelius.git",
+ "resolved": "git+https://github.com/restorando/cornelius.git#24d935811186c165c8ba63244ff363da71f32dcf"
+ },
+ "country-regex": {
+ "version": "1.0.3",
+ "from": "country-regex@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/country-regex/-/country-regex-1.0.3.tgz"
+ },
+ "cryptiles": {
+ "version": "2.0.5",
+ "from": "cryptiles@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz"
+ },
+ "csscolorparser": {
+ "version": "1.0.3",
+ "from": "csscolorparser@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz"
+ },
+ "cubic-hermite": {
+ "version": "1.0.0",
+ "from": "cubic-hermite@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/cubic-hermite/-/cubic-hermite-1.0.0.tgz"
+ },
+ "cwise": {
+ "version": "1.0.9",
+ "from": "cwise@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/cwise/-/cwise-1.0.9.tgz"
+ },
+ "cwise-compiler": {
+ "version": "1.1.2",
+ "from": "cwise-compiler@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.2.tgz"
+ },
+ "cwise-parser": {
+ "version": "1.0.3",
+ "from": "cwise-parser@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/cwise-parser/-/cwise-parser-1.0.3.tgz",
+ "dependencies": {
+ "esprima": {
+ "version": "1.2.5",
+ "from": "esprima@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz"
+ }
+ }
+ },
+ "d3": {
+ "version": "3.5.17",
+ "from": "d3@>=3.5.6 <3.6.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz"
+ },
+ "d3-cloud": {
+ "version": "1.2.1",
+ "from": "d3-cloud@latest",
+ "resolved": "https://registry.npmjs.org/d3-cloud/-/d3-cloud-1.2.1.tgz"
+ },
+ "d3-dispatch": {
+ "version": "0.2.6",
+ "from": "d3-dispatch@>=0.2.0 <0.3.0",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-0.2.6.tgz"
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "from": "dashdash@>=1.12.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "from": "assert-plus@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
+ }
+ }
+ },
+ "debug": {
+ "version": "2.2.0",
+ "from": "debug@latest",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "from": "decamelize@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
+ },
+ "deep-equal": {
+ "version": "1.0.1",
+ "from": "deep-equal@>=1.0.1 <1.1.0",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz"
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "from": "deep-is@>=0.1.3 <0.2.0",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz"
+ },
+ "define-properties": {
+ "version": "1.1.2",
+ "from": "define-properties@>=1.1.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz"
+ },
+ "defined": {
+ "version": "1.0.0",
+ "from": "defined@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz"
+ },
+ "delaunay-triangulate": {
+ "version": "1.1.6",
+ "from": "delaunay-triangulate@>=1.1.6 <2.0.0",
+ "resolved": "https://registry.npmjs.org/delaunay-triangulate/-/delaunay-triangulate-1.1.6.tgz"
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "from": "delayed-stream@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
+ },
+ "diff": {
+ "version": "3.2.0",
+ "from": "diff@latest",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz"
+ },
+ "double-bits": {
+ "version": "1.1.1",
+ "from": "double-bits@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/double-bits/-/double-bits-1.1.1.tgz"
+ },
+ "dup": {
+ "version": "1.0.0",
+ "from": "dup@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/dup/-/dup-1.0.0.tgz"
+ },
+ "duplexer2": {
+ "version": "0.0.2",
+ "from": "duplexer2@>=0.0.2 <0.1.0",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "from": "readable-stream@>=1.1.9 <1.2.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz"
+ }
+ }
+ },
+ "earcut": {
+ "version": "2.1.1",
+ "from": "earcut@>=2.0.3 <3.0.0",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.1.1.tgz"
+ },
+ "ecc-jsbn": {
+ "version": "0.1.1",
+ "from": "ecc-jsbn@>=0.1.1 <0.2.0",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
+ "optional": true
+ },
+ "edges-to-adjacency-list": {
+ "version": "1.0.0",
+ "from": "edges-to-adjacency-list@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/edges-to-adjacency-list/-/edges-to-adjacency-list-1.0.0.tgz"
+ },
+ "emojis-list": {
+ "version": "2.1.0",
+ "from": "emojis-list@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz"
+ },
+ "es-abstract": {
+ "version": "1.6.1",
+ "from": "es-abstract@>=1.5.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.6.1.tgz"
+ },
+ "es-to-primitive": {
+ "version": "1.1.1",
+ "from": "es-to-primitive@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz"
+ },
+ "es6-promise": {
+ "version": "3.3.1",
+ "from": "es6-promise@>=3.0.2 <4.0.0",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz"
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "from": "escape-string-regexp@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
+ },
+ "escodegen": {
+ "version": "1.3.3",
+ "from": "escodegen@>=1.3.2 <1.4.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.3.3.tgz",
+ "dependencies": {
+ "esprima": {
+ "version": "1.1.1",
+ "from": "esprima@>=1.1.1 <1.2.0",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.1.1.tgz"
+ },
+ "esutils": {
+ "version": "1.0.0",
+ "from": "esutils@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz"
+ },
+ "source-map": {
+ "version": "0.1.43",
+ "from": "source-map@>=0.1.33 <0.2.0",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+ "optional": true
+ }
+ }
+ },
+ "esprima": {
+ "version": "2.7.3",
+ "from": "esprima@>=2.6.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz"
+ },
+ "espurify": {
+ "version": "1.6.0",
+ "from": "espurify@>=1.3.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/espurify/-/espurify-1.6.0.tgz"
+ },
+ "estraverse": {
+ "version": "1.5.1",
+ "from": "estraverse@>=1.5.0 <1.6.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz"
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "from": "esutils@>=2.0.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz"
+ },
+ "events": {
+ "version": "1.1.1",
+ "from": "events@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz"
+ },
+ "extend": {
+ "version": "3.0.0",
+ "from": "extend@>=3.0.0 <3.1.0",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz"
+ },
+ "extract-frustum-planes": {
+ "version": "1.0.0",
+ "from": "extract-frustum-planes@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/extract-frustum-planes/-/extract-frustum-planes-1.0.0.tgz"
+ },
+ "extsprintf": {
+ "version": "1.0.2",
+ "from": "extsprintf@1.0.2",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz"
+ },
+ "falafel": {
+ "version": "1.2.0",
+ "from": "falafel@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/falafel/-/falafel-1.2.0.tgz",
+ "dependencies": {
+ "acorn": {
+ "version": "1.2.2",
+ "from": "acorn@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ }
+ }
+ },
+ "fast-isnumeric": {
+ "version": "1.1.1",
+ "from": "fast-isnumeric@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-isnumeric/-/fast-isnumeric-1.1.1.tgz"
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "from": "fast-levenshtein@>=2.0.4 <2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
+ },
+ "feature-filter": {
+ "version": "2.2.0",
+ "from": "feature-filter@>=2.2.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/feature-filter/-/feature-filter-2.2.0.tgz"
+ },
+ "filtered-vector": {
+ "version": "1.2.4",
+ "from": "filtered-vector@>=1.2.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/filtered-vector/-/filtered-vector-1.2.4.tgz"
+ },
+ "findup": {
+ "version": "0.1.5",
+ "from": "findup@>=0.1.5 <0.2.0",
+ "resolved": "https://registry.npmjs.org/findup/-/findup-0.1.5.tgz",
+ "dependencies": {
+ "colors": {
+ "version": "0.6.2",
+ "from": "colors@>=0.6.0-1 <0.7.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz"
+ },
+ "commander": {
+ "version": "2.1.0",
+ "from": "commander@>=2.1.0 <2.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz"
+ }
+ }
+ },
+ "font-awesome": {
+ "version": "4.7.0",
+ "from": "font-awesome@latest",
+ "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz"
+ },
+ "for-each": {
+ "version": "0.3.2",
+ "from": "for-each@>=0.3.2 <0.4.0",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.2.tgz"
+ },
+ "foreach": {
+ "version": "2.0.5",
+ "from": "foreach@>=2.0.5 <3.0.0",
+ "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz"
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "from": "forever-agent@>=0.6.1 <0.7.0",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
+ },
+ "form-data": {
+ "version": "2.1.2",
+ "from": "form-data@>=2.1.1 <2.2.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.2.tgz"
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "from": "fs.realpath@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
+ },
+ "function-bind": {
+ "version": "1.1.0",
+ "from": "function-bind@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz"
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "from": "functional-red-black-tree@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz"
+ },
+ "gamma": {
+ "version": "0.1.0",
+ "from": "gamma@>=0.1.0 <0.2.0",
+ "resolved": "https://registry.npmjs.org/gamma/-/gamma-0.1.0.tgz"
+ },
+ "generate-function": {
+ "version": "2.0.0",
+ "from": "generate-function@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
+ },
+ "generate-object-property": {
+ "version": "1.2.0",
+ "from": "generate-object-property@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz"
+ },
+ "geojson-area": {
+ "version": "0.1.0",
+ "from": "geojson-area@0.1.0",
+ "resolved": "https://registry.npmjs.org/geojson-area/-/geojson-area-0.1.0.tgz"
+ },
+ "geojson-rewind": {
+ "version": "0.1.0",
+ "from": "geojson-rewind@>=0.1.0 <0.2.0",
+ "resolved": "https://registry.npmjs.org/geojson-rewind/-/geojson-rewind-0.1.0.tgz",
+ "dependencies": {
+ "concat-stream": {
+ "version": "1.2.1",
+ "from": "concat-stream@>=1.2.1 <1.3.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.2.1.tgz"
+ },
+ "minimist": {
+ "version": "0.0.5",
+ "from": "minimist@0.0.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz"
+ }
+ }
+ },
+ "geojson-vt": {
+ "version": "2.4.0",
+ "from": "geojson-vt@>=2.4.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-2.4.0.tgz"
+ },
+ "get-canvas-context": {
+ "version": "1.0.2",
+ "from": "get-canvas-context@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/get-canvas-context/-/get-canvas-context-1.0.2.tgz"
+ },
+ "getpass": {
+ "version": "0.1.6",
+ "from": "getpass@>=0.1.1 <0.2.0",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz",
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "from": "assert-plus@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
+ }
+ }
+ },
+ "gl-axes3d": {
+ "version": "1.2.5",
+ "from": "gl-axes3d@>=1.2.5 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-axes3d/-/gl-axes3d-1.2.5.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-buffer": {
+ "version": "2.1.2",
+ "from": "gl-buffer@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/gl-buffer/-/gl-buffer-2.1.2.tgz"
+ },
+ "gl-constants": {
+ "version": "1.0.0",
+ "from": "gl-constants@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-constants/-/gl-constants-1.0.0.tgz"
+ },
+ "gl-contour2d": {
+ "version": "1.1.3",
+ "from": "gl-contour2d@>=1.1.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-contour2d/-/gl-contour2d-1.1.3.tgz",
+ "dependencies": {
+ "binary-search-bounds": {
+ "version": "2.0.3",
+ "from": "binary-search-bounds@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz"
+ }
+ }
+ },
+ "gl-error2d": {
+ "version": "1.2.1",
+ "from": "gl-error2d@>=1.2.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-error2d/-/gl-error2d-1.2.1.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.3.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-error3d": {
+ "version": "1.0.4",
+ "from": "gl-error3d@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-error3d/-/gl-error3d-1.0.4.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-fbo": {
+ "version": "2.0.5",
+ "from": "gl-fbo@>=2.0.3 <3.0.0",
+ "resolved": "https://registry.npmjs.org/gl-fbo/-/gl-fbo-2.0.5.tgz"
+ },
+ "gl-format-compiler-error": {
+ "version": "1.0.2",
+ "from": "gl-format-compiler-error@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-format-compiler-error/-/gl-format-compiler-error-1.0.2.tgz"
+ },
+ "gl-heatmap2d": {
+ "version": "1.0.3",
+ "from": "gl-heatmap2d@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-heatmap2d/-/gl-heatmap2d-1.0.3.tgz",
+ "dependencies": {
+ "binary-search-bounds": {
+ "version": "2.0.3",
+ "from": "binary-search-bounds@>=2.0.3 <3.0.0",
+ "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz"
+ }
+ }
+ },
+ "gl-line2d": {
+ "version": "1.4.1",
+ "from": "gl-line2d@>=1.4.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-line2d/-/gl-line2d-1.4.1.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-line3d": {
+ "version": "1.1.0",
+ "from": "gl-line3d@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-line3d/-/gl-line3d-1.1.0.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-mat2": {
+ "version": "1.0.0",
+ "from": "gl-mat2@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-mat2/-/gl-mat2-1.0.0.tgz"
+ },
+ "gl-mat3": {
+ "version": "1.0.0",
+ "from": "gl-mat3@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-mat3/-/gl-mat3-1.0.0.tgz"
+ },
+ "gl-mat4": {
+ "version": "1.1.4",
+ "from": "gl-mat4@>=1.1.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.1.4.tgz"
+ },
+ "gl-matrix": {
+ "version": "2.3.2",
+ "from": "gl-matrix@>=2.3.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-2.3.2.tgz"
+ },
+ "gl-matrix-invert": {
+ "version": "1.0.0",
+ "from": "gl-matrix-invert@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-matrix-invert/-/gl-matrix-invert-1.0.0.tgz"
+ },
+ "gl-mesh3d": {
+ "version": "1.2.0",
+ "from": "gl-mesh3d@>=1.2.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-mesh3d/-/gl-mesh3d-1.2.0.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-plot2d": {
+ "version": "1.2.0",
+ "from": "gl-plot2d@>=1.1.6 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-plot2d/-/gl-plot2d-1.2.0.tgz",
+ "dependencies": {
+ "binary-search-bounds": {
+ "version": "2.0.3",
+ "from": "binary-search-bounds@>=2.0.3 <3.0.0",
+ "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz"
+ },
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "gl-shader": {
+ "version": "4.2.1",
+ "from": "gl-shader@>=4.2.1 <5.0.0",
+ "resolved": "https://registry.npmjs.org/gl-shader/-/gl-shader-4.2.1.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.2.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-plot3d": {
+ "version": "1.5.1",
+ "from": "gl-plot3d@>=1.5.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-plot3d/-/gl-plot3d-1.5.1.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-pointcloud2d": {
+ "version": "1.0.0",
+ "from": "gl-pointcloud2d@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-pointcloud2d/-/gl-pointcloud2d-1.0.0.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "gl-shader": {
+ "version": "4.2.1",
+ "from": "gl-shader@>=4.2.1 <5.0.0",
+ "resolved": "https://registry.npmjs.org/gl-shader/-/gl-shader-4.2.1.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-quat": {
+ "version": "1.0.0",
+ "from": "gl-quat@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-quat/-/gl-quat-1.0.0.tgz"
+ },
+ "gl-scatter2d": {
+ "version": "1.2.2",
+ "from": "gl-scatter2d@>=1.2.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-scatter2d/-/gl-scatter2d-1.2.2.tgz",
+ "dependencies": {
+ "binary-search-bounds": {
+ "version": "2.0.3",
+ "from": "binary-search-bounds@>=2.0.3 <3.0.0",
+ "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz"
+ },
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ },
+ "snap-points-2d": {
+ "version": "3.1.0",
+ "from": "snap-points-2d@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/snap-points-2d/-/snap-points-2d-3.1.0.tgz"
+ }
+ }
+ },
+ "gl-scatter2d-fancy": {
+ "version": "1.2.1",
+ "from": "gl-scatter2d-fancy@>=1.2.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-scatter2d-fancy/-/gl-scatter2d-fancy-1.2.1.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "gl-shader": {
+ "version": "4.2.1",
+ "from": "gl-shader@>=4.2.1 <5.0.0",
+ "resolved": "https://registry.npmjs.org/gl-shader/-/gl-shader-4.2.1.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.3.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-scatter3d": {
+ "version": "1.0.10",
+ "from": "gl-scatter3d@>=1.0.4 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-scatter3d/-/gl-scatter3d-1.0.10.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.2.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-select-box": {
+ "version": "1.0.1",
+ "from": "gl-select-box@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-select-box/-/gl-select-box-1.0.1.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.2.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-select-static": {
+ "version": "2.0.2",
+ "from": "gl-select-static@>=2.0.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/gl-select-static/-/gl-select-static-2.0.2.tgz"
+ },
+ "gl-shader": {
+ "version": "4.2.0",
+ "from": "gl-shader@4.2.0",
+ "resolved": "https://registry.npmjs.org/gl-shader/-/gl-shader-4.2.0.tgz"
+ },
+ "gl-spikes2d": {
+ "version": "1.0.1",
+ "from": "gl-spikes2d@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-spikes2d/-/gl-spikes2d-1.0.1.tgz"
+ },
+ "gl-spikes3d": {
+ "version": "1.0.5",
+ "from": "gl-spikes3d@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-spikes3d/-/gl-spikes3d-1.0.5.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-state": {
+ "version": "1.0.0",
+ "from": "gl-state@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-state/-/gl-state-1.0.0.tgz"
+ },
+ "gl-surface3d": {
+ "version": "1.3.0",
+ "from": "gl-surface3d@>=1.2.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-surface3d/-/gl-surface3d-1.3.0.tgz",
+ "dependencies": {
+ "bl": {
+ "version": "0.9.5",
+ "from": "bl@>=0.9.4 <0.10.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-0.9.5.tgz"
+ },
+ "glslify": {
+ "version": "2.3.1",
+ "from": "glslify@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-2.3.1.tgz"
+ },
+ "glslify-bundle": {
+ "version": "2.0.4",
+ "from": "glslify-bundle@>=2.0.4 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-2.0.4.tgz"
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.1.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.26",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "gl-texture2d": {
+ "version": "2.1.0",
+ "from": "gl-texture2d@>=2.0.9 <3.0.0",
+ "resolved": "https://registry.npmjs.org/gl-texture2d/-/gl-texture2d-2.1.0.tgz"
+ },
+ "gl-vao": {
+ "version": "1.3.0",
+ "from": "gl-vao@>=1.1.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-vao/-/gl-vao-1.3.0.tgz"
+ },
+ "gl-vec3": {
+ "version": "1.0.3",
+ "from": "gl-vec3@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-vec3/-/gl-vec3-1.0.3.tgz"
+ },
+ "gl-vec4": {
+ "version": "1.0.1",
+ "from": "gl-vec4@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/gl-vec4/-/gl-vec4-1.0.1.tgz"
+ },
+ "glob": {
+ "version": "7.1.1",
+ "from": "glob@>=7.0.3 <8.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz"
+ },
+ "glsl-inject-defines": {
+ "version": "1.0.3",
+ "from": "glsl-inject-defines@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-inject-defines/-/glsl-inject-defines-1.0.3.tgz"
+ },
+ "glsl-inverse": {
+ "version": "1.0.0",
+ "from": "glsl-inverse@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-inverse/-/glsl-inverse-1.0.0.tgz"
+ },
+ "glsl-read-float": {
+ "version": "1.1.0",
+ "from": "glsl-read-float@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-read-float/-/glsl-read-float-1.1.0.tgz"
+ },
+ "glsl-resolve": {
+ "version": "0.0.1",
+ "from": "glsl-resolve@0.0.1",
+ "resolved": "https://registry.npmjs.org/glsl-resolve/-/glsl-resolve-0.0.1.tgz",
+ "dependencies": {
+ "resolve": {
+ "version": "0.6.3",
+ "from": "resolve@>=0.6.1 <0.7.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz"
+ },
+ "xtend": {
+ "version": "2.2.0",
+ "from": "xtend@>=2.1.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.2.0.tgz"
+ }
+ }
+ },
+ "glsl-shader-name": {
+ "version": "1.0.0",
+ "from": "glsl-shader-name@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-shader-name/-/glsl-shader-name-1.0.0.tgz"
+ },
+ "glsl-specular-beckmann": {
+ "version": "1.1.2",
+ "from": "glsl-specular-beckmann@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-specular-beckmann/-/glsl-specular-beckmann-1.1.2.tgz"
+ },
+ "glsl-specular-cook-torrance": {
+ "version": "2.0.1",
+ "from": "glsl-specular-cook-torrance@>=2.0.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-specular-cook-torrance/-/glsl-specular-cook-torrance-2.0.1.tgz"
+ },
+ "glsl-token-assignments": {
+ "version": "2.0.2",
+ "from": "glsl-token-assignments@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-assignments/-/glsl-token-assignments-2.0.2.tgz"
+ },
+ "glsl-token-defines": {
+ "version": "1.0.0",
+ "from": "glsl-token-defines@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-defines/-/glsl-token-defines-1.0.0.tgz"
+ },
+ "glsl-token-depth": {
+ "version": "1.1.2",
+ "from": "glsl-token-depth@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-depth/-/glsl-token-depth-1.1.2.tgz"
+ },
+ "glsl-token-descope": {
+ "version": "1.0.2",
+ "from": "glsl-token-descope@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-descope/-/glsl-token-descope-1.0.2.tgz"
+ },
+ "glsl-token-inject-block": {
+ "version": "1.1.0",
+ "from": "glsl-token-inject-block@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-inject-block/-/glsl-token-inject-block-1.1.0.tgz"
+ },
+ "glsl-token-properties": {
+ "version": "1.0.1",
+ "from": "glsl-token-properties@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-properties/-/glsl-token-properties-1.0.1.tgz"
+ },
+ "glsl-token-scope": {
+ "version": "1.1.2",
+ "from": "glsl-token-scope@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-scope/-/glsl-token-scope-1.1.2.tgz"
+ },
+ "glsl-token-string": {
+ "version": "1.0.1",
+ "from": "glsl-token-string@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-string/-/glsl-token-string-1.0.1.tgz"
+ },
+ "glsl-token-whitespace-trim": {
+ "version": "1.0.0",
+ "from": "glsl-token-whitespace-trim@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-token-whitespace-trim/-/glsl-token-whitespace-trim-1.0.0.tgz"
+ },
+ "glsl-tokenizer": {
+ "version": "2.1.2",
+ "from": "glsl-tokenizer@>=2.0.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-tokenizer/-/glsl-tokenizer-2.1.2.tgz"
+ },
+ "glslify": {
+ "version": "4.0.0",
+ "from": "glslify@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/glslify/-/glslify-4.0.0.tgz",
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@^1.2.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ }
+ }
+ },
+ "glslify-bundle": {
+ "version": "4.0.1",
+ "from": "glslify-bundle@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-bundle/-/glslify-bundle-4.0.1.tgz"
+ },
+ "glslify-deps": {
+ "version": "1.3.0",
+ "from": "glslify-deps@>=1.2.5 <2.0.0",
+ "resolved": "https://registry.npmjs.org/glslify-deps/-/glslify-deps-1.3.0.tgz"
+ },
+ "graceful-fs": {
+ "version": "4.1.9",
+ "from": "graceful-fs@>=4.1.2 <5.0.0",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.9.tgz"
+ },
+ "graceful-readlink": {
+ "version": "1.0.1",
+ "from": "graceful-readlink@>=1.0.0",
+ "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
+ },
+ "grid-index": {
+ "version": "1.0.0",
+ "from": "grid-index@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.0.0.tgz"
+ },
+ "har-validator": {
+ "version": "2.0.6",
+ "from": "har-validator@>=2.0.6 <2.1.0",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz"
+ },
+ "has": {
+ "version": "1.0.1",
+ "from": "has@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz"
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "from": "has-ansi@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz"
+ },
+ "has-color": {
+ "version": "0.1.7",
+ "from": "has-color@>=0.1.0 <0.2.0",
+ "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz"
+ },
+ "hawk": {
+ "version": "3.1.3",
+ "from": "hawk@>=3.1.3 <3.2.0",
+ "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz"
+ },
+ "hoek": {
+ "version": "2.16.3",
+ "from": "hoek@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz"
+ },
+ "http-signature": {
+ "version": "1.1.1",
+ "from": "http-signature@>=1.1.0 <1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz"
+ },
+ "ieee754": {
+ "version": "1.1.8",
+ "from": "ieee754@>=1.1.4 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz"
+ },
+ "incremental-convex-hull": {
+ "version": "1.0.1",
+ "from": "incremental-convex-hull@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/incremental-convex-hull/-/incremental-convex-hull-1.0.1.tgz"
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "from": "inflight@>=1.0.4 <2.0.0",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "from": "inherits@>=2.0.1 <2.1.0",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz"
+ },
+ "interval-tree-1d": {
+ "version": "1.0.3",
+ "from": "interval-tree-1d@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/interval-tree-1d/-/interval-tree-1d-1.0.3.tgz"
+ },
+ "invert-permutation": {
+ "version": "1.0.0",
+ "from": "invert-permutation@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/invert-permutation/-/invert-permutation-1.0.0.tgz"
+ },
+ "iota-array": {
+ "version": "1.0.0",
+ "from": "iota-array@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz"
+ },
+ "is-buffer": {
+ "version": "1.1.4",
+ "from": "is-buffer@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.4.tgz"
+ },
+ "is-callable": {
+ "version": "1.1.3",
+ "from": "is-callable@>=1.1.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz"
+ },
+ "is-date-object": {
+ "version": "1.0.1",
+ "from": "is-date-object@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz"
+ },
+ "is-function": {
+ "version": "1.0.1",
+ "from": "is-function@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.1.tgz"
+ },
+ "is-my-json-valid": {
+ "version": "2.15.0",
+ "from": "is-my-json-valid@>=2.10.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz"
+ },
+ "is-property": {
+ "version": "1.0.2",
+ "from": "is-property@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
+ },
+ "is-regex": {
+ "version": "1.0.3",
+ "from": "is-regex@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.3.tgz"
+ },
+ "is-symbol": {
+ "version": "1.0.1",
+ "from": "is-symbol@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz"
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "from": "is-typedarray@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "from": "isarray@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "from": "isstream@>=0.1.2 <0.2.0",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz"
+ },
+ "jodid25519": {
+ "version": "1.0.2",
+ "from": "jodid25519@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz",
+ "optional": true
+ },
+ "jquery": {
+ "version": "3.1.1",
+ "from": "jquery@latest",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.1.1.tgz"
+ },
+ "jquery-ui": {
+ "version": "1.12.1",
+ "from": "jquery-ui@latest",
+ "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz"
+ },
+ "jsbn": {
+ "version": "0.1.0",
+ "from": "jsbn@>=0.1.0 <0.2.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz",
+ "optional": true
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "from": "json-schema@0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz"
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "from": "json-stringify-safe@>=5.0.1 <5.1.0",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
+ },
+ "json5": {
+ "version": "0.5.0",
+ "from": "json5@>=0.5.0 <0.6.0",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.0.tgz"
+ },
+ "jsonlint-lines-primitives": {
+ "version": "1.6.0",
+ "from": "jsonlint-lines-primitives@>=1.6.0 <1.7.0",
+ "resolved": "https://registry.npmjs.org/jsonlint-lines-primitives/-/jsonlint-lines-primitives-1.6.0.tgz"
+ },
+ "jsonpointer": {
+ "version": "4.0.1",
+ "from": "jsonpointer@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz"
+ },
+ "jsprim": {
+ "version": "1.3.1",
+ "from": "jsprim@>=1.2.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz"
+ },
+ "JSV": {
+ "version": "4.0.2",
+ "from": "JSV@>=4.0.0",
+ "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz"
+ },
+ "kdbush": {
+ "version": "1.0.1",
+ "from": "kdbush@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-1.0.1.tgz"
+ },
+ "kind-of": {
+ "version": "3.0.4",
+ "from": "kind-of@>=3.0.2 <4.0.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.0.4.tgz"
+ },
+ "lazy-cache": {
+ "version": "1.0.4",
+ "from": "lazy-cache@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz"
+ },
+ "leaflet": {
+ "version": "1.0.2",
+ "from": "leaflet@latest",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.0.2.tgz"
+ },
+ "leaflet.markercluster": {
+ "version": "1.0.0",
+ "from": "leaflet.markercluster@latest",
+ "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.0.0.tgz"
+ },
+ "levn": {
+ "version": "0.3.0",
+ "from": "levn@>=0.3.0 <0.4.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz"
+ },
+ "loader-utils": {
+ "version": "0.2.16",
+ "from": "loader-utils@>=0.2.11 <0.3.0",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.16.tgz"
+ },
+ "lodash._baseisequal": {
+ "version": "3.0.7",
+ "from": "lodash._baseisequal@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz"
+ },
+ "lodash._bindcallback": {
+ "version": "3.0.1",
+ "from": "lodash._bindcallback@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz"
+ },
+ "lodash._getnative": {
+ "version": "3.9.1",
+ "from": "lodash._getnative@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz"
+ },
+ "lodash.isarguments": {
+ "version": "3.1.0",
+ "from": "lodash.isarguments@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz"
+ },
+ "lodash.isarray": {
+ "version": "3.0.4",
+ "from": "lodash.isarray@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz"
+ },
+ "lodash.isequal": {
+ "version": "3.0.4",
+ "from": "lodash.isequal@>=3.0.4 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-3.0.4.tgz"
+ },
+ "lodash.istypedarray": {
+ "version": "3.0.6",
+ "from": "lodash.istypedarray@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz"
+ },
+ "lodash.keys": {
+ "version": "3.1.2",
+ "from": "lodash.keys@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz"
+ },
+ "longest": {
+ "version": "1.0.1",
+ "from": "longest@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz"
+ },
+ "map-limit": {
+ "version": "0.0.1",
+ "from": "map-limit@0.0.1",
+ "resolved": "https://registry.npmjs.org/map-limit/-/map-limit-0.0.1.tgz",
+ "dependencies": {
+ "once": {
+ "version": "1.3.3",
+ "from": "once@>=1.3.0 <1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz"
+ }
+ }
+ },
+ "mapbox-gl": {
+ "version": "0.22.1",
+ "from": "mapbox-gl@>=0.22.0 <0.23.0",
+ "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-0.22.1.tgz"
+ },
+ "mapbox-gl-function": {
+ "version": "1.3.0",
+ "from": "mapbox-gl-function@>=1.2.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/mapbox-gl-function/-/mapbox-gl-function-1.3.0.tgz"
+ },
+ "mapbox-gl-shaders": {
+ "version": "1.0.0",
+ "from": "mapbox/mapbox-gl-shaders#de2ab007455aa2587c552694c68583f94c9f2747",
+ "resolved": "https://github.com/mapbox/mapbox-gl-shaders.git#de2ab007455aa2587c552694c68583f94c9f2747"
+ },
+ "mapbox-gl-style-spec": {
+ "version": "8.8.0",
+ "from": "mapbox/mapbox-gl-style-spec#83b1a3e5837d785af582efd5ed1a212f2df6a4ae",
+ "resolved": "https://github.com/mapbox/mapbox-gl-style-spec.git#83b1a3e5837d785af582efd5ed1a212f2df6a4ae"
+ },
+ "mapbox-gl-supported": {
+ "version": "1.2.0",
+ "from": "mapbox-gl-supported@>=1.2.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/mapbox-gl-supported/-/mapbox-gl-supported-1.2.0.tgz"
+ },
+ "marching-simplex-table": {
+ "version": "1.0.0",
+ "from": "marching-simplex-table@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/marching-simplex-table/-/marching-simplex-table-1.0.0.tgz"
+ },
+ "marked": {
+ "version": "0.3.6",
+ "from": "marked@latest",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz"
+ },
+ "mat4-decompose": {
+ "version": "1.0.4",
+ "from": "mat4-decompose@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/mat4-decompose/-/mat4-decompose-1.0.4.tgz"
+ },
+ "mat4-interpolate": {
+ "version": "1.0.4",
+ "from": "mat4-interpolate@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/mat4-interpolate/-/mat4-interpolate-1.0.4.tgz"
+ },
+ "mat4-recompose": {
+ "version": "1.0.4",
+ "from": "mat4-recompose@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/mat4-recompose/-/mat4-recompose-1.0.4.tgz"
+ },
+ "material-design-iconic-font": {
+ "version": "2.2.0",
+ "from": "material-design-iconic-font@latest",
+ "resolved": "https://registry.npmjs.org/material-design-iconic-font/-/material-design-iconic-font-2.2.0.tgz"
+ },
+ "matrix-camera-controller": {
+ "version": "2.1.1",
+ "from": "matrix-camera-controller@>=2.1.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/matrix-camera-controller/-/matrix-camera-controller-2.1.1.tgz"
+ },
+ "mime-db": {
+ "version": "1.24.0",
+ "from": "mime-db@>=1.24.0 <1.25.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.24.0.tgz"
+ },
+ "mime-types": {
+ "version": "2.1.12",
+ "from": "mime-types@>=2.1.11 <2.2.0",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.12.tgz"
+ },
+ "minimatch": {
+ "version": "3.0.3",
+ "from": "minimatch@>=3.0.2 <4.0.0",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz"
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "from": "minimist@0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+ },
+ "moment": {
+ "version": "2.15.2",
+ "from": "moment@latest",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.15.2.tgz"
+ },
+ "monotone-convex-hull-2d": {
+ "version": "1.0.1",
+ "from": "monotone-convex-hull-2d@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz"
+ },
+ "mouse-change": {
+ "version": "1.4.0",
+ "from": "mouse-change@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/mouse-change/-/mouse-change-1.4.0.tgz"
+ },
+ "mouse-event": {
+ "version": "1.0.5",
+ "from": "mouse-event@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/mouse-event/-/mouse-event-1.0.5.tgz"
+ },
+ "mouse-wheel": {
+ "version": "1.2.0",
+ "from": "mouse-wheel@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/mouse-wheel/-/mouse-wheel-1.2.0.tgz",
+ "dependencies": {
+ "signum": {
+ "version": "1.0.0",
+ "from": "signum@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/signum/-/signum-1.0.0.tgz"
+ }
+ }
+ },
+ "mousetrap": {
+ "version": "1.6.0",
+ "from": "mousetrap@latest",
+ "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.0.tgz"
+ },
+ "ms": {
+ "version": "0.7.1",
+ "from": "ms@0.7.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+ },
+ "multi-stage-sourcemap": {
+ "version": "0.2.1",
+ "from": "multi-stage-sourcemap@>=0.2.1 <0.3.0",
+ "resolved": "https://registry.npmjs.org/multi-stage-sourcemap/-/multi-stage-sourcemap-0.2.1.tgz",
+ "dependencies": {
+ "source-map": {
+ "version": "0.1.43",
+ "from": "source-map@^0.1.34",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz"
+ }
+ }
+ },
+ "murmurhash-js": {
+ "version": "1.0.0",
+ "from": "murmurhash-js@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz"
+ },
+ "mustache": {
+ "version": "2.2.1",
+ "from": "mustache@latest",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.2.1.tgz"
+ },
+ "ndarray": {
+ "version": "1.0.18",
+ "from": "ndarray@>=1.0.16 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.18.tgz"
+ },
+ "ndarray-extract-contour": {
+ "version": "1.0.1",
+ "from": "ndarray-extract-contour@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-extract-contour/-/ndarray-extract-contour-1.0.1.tgz"
+ },
+ "ndarray-fill": {
+ "version": "1.0.1",
+ "from": "ndarray-fill@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-fill/-/ndarray-fill-1.0.1.tgz"
+ },
+ "ndarray-gradient": {
+ "version": "1.0.0",
+ "from": "ndarray-gradient@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-gradient/-/ndarray-gradient-1.0.0.tgz"
+ },
+ "ndarray-homography": {
+ "version": "1.0.0",
+ "from": "ndarray-homography@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-homography/-/ndarray-homography-1.0.0.tgz"
+ },
+ "ndarray-linear-interpolate": {
+ "version": "1.0.0",
+ "from": "ndarray-linear-interpolate@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-linear-interpolate/-/ndarray-linear-interpolate-1.0.0.tgz"
+ },
+ "ndarray-ops": {
+ "version": "1.2.2",
+ "from": "ndarray-ops@>=1.2.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz"
+ },
+ "ndarray-pack": {
+ "version": "1.2.1",
+ "from": "ndarray-pack@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-pack/-/ndarray-pack-1.2.1.tgz"
+ },
+ "ndarray-scratch": {
+ "version": "1.2.0",
+ "from": "ndarray-scratch@>=1.1.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-scratch/-/ndarray-scratch-1.2.0.tgz"
+ },
+ "ndarray-sort": {
+ "version": "1.0.1",
+ "from": "ndarray-sort@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-sort/-/ndarray-sort-1.0.1.tgz"
+ },
+ "ndarray-warp": {
+ "version": "1.0.1",
+ "from": "ndarray-warp@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/ndarray-warp/-/ndarray-warp-1.0.1.tgz"
+ },
+ "nextafter": {
+ "version": "1.0.0",
+ "from": "nextafter@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/nextafter/-/nextafter-1.0.0.tgz"
+ },
+ "ng-annotate": {
+ "version": "1.2.1",
+ "from": "ng-annotate@latest",
+ "resolved": "https://registry.npmjs.org/ng-annotate/-/ng-annotate-1.2.1.tgz",
+ "dependencies": {
+ "acorn": {
+ "version": "2.6.4",
+ "from": "acorn@>=2.6.4 <2.7.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.6.4.tgz"
+ },
+ "convert-source-map": {
+ "version": "1.1.3",
+ "from": "convert-source-map@>=1.1.2 <1.2.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz"
+ }
+ }
+ },
+ "ng-annotate-loader": {
+ "version": "0.2.0",
+ "from": "ng-annotate-loader@latest",
+ "resolved": "https://registry.npmjs.org/ng-annotate-loader/-/ng-annotate-loader-0.2.0.tgz"
+ },
+ "nomnom": {
+ "version": "1.8.1",
+ "from": "nomnom@>=1.5.0",
+ "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz",
+ "dependencies": {
+ "ansi-styles": {
+ "version": "1.0.0",
+ "from": "ansi-styles@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz"
+ },
+ "chalk": {
+ "version": "0.4.0",
+ "from": "chalk@>=0.4.0 <0.5.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz"
+ },
+ "strip-ansi": {
+ "version": "0.1.1",
+ "from": "strip-ansi@>=0.1.0 <0.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz"
+ },
+ "underscore": {
+ "version": "1.6.0",
+ "from": "underscore@>=1.6.0 <1.7.0",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz"
+ }
+ }
+ },
+ "normalize-path": {
+ "version": "2.0.1",
+ "from": "normalize-path@>=2.0.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz"
+ },
+ "normals": {
+ "version": "1.1.0",
+ "from": "normals@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/normals/-/normals-1.1.0.tgz"
+ },
+ "numeric": {
+ "version": "1.2.6",
+ "from": "numeric@>=1.2.6 <2.0.0",
+ "resolved": "https://registry.npmjs.org/numeric/-/numeric-1.2.6.tgz"
+ },
+ "oauth-sign": {
+ "version": "0.8.2",
+ "from": "oauth-sign@>=0.8.1 <0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz"
+ },
+ "object-assign": {
+ "version": "4.1.0",
+ "from": "object-assign@>=4.0.1 <5.0.0",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz"
+ },
+ "object-inspect": {
+ "version": "1.2.1",
+ "from": "object-inspect@>=1.2.1 <1.3.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.2.1.tgz"
+ },
+ "object-keys": {
+ "version": "1.0.11",
+ "from": "object-keys@>=1.0.8 <2.0.0",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz"
+ },
+ "once": {
+ "version": "1.4.0",
+ "from": "once@>=1.3.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
+ },
+ "optimist": {
+ "version": "0.6.1",
+ "from": "optimist@>=0.6.0 <0.7.0",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz"
+ },
+ "optionator": {
+ "version": "0.8.2",
+ "from": "optionator@>=0.8.2 <0.9.0",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+ "dependencies": {
+ "wordwrap": {
+ "version": "1.0.0",
+ "from": "wordwrap@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
+ }
+ }
+ },
+ "orbit-camera-controller": {
+ "version": "4.0.0",
+ "from": "orbit-camera-controller@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/orbit-camera-controller/-/orbit-camera-controller-4.0.0.tgz"
+ },
+ "ordered-ast-traverse": {
+ "version": "1.1.1",
+ "from": "ordered-ast-traverse@>=1.1.1 <1.2.0",
+ "resolved": "https://registry.npmjs.org/ordered-ast-traverse/-/ordered-ast-traverse-1.1.1.tgz"
+ },
+ "ordered-esprima-props": {
+ "version": "1.1.0",
+ "from": "ordered-esprima-props@>=1.1.0 <1.2.0",
+ "resolved": "https://registry.npmjs.org/ordered-esprima-props/-/ordered-esprima-props-1.1.0.tgz"
+ },
+ "pace-progress": {
+ "version": "1.0.2",
+ "from": "git+https://github.com/getredash/pace.git",
+ "resolved": "git+https://github.com/getredash/pace.git#9edab5c9102aef9f24dd3687de2728bbc419751e"
+ },
+ "pad-left": {
+ "version": "1.0.2",
+ "from": "pad-left@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/pad-left/-/pad-left-1.0.2.tgz"
+ },
+ "parse-unit": {
+ "version": "1.0.1",
+ "from": "parse-unit@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/parse-unit/-/parse-unit-1.0.1.tgz"
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "from": "path-is-absolute@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
+ },
+ "pbf": {
+ "version": "1.3.7",
+ "from": "pbf@>=1.3.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-1.3.7.tgz"
+ },
+ "permutation-parity": {
+ "version": "1.0.0",
+ "from": "permutation-parity@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/permutation-parity/-/permutation-parity-1.0.0.tgz"
+ },
+ "permutation-rank": {
+ "version": "1.0.0",
+ "from": "permutation-rank@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/permutation-rank/-/permutation-rank-1.0.0.tgz"
+ },
+ "pinkie": {
+ "version": "2.0.4",
+ "from": "pinkie@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "from": "pinkie-promise@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz"
+ },
+ "pivottable": {
+ "version": "2.3.0",
+ "from": "pivottable@latest",
+ "resolved": "https://registry.npmjs.org/pivottable/-/pivottable-2.3.0.tgz"
+ },
+ "planar-dual": {
+ "version": "1.0.2",
+ "from": "planar-dual@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/planar-dual/-/planar-dual-1.0.2.tgz"
+ },
+ "planar-graph-to-polyline": {
+ "version": "1.0.5",
+ "from": "planar-graph-to-polyline@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/planar-graph-to-polyline/-/planar-graph-to-polyline-1.0.5.tgz"
+ },
+ "plotly.js": {
+ "version": "1.21.2",
+ "from": "plotly.js@1.21.2",
+ "resolved": "https://registry.npmjs.org/plotly.js/-/plotly.js-1.21.2.tgz"
+ },
+ "pngjs": {
+ "version": "2.3.1",
+ "from": "pngjs@>=2.2.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-2.3.1.tgz"
+ },
+ "point-geometry": {
+ "version": "0.0.0",
+ "from": "point-geometry@>=0.0.0 <0.0.1",
+ "resolved": "https://registry.npmjs.org/point-geometry/-/point-geometry-0.0.0.tgz"
+ },
+ "point-in-big-polygon": {
+ "version": "2.0.0",
+ "from": "point-in-big-polygon@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/point-in-big-polygon/-/point-in-big-polygon-2.0.0.tgz"
+ },
+ "polytope-closest-point": {
+ "version": "1.0.0",
+ "from": "polytope-closest-point@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/polytope-closest-point/-/polytope-closest-point-1.0.0.tgz"
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "from": "prelude-ls@>=1.1.2 <1.2.0",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz"
+ },
+ "process-nextick-args": {
+ "version": "1.0.7",
+ "from": "process-nextick-args@>=1.0.6 <1.1.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
+ },
+ "protocol-buffers-schema": {
+ "version": "2.2.0",
+ "from": "protocol-buffers-schema@>=2.0.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-2.2.0.tgz"
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "from": "punycode@>=1.2.4 <2.0.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz"
+ },
+ "quat-slerp": {
+ "version": "1.0.1",
+ "from": "quat-slerp@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/quat-slerp/-/quat-slerp-1.0.1.tgz"
+ },
+ "quickselect": {
+ "version": "1.0.0",
+ "from": "quickselect@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-1.0.0.tgz"
+ },
+ "quote-stream": {
+ "version": "0.0.0",
+ "from": "quote-stream@>=0.0.0 <0.1.0",
+ "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-0.0.0.tgz",
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "object-keys": {
+ "version": "0.4.0",
+ "from": "object-keys@>=0.4.0 <0.5.0",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.17",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ },
+ "through2": {
+ "version": "0.4.2",
+ "from": "through2@>=0.4.1 <0.5.0",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz"
+ },
+ "xtend": {
+ "version": "2.1.2",
+ "from": "xtend@>=2.1.1 <2.2.0",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz"
+ }
+ }
+ },
+ "rat-vec": {
+ "version": "1.1.0",
+ "from": "rat-vec@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/rat-vec/-/rat-vec-1.1.0.tgz"
+ },
+ "readable-stream": {
+ "version": "2.1.5",
+ "from": "readable-stream@>=2.0.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz"
+ },
+ "reduce-simplicial-complex": {
+ "version": "1.0.0",
+ "from": "reduce-simplicial-complex@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/reduce-simplicial-complex/-/reduce-simplicial-complex-1.0.0.tgz"
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "from": "repeat-string@>=1.5.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz"
+ },
+ "request": {
+ "version": "2.79.0",
+ "from": "request@>=2.39.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
+ "dependencies": {
+ "qs": {
+ "version": "6.3.0",
+ "from": "qs@>=6.3.0 <6.4.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.0.tgz"
+ },
+ "uuid": {
+ "version": "3.0.1",
+ "from": "uuid@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz"
+ }
+ }
+ },
+ "resolve": {
+ "version": "1.1.7",
+ "from": "resolve@>=1.1.6 <2.0.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz"
+ },
+ "resolve-protobuf-schema": {
+ "version": "2.0.0",
+ "from": "resolve-protobuf-schema@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.0.0.tgz"
+ },
+ "resolve-url": {
+ "version": "0.2.1",
+ "from": "resolve-url@>=0.2.1 <0.3.0",
+ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz"
+ },
+ "resumer": {
+ "version": "0.0.0",
+ "from": "resumer@>=0.0.0 <0.1.0",
+ "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz"
+ },
+ "right-align": {
+ "version": "0.1.3",
+ "from": "right-align@>=0.1.1 <0.2.0",
+ "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz"
+ },
+ "right-now": {
+ "version": "1.0.0",
+ "from": "right-now@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/right-now/-/right-now-1.0.0.tgz"
+ },
+ "robust-compress": {
+ "version": "1.0.0",
+ "from": "robust-compress@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-compress/-/robust-compress-1.0.0.tgz"
+ },
+ "robust-determinant": {
+ "version": "1.1.0",
+ "from": "robust-determinant@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-determinant/-/robust-determinant-1.1.0.tgz"
+ },
+ "robust-dot-product": {
+ "version": "1.0.0",
+ "from": "robust-dot-product@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-dot-product/-/robust-dot-product-1.0.0.tgz"
+ },
+ "robust-in-sphere": {
+ "version": "1.1.3",
+ "from": "robust-in-sphere@>=1.1.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-in-sphere/-/robust-in-sphere-1.1.3.tgz"
+ },
+ "robust-linear-solve": {
+ "version": "1.0.0",
+ "from": "robust-linear-solve@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-linear-solve/-/robust-linear-solve-1.0.0.tgz"
+ },
+ "robust-orientation": {
+ "version": "1.1.3",
+ "from": "robust-orientation@>=1.1.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-orientation/-/robust-orientation-1.1.3.tgz"
+ },
+ "robust-product": {
+ "version": "1.0.0",
+ "from": "robust-product@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-product/-/robust-product-1.0.0.tgz"
+ },
+ "robust-scale": {
+ "version": "1.0.2",
+ "from": "robust-scale@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-scale/-/robust-scale-1.0.2.tgz"
+ },
+ "robust-segment-intersect": {
+ "version": "1.0.1",
+ "from": "robust-segment-intersect@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-segment-intersect/-/robust-segment-intersect-1.0.1.tgz"
+ },
+ "robust-subtract": {
+ "version": "1.0.0",
+ "from": "robust-subtract@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-subtract/-/robust-subtract-1.0.0.tgz"
+ },
+ "robust-sum": {
+ "version": "1.0.0",
+ "from": "robust-sum@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/robust-sum/-/robust-sum-1.0.0.tgz"
+ },
+ "rw": {
+ "version": "0.1.4",
+ "from": "rw@>=0.1.4 <0.2.0",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-0.1.4.tgz"
+ },
+ "sane-topojson": {
+ "version": "2.0.0",
+ "from": "sane-topojson@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/sane-topojson/-/sane-topojson-2.0.0.tgz"
+ },
+ "shallow-copy": {
+ "version": "0.0.1",
+ "from": "shallow-copy@0.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz"
+ },
+ "shelf-pack": {
+ "version": "1.1.0",
+ "from": "shelf-pack@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/shelf-pack/-/shelf-pack-1.1.0.tgz"
+ },
+ "signum": {
+ "version": "0.0.0",
+ "from": "signum@>=0.0.0 <0.0.1",
+ "resolved": "https://registry.npmjs.org/signum/-/signum-0.0.0.tgz"
+ },
+ "simple-fmt": {
+ "version": "0.1.0",
+ "from": "simple-fmt@>=0.1.0 <0.2.0",
+ "resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz"
+ },
+ "simple-is": {
+ "version": "0.2.0",
+ "from": "simple-is@>=0.2.0 <0.3.0",
+ "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz"
+ },
+ "simplicial-complex": {
+ "version": "1.0.0",
+ "from": "simplicial-complex@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/simplicial-complex/-/simplicial-complex-1.0.0.tgz"
+ },
+ "simplicial-complex-boundary": {
+ "version": "1.0.1",
+ "from": "simplicial-complex-boundary@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/simplicial-complex-boundary/-/simplicial-complex-boundary-1.0.1.tgz"
+ },
+ "simplicial-complex-contour": {
+ "version": "1.0.2",
+ "from": "simplicial-complex-contour@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/simplicial-complex-contour/-/simplicial-complex-contour-1.0.2.tgz"
+ },
+ "simplify-planar-graph": {
+ "version": "2.0.1",
+ "from": "simplify-planar-graph@>=2.0.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/simplify-planar-graph/-/simplify-planar-graph-2.0.1.tgz",
+ "dependencies": {
+ "bit-twiddle": {
+ "version": "0.0.2",
+ "from": "bit-twiddle@>=0.0.1 <0.1.0",
+ "resolved": "https://registry.npmjs.org/bit-twiddle/-/bit-twiddle-0.0.2.tgz"
+ },
+ "simplicial-complex": {
+ "version": "0.3.3",
+ "from": "simplicial-complex@>=0.3.3 <0.4.0",
+ "resolved": "https://registry.npmjs.org/simplicial-complex/-/simplicial-complex-0.3.3.tgz"
+ },
+ "union-find": {
+ "version": "0.0.4",
+ "from": "union-find@>=0.0.3 <0.1.0",
+ "resolved": "https://registry.npmjs.org/union-find/-/union-find-0.0.4.tgz"
+ }
+ }
+ },
+ "slab-decomposition": {
+ "version": "1.0.2",
+ "from": "slab-decomposition@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/slab-decomposition/-/slab-decomposition-1.0.2.tgz"
+ },
+ "snap-points-2d": {
+ "version": "1.0.1",
+ "from": "snap-points-2d@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/snap-points-2d/-/snap-points-2d-1.0.1.tgz"
+ },
+ "sntp": {
+ "version": "1.0.9",
+ "from": "sntp@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz"
+ },
+ "sort-asc": {
+ "version": "0.1.0",
+ "from": "sort-asc@>=0.1.0 <0.2.0",
+ "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz"
+ },
+ "sort-desc": {
+ "version": "0.1.1",
+ "from": "sort-desc@>=0.1.1 <0.2.0",
+ "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz"
+ },
+ "sort-object": {
+ "version": "0.3.2",
+ "from": "sort-object@>=0.3.2 <0.4.0",
+ "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz"
+ },
+ "source-map": {
+ "version": "0.5.6",
+ "from": "source-map@>=0.5.1 <0.6.0",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"
+ },
+ "split-polygon": {
+ "version": "1.0.0",
+ "from": "split-polygon@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/split-polygon/-/split-polygon-1.0.0.tgz"
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "from": "sprintf-js@>=1.0.2 <1.1.0",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
+ },
+ "sshpk": {
+ "version": "1.10.1",
+ "from": "sshpk@>=1.7.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.10.1.tgz",
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "from": "assert-plus@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
+ }
+ }
+ },
+ "stable": {
+ "version": "0.1.5",
+ "from": "stable@>=0.1.5 <0.2.0",
+ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.5.tgz"
+ },
+ "static-eval": {
+ "version": "0.2.4",
+ "from": "static-eval@>=0.2.0 <0.3.0",
+ "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-0.2.4.tgz",
+ "dependencies": {
+ "escodegen": {
+ "version": "0.0.28",
+ "from": "escodegen@>=0.0.24 <0.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-0.0.28.tgz"
+ },
+ "esprima": {
+ "version": "1.0.4",
+ "from": "esprima@>=1.0.2 <1.1.0",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz"
+ },
+ "estraverse": {
+ "version": "1.3.2",
+ "from": "estraverse@>=1.3.0 <1.4.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.3.2.tgz"
+ }
+ }
+ },
+ "static-module": {
+ "version": "1.3.1",
+ "from": "static-module@>=1.1.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/static-module/-/static-module-1.3.1.tgz",
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "object-inspect": {
+ "version": "0.4.0",
+ "from": "object-inspect@>=0.4.0 <0.5.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-0.4.0.tgz"
+ },
+ "object-keys": {
+ "version": "0.4.0",
+ "from": "object-keys@>=0.4.0 <0.5.0",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@~1.0.27-1",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ },
+ "through2": {
+ "version": "0.4.2",
+ "from": "through2@>=0.4.1 <0.5.0",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz"
+ },
+ "xtend": {
+ "version": "2.1.2",
+ "from": "xtend@>=2.1.1 <2.2.0",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz"
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "from": "string_decoder@>=0.10.0 <0.11.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+ },
+ "string.prototype.trim": {
+ "version": "1.1.2",
+ "from": "string.prototype.trim@>=1.1.2 <1.2.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz"
+ },
+ "stringmap": {
+ "version": "0.2.2",
+ "from": "stringmap@>=0.2.2 <0.3.0",
+ "resolved": "https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz"
+ },
+ "stringset": {
+ "version": "0.2.1",
+ "from": "stringset@>=0.2.1 <0.3.0",
+ "resolved": "https://registry.npmjs.org/stringset/-/stringset-0.2.1.tgz"
+ },
+ "stringstream": {
+ "version": "0.0.5",
+ "from": "stringstream@>=0.0.4 <0.1.0",
+ "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz"
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "from": "strip-ansi@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
+ },
+ "supercluster": {
+ "version": "2.2.0",
+ "from": "supercluster@>=2.0.1 <3.0.0",
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-2.2.0.tgz"
+ },
+ "superscript-text": {
+ "version": "1.0.0",
+ "from": "superscript-text@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/superscript-text/-/superscript-text-1.0.0.tgz"
+ },
+ "surface-nets": {
+ "version": "1.0.2",
+ "from": "surface-nets@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/surface-nets/-/surface-nets-1.0.2.tgz"
+ },
+ "tape": {
+ "version": "4.6.3",
+ "from": "tape@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/tape/-/tape-4.6.3.tgz",
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "from": "minimist@>=1.2.0 <1.3.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz"
+ }
+ }
+ },
+ "text-cache": {
+ "version": "4.1.0",
+ "from": "text-cache@>=4.1.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/text-cache/-/text-cache-4.1.0.tgz"
+ },
+ "through": {
+ "version": "2.3.8",
+ "from": "through@>=2.3.6 <3.0.0",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
+ },
+ "through2": {
+ "version": "0.6.5",
+ "from": "through2@>=0.6.3 <0.7.0",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "from": "isarray@0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "from": "readable-stream@>=1.0.33-1 <1.1.0-0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz"
+ }
+ }
+ },
+ "tinycolor2": {
+ "version": "1.4.1",
+ "from": "tinycolor2@>=1.3.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz"
+ },
+ "to-px": {
+ "version": "1.0.1",
+ "from": "to-px@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/to-px/-/to-px-1.0.1.tgz"
+ },
+ "to-utf8": {
+ "version": "0.0.1",
+ "from": "to-utf8@0.0.1",
+ "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz"
+ },
+ "topojson-client": {
+ "version": "2.1.0",
+ "from": "topojson-client@>=2.1.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-2.1.0.tgz"
+ },
+ "tough-cookie": {
+ "version": "2.3.2",
+ "from": "tough-cookie@>=2.3.0 <2.4.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz"
+ },
+ "triangulate-hypercube": {
+ "version": "1.0.1",
+ "from": "triangulate-hypercube@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/triangulate-hypercube/-/triangulate-hypercube-1.0.1.tgz"
+ },
+ "triangulate-polyline": {
+ "version": "1.0.3",
+ "from": "triangulate-polyline@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/triangulate-polyline/-/triangulate-polyline-1.0.3.tgz"
+ },
+ "tryor": {
+ "version": "0.1.2",
+ "from": "tryor@>=0.1.2 <0.2.0",
+ "resolved": "https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz"
+ },
+ "tunnel-agent": {
+ "version": "0.4.3",
+ "from": "tunnel-agent@>=0.4.1 <0.5.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz"
+ },
+ "turntable-camera-controller": {
+ "version": "3.0.1",
+ "from": "turntable-camera-controller@>=3.0.0 <4.0.0",
+ "resolved": "https://registry.npmjs.org/turntable-camera-controller/-/turntable-camera-controller-3.0.1.tgz"
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "from": "tweetnacl@>=0.14.0 <0.15.0",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "optional": true
+ },
+ "two-product": {
+ "version": "1.0.2",
+ "from": "two-product@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz"
+ },
+ "two-sum": {
+ "version": "1.0.0",
+ "from": "two-sum@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/two-sum/-/two-sum-1.0.0.tgz"
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "from": "type-check@>=0.3.2 <0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz"
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "from": "typedarray@>=0.0.5 <0.1.0",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
+ },
+ "typedarray-pool": {
+ "version": "1.1.0",
+ "from": "typedarray-pool@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/typedarray-pool/-/typedarray-pool-1.1.0.tgz"
+ },
+ "uglify-js": {
+ "version": "2.7.4",
+ "from": "uglify-js@>=2.7.3 <2.8.0",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.4.tgz",
+ "dependencies": {
+ "async": {
+ "version": "0.2.10",
+ "from": "async@>=0.2.6 <0.3.0",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
+ }
+ }
+ },
+ "uglify-to-browserify": {
+ "version": "1.0.2",
+ "from": "uglify-to-browserify@>=1.0.0 <1.1.0",
+ "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz"
+ },
+ "ui-select": {
+ "version": "0.19.6",
+ "from": "ui-select@latest",
+ "resolved": "https://registry.npmjs.org/ui-select/-/ui-select-0.19.6.tgz"
+ },
+ "unassert": {
+ "version": "1.5.1",
+ "from": "unassert@>=1.3.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/unassert/-/unassert-1.5.1.tgz",
+ "dependencies": {
+ "acorn": {
+ "version": "4.0.4",
+ "from": "acorn@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.4.tgz"
+ },
+ "estraverse": {
+ "version": "4.2.0",
+ "from": "estraverse@>=4.1.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz"
+ }
+ }
+ },
+ "unassertify": {
+ "version": "2.0.4",
+ "from": "unassertify@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/unassertify/-/unassertify-2.0.4.tgz",
+ "dependencies": {
+ "acorn": {
+ "version": "4.0.4",
+ "from": "acorn@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.4.tgz"
+ },
+ "escodegen": {
+ "version": "1.8.1",
+ "from": "escodegen@>=1.6.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz"
+ },
+ "estraverse": {
+ "version": "1.9.3",
+ "from": "estraverse@>=1.9.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz"
+ },
+ "source-map": {
+ "version": "0.2.0",
+ "from": "source-map@>=0.2.0 <0.3.0",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
+ "optional": true
+ }
+ }
+ },
+ "underscore": {
+ "version": "1.8.3",
+ "from": "underscore@latest",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz"
+ },
+ "underscore.string": {
+ "version": "3.3.4",
+ "from": "underscore.string@latest",
+ "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz"
+ },
+ "union-find": {
+ "version": "1.0.2",
+ "from": "union-find@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/union-find/-/union-find-1.0.2.tgz"
+ },
+ "uniq": {
+ "version": "1.0.1",
+ "from": "uniq@>=1.0.1 <2.0.0",
+ "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz"
+ },
+ "unitbezier": {
+ "version": "0.0.0",
+ "from": "unitbezier@>=0.0.0 <0.0.1",
+ "resolved": "https://registry.npmjs.org/unitbezier/-/unitbezier-0.0.0.tgz"
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "from": "util-deprecate@>=1.0.1 <1.1.0",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+ },
+ "vector-tile": {
+ "version": "1.3.0",
+ "from": "vector-tile@>=1.3.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/vector-tile/-/vector-tile-1.3.0.tgz"
+ },
+ "vectorize-text": {
+ "version": "3.0.2",
+ "from": "vectorize-text@>=3.0.1 <4.0.0",
+ "resolved": "https://registry.npmjs.org/vectorize-text/-/vectorize-text-3.0.2.tgz"
+ },
+ "verror": {
+ "version": "1.3.6",
+ "from": "verror@1.3.6",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz"
+ },
+ "vt-pbf": {
+ "version": "2.1.2",
+ "from": "vt-pbf@>=2.0.2 <3.0.0",
+ "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-2.1.2.tgz"
+ },
+ "w3c-blob": {
+ "version": "0.0.1",
+ "from": "w3c-blob@0.0.1",
+ "resolved": "https://registry.npmjs.org/w3c-blob/-/w3c-blob-0.0.1.tgz"
+ },
+ "weak-map": {
+ "version": "1.0.5",
+ "from": "weak-map@>=1.0.5 <2.0.0",
+ "resolved": "https://registry.npmjs.org/weak-map/-/weak-map-1.0.5.tgz"
+ },
+ "weakmap-shim": {
+ "version": "1.1.1",
+ "from": "weakmap-shim@>=1.1.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/weakmap-shim/-/weakmap-shim-1.1.1.tgz"
+ },
+ "webgl-context": {
+ "version": "2.2.0",
+ "from": "webgl-context@>=2.2.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/webgl-context/-/webgl-context-2.2.0.tgz"
+ },
+ "webworkify": {
+ "version": "1.4.0",
+ "from": "webworkify@>=1.3.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/webworkify/-/webworkify-1.4.0.tgz"
+ },
+ "wgs84": {
+ "version": "0.0.0",
+ "from": "wgs84@0.0.0",
+ "resolved": "https://registry.npmjs.org/wgs84/-/wgs84-0.0.0.tgz"
+ },
+ "whoots-js": {
+ "version": "2.1.0",
+ "from": "whoots-js@>=2.0.0 <3.0.0",
+ "resolved": "https://registry.npmjs.org/whoots-js/-/whoots-js-2.1.0.tgz"
+ },
+ "window-size": {
+ "version": "0.1.0",
+ "from": "window-size@0.1.0",
+ "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz"
+ },
+ "wordwrap": {
+ "version": "0.0.3",
+ "from": "wordwrap@>=0.0.2 <0.1.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz"
+ },
+ "world-calendars": {
+ "version": "1.0.3",
+ "from": "world-calendars@>=1.0.3 <2.0.0",
+ "resolved": "https://registry.npmjs.org/world-calendars/-/world-calendars-1.0.3.tgz"
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "from": "wrappy@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+ },
+ "xtend": {
+ "version": "4.0.1",
+ "from": "xtend@>=4.0.0 <5.0.0",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
+ },
+ "yargs": {
+ "version": "3.10.0",
+ "from": "yargs@>=3.10.0 <3.11.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz"
+ },
+ "zero-crossings": {
+ "version": "1.0.1",
+ "from": "zero-crossings@>=1.0.0 <2.0.0",
+ "resolved": "https://registry.npmjs.org/zero-crossings/-/zero-crossings-1.0.1.tgz"
+ }
+ }
+}
diff --git a/package.json b/package.json
index b2092e14db..52816f4c89 100644
--- a/package.json
+++ b/package.json
@@ -42,11 +42,13 @@
"d3": "^3.5.17",
"d3-cloud": "^1.2.4",
"debug": "^3.1.0",
+ "diff": "^3.3.0",
"font-awesome": "^4.7.0",
"jquery": "^3.2.1",
"jquery-ui": "^1.12.1",
"leaflet": "^1.2.0",
"leaflet.markercluster": "^1.1.0",
+ "loader-utils": "^1.1.0",
"markdown": "0.5.0",
"material-design-iconic-font": "^2.2.0",
"moment": "^2.18.1",
@@ -77,6 +79,10 @@
"file-loader": "^1.1.4",
"html-webpack-plugin": "^2.30.1",
"node-sass": "^4.5.3",
+ "less": "^2.7.3",
+ "less-loader": "^4.0.5",
+ "less-plugin-autoprefix": "^1.5.1",
+ "ng-annotate-loader": "^0.6.1",
"raw-loader": "^0.5.1",
"sass-loader": "^6.0.6",
"url-loader": "^0.5.9",
diff --git a/redash/__init__.py b/redash/__init__.py
index 6bfd5b6f0a..fb5b17820f 100644
--- a/redash/__init__.py
+++ b/redash/__init__.py
@@ -124,6 +124,11 @@ def create_app(load_admin=True):
app.config['SQLALCHEMY_DATABASE_URI'] = settings.SQLALCHEMY_DATABASE_URI
app.config.update(settings.all_settings())
+ def set_response_headers(response):
+ response.headers['X-Content-Type-Options'] = 'nosniff'
+ return response
+
+ app.after_request(set_response_headers)
provision_app(app)
db.init_app(app)
migrate.init_app(app, db)
diff --git a/redash/admin.py b/redash/admin.py
index f2a892e1ea..3173e90cd5 100644
--- a/redash/admin.py
+++ b/redash/admin.py
@@ -55,7 +55,8 @@ class QueryResultModelView(BaseModelView):
class QueryModelView(BaseModelView):
column_exclude_list = ('latest_query_data',)
- form_excluded_columns = ('version', 'visualizations', 'alerts', 'org', 'created_at', 'updated_at', 'latest_query_data')
+ form_excluded_columns = ('version', 'visualizations', 'alerts', 'org', 'created_at',
+ 'updated_at', 'latest_query_data', 'search_vector')
class DashboardModelView(BaseModelView):
diff --git a/redash/authentication/google_oauth.py b/redash/authentication/google_oauth.py
index 7ceec344e1..ffafe4abf5 100644
--- a/redash/authentication/google_oauth.py
+++ b/redash/authentication/google_oauth.py
@@ -88,7 +88,17 @@ def login():
next_path = request.args.get('next', url_for("redash.index", org_slug=session.get('org_slug')))
logger.debug("Callback url: %s", callback)
logger.debug("Next is: %s", next_path)
- return google_remote_app().authorize(callback=callback, state=next_path)
+ extra = {}
+ if 'org_slug' in session:
+ org = models.Organization.get_by_slug(session.pop('org_slug'))
+ else:
+ org = current_org
+ if org.google_apps_domains:
+ extra['hd'] = org.google_apps_domains[0]
+ if session.get('relogin') == '1':
+ extra['prompt'] = 'consent'
+ session['relogin'] = '0'
+ return google_remote_app().authorize(callback=callback, state=next_path, **extra)
@blueprint.route('/oauth/google_callback', endpoint="callback")
@@ -113,7 +123,7 @@ def authorized():
if not verify_profile(org, profile):
logger.warning("User tried to login with unauthorized domain name: %s (org: %s)", profile['email'], org)
- flash("Your Google Apps account ({}) isn't allowed.".format(profile['email']))
+ session['relogin'] = '1'
return redirect(url_for('redash.login', org_slug=org.slug))
picture_url = "%s?sz=40" % profile['picture']
diff --git a/redash/cli/data_sources.py b/redash/cli/data_sources.py
index 79827132a0..0b7b18ff5d 100644
--- a/redash/cli/data_sources.py
+++ b/redash/cli/data_sources.py
@@ -67,6 +67,30 @@ def test(name, organization='default'):
print("Couldn't find data source named: {}".format(name))
exit(1)
+@manager.command()
+@click.argument('name')
+@click.option('--org', 'organization', default='default',
+ help="The organization the user belongs to "
+ "(leave blank for 'default').")
+def get_data_source_version(name, organization='default'):
+ """Get version of data source connection by issuing a trivial query."""
+ try:
+ org = models.Organization.get_by_slug(organization)
+ data_source = models.DataSource.query.filter(
+ models.DataSource.name == name,
+ models.DataSource.org == org).one()
+ print("Testing get connection data source version: {} (id={})".format(
+ name, data_source.id))
+ try:
+ info = data_source.query_runner.get_data_source_version()
+ except Exception as e:
+ print("Failure: {}".format(e))
+ exit(1)
+ else:
+ print(info)
+ except NoResultFound:
+ print("Couldn't find data source named: {}".format(name))
+ exit(1)
@manager.command()
@click.argument('name', default=None, required=False)
diff --git a/redash/handlers/admin.py b/redash/handlers/admin.py
index 919dc91924..51b0c0ca05 100644
--- a/redash/handlers/admin.py
+++ b/redash/handlers/admin.py
@@ -1,10 +1,12 @@
import json
+import time
from flask import request
-from flask_login import login_required
+from flask_login import current_user, login_required
from redash import models, redis_connection
+from redash.authentication import current_org
from redash.handlers import routes
-from redash.handlers.base import json_response
+from redash.handlers.base import json_response, record_event
from redash.permissions import require_super_admin
from redash.tasks.queries import QueryTaskTracker
@@ -23,6 +25,13 @@ def outdated_queries():
else:
outdated_queries = []
+ record_event(current_org, current_user, {
+ 'action': 'view',
+ 'object_type': 'api_call',
+ 'object_id': 'admin/outdated_queries',
+ 'timestamp': int(time.time()),
+ })
+
return json_response(
dict(queries=[q.to_dict(with_stats=True, with_last_modified_by=False)
for q in outdated_queries],
@@ -41,6 +50,12 @@ def queries_tasks():
waiting = QueryTaskTracker.all(QueryTaskTracker.WAITING_LIST, limit=waiting_limit)
in_progress = QueryTaskTracker.all(QueryTaskTracker.IN_PROGRESS_LIST, limit=progress_limit)
done = QueryTaskTracker.all(QueryTaskTracker.DONE_LIST, limit=done_limit)
+ record_event(current_org, current_user, {
+ 'action': 'view',
+ 'object_type': 'api_call',
+ 'object_id': 'admin/tasks',
+ 'timestamp': int(time.time()),
+ })
response = {
'waiting': [t.data for t in waiting if t is not None],
diff --git a/redash/handlers/alerts.py b/redash/handlers/alerts.py
index fa9b0b15fe..43bf5bd12a 100644
--- a/redash/handlers/alerts.py
+++ b/redash/handlers/alerts.py
@@ -14,6 +14,12 @@ class AlertResource(BaseResource):
def get(self, alert_id):
alert = get_object_or_404(models.Alert.get_by_id_and_org, alert_id, self.current_org)
require_access(alert.groups, self.current_user, view_only)
+ self.record_event({
+ 'action': 'view',
+ 'timestamp': int(time.time()),
+ 'object_id': alert.id,
+ 'object_type': 'alert'
+ })
return alert.to_dict()
def post(self, alert_id):
@@ -73,6 +79,12 @@ def post(self):
@require_permission('list_alerts')
def get(self):
+ self.record_event({
+ 'action': 'view',
+ 'timestamp': int(time.time()),
+ 'object_id': 'alerts',
+ 'object_type': 'api_call'
+ })
return [alert.to_dict() for alert in models.Alert.all(group_ids=self.current_user.group_ids)]
diff --git a/redash/handlers/api.py b/redash/handlers/api.py
index 0e1003c7e6..8e55e82873 100644
--- a/redash/handlers/api.py
+++ b/redash/handlers/api.py
@@ -6,10 +6,13 @@
from redash.handlers.base import org_scoped_rule
from redash.handlers.permissions import ObjectPermissionsListResource, CheckPermissionResource
from redash.handlers.alerts import AlertResource, AlertListResource, AlertSubscriptionListResource, AlertSubscriptionResource
-from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource, PublicDashboardResource
-from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource
+from redash.handlers.dashboards import DashboardListResource, RecentDashboardsResource, DashboardResource, DashboardShareResource, PublicDashboardResource, SearchDashboardResource
+from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource, DataSourceVersionResource
from redash.handlers.events import EventResource
-from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource
+from redash.handlers.queries import (
+ MyQueriesResource, QueryForkResource, QueryListResource,
+ QueryRecentResource, QueryRefreshResource, QueryResource,
+ QuerySearchResource, QueryVersionListResource, ChangeResource)
from redash.handlers.query_results import QueryResultListResource, QueryResultResource, JobResource
from redash.handlers.users import UserResource, UserListResource, UserInviteResource, UserResetPasswordResource
from redash.handlers.visualizations import VisualizationListResource
@@ -49,12 +52,14 @@ def json_representation(data, code, headers=None):
api.add_org_resource(DashboardResource, '/api/dashboards/', endpoint='dashboard')
api.add_org_resource(PublicDashboardResource, '/api/dashboards/public/', endpoint='public_dashboard')
api.add_org_resource(DashboardShareResource, '/api/dashboards//share', endpoint='dashboard_share')
+api.add_org_resource(SearchDashboardResource, '/api/dashboards/search')
api.add_org_resource(DataSourceTypeListResource, '/api/data_sources/types', endpoint='data_source_types')
api.add_org_resource(DataSourceListResource, '/api/data_sources', endpoint='data_sources')
api.add_org_resource(DataSourceSchemaResource, '/api/data_sources//schema')
api.add_org_resource(DataSourcePauseResource, '/api/data_sources//pause')
api.add_org_resource(DataSourceTestResource, '/api/data_sources//test')
+api.add_org_resource(DataSourceVersionResource, '/api/data_sources//version')
api.add_org_resource(DataSourceResource, '/api/data_sources/', endpoint='data_source')
api.add_org_resource(GroupListResource, '/api/groups', endpoint='groups')
@@ -73,6 +78,8 @@ def json_representation(data, code, headers=None):
api.add_org_resource(QueryRefreshResource, '/api/queries//refresh', endpoint='query_refresh')
api.add_org_resource(QueryResource, '/api/queries/', endpoint='query')
api.add_org_resource(QueryForkResource, '/api/queries//fork', endpoint='query_fork')
+api.add_org_resource(QueryVersionListResource, '/api/queries//version', endpoint='query_versions')
+api.add_org_resource(ChangeResource, '/api/changes/', endpoint='changes')
api.add_org_resource(ObjectPermissionsListResource, '/api///acl', endpoint='object_permissions')
api.add_org_resource(CheckPermissionResource, '/api///acl/', endpoint='check_permissions')
diff --git a/redash/handlers/dashboards.py b/redash/handlers/dashboards.py
index 21564e05ee..30ab41f801 100644
--- a/redash/handlers/dashboards.py
+++ b/redash/handlers/dashboards.py
@@ -1,4 +1,5 @@
from itertools import chain
+import json
from flask import request, url_for
from funcy import distinct, project, take
@@ -9,6 +10,7 @@
from redash.permissions import (can_modify, require_admin_or_owner,
require_object_modify_permission,
require_permission)
+from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.exc import StaleDataError
@@ -57,11 +59,11 @@ def post(self):
user=self.current_user,
is_draft=True,
layout='[]')
+ dashboard.record_changes(changed_by=self.current_user)
models.db.session.add(dashboard)
models.db.session.commit()
return dashboard.to_dict()
-
class DashboardResource(BaseResource):
@require_permission('list_dashboards')
def get(self, dashboard_slug=None):
@@ -108,6 +110,12 @@ def get(self, dashboard_slug=None):
response['can_edit'] = can_modify(dashboard, self.current_user)
+ self.record_event({
+ 'action': 'view',
+ 'object_id': dashboard.id,
+ 'object_type': 'dashboard',
+ })
+
return response
@require_permission('edit_dashboard')
@@ -127,6 +135,13 @@ def post(self, dashboard_slug):
dashboard = models.Dashboard.get_by_id_and_org(dashboard_slug, self.current_org)
require_object_modify_permission(dashboard, self.current_user)
+ if 'layout' in dashboard_properties:
+ try:
+ layout = json.loads(dashboard_properties['layout'])
+ except ValueError:
+ abort(400)
+ if not isinstance(layout, list):
+ abort(400)
updates = project(dashboard_properties, ('name', 'layout', 'version',
'is_draft', 'dashboard_filters_enabled'))
@@ -145,8 +160,15 @@ def post(self, dashboard_slug):
models.db.session.commit()
except StaleDataError:
abort(409)
+ except IntegrityError:
+ abort(400)
result = dashboard.to_dict(with_widgets=True, user=self.current_user)
+ self.record_event({
+ 'action': 'edit',
+ 'object_id': dashboard.id,
+ 'object_type': 'dashboard',
+ })
return result
@require_permission('edit_dashboard')
@@ -164,6 +186,11 @@ def delete(self, dashboard_slug):
models.db.session.add(dashboard)
d = dashboard.to_dict(with_widgets=True, user=self.current_user)
models.db.session.commit()
+ self.record_event({
+ 'action': 'archive',
+ 'object_id': dashboard.id,
+ 'object_type': 'dashboard',
+ })
return d
@@ -229,3 +256,21 @@ def delete(self, dashboard_id):
'object_id': dashboard.id,
'object_type': 'dashboard',
})
+
+class SearchDashboardResource(BaseResource):
+ @require_permission('list_dashboards')
+ def get(self):
+ """
+ Searches for a dashboard.
+
+ Sends to models.py > Dashboard > search()
+ search(cls, term, user_id, group_ids, limit_to_users_dashboards=False, include_drafts=False)
+ """
+ term = request.args.get('q', '')
+ include_drafts = request.args.get('include_drafts') is not None
+ user_id = request.args.get('user_id', '')
+ group_ids = self.current_user.group_ids
+ if group_ids == None and request.args.get('test',False):
+ group_ids = [2] # the array that's used for test factory users
+ return [q.to_dict() for q in models.Dashboard.search(term, user_id, group_ids, include_drafts=include_drafts)]
+
diff --git a/redash/handlers/data_sources.py b/redash/handlers/data_sources.py
index 3660c0113b..073c9cdccb 100644
--- a/redash/handlers/data_sources.py
+++ b/redash/handlers/data_sources.py
@@ -25,7 +25,13 @@ class DataSourceResource(BaseResource):
@require_admin
def get(self, data_source_id):
data_source = models.DataSource.get_by_id_and_org(data_source_id, self.current_org)
- return data_source.to_dict(all=True)
+ ds = data_source.to_dict(all=True)
+ self.record_event({
+ 'action': 'view',
+ 'object_id': data_source.id,
+ 'object_type': 'data_source',
+ })
+ return ds
@require_admin
def post(self, data_source_id):
@@ -59,6 +65,11 @@ def post(self, data_source_id):
def delete(self, data_source_id):
data_source = models.DataSource.get_by_id_and_org(data_source_id, self.current_org)
data_source.delete()
+ self.record_event({
+ 'action': 'delete',
+ 'object_id': data_source_id,
+ 'object_type': 'datasource',
+ })
return make_response('', 204)
@@ -83,6 +94,11 @@ def get(self):
except AttributeError:
logging.exception("Error with DataSource#to_dict (data source id: %d)", ds.id)
+ self.record_event({
+ 'action': 'view',
+ 'object_id': 'admin/data_sources',
+ 'object_type': 'api_call',
+ })
return sorted(response.values(), key=lambda d: d['id'])
@require_admin
@@ -172,9 +188,33 @@ class DataSourceTestResource(BaseResource):
def post(self, data_source_id):
data_source = get_object_or_404(models.DataSource.get_by_id_and_org, data_source_id, self.current_org)
+ self.record_event({
+ 'action': 'test',
+ 'object_id': data_source_id,
+ 'object_type': 'datasource',
+ })
+
try:
data_source.query_runner.test_connection()
except Exception as e:
return {"message": unicode(e), "ok": False}
else:
return {"message": "success", "ok": True}
+
+class DataSourceVersionResource(BaseResource):
+ def get(self, data_source_id):
+ data_source = get_object_or_404(models.DataSource.get_by_id_and_org, data_source_id, self.current_org)
+ require_access(data_source.groups, self.current_user, view_only)
+ self.record_event({
+ 'action': 'test',
+ 'object_id': data_source_id,
+ 'object_type': 'data_source_version',
+ })
+ try:
+ version_info = data_source.query_runner.get_data_source_version()
+ except Exception as e:
+ return {"message": unicode(e), "ok": False}
+ else:
+ return {"message": version_info, "ok": True}
+
+
diff --git a/redash/handlers/destinations.py b/redash/handlers/destinations.py
index c1895b7321..254e51f078 100644
--- a/redash/handlers/destinations.py
+++ b/redash/handlers/destinations.py
@@ -19,7 +19,13 @@ class DestinationResource(BaseResource):
@require_admin
def get(self, destination_id):
destination = models.NotificationDestination.get_by_id_and_org(destination_id, self.current_org)
- return destination.to_dict(all=True)
+ d = destination.to_dict(all=True)
+ self.record_event({
+ 'action': 'view',
+ 'object_id': destination_id,
+ 'object_type': 'destination'
+ })
+ return d
@require_admin
def post(self, destination_id):
@@ -48,6 +54,12 @@ def delete(self, destination_id):
models.db.session.delete(destination)
models.db.session.commit()
+ self.record_event({
+ 'action': 'delete',
+ 'object_id': destination_id,
+ 'object_type': 'destination',
+ })
+
return make_response('', 204)
@@ -63,6 +75,12 @@ def get(self):
d = ds.to_dict()
response[ds.id] = d
+ self.record_event({
+ 'action': 'view',
+ 'object_id': 'admin/destinations',
+ 'object_type': 'api_call',
+ })
+
return response.values()
@require_admin
diff --git a/redash/handlers/groups.py b/redash/handlers/groups.py
index 7790044468..ba72346b10 100644
--- a/redash/handlers/groups.py
+++ b/redash/handlers/groups.py
@@ -30,6 +30,12 @@ def get(self):
groups = models.Group.query.filter(
models.Group.id.in_(self.current_user.group_ids))
+ self.record_event({
+ 'action': 'view',
+ 'object_id': 'groups',
+ 'object_type': 'api_call',
+ })
+
return [g.to_dict() for g in groups]
@@ -59,6 +65,12 @@ def get(self, group_id):
group = models.Group.get_by_id_and_org(group_id, self.current_org)
+ self.record_event({
+ 'action': 'view',
+ 'object_id': group_id,
+ 'object_type': 'group',
+ })
+
return group.to_dict()
@require_admin
@@ -154,6 +166,12 @@ def get(self, group_id):
data_sources = (models.DataSource.query
.join(models.DataSourceGroup)
.filter(models.DataSourceGroup.group == group))
+
+ self.record_event({
+ 'action': 'view',
+ 'object_id': group_id,
+ 'object_type': 'group_data_sources',
+ })
return [ds.to_dict(with_permissions_for=group) for ds in data_sources]
diff --git a/redash/handlers/queries.py b/redash/handlers/queries.py
index 8aff0beabb..b6308846c4 100644
--- a/redash/handlers/queries.py
+++ b/redash/handlers/queries.py
@@ -37,7 +37,7 @@ class QuerySearchResource(BaseResource):
@require_permission('view_query')
def get(self):
"""
- Search query text, titles, and descriptions.
+ Search query text, names, and descriptions.
:qparam string q: Search term
@@ -45,8 +45,15 @@ def get(self):
"""
term = request.args.get('q', '')
include_drafts = request.args.get('include_drafts') is not None
-
- return [q.to_dict(with_last_modified_by=False) for q in models.Query.search(term, self.current_user.group_ids, include_drafts=include_drafts)]
+ self.record_event({
+ 'action': 'search',
+ 'object_id': term,
+ 'object_type': 'query',
+ })
+ return [q.to_dict(with_last_modified_by=False)
+ for q in models.Query.search(term,
+ self.current_user.group_ids,
+ include_drafts=include_drafts)]
class QueryRecentResource(BaseResource):
@@ -85,6 +92,7 @@ def post(self):
:json string query: Query text
:>json string query_hash: Hash of query text
:>json string schedule: Schedule interval, in seconds, for repeated execution of this query
+ :json string api_key: Key for public access to this query's results.
:>json boolean is_archived: Whether this query is displayed in indexes and search results or not.
:>json boolean is_draft: Whether this query is a draft or not
@@ -122,6 +131,7 @@ def post(self):
query_def['org'] = self.current_org
query_def['is_draft'] = True
query = models.Query.create(**query_def)
+ query.record_changes(changed_by=self.current_user)
models.db.session.add(query)
models.db.session.commit()
@@ -205,6 +215,7 @@ def post(self, query_id):
try:
self.update_model(query, query_def)
+ query.record_changes(self.current_user)
models.db.session.commit()
except StaleDataError:
abort(409)
@@ -225,6 +236,12 @@ def get(self, query_id):
result = q.to_dict(with_visualizations=True)
result['can_edit'] = can_modify(q, self.current_user)
+
+ self.record_event({
+ 'action': 'view',
+ 'object_id': query_id,
+ 'object_type': 'query',
+ })
return result
# TODO: move to resource of its own? (POST /queries/{id}/archive)
@@ -254,6 +271,11 @@ def post(self, query_id):
require_access(query.data_source.groups, self.current_user, not_view_only)
forked_query = query.fork(self.current_user)
models.db.session.commit()
+ self.record_event({
+ 'action': 'fork',
+ 'object_id': query_id,
+ 'object_type': 'query',
+ })
return forked_query.to_dict(with_visualizations=True)
@@ -278,3 +300,16 @@ def post(self, query_id):
parameter_values = collect_parameters_from_request(request.args)
return run_query(query.data_source, parameter_values, query.query_text, query.id)
+
+
+class QueryVersionListResource(BaseResource):
+ @require_permission('view_query')
+ def get(self, query_id):
+ results = models.Change.list_versions(models.Query.get_by_id(query_id))
+ return [q.to_dict() for q in results]
+
+
+class ChangeResource(BaseResource):
+ @require_permission('view_query')
+ def get(self, change_id):
+ return models.Change.query.get(change_id).to_dict()
diff --git a/redash/handlers/query_snippets.py b/redash/handlers/query_snippets.py
index fc74865771..fbc6a2871c 100644
--- a/redash/handlers/query_snippets.py
+++ b/redash/handlers/query_snippets.py
@@ -11,6 +11,11 @@ class QuerySnippetResource(BaseResource):
def get(self, snippet_id):
snippet = get_object_or_404(models.QuerySnippet.get_by_id_and_org,
snippet_id, self.current_org)
+ self.record_event({
+ 'action': 'view',
+ 'object_id': snippet_id,
+ 'object_type': 'query_snippet',
+ })
return snippet.to_dict()
def post(self, snippet_id):
@@ -69,5 +74,10 @@ def post(self):
return snippet.to_dict()
def get(self):
+ self.record_event({
+ 'action': 'view',
+ 'object_id': 'query_snippets',
+ 'object_type': 'api_call',
+ })
return [snippet.to_dict() for snippet in
models.QuerySnippet.all(org=self.current_org)]
diff --git a/redash/handlers/users.py b/redash/handlers/users.py
index ea8ed73158..65ee9e8989 100644
--- a/redash/handlers/users.py
+++ b/redash/handlers/users.py
@@ -21,6 +21,11 @@ def invite_user(org, inviter, user):
class UserListResource(BaseResource):
@require_permission('list_users')
def get(self):
+ self.record_event({
+ 'action': 'view',
+ 'object_id': 'users',
+ 'object_type': 'api_call',
+ })
return [u.to_dict() for u in models.User.all(self.current_org)]
@require_admin
@@ -87,7 +92,11 @@ class UserResource(BaseResource):
def get(self, user_id):
require_permission_or_owner('list_users', user_id)
user = get_object_or_404(models.User.get_by_id_and_org, user_id, self.current_org)
-
+ self.record_event({
+ 'action': 'view',
+ 'object_id': user_id,
+ 'object_type': 'user',
+ })
return user.to_dict(with_api_key=is_admin_or_owner(user_id))
def post(self, user_id):
diff --git a/redash/handlers/visualizations.py b/redash/handlers/visualizations.py
index 2af1e60852..52a958193c 100644
--- a/redash/handlers/visualizations.py
+++ b/redash/handlers/visualizations.py
@@ -49,5 +49,10 @@ def post(self, visualization_id):
def delete(self, visualization_id):
vis = get_object_or_404(models.Visualization.get_by_id_and_org, visualization_id, self.current_org)
require_object_modify_permission(vis.query_rel, self.current_user)
+ self.record_event({
+ 'action': 'delete',
+ 'object_id': visualization_id,
+ 'object_type': 'visualization',
+ })
models.db.session.delete(vis)
models.db.session.commit()
diff --git a/redash/handlers/widgets.py b/redash/handlers/widgets.py
index 22acd9cfe0..31e1648998 100644
--- a/redash/handlers/widgets.py
+++ b/redash/handlers/widgets.py
@@ -99,4 +99,9 @@ def delete(self, widget_id):
require_object_modify_permission(widget.dashboard, self.current_user)
widget.delete()
models.db.session.commit()
+ self.record_event({
+ 'action': 'delete',
+ 'object_id': widget_id,
+ 'object_type': 'widget',
+ })
return {'layout': widget.dashboard.layout, 'version': widget.dashboard.version}
diff --git a/redash/models.py b/redash/models.py
index ee77f2ebea..547942f1e6 100644
--- a/redash/models.py
+++ b/redash/models.py
@@ -12,7 +12,7 @@
import xlsxwriter
from flask_login import AnonymousUserMixin, UserMixin
-from flask_sqlalchemy import SQLAlchemy
+from flask_sqlalchemy import SQLAlchemy, BaseQuery
from passlib.apps import custom_app_context as pwd_context
from redash import settings, redis_connection, utils
from redash.destinations import (get_configuration_schema_for_destination_type,
@@ -29,10 +29,12 @@
from sqlalchemy.event import listens_for
from sqlalchemy.ext.mutable import Mutable
from sqlalchemy.inspection import inspect
-from sqlalchemy.orm import backref, joinedload, object_session, subqueryload
+from sqlalchemy.orm import backref, joinedload, object_session
from sqlalchemy.orm.exc import NoResultFound # noqa: F401
from sqlalchemy.types import TypeDecorator
from functools import reduce
+from sqlalchemy_searchable import SearchQueryMixin, make_searchable, vectorizer
+from sqlalchemy_utils.types import TSVectorType
class SQLAlchemyExt(SQLAlchemy):
@@ -47,6 +49,21 @@ def apply_pool_defaults(self, app, options):
db = SQLAlchemyExt(session_options={
'expire_on_commit': False
})
+# Make sure the SQLAlchemy mappers are all properly configured first.
+# This is required by SQLAlchemy-Searchable as it adds DDL listeners
+# on the configuration phase of models.
+db.configure_mappers()
+
+# listen to a few database events to set up functions, trigger updates
+# and indexes for the full text search
+make_searchable(options={'regconfig': 'pg_catalog.simple'})
+
+
+class SearchBaseQuery(BaseQuery, SearchQueryMixin):
+ """
+ The SQA query class to use when full text search is wanted.
+ """
+
Column = functools.partial(db.Column, nullable=False)
@@ -178,10 +195,6 @@ class ChangeTrackingMixin(object):
skipped_fields = ('id', 'created_at', 'updated_at', 'version')
_clean_values = None
- def __init__(self, *a, **kw):
- super(ChangeTrackingMixin, self).__init__(*a, **kw)
- self.record_changes(self.user)
-
def prep_cleanvalues(self):
self.__dict__['_clean_values'] = {}
for attr in inspect(self.__class__).column_attrs:
@@ -192,10 +205,10 @@ def prep_cleanvalues(self):
def __setattr__(self, key, value):
if self._clean_values is None:
self.prep_cleanvalues()
- for attr in inspect(self.__class__).column_attrs:
- col, = attr.columns
- previous = getattr(self, attr.key, None)
- self._clean_values[col.name] = previous
+
+ if key in inspect(self.__class__).column_attrs:
+ previous = getattr(self, key, None)
+ self._clean_values[key] = previous
super(ChangeTrackingMixin, self).__setattr__(key, value)
@@ -206,13 +219,19 @@ def record_changes(self, changed_by):
for attr in inspect(self.__class__).column_attrs:
col, = attr.columns
if attr.key not in self.skipped_fields:
- changes[col.name] = {'previous': self._clean_values[col.name],
- 'current': getattr(self, attr.key)}
+ prev = self._clean_values[col.name]
+ current = getattr(self, attr.key)
+ if prev != current:
+ changes[col.name] = {'previous': prev, 'current': current}
- db.session.add(Change(object=self,
- object_version=self.version,
- user=changed_by,
- change=changes))
+ if changes:
+ self.version = (self.version or 0) + 1
+ change = Change(object=self,
+ object_version=self.version,
+ user=changed_by,
+ change=changes)
+ db.session.add(change)
+ return change
class BelongsToOrgMixin(object):
@@ -430,6 +449,8 @@ def to_dict(self, with_api_key=False):
if with_api_key:
d['api_key'] = self.api_key
+ d['last_active_at'] = Event.query.filter(Event.user_id == self.id).with_entities(Event.created_at).order_by(Event.created_at.desc()).first()
+
return d
def is_api_user(self):
@@ -522,13 +543,14 @@ def to_dict(self, all=False, with_permissions_for=None):
'type': self.type,
'syntax': self.query_runner.syntax,
'paused': self.paused,
- 'pause_reason': self.pause_reason
+ 'pause_reason': self.pause_reason,
+ 'type_name': self.query_runner.name()
}
+ schema = get_configuration_schema_for_query_runner_type(self.type)
+ self.options.set_schema(schema)
+ d['options'] = self.options.to_dict(mask_secrets=True)
if all:
- schema = get_configuration_schema_for_query_runner_type(self.type)
- self.options.set_schema(schema)
- d['options'] = self.options.to_dict(mask_secrets=True)
d['queue_name'] = self.queue_name
d['scheduled_queue_name'] = self.scheduled_queue_name
d['groups'] = self.groups
@@ -665,10 +687,16 @@ class QueryResult(db.Model, BelongsToOrgMixin):
data = Column(db.Text)
runtime = Column(postgresql.DOUBLE_PRECISION)
retrieved_at = Column(db.DateTime(True))
+ data_scanned = Column(db.String(255), nullable=True)
__tablename__ = 'query_results'
def to_dict(self):
+ if hasattr(self, 'data_scanned') and self.data_scanned:
+ data_scanned_info = self.data_scanned
+ else:
+ data_scanned_info = ''
+
return {
'id': self.id,
'query_hash': self.query_hash,
@@ -676,7 +704,8 @@ def to_dict(self):
'data': json.loads(self.data),
'data_source_id': self.data_source_id,
'runtime': self.runtime,
- 'retrieved_at': self.retrieved_at
+ 'retrieved_at': self.retrieved_at,
+ 'data_scanned': data_scanned_info
}
@classmethod
@@ -711,13 +740,20 @@ def get_latest(cls, data_source, query, max_age=0):
@classmethod
def store_result(cls, org, data_source, query_hash, query, data, run_time, retrieved_at):
+ try:
+ data_scanned_information = json.loads(data)['data_scanned']
+ except (ValueError, TypeError, KeyError) as e:
+ data_scanned_information = ''
+
query_result = cls(org=org,
query_hash=query_hash,
query_text=query,
runtime=run_time,
data_source=data_source,
retrieved_at=retrieved_at,
- data=data)
+ data=data,
+ data_scanned=data_scanned_information
+ )
db.session.add(query_result)
logging.info("Inserted query (%s) data; id=%s", query_hash, query_result.id)
# TODO: Investigate how big an impact this select-before-update makes.
@@ -799,7 +835,7 @@ def should_schedule_next(previous_iteration, now, schedule, failures):
class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
id = Column(db.Integer, primary_key=True)
- version = Column(db.Integer, default=1)
+ version = Column(db.Integer, default=0)
org_id = Column(db.Integer, db.ForeignKey('organizations.id'))
org = db.relationship(Organization, backref="queries")
data_source_id = Column(db.Integer, db.ForeignKey("data_sources.id"), nullable=True)
@@ -820,9 +856,17 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
is_draft = Column(db.Boolean, default=True, index=True)
schedule = Column(db.String(10), nullable=True)
schedule_failures = Column(db.Integer, default=0)
+ schedule_until = Column(db.DateTime(True), nullable=True)
visualizations = db.relationship("Visualization", cascade="all, delete-orphan")
options = Column(MutableDict.as_mutable(PseudoJSON), default={})
-
+ search_vector = Column(TSVectorType('id', 'name', 'description', 'query',
+ weights={'name': 'A',
+ 'id': 'B',
+ 'description': 'C',
+ 'query': 'D'}),
+ nullable=True)
+
+ query_class = SearchBaseQuery
__tablename__ = 'queries'
__mapper_args__ = {
"version_id_col": version,
@@ -838,6 +882,7 @@ def to_dict(self, with_stats=False, with_visualizations=False, with_user=True, w
'query': self.query_text,
'query_hash': self.query_hash,
'schedule': self.schedule,
+ 'schedule_until': self.schedule_until,
'api_key': self.api_key,
'is_archived': self.is_archived,
'is_draft': self.is_draft,
@@ -923,7 +968,9 @@ def by_user(cls, user):
def outdated_queries(cls):
queries = (db.session.query(Query)
.options(joinedload(Query.latest_query_data).load_only('retrieved_at'))
- .filter(Query.schedule != None)
+ .filter(Query.schedule != None,
+ (Query.schedule_until == None) |
+ (Query.schedule_until > db.func.now()))
.order_by(Query.id))
now = utils.utcnow()
@@ -945,27 +992,24 @@ def outdated_queries(cls):
return outdated_queries.values()
@classmethod
- def search(cls, term, group_ids, include_drafts=False):
- # TODO: This is very naive implementation of search, to be replaced with PostgreSQL full-text-search solution.
- where = (Query.name.ilike(u"%{}%".format(term)) |
- Query.description.ilike(u"%{}%".format(term)))
-
- if term.isdigit():
- where |= Query.id == term
-
- where &= Query.is_archived == False
+ def search(cls, term, group_ids, include_drafts=False, limit=20):
+ where = cls.is_archived == False
if not include_drafts:
- where &= Query.is_draft == False
+ where &= cls.is_draft == False
where &= DataSourceGroup.group_id.in_(group_ids)
- query_ids = (
- db.session.query(Query.id).join(
- DataSourceGroup,
- Query.data_source_id == DataSourceGroup.data_source_id)
- .filter(where)).distinct()
- return Query.query.options(joinedload(Query.user)).filter(Query.id.in_(query_ids))
+ return cls.query.join(
+ DataSourceGroup,
+ cls.data_source_id == DataSourceGroup.data_source_id
+ ).options(
+ joinedload(cls.user)
+ ).filter(where).search(
+ term,
+ # sort the result using the weight as defined in the search vector column
+ sort=True
+ ).distinct().limit(limit)
@classmethod
def recent(cls, group_ids, user_id=None, limit=20):
@@ -1001,6 +1045,7 @@ def fork(self, user):
kwargs = {a: getattr(self, a) for a in forked_list}
forked_query = Query.create(name=u'Copy of (#{}) {}'.format(self.id, self.name),
user=user, **kwargs)
+ forked_query.record_changes(changed_by=user)
for v in self.visualizations:
if v.type == 'TABLE':
@@ -1031,6 +1076,14 @@ def groups(self):
def __unicode__(self):
return unicode(self.id)
+ def __repr__(self):
+ return '' % (self.id, self.name or 'untitled')
+
+
+@vectorizer(db.Integer)
+def integer_vectorizer(column):
+ return db.func.cast(column, db.Text)
+
@listens_for(Query.query_text, 'set')
def gen_query_hash(target, val, oldval, initiator):
@@ -1129,7 +1182,6 @@ def to_dict(self, full=True):
'id': self.id,
'object_id': self.object_id,
'object_type': self.object_type,
- 'change_type': self.change_type,
'object_version': self.object_version,
'change': self.change,
'created_at': self.created_at
@@ -1149,6 +1201,12 @@ def last_change(cls, obj):
cls.object_type == obj.__class__.__tablename__).order_by(
cls.object_version.desc()).first()
+ @classmethod
+ def list_versions(cls, query):
+ return cls.query.filter(
+ cls.object_id == query.id,
+ cls.object_type == 'queries')
+
class Alert(TimestampMixin, db.Model):
UNKNOWN_STATE = 'unknown'
@@ -1244,7 +1302,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
org_id = Column(db.Integer, db.ForeignKey("organizations.id"))
org = db.relationship(Organization, backref="dashboards")
slug = Column(db.String(140), index=True, default=generate_slug)
- name = Column(db.String(100))
+ name = Column(db.String(100), db.CheckConstraint("name<>''", name="dashboard_name_c"))
user_id = Column(db.Integer, db.ForeignKey("users.id"))
user = db.relationship(User)
# TODO: The layout should dynamically be built from position and size information on each widget.
@@ -1332,6 +1390,30 @@ def all(cls, org, group_ids, user_id):
return query
+ @classmethod
+ def search(cls, term, user_id, group_ids, include_drafts=False):
+ # limit_to_users_dashboards=False,
+ # TODO: This is very naive implementation of search, to be replaced with PostgreSQL full-text-search solution.
+ where = (Dashboard.name.ilike(u"%{}%".format(term)))
+
+ if term.isdigit():
+ where |= Dashboard.id == term
+
+ #if limit_to_users_dashboards:
+ # where &= Dashboard.user_id == user_id
+
+ where &= Dashboard.is_archived == False
+
+ if not include_drafts:
+ where &= Dashboard.is_draft == False
+
+ where &= DataSourceGroup.group_id.in_(group_ids)
+ dashboard_ids = (
+ db.session.query(Dashboard.id)
+ .filter(where)).distinct()
+
+ return Dashboard.query.filter(Dashboard.id.in_(dashboard_ids))
+
@classmethod
def recent(cls, org, group_ids, user_id, for_user=False, limit=20):
query = (Dashboard.query
diff --git a/redash/monitor.py b/redash/monitor.py
index 4adab7f38f..a5fae559fd 100644
--- a/redash/monitor.py
+++ b/redash/monitor.py
@@ -30,4 +30,14 @@ def get_status():
'size': redis_connection.llen(queue)
}
+ status['database_metrics'] = []
+ # have to include the fake FROM in the SQL to prevent an IndexError
+ queries = [
+ ['Query Results Size', "pg_size_pretty(pg_total_relation_size('query_results')) as size from (select 1) as a"],
+ ['Redash DB Size', "pg_size_pretty(pg_database_size('postgres')) as size from (select 1) as a"]
+ ]
+ for query_name, query in queries:
+ result = models.db.session.query(query).first()
+ status['database_metrics'].append([query_name, result[0]])
+
return status
diff --git a/redash/query_runner/__init__.py b/redash/query_runner/__init__.py
index 232d212798..868da2b1dc 100644
--- a/redash/query_runner/__init__.py
+++ b/redash/query_runner/__init__.py
@@ -47,6 +47,8 @@ class InterruptException(Exception):
class BaseQueryRunner(object):
noop_query = None
+ default_doc_url = None
+ data_source_version_query = None
def __init__(self, configuration):
self.syntax = 'sql'
@@ -72,6 +74,28 @@ def annotate_query(cls):
def configuration_schema(cls):
return {}
+ def get_data_source_version(self):
+ if self.data_source_version_query is None:
+ raise NotImplementedError
+ data, error = self.run_query(self.data_source_version_query, None)
+
+ if error is not None:
+ raise Exception(error)
+
+ try:
+ version = json.loads(data)['rows'][0]['version']
+ except KeyError as e:
+ raise Exception(e)
+
+ if self.data_source_version_post_process == "split by space take second":
+ version = version.split(" ")[1]
+ elif self.data_source_version_post_process == "split by space take last":
+ version = version.split(" ")[-1]
+ elif self.data_source_version_post_process == "none":
+ version = version
+
+ return version
+
def test_connection(self):
if self.noop_query is None:
raise NotImplementedError()
diff --git a/redash/query_runner/activedata.py b/redash/query_runner/activedata.py
new file mode 100644
index 0000000000..38dd1d8b5a
--- /dev/null
+++ b/redash/query_runner/activedata.py
@@ -0,0 +1,184 @@
+import json
+import logging
+
+import requests
+
+from redash.query_runner import TYPE_INTEGER, TYPE_STRING, TYPE_FLOAT, BaseSQLQueryRunner, register
+from redash.utils import JSONEncoder
+
+#Originally written by Github user @klahnakoski
+#Original link: https://github.com/klahnakoski/ActiveData-redash-query-runner/blob/c0e7286c09c6f1eb6746a6c7cca581bea79f4757/active_data.py
+
+logger = logging.getLogger(__name__)
+
+types_map = {
+ bool: TYPE_INTEGER,
+ str: TYPE_STRING,
+ unicode: TYPE_STRING,
+ dict: TYPE_STRING,
+ list: TYPE_STRING,
+ int: TYPE_INTEGER,
+ long: TYPE_INTEGER,
+ float: TYPE_FLOAT,
+ "string": TYPE_STRING,
+ "object": TYPE_STRING,
+ "long": TYPE_STRING,
+ "double": TYPE_FLOAT,
+ "integer": TYPE_FLOAT
+}
+
+
+class ActiveData(BaseSQLQueryRunner):
+ noop_query = "SELECT 1"
+
+ @classmethod
+ def configuration_schema(cls):
+ return {
+ "type": "object",
+ "properties": {
+ "host_url": {
+ "type": "string",
+ "title": "Host URL",
+ "default": "https://activedata.allizom.org:80",
+ "info": "Please include a port. Do not end with a trailing slash."
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": "https://github.com/klahnakoski/ActiveData/tree/dev/docs"
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
+ }
+ },
+ "required": ["host_url"]
+ }
+
+ @classmethod
+ def name(cls):
+ return "ActiveData"
+
+ @classmethod
+ def type(cls):
+ return "activedata"
+
+ @classmethod
+ def enabled(cls):
+ return True
+
+ def _get_tables(self, schema):
+ query = {
+ "from": "meta.columns",
+ "select": [
+ "name",
+ "type",
+ "table"
+ ],
+ "where": {"not": {"prefix": {"es_index": "meta."}}},
+ "limit": 1000,
+ "format": "list"
+ }
+ results = self.run_jx_query(query, None)
+
+ for row in results['data']:
+ table_name = row['table']
+
+ if table_name not in schema:
+ schema[table_name] = {'name': table_name, 'columns': []}
+
+ schema[table_name]['columns'].append(row['name'] + ' (' + types_map.get(row['type'], TYPE_STRING) + ')')
+
+ return [{'name': r['name'], 'columns': sorted(r['columns'])} for r in schema.values()]
+
+ def run_jx_query(self, query, user):
+ data = json.dumps(query, ensure_ascii=False)
+ result = requests.post(self.configuration['host_url']+"/query", data=data)
+ response = json.loads(result.content)
+
+ if response.get('type') == "ERROR":
+ cause = find_cause(response)
+ raise Exception(cause)
+ return response
+
+ def run_query(self, annotated_query, user):
+ request = {}
+ comment, request["sql"] = annotated_query.split("*/", 2)
+ meta = request['meta'] = {}
+ for kv in comment.strip()[2:].split(","):
+ k, v = [s.strip() for s in kv.split(':')]
+ meta[k] = v
+
+ logger.debug("Send ActiveData a SQL query: %s", request['sql'])
+ data = json.dumps(request, ensure_ascii=False)
+ result = requests.post(self.configuration['host_url']+"/sql", data=data)
+ response = json.loads(result.content)
+
+ if response.get('type') == "ERROR":
+ cause = find_cause(response)
+ return None, cause
+
+ output = normalize(response)
+ output.update({'data_scanned':'N/A'})
+ json_data = json.dumps(output, cls=JSONEncoder)
+ return json_data, None
+
+
+
+def normalize(table):
+ columns = {} # MAP FROM name TO (MAP FROM type TO (full_name))
+ output = []
+
+ def get_unique_name(name, type):
+ all_types = columns.get(name)
+ if all_types is None:
+ all_types = columns[name] = {}
+ specific_type = all_types.get(type)
+ if specific_type is None:
+ if all_types:
+ specific_type = all_types[type] = name + "." + type
+ else:
+ specific_type = all_types[type] = name
+ return specific_type
+
+ for r in table['data']:
+ new_row = {}
+ for i, cname in enumerate(table['header']):
+ val = r[i]
+ if val is None:
+ continue
+ type_ = val.__class__
+ if isinstance(val, (dict, list)):
+ val = json.dumps(val, cls=JSONEncoder)
+ col = get_unique_name(cname, types_map.get(type(val), TYPE_STRING))
+ new_row[col] = val
+ output.append(new_row)
+
+ output_columns = [
+ {
+ "name": full_name,
+ "type": ctype,
+ "friendly_name": full_name
+ }
+ for cname, types in columns.items()
+ for ctype, full_name in types.items()
+ ]
+
+ return {
+ 'columns': output_columns,
+ 'rows': output
+ }
+
+
+def find_cause(e):
+ while e.get('cause') is not None:
+ c = e['cause']
+ if isinstance(c, list):
+ e = c[0]
+ else:
+ e = c
+ return e.get('template')
+
+register(ActiveData)
diff --git a/redash/query_runner/athena.py b/redash/query_runner/athena.py
index 0075d9830e..8ab62e2ce2 100644
--- a/redash/query_runner/athena.py
+++ b/redash/query_runner/athena.py
@@ -79,6 +79,17 @@ def configuration_schema(cls):
'type': 'boolean',
'title': 'Use Glue Data Catalog',
},
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
+ }
},
'required': ['region', 's3_staging_dir'],
'order': ['region', 'aws_access_key', 'aws_secret_key', 's3_staging_dir', 'schema'],
@@ -143,9 +154,10 @@ def get_schema(self, get_stats=False):
schema = {}
query = """
- SELECT table_schema, table_name, column_name
+ SELECT table_schema, table_name, column_name, data_type as column_type, comment as extra_info
FROM information_schema.columns
WHERE table_schema NOT IN ('information_schema')
+ ORDER BY 1, 5 DESC
"""
results, error = self.run_query(query, None)
@@ -157,7 +169,16 @@ def get_schema(self, get_stats=False):
table_name = '{0}.{1}'.format(row['table_schema'], row['table_name'])
if table_name not in schema:
schema[table_name] = {'name': table_name, 'columns': []}
- schema[table_name]['columns'].append(row['column_name'])
+
+ if row['extra_info'] == 'Partition Key':
+ schema[table_name]['columns'].append('[P] ' + row['column_name'] + ' (' + row['column_type'] + ')')
+ elif row['column_type'] == 'integer' or row['column_type'] == 'varchar' or row['column_type'] == 'timestamp' or row['column_type'] == 'boolean' or row['column_type'] == 'bigint':
+ schema[table_name]['columns'].append(row['column_name'] + ' (' + row['column_type'] + ')')
+ elif row['column_type'][0:2] == 'row' or row['column_type'][0:2] == 'map' or row['column_type'][0:2] == 'arr':
+ schema[table_name]['columns'].append(row['column_name'] + ' (row or map or array)')
+ else:
+ schema[table_name]['columns'].append(row['column_name'])
+
return schema.values()
@@ -177,7 +198,12 @@ def run_query(self, query, user):
column_tuples = [(i[0], _TYPE_MAPPINGS.get(i[1], None)) for i in cursor.description]
columns = self.fetch_columns(column_tuples)
rows = [dict(zip(([c['name'] for c in columns]), r)) for i, r in enumerate(cursor.fetchall())]
- data = {'columns': columns, 'rows': rows}
+ qbytes = 'upstream2'
+ try:
+ qbytes = cursor.data_scanned_in_bytes
+ except AttributeError as e:
+ logger.debug("Athena Upstream can't get data_scanned_in_bytes: %s", e)
+ data = {'columns': columns, 'rows': rows, 'data_scanned': qbytes}
json_data = json.dumps(data, cls=JSONEncoder)
error = None
except KeyboardInterrupt:
@@ -185,10 +211,10 @@ def run_query(self, query, user):
cursor.cancel()
error = "Query cancelled by user."
json_data = None
- except Exception as ex:
+ except Exception as exc:
if cursor.query_id:
cursor.cancel()
- error = ex.message
+ error = exc.message
json_data = None
return json_data, error
diff --git a/redash/query_runner/axibase_tsd.py b/redash/query_runner/axibase_tsd.py
index 9737b6bc87..9d50cd3b26 100644
--- a/redash/query_runner/axibase_tsd.py
+++ b/redash/query_runner/axibase_tsd.py
@@ -133,6 +133,12 @@ def configuration_schema(cls):
'trust_certificate': {
'type': 'boolean',
'title': 'Trust SSL Certificate'
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['username', 'password', 'hostname', 'protocol', 'port'],
diff --git a/redash/query_runner/big_query.py b/redash/query_runner/big_query.py
index c2dcf4c5b0..863f68d83e 100644
--- a/redash/query_runner/big_query.py
+++ b/redash/query_runner/big_query.py
@@ -80,6 +80,7 @@ def _get_query_results(jobs, project_id, job_id, start_index):
class BigQuery(BaseQueryRunner):
noop_query = "SELECT 1"
+ default_doc_url = "https://cloud.google.com/bigquery/docs/reference/legacy-sql"
@classmethod
def enabled(cls):
@@ -117,6 +118,17 @@ def configuration_schema(cls):
'maximumBillingTier': {
"type": "number",
"title": "Maximum Billing Tier"
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['jsonKeyFile', 'projectId'],
@@ -242,6 +254,7 @@ def run_query(self, query, user):
return None, "Larger than %d MBytes will be processed (%f MBytes)" % (limitMB, processedMB)
data = self._get_query_result(jobs, query)
+ data.update({'data_scanned':'N/A'})
error = None
json_data = json.dumps(data, cls=JSONEncoder)
diff --git a/redash/query_runner/cass.py b/redash/query_runner/cass.py
index ca8e4537fe..f11ab91374 100644
--- a/redash/query_runner/cass.py
+++ b/redash/query_runner/cass.py
@@ -27,6 +27,7 @@ def default(self, o):
class Cassandra(BaseQueryRunner):
noop_query = "SELECT dateof(now()) FROM system.local"
+ default_doc_url = "http://cassandra.apache.org/doc/latest/cql/index.html"
@classmethod
def enabled(cls):
@@ -65,6 +66,17 @@ def configuration_schema(cls):
'type': 'number',
'title': 'Timeout',
'default': 10
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['keyspace', 'host']
diff --git a/redash/query_runner/clickhouse.py b/redash/query_runner/clickhouse.py
index 7de1b396f8..72d37dd88c 100644
--- a/redash/query_runner/clickhouse.py
+++ b/redash/query_runner/clickhouse.py
@@ -29,6 +29,12 @@ def configuration_schema(cls):
"dbname": {
"type": "string",
"title": "Database Name"
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["dbname"],
diff --git a/redash/query_runner/dynamodb_sql.py b/redash/query_runner/dynamodb_sql.py
index 44f8ccc1d1..e5018fcb28 100644
--- a/redash/query_runner/dynamodb_sql.py
+++ b/redash/query_runner/dynamodb_sql.py
@@ -33,6 +33,9 @@
class DynamoDBSQL(BaseSQLQueryRunner):
+ noop_query = "SELECT 1"
+ default_doc_url = "https://dql.readthedocs.io/en/latest/"
+
@classmethod
def configuration_schema(cls):
return {
@@ -47,6 +50,17 @@ def configuration_schema(cls):
},
"secret_key": {
"type": "string",
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["access_key", "secret_key"],
diff --git a/redash/query_runner/elasticsearch.py b/redash/query_runner/elasticsearch.py
index 085c92a0f8..f0238f3966 100644
--- a/redash/query_runner/elasticsearch.py
+++ b/redash/query_runner/elasticsearch.py
@@ -44,7 +44,8 @@
class BaseElasticSearch(BaseQueryRunner):
- DEBUG_ENABLED = False
+ DEBUG_ENABLED = True
+ default_doc_url = "https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html"
@classmethod
def configuration_schema(cls):
@@ -62,6 +63,17 @@ def configuration_schema(cls):
'basic_auth_password': {
'type': 'string',
'title': 'Basic Auth Password'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"secret": ["basic_auth_password"],
diff --git a/redash/query_runner/google_analytics.py b/redash/query_runner/google_analytics.py
index 225903c427..7cb51a9b14 100644
--- a/redash/query_runner/google_analytics.py
+++ b/redash/query_runner/google_analytics.py
@@ -103,6 +103,12 @@ def configuration_schema(cls):
'jsonKeyFile': {
"type": "string",
'title': 'JSON Key File'
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['jsonKeyFile'],
diff --git a/redash/query_runner/google_spreadsheets.py b/redash/query_runner/google_spreadsheets.py
index 33be529249..297b2b29b9 100644
--- a/redash/query_runner/google_spreadsheets.py
+++ b/redash/query_runner/google_spreadsheets.py
@@ -108,6 +108,7 @@ def parse_worksheet(worksheet):
columns.append({
'name': column_name,
'friendly_name': column_name,
+
'type': TYPE_STRING
})
@@ -139,6 +140,9 @@ def request(self, *args, **kwargs):
class GoogleSpreadsheet(BaseQueryRunner):
+ default_doc_url = ("http://redash.readthedocs.io/en/latest/"
+ "datasources.html#google-spreadsheets")
+
@classmethod
def annotate_query(cls):
return False
@@ -159,6 +163,17 @@ def configuration_schema(cls):
'jsonKeyFile': {
"type": "string",
'title': 'JSON Key File'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['jsonKeyFile'],
@@ -196,6 +211,7 @@ def run_query(self, query, user):
spreadsheet = spreadsheet_service.open_by_key(key)
data = parse_spreadsheet(spreadsheet, worksheet_num)
+ data.update({'data_scanned': 'N/A'})
json_data = json.dumps(data, cls=JSONEncoder)
error = None
diff --git a/redash/query_runner/graphite.py b/redash/query_runner/graphite.py
index 023ec04940..edb1c6449d 100644
--- a/redash/query_runner/graphite.py
+++ b/redash/query_runner/graphite.py
@@ -42,6 +42,17 @@ def configuration_schema(cls):
'verify': {
'type': 'boolean',
'title': 'Verify SSL certificate'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['url'],
diff --git a/redash/query_runner/hive_ds.py b/redash/query_runner/hive_ds.py
index 73be16021b..b887732396 100644
--- a/redash/query_runner/hive_ds.py
+++ b/redash/query_runner/hive_ds.py
@@ -36,6 +36,8 @@
class Hive(BaseSQLQueryRunner):
noop_query = "SELECT 1"
+ default_doc_url = ("https://cwiki.apache.org/confluence/display/Hive/"
+ "LanguageManual")
@classmethod
def configuration_schema(cls):
@@ -53,6 +55,17 @@ def configuration_schema(cls):
},
"username": {
"type": "string"
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["host"]
diff --git a/redash/query_runner/impala_ds.py b/redash/query_runner/impala_ds.py
index d3c42112ce..b75db07864 100644
--- a/redash/query_runner/impala_ds.py
+++ b/redash/query_runner/impala_ds.py
@@ -36,6 +36,8 @@
class Impala(BaseSQLQueryRunner):
noop_query = "show schemas"
+ default_doc_url = ("http://www.cloudera.com/documentation/enterprise/"
+ "latest/topics/impala_langref.html")
@classmethod
def configuration_schema(cls):
@@ -66,6 +68,17 @@ def configuration_schema(cls):
},
"timeout": {
"type": "number"
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["host"],
diff --git a/redash/query_runner/influx_db.py b/redash/query_runner/influx_db.py
index 8360a3af72..85ceb9b911 100644
--- a/redash/query_runner/influx_db.py
+++ b/redash/query_runner/influx_db.py
@@ -50,6 +50,8 @@ def _transform_result(results):
class InfluxDB(BaseQueryRunner):
noop_query = "show measurements limit 1"
+ default_doc_url = ("https://docs.influxdata.com/influxdb/v1.0/"
+ "query_language/spec/")
@classmethod
def configuration_schema(cls):
@@ -58,6 +60,17 @@ def configuration_schema(cls):
'properties': {
'url': {
'type': 'string'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['url']
diff --git a/redash/query_runner/jql.py b/redash/query_runner/jql.py
index 37b1f345c6..61d8fc6598 100644
--- a/redash/query_runner/jql.py
+++ b/redash/query_runner/jql.py
@@ -139,6 +139,8 @@ def get_dict_output_field_name(cls,field_name, member_name):
class JiraJQL(BaseQueryRunner):
noop_query = '{"queryType": "count"}'
+ default_doc_url = ("https://confluence.atlassian.com/jirasoftwarecloud/"
+ "advanced-searching-764478330.html")
@classmethod
def configuration_schema(cls):
@@ -154,6 +156,17 @@ def configuration_schema(cls):
},
'password': {
'type': 'string'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['url', 'username', 'password'],
diff --git a/redash/query_runner/memsql_ds.py b/redash/query_runner/memsql_ds.py
index bd3e106c2f..284874119d 100644
--- a/redash/query_runner/memsql_ds.py
+++ b/redash/query_runner/memsql_ds.py
@@ -56,6 +56,12 @@ def configuration_schema(cls):
},
"password": {
"type": "string"
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
diff --git a/redash/query_runner/mongodb.py b/redash/query_runner/mongodb.py
index 54a94c8665..4c430b4bf5 100644
--- a/redash/query_runner/mongodb.py
+++ b/redash/query_runner/mongodb.py
@@ -75,6 +75,9 @@ def parse_query_json(query):
class MongoDB(BaseQueryRunner):
+ default_doc_url = ("https://docs.mongodb.com/manual/reference/operator/"
+ "query/")
+
@classmethod
def configuration_schema(cls):
return {
@@ -92,6 +95,17 @@ def configuration_schema(cls):
'type': 'string',
'title': 'Replica Set Name'
},
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
+ }
},
'required': ['connectionString', 'dbName']
}
diff --git a/redash/query_runner/mssql.py b/redash/query_runner/mssql.py
index bd120f8395..72ce3b74fd 100644
--- a/redash/query_runner/mssql.py
+++ b/redash/query_runner/mssql.py
@@ -35,6 +35,7 @@ def default(self, o):
class SqlServer(BaseSQLQueryRunner):
noop_query = "SELECT 1"
+ default_doc_url = "https://msdn.microsoft.com/en-us/library/bb510741.aspx"
@classmethod
def configuration_schema(cls):
@@ -68,6 +69,17 @@ def configuration_schema(cls):
"db": {
"type": "string",
"title": "Database Name"
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["db"],
diff --git a/redash/query_runner/mysql.py b/redash/query_runner/mysql.py
index fafa5edf16..aaffe39a93 100644
--- a/redash/query_runner/mysql.py
+++ b/redash/query_runner/mysql.py
@@ -29,6 +29,9 @@
class Mysql(BaseSQLQueryRunner):
noop_query = "SELECT 1"
+ default_doc_url = 'https://dev.mysql.com/doc/refman/5.7/en/'
+ data_source_version_query = "select version()"
+ data_source_version_post_process = "none"
@classmethod
def configuration_schema(cls):
@@ -55,6 +58,12 @@ def configuration_schema(cls):
'port': {
'type': 'number',
'default': 3306,
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"order": ['host', 'port', 'user', 'passwd', 'db'],
@@ -79,6 +88,11 @@ def configuration_schema(cls):
'ssl_key': {
'type': 'string',
'title': 'Path to private key file (SSL)'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
}
})
@@ -91,7 +105,7 @@ def name(cls):
@classmethod
def enabled(cls):
try:
- import MySQLdb
+ import pymysql
except ImportError:
return False
@@ -101,7 +115,8 @@ def _get_tables(self, schema):
query = """
SELECT col.table_schema,
col.table_name,
- col.column_name
+ col.column_name,
+ col.column_type
FROM `information_schema`.`columns` col
WHERE col.table_schema NOT IN ('information_schema', 'performance_schema', 'mysql');
"""
@@ -122,16 +137,16 @@ def _get_tables(self, schema):
if table_name not in schema:
schema[table_name] = {'name': table_name, 'columns': []}
- schema[table_name]['columns'].append(row['column_name'])
+ schema[table_name]['columns'].append(row['column_name'] + ' (' + row['column_type'] + ')')
return schema.values()
def run_query(self, query, user):
- import MySQLdb
+ import pymysql
connection = None
try:
- connection = MySQLdb.connect(host=self.configuration.get('host', ''),
+ connection = pymysql.connect(host=self.configuration.get('host', ''),
user=self.configuration.get('user', ''),
passwd=self.configuration.get('passwd', ''),
db=self.configuration['db'],
@@ -153,7 +168,7 @@ def run_query(self, query, user):
columns = self.fetch_columns([(i[0], types_map.get(i[1], None)) for i in cursor.description])
rows = [dict(zip((c['name'] for c in columns), row)) for row in data]
- data = {'columns': columns, 'rows': rows}
+ data = {'columns': columns, 'rows': rows, 'data_scanned': 'N/A'}
json_data = json.dumps(data, cls=JSONEncoder)
error = None
else:
@@ -161,7 +176,7 @@ def run_query(self, query, user):
error = "No data was returned."
cursor.close()
- except MySQLdb.Error as e:
+ except pymysql.Error as e:
json_data = None
error = e.args[1]
except KeyboardInterrupt:
diff --git a/redash/query_runner/oracle.py b/redash/query_runner/oracle.py
index 0e3784f9d6..9f03dafc25 100644
--- a/redash/query_runner/oracle.py
+++ b/redash/query_runner/oracle.py
@@ -31,8 +31,10 @@
logger = logging.getLogger(__name__)
+
class Oracle(BaseSQLQueryRunner):
noop_query = "SELECT 1 FROM dual"
+ default_doc_url = "http://docs.oracle.com/database/121/SQLRF/toc.htm"
@classmethod
def get_col_type(cls, col_type, scale):
@@ -65,6 +67,17 @@ def configuration_schema(cls):
"servicename": {
"type": "string",
"title": "DSN Service Name"
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["servicename", "user", "password", "host", "port"],
diff --git a/redash/query_runner/pg.py b/redash/query_runner/pg.py
index 50f38a4873..ac5e498b2e 100644
--- a/redash/query_runner/pg.py
+++ b/redash/query_runner/pg.py
@@ -47,6 +47,9 @@ def _wait(conn, timeout=None):
class PostgreSQL(BaseSQLQueryRunner):
noop_query = "SELECT 1"
+ default_doc_url = "https://www.postgresql.org/docs/current/"
+ data_source_version_query = "select version()"
+ data_source_version_post_process = "split by space take second"
@classmethod
def configuration_schema(cls):
@@ -75,6 +78,17 @@ def configuration_schema(cls):
"type": "string",
"title": "SSL Mode",
"default": "prefer"
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"order": ['host', 'port', 'user', 'password'],
@@ -103,11 +117,11 @@ def _get_definitions(self, schema, query):
if table_name not in schema:
schema[table_name] = {'name': table_name, 'columns': []}
- schema[table_name]['columns'].append(row['column_name'])
+ schema[table_name]['columns'].append(row['column_name'] + ' (' + row['column_type'] + ')')
def _get_tables(self, schema):
query = """
- SELECT table_schema, table_name, column_name
+ SELECT table_schema, table_name, column_name, udt_name as column_type
FROM information_schema.columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema');
"""
@@ -156,7 +170,7 @@ def run_query(self, query, user):
columns = self.fetch_columns([(i[0], types_map.get(i[1], None)) for i in cursor.description])
rows = [dict(zip((c['name'] for c in columns), row)) for row in cursor]
- data = {'columns': columns, 'rows': rows}
+ data = {'columns': columns, 'rows': rows, 'data_scanned': 'N/A'}
error = None
json_data = json.dumps(data, cls=JSONEncoder)
else:
@@ -179,6 +193,11 @@ def run_query(self, query, user):
class Redshift(PostgreSQL):
+ default_doc_url = ("http://docs.aws.amazon.com/redshift/latest/"
+ "dg/cm_chap_SQLCommandRef.html")
+ data_source_version_query = "select version()"
+ data_source_version_post_process = "split by space take last"
+
@classmethod
def type(cls):
return "redshift"
@@ -218,6 +237,11 @@ def configuration_schema(cls):
"dbname": {
"type": "string",
"title": "Database Name"
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
}
},
"order": ['host', 'port', 'user', 'password'],
diff --git a/redash/query_runner/presto.py b/redash/query_runner/presto.py
index d910a7c56b..a4ce8b64ef 100644
--- a/redash/query_runner/presto.py
+++ b/redash/query_runner/presto.py
@@ -1,4 +1,5 @@
import json
+from markupsafe import Markup, escape
from redash.utils import JSONEncoder
from redash.query_runner import *
@@ -33,6 +34,9 @@
class Presto(BaseQueryRunner):
noop_query = 'SHOW TABLES'
+ default_doc_url = 'https://prestodb.io/docs/current/'
+ data_source_version_query = "SELECT node_version AS version FROM system.runtime.nodes WHERE coordinator = true AND state = 'active'"
+ data_source_version_post_process = "none"
@classmethod
def configuration_schema(cls):
@@ -53,6 +57,17 @@ def configuration_schema(cls):
},
'username': {
'type': 'string'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['host']
@@ -72,9 +87,10 @@ def __init__(self, configuration):
def get_schema(self, get_stats=False):
schema = {}
query = """
- SELECT table_schema, table_name, column_name
+ SELECT table_schema, table_name, column_name, data_type as column_type, extra_info
FROM information_schema.columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
+ ORDER BY 1, 5 DESC
"""
results, error = self.run_query(query, None)
@@ -90,7 +106,14 @@ def get_schema(self, get_stats=False):
if table_name not in schema:
schema[table_name] = {'name': table_name, 'columns': []}
- schema[table_name]['columns'].append(row['column_name'])
+ if row['extra_info'] == 'partition key':
+ schema[table_name]['columns'].append('[P] ' + row['column_name'] + ' (' + row['column_type'] + ')')
+ elif row['column_type'] == 'integer' or row['column_type'] == 'varchar' or row['column_type'] == 'timestamp' or row['column_type'] == 'boolean' or row['column_type'] == 'bigint':
+ schema[table_name]['columns'].append(row['column_name'] + ' (' + row['column_type'] + ')')
+ elif row['column_type'][0:2] == 'row' or row['column_type'][0:2] == 'map' or row['column_type'][0:2] == 'arr':
+ schema[table_name]['columns'].append(row['column_name'] + ' (row or map or array)')
+ else:
+ schema[table_name]['columns'].append(row['column_name'])
return schema.values()
@@ -110,7 +133,10 @@ def run_query(self, query, user):
column_tuples = [(i[0], PRESTO_TYPES_MAPPING.get(i[1], None)) for i in cursor.description]
columns = self.fetch_columns(column_tuples)
rows = [dict(zip(([c['name'] for c in columns]), r)) for i, r in enumerate(cursor.fetchall())]
- data = {'columns': columns, 'rows': rows}
+ for row in rows:
+ for field in row:
+ field = escape(field)
+ data = {'columns': columns, 'rows': rows, 'data_scanned': 'N/A'}
json_data = json.dumps(data, cls=JSONEncoder)
error = None
except DatabaseError as db:
diff --git a/redash/query_runner/python.py b/redash/query_runner/python.py
index eb5ea33e81..3482d3c891 100644
--- a/redash/query_runner/python.py
+++ b/redash/query_runner/python.py
@@ -46,6 +46,9 @@ class Python(BaseQueryRunner):
'tuple', 'set', 'list', 'dict', 'bool',
)
+ default_doc_url = ("http://redash.readthedocs.io/en/latest/"
+ "datasources.html#python")
+
@classmethod
def configuration_schema(cls):
return {
@@ -57,6 +60,17 @@ def configuration_schema(cls):
},
'additionalModulesPaths' : {
'type' : 'string'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
}
diff --git a/redash/query_runner/salesforce.py b/redash/query_runner/salesforce.py
index fcda23c687..06f055425a 100644
--- a/redash/query_runner/salesforce.py
+++ b/redash/query_runner/salesforce.py
@@ -76,6 +76,12 @@ def configuration_schema(cls):
},
"sandbox": {
"type": "boolean"
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["username", "password", "token"],
diff --git a/redash/query_runner/script.py b/redash/query_runner/script.py
index e1c26c7fcd..fa142166e6 100644
--- a/redash/query_runner/script.py
+++ b/redash/query_runner/script.py
@@ -6,6 +6,9 @@
class Script(BaseQueryRunner):
+ default_doc_url = ("http://redash.readthedocs.io/en/latest/"
+ "datasources.html#python")
+
@classmethod
def enabled(cls):
return "check_output" in subprocess.__dict__
@@ -22,6 +25,17 @@ def configuration_schema(cls):
'shell': {
'type': 'boolean',
'title': 'Execute command through the shell'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['path']
diff --git a/redash/query_runner/snowflake.py b/redash/query_runner/snowflake.py
index a1a7ca447e..f223f54c9c 100644
--- a/redash/query_runner/snowflake.py
+++ b/redash/query_runner/snowflake.py
@@ -46,6 +46,12 @@ def configuration_schema(cls):
},
"database": {
"type": "string"
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["user", "password", "account", "database", "warehouse"],
diff --git a/redash/query_runner/sqlite.py b/redash/query_runner/sqlite.py
index 2bab1f27c4..1819dd38bb 100644
--- a/redash/query_runner/sqlite.py
+++ b/redash/query_runner/sqlite.py
@@ -13,6 +13,7 @@
class Sqlite(BaseSQLQueryRunner):
noop_query = "pragma quick_check"
+ default_doc_url = "http://sqlite.org/lang.html"
@classmethod
def configuration_schema(cls):
@@ -22,6 +23,17 @@ def configuration_schema(cls):
"dbpath": {
"type": "string",
"title": "Database Path"
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
"required": ["dbpath"],
diff --git a/redash/query_runner/treasuredata.py b/redash/query_runner/treasuredata.py
index f9e2645e38..ab10d815a4 100644
--- a/redash/query_runner/treasuredata.py
+++ b/redash/query_runner/treasuredata.py
@@ -35,6 +35,7 @@
class TreasureData(BaseQueryRunner):
noop_query = "SELECT 1"
+ default_doc_url = "https://docs.treasuredata.com/categories/hive"
@classmethod
def configuration_schema(cls):
@@ -58,6 +59,17 @@ def configuration_schema(cls):
'type': 'boolean',
'title': 'Auto Schema Retrieval',
'default': False
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
},
'required': ['apikey','db']
diff --git a/redash/query_runner/url.py b/redash/query_runner/url.py
index 8763b63ed2..c99289cca4 100644
--- a/redash/query_runner/url.py
+++ b/redash/query_runner/url.py
@@ -3,6 +3,9 @@
class Url(BaseQueryRunner):
+ default_doc_url = ("http://redash.readthedocs.io/en/latest/"
+ "datasources.html#url")
+
@classmethod
def configuration_schema(cls):
return {
@@ -11,6 +14,17 @@ def configuration_schema(cls):
'url': {
'type': 'string',
'title': 'URL base path'
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
}
}
}
diff --git a/redash/query_runner/vertica.py b/redash/query_runner/vertica.py
index 0bfe2b6611..ed45639879 100644
--- a/redash/query_runner/vertica.py
+++ b/redash/query_runner/vertica.py
@@ -30,6 +30,10 @@
class Vertica(BaseSQLQueryRunner):
noop_query = "SELECT 1"
+ default_doc_url = (
+ "https://my.vertica.com/docs/8.0.x/HTML/index.htm#Authoring/"
+ "ConceptsGuide/Other/SQLOverview.htm%3FTocPath%3DSQL"
+ "%2520Reference%2520Manual%7C_____1")
@classmethod
def configuration_schema(cls):
@@ -56,7 +60,18 @@ def configuration_schema(cls):
"read_timeout": {
"type": "number",
"title": "Read Timeout"
- },
+ },
+ "doc_url": {
+ "type": "string",
+ "title": "Documentation URL",
+ "default": cls.default_doc_url
+ },
+ "toggle_table_string": {
+ "type": "string",
+ "title": "Toggle Table String",
+ "default": "_v",
+ "info": "This string will be used to toggle visibility of tables in the schema browser when editing a query in order to remove non-useful tables from sight."
+ }
},
'required': ['database'],
'secret': ['password']
diff --git a/redash/serializers.py b/redash/serializers.py
index d186e70200..312b9d6a70 100644
--- a/redash/serializers.py
+++ b/redash/serializers.py
@@ -6,9 +6,11 @@
import json
from funcy import project
from redash import models
+from redash.handlers.query_results import run_query_sync
def public_widget(widget):
+
res = {
'id': widget.id,
'width': widget.width,
@@ -18,8 +20,15 @@ def public_widget(widget):
'created_at': widget.created_at
}
- if widget.visualization and widget.visualization.id:
- query_data = models.QueryResult.query.get(widget.visualization.query_rel.latest_query_data_id).to_dict()
+ if (widget.visualization and widget.visualization.id and
+ widget.visualization.query_rel is not None):
+ q = widget.visualization.query_rel
+ # make sure the widget's query has a latest_query_data_id that is
+ # not null so public dashboards work
+ if (q.latest_query_data_id is None):
+ run_query_sync(q.data_source, {}, q.query_text)
+
+ query_data = q.to_dict()
res['visualization'] = {
'type': widget.visualization.type,
'name': widget.visualization.name,
@@ -28,9 +37,10 @@ def public_widget(widget):
'updated_at': widget.visualization.updated_at,
'created_at': widget.visualization.created_at,
'query': {
+ 'id': q.id,
'query': ' ', # workaround, as otherwise the query data won't be loaded.
- 'name': widget.visualization.query_rel.name,
- 'description': widget.visualization.query_rel.description,
+ 'name': q.name,
+ 'description': q.description,
'options': {},
'latest_query_data': query_data
}
@@ -45,7 +55,9 @@ def public_dashboard(dashboard):
widget_list = (models.Widget.query
.filter(models.Widget.dashboard_id == dashboard.id)
.outerjoin(models.Visualization)
- .outerjoin(models.Query))
+ .outerjoin(models.Query)
+ )
+
widgets = {w.id: public_widget(w) for w in widget_list}
widgets_layout = []
diff --git a/redash/settings.py b/redash/settings.py
index 650d8e451f..d9b83cf5c8 100644
--- a/redash/settings.py
+++ b/redash/settings.py
@@ -60,6 +60,9 @@ def all_settings():
return settings
+SESSION_COOKIE_SECURE = True
+
+
NAME = os.environ.get('REDASH_NAME', 'Redash')
LOGO_URL = os.environ.get('REDASH_LOGO_URL', '/images/redash_icon_small.png')
@@ -226,7 +229,8 @@ def all_settings():
'redash.query_runner.axibase_tsd',
'redash.query_runner.salesforce',
'redash.query_runner.query_results',
- 'redash.query_runner.qubole'
+ 'redash.query_runner.qubole',
+ 'redash.query_runner.activedata',
]
enabled_query_runners = array_from_string(os.environ.get("REDASH_ENABLED_QUERY_RUNNERS", ",".join(default_query_runners)))
diff --git a/requirements.txt b/requirements.txt
index b8b6a1048e..cc78ec2f35 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,7 @@ httplib2==0.10.3
Flask-Admin==1.4.2
Flask-RESTful==0.3.5
Flask-Login==0.4.0
-Flask-OAuthLib==0.9.2
+Flask-OAuthLib==0.9.3
Flask-SQLAlchemy==2.1
Flask-Migrate==2.0.1
flask-mail==0.9.1
@@ -25,6 +25,8 @@ redis==2.10.5
requests==2.11.1
six==1.10.0
SQLAlchemy==1.1.4
+SQLAlchemy-Searchable==0.10.6
+SQLAlchemy-Utils>=0.29.0
sqlparse==0.1.8
wsgiref==0.1.2
honcho==0.5.0
@@ -45,4 +47,4 @@ cryptography==2.0.2
simplejson==3.10.0
# Uncomment the requirement for ldap3 if using ldap.
# It is not included by default because of the GPL license conflict.
-# ldap3==2.2.4
\ No newline at end of file
+# ldap3==2.2.4
diff --git a/requirements_all_ds.txt b/requirements_all_ds.txt
index 133d52a27f..4f38ee9335 100644
--- a/requirements_all_ds.txt
+++ b/requirements_all_ds.txt
@@ -3,7 +3,7 @@ google-api-python-client==1.5.1
gspread==0.6.2
impyla==0.10.0
influxdb==2.7.1
-MySQL-python==1.2.5
+PyMySQL==0.7.11
oauth2client==3.0.0
pyhive==0.3.0
pymongo==3.2.1
@@ -21,7 +21,7 @@ cassandra-driver==3.11.0
memsql==2.16.0
atsd_client==2.0.12
simple_salesforce==0.72.2
-PyAthena>=1.0.0
+PyAthena>=1.2.0
pymapd>=0.2.1
qds-sdk>=1.9.6
# certifi is needed to support MongoDB and SSL:
diff --git a/tests/handlers/test_dashboards.py b/tests/handlers/test_dashboards.py
index f2602427fb..d2bab78c23 100644
--- a/tests/handlers/test_dashboards.py
+++ b/tests/handlers/test_dashboards.py
@@ -4,6 +4,18 @@
from redash.permissions import ACCESS_TYPE_MODIFY
+class TestRecentDashboardResourceGet(BaseTestCase):
+ def test_get_recent_dashboard_list_does_not_include_deleted(self):
+ d1 = self.factory.create_dashboard()
+ expected = d1.to_dict()
+ d2 = self.factory.create_dashboard() # this shouldn't be required but test fails without it
+ rv = self.make_request('post', '/api/dashboards/{0}'.format(d1.id),
+ data={'name': 'New Name', 'layout': '[]', 'is_archived': True})
+ rvrecent = self.make_request('get', '/api/dashboards/recent')
+ self.assertEquals(rvrecent.status_code, 200)
+ actual = json.loads(rvrecent.data)
+ self.assertNotIn(expected['id'], actual)
+
class TestDashboardListResource(BaseTestCase):
def test_create_new_dashboard(self):
dashboard_name = 'Test Dashboard'
@@ -151,3 +163,31 @@ def test_requires_admin_or_owner(self):
res = self.make_request('delete', '/api/dashboards/{}/share'.format(dashboard.id), user=user)
self.assertEqual(res.status_code, 200)
+
+class TestDashboardSearchResourceGet(BaseTestCase):
+ def create_dashboard_sequence(self):
+ d1 = self.factory.create_dashboard()
+ new_name = 'Analytics'
+ rv1 = self.make_request('post', '/api/dashboards/{0}'.format(d1.id),
+ data={'name': new_name, 'layout': '[]', 'is_draft': False})
+ d2 = self.factory.create_dashboard()
+ rv2 = self.make_request('post', '/api/dashboards/{0}'.format(d2.id),
+ data={'name': 'Metrics', 'layout': '[]', 'is_draft': True})
+ user = self.factory.create_user()
+ return d1, d2, user
+
+ def test_get_dashboard_search_results_does_not_contain_deleted(self):
+ d1, d2, user = self.create_dashboard_sequence()
+ res = self.make_request('delete', '/api/dashboards/{}/share'.format(d2.id))
+ dash_search_list = self.make_request('get','/api/dashboards/search?q=Metrics')
+ dash_search_list_json = json.loads(dash_search_list.data)
+ self.assertNotIn(d2.id, dash_search_list_json)
+
+ def test_get_dashboard_search_results_obeys_draft_flag(self):
+ d1, d2, user = self.create_dashboard_sequence()
+ dash_search_list = self.make_request('get','/api/dashboards/search?q=Metrics&test=True&user_id={}'.format(user.id))
+ dash_search_list_json = json.loads(dash_search_list.data)
+ self.assertNotIn(d2.id, dash_search_list_json)
+ #self.assertIn(d1.id, dash_search_list_json)
+
+
diff --git a/tests/handlers/test_data_sources.py b/tests/handlers/test_data_sources.py
index f07a2b3719..4590056fd4 100644
--- a/tests/handlers/test_data_sources.py
+++ b/tests/handlers/test_data_sources.py
@@ -60,7 +60,8 @@ def test_updates_data_source(self):
new_name = 'New Name'
new_options = {"dbname": "newdb"}
rv = self.make_request('post', self.path,
- data={'name': new_name, 'type': 'pg', 'options': new_options},
+ data={'name': new_name, 'type': 'pg', 'options': new_options,
+ 'doc_url': None},
user=admin)
self.assertEqual(rv.status_code, 200)
@@ -101,7 +102,9 @@ def test_returns_400_when_configuration_invalid(self):
def test_creates_data_source(self):
admin = self.factory.create_admin()
rv = self.make_request('post', '/api/data_sources',
- data={'name': 'DS 1', 'type': 'pg', 'options': {"dbname": "redash"}}, user=admin)
+ data={'name': 'DS 1', 'type': 'pg',
+ 'options': {"dbname": "redash"},
+ 'doc_url': None}, user=admin)
self.assertEqual(rv.status_code, 200)
diff --git a/tests/handlers/test_embed.py b/tests/handlers/test_embed.py
index 18f119d786..905a6f8672 100644
--- a/tests/handlers/test_embed.py
+++ b/tests/handlers/test_embed.py
@@ -1,5 +1,8 @@
+import mock
+
from tests import BaseTestCase
from redash.models import db
+from redash.query_runner.pg import PostgreSQL
class TestEmbedVisualization(BaseTestCase):
@@ -97,6 +100,15 @@ def test_inactive_token(self):
res = self.make_request('get', '/api/dashboards/public/{}'.format(api_key.api_key), user=False, is_json=False)
self.assertEqual(res.status_code, 404)
+ def test_dashboard_widgets(self):
+ dashboard = self.factory.create_dashboard()
+ w1 = self.factory.create_widget(dashboard=dashboard)
+ w2 = self.factory.create_widget(dashboard=dashboard, visualization=None, text="a text box")
+ api_key = self.factory.create_api_key(object=dashboard)
+ with mock.patch.object(PostgreSQL, "run_query") as qr:
+ qr.return_value = ("[1, 2]", None)
+ res = self.make_request('get', '/api/dashboards/public/{}'.format(api_key.api_key), user=False, is_json=False)
+ self.assertEqual(res.status_code, 200)
# Not relevant for now, as tokens in api_keys table are only created for dashboards. Once this changes, we should
# add this test.
# def test_token_doesnt_belong_to_dashboard(self):
diff --git a/tests/handlers/test_queries.py b/tests/handlers/test_queries.py
index 0092045b26..8853f25b4a 100644
--- a/tests/handlers/test_queries.py
+++ b/tests/handlers/test_queries.py
@@ -183,3 +183,30 @@ def test_must_have_full_access_to_data_source(self):
rv = self.make_request('post', '/api/queries/{}/fork'.format(query.id))
self.assertEqual(rv.status_code, 403)
+
+
+class ChangeResourceTests(BaseTestCase):
+ def test_list(self):
+ query = self.factory.create_query()
+ query.name = 'version A'
+ query.record_changes(self.factory.user)
+ query.name = 'version B'
+ query.record_changes(self.factory.user)
+ rv = self.make_request('get', '/api/queries/{0}/version'.format(query.id))
+ self.assertEquals(rv.status_code, 200)
+ self.assertEquals(len(rv.json), 2)
+ self.assertEquals(rv.json[0]['change']['name']['current'], 'version A')
+ self.assertEquals(rv.json[1]['change']['name']['current'], 'version B')
+
+ def test_get(self):
+ query = self.factory.create_query()
+ query.name = 'version A'
+ ch1 = query.record_changes(self.factory.user)
+ query.name = 'version B'
+ ch2 = query.record_changes(self.factory.user)
+ rv1 = self.make_request('get', '/api/changes/' + str(ch1.id))
+ self.assertEqual(rv1.status_code, 200)
+ self.assertEqual(rv1.json['change']['name']['current'], 'version A')
+ rv2 = self.make_request('get', '/api/changes/' + str(ch2.id))
+ self.assertEqual(rv2.status_code, 200)
+ self.assertEqual(rv2.json['change']['name']['current'], 'version B')
diff --git a/tests/handlers/test_widgets.py b/tests/handlers/test_widgets.py
index d2ce8f6471..9c80eb1437 100644
--- a/tests/handlers/test_widgets.py
+++ b/tests/handlers/test_widgets.py
@@ -90,4 +90,14 @@ def test_delete_widget(self):
# TODO: test how it updates the layout
+ def test_updates_textbox_widget(self):
+ widget = self.factory.create_widget()
+
+ rv = self.make_request('post', '/api/widgets/{0}'.format(widget.id), data={'width':2,'text':'sing and shine on', 'options': {}})
+
+ self.assertEquals(rv.status_code, 200)
+ dashboard = models.Dashboard.get_by_slug_and_org(widget.dashboard.slug, widget.dashboard.org)
+ self.assertEquals(dashboard.widgets.count(), 1)
+ self.assertEquals(dashboard.layout, '[]')
+
diff --git a/tests/models/test_changes.py b/tests/models/test_changes.py
index 124e17a30d..3d7c7496e8 100644
--- a/tests/models/test_changes.py
+++ b/tests/models/test_changes.py
@@ -56,23 +56,12 @@ def test_properly_log_modification(self):
obj.record_changes(changed_by=self.factory.user)
obj.name = 'Query 2'
obj.description = 'description'
- db.session.flush()
obj.record_changes(changed_by=self.factory.user)
change = Change.last_change(obj)
self.assertIsNotNone(change)
- # TODO: https://github.com/getredash/redash/issues/1550
- # self.assertEqual(change.object_version, 2)
+ self.assertEqual(change.object_version, 2)
self.assertEqual(change.object_version, obj.version)
self.assertIn('name', change.change)
self.assertIn('description', change.change)
-
- def test_logs_create_method(self):
- q = Query(name='Query', description='', query_text='',
- user=self.factory.user, data_source=self.factory.data_source,
- org=self.factory.org)
- change = Change.last_change(q)
-
- self.assertIsNotNone(change)
- self.assertEqual(q.user, change.user)
diff --git a/tests/models/test_queries.py b/tests/models/test_queries.py
index f38f8b0712..299c5508d3 100644
--- a/tests/models/test_queries.py
+++ b/tests/models/test_queries.py
@@ -47,6 +47,13 @@ def test_search_by_id_returns_query(self):
self.assertNotIn(q1, queries)
self.assertNotIn(q2, queries)
+ def test_search_by_number(self):
+ q = self.factory.create_query(description="Testing search 12345")
+ db.session.flush()
+ queries = Query.search('12345', [self.factory.default_group.id])
+
+ self.assertIn(q, queries)
+
def test_search_respects_groups(self):
other_group = Group(org=self.factory.org, name="Other Group")
db.session.add(other_group)
@@ -98,6 +105,56 @@ def test_search_is_case_insensitive(self):
self.assertIn(q, Query.search('testing', [self.factory.default_group.id]))
+ def test_search_query_parser_or(self):
+ q1 = self.factory.create_query(name="Testing")
+ q2 = self.factory.create_query(name="search")
+
+ queries = list(Query.search('testing or search', [self.factory.default_group.id]))
+ self.assertIn(q1, queries)
+ self.assertIn(q2, queries)
+
+ def test_search_query_parser_negation(self):
+ q1 = self.factory.create_query(name="Testing")
+ q2 = self.factory.create_query(name="search")
+
+ queries = list(Query.search('testing -search', [self.factory.default_group.id]))
+ self.assertIn(q1, queries)
+ self.assertNotIn(q2, queries)
+
+ def test_search_query_parser_parenthesis(self):
+ q1 = self.factory.create_query(name="Testing search")
+ q2 = self.factory.create_query(name="Testing searching")
+ q3 = self.factory.create_query(name="Testing finding")
+
+ queries = list(Query.search('(testing search) or finding', [self.factory.default_group.id]))
+ self.assertIn(q1, queries)
+ self.assertIn(q2, queries)
+ self.assertIn(q3, queries)
+
+ def test_search_query_parser_hyphen(self):
+ q1 = self.factory.create_query(name="Testing search")
+ q2 = self.factory.create_query(name="Testing-search")
+
+ queries = list(Query.search('testing search', [self.factory.default_group.id]))
+ self.assertIn(q1, queries)
+ self.assertIn(q2, queries)
+
+ def test_search_query_parser_emails(self):
+ q1 = self.factory.create_query(name="janedoe@example.com")
+ q2 = self.factory.create_query(name="johndoe@example.com")
+
+ queries = list(Query.search('example', [self.factory.default_group.id]))
+ self.assertIn(q1, queries)
+ self.assertIn(q2, queries)
+
+ queries = list(Query.search('com', [self.factory.default_group.id]))
+ self.assertIn(q1, queries)
+ self.assertIn(q2, queries)
+
+ queries = list(Query.search('johndoe', [self.factory.default_group.id]))
+ self.assertNotIn(q1, queries)
+ self.assertIn(q2, queries)
+
class QueryRecentTest(BaseTestCase):
def test_global_recent(self):
diff --git a/tests/tasks/test_refresh_queries.py b/tests/tasks/test_refresh_queries.py
index 90641ed1a3..5202559671 100644
--- a/tests/tasks/test_refresh_queries.py
+++ b/tests/tasks/test_refresh_queries.py
@@ -45,3 +45,23 @@ def test_doesnt_enqueue_outdated_queries_for_paused_data_source(self):
add_job_mock.assert_called_with(
query.query_text, query.data_source, query.user_id,
scheduled_query=query, metadata=ANY)
+
+ def test_enqueues_parameterized_queries(self):
+ """
+ Scheduled queries with parameters use saved values.
+ """
+ query = self.factory.create_query(
+ query_text="select {{n}}",
+ options={"parameters": [{
+ "global": False,
+ "type": "text",
+ "name": "n",
+ "value": "42",
+ "title": "n"}]})
+ oq = staticmethod(lambda: [query])
+ with patch('redash.tasks.queries.enqueue_query') as add_job_mock, \
+ patch.object(Query, 'outdated_queries', oq):
+ refresh_queries()
+ add_job_mock.assert_called_with(
+ "select 42", query.data_source, query.user_id,
+ scheduled_query=query, metadata=ANY)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index b46da2094e..61f65abc0e 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -16,7 +16,7 @@ def test_interactive_new(self):
result = runner.invoke(
manager,
['ds', 'new'],
- input="test\n%s\n\n\nexample.com\n\n\ntestdb\n" % (pg_i,))
+ input="test\n%s\n\n\n\n\nexample.com\n\n\ntestdb\n" % (pg_i,))
self.assertFalse(result.exception)
self.assertEqual(result.exit_code, 0)
self.assertEqual(DataSource.query.count(), 1)
diff --git a/tests/test_models.py b/tests/test_models.py
index bac0202f5d..ab47e664e8 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -180,7 +180,8 @@ def test_failure_extends_schedule(self):
Execution failures recorded for a query result in exponential backoff
for scheduling future execution.
"""
- query = self.factory.create_query(schedule="60", schedule_failures=4)
+ query = self.factory.create_query(schedule="60")
+ query.schedule_failures = 4
retrieved_at = utcnow() - datetime.timedelta(minutes=16)
query_result = self.factory.create_query_result(
retrieved_at=retrieved_at, query_text=query.query_text,
@@ -192,6 +193,34 @@ def test_failure_extends_schedule(self):
query_result.retrieved_at = utcnow() - datetime.timedelta(minutes=17)
self.assertEqual(list(models.Query.outdated_queries()), [query])
+ def test_schedule_until_after(self):
+ """
+ Queries with non-null ``schedule_until`` are not reported by
+ Query.outdated_queries() after the given time is past.
+ """
+ three_hours_ago = utcnow() - datetime.timedelta(hours=3)
+ two_hours_ago = utcnow() - datetime.timedelta(hours=2)
+ query = self.factory.create_query(schedule="3600", schedule_until=three_hours_ago)
+ query_result = self.factory.create_query_result(query=query.query_text, retrieved_at=two_hours_ago)
+ query.latest_query_data = query_result
+
+ queries = models.Query.outdated_queries()
+ self.assertNotIn(query, queries)
+
+ def test_schedule_until_before(self):
+ """
+ Queries with non-null ``schedule_until`` are reported by
+ Query.outdated_queries() before the given time is past.
+ """
+ one_hour_from_now = utcnow() + datetime.timedelta(hours=1)
+ two_hours_ago = utcnow() - datetime.timedelta(hours=2)
+ query = self.factory.create_query(schedule="3600", schedule_until=one_hour_from_now)
+ query_result = self.factory.create_query_result(query=query.query_text, retrieved_at=two_hours_ago)
+ query.latest_query_data = query_result
+
+ queries = models.Query.outdated_queries()
+ self.assertIn(query, queries)
+
class QueryArchiveTest(BaseTestCase):
def setUp(self):