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 @@ 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 @@ + + 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 @@

@@ -95,15 +96,26 @@

Execute - - -   - + + {{dataSource.type_name}} documentation + {{ dataSource.type_name }} documentation +
+ @@ -114,7 +126,7 @@

Save* -

@@ -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 +
  • @@ -32,7 +37,6 @@
    -
    @@ -138,18 +142,18 @@
    -
    - -
    +
    + + +
    -
    - +
    + +
    @@ -186,6 +190,12 @@
    + +
    + + + How many characters should X Axis Labels be truncated at in the legend? +
    @@ -216,7 +226,7 @@

    {{$index == 0 ? 'Left' : 'Right'}} Y Axis

    -
    +
    @@ -229,7 +239,8 @@

    {{$index == 0 ? 'Left' : 'Right'}} Y Axis

    + ng-bind="options.seriesOptions[name].zIndex + 1"> + @@ -269,3 +280,34 @@

    {{$index == 0 ? 'Left' : 'Right'}} Y Axis

    zIndex
    + +
    + + + + + + + + + + + + + +
    zIndexLabelColor
    + + {{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 @@
    +
    +
    +
    +
    +
    +
    +
    +
    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: '; + 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):