From c0859642fd2a84e298daa0520347b4dc0ce67bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zsolt=20Kocsm=C3=A1rszky?= Date: Mon, 28 Jan 2019 13:20:55 +0100 Subject: [PATCH 01/27] WIP: Add dashboard details section for dashboard owner and more (#2934) Show dashboard creator on dashboard page --- client/app/pages/dashboards/dashboard.html | 4 ++++ client/app/pages/dashboards/dashboard.less | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/client/app/pages/dashboards/dashboard.html b/client/app/pages/dashboards/dashboard.html index 0e5a675f9e..c28ec8ebd9 100644 --- a/client/app/pages/dashboards/dashboard.html +++ b/client/app/pages/dashboards/dashboard.html @@ -5,9 +5,13 @@

+ + {{$ctrl.dashboard.user.name}} + +
diff --git a/client/app/pages/dashboards/dashboard.less b/client/app/pages/dashboards/dashboard.less index a226e3832f..bb685b4fbf 100644 --- a/client/app/pages/dashboards/dashboard.less +++ b/client/app/pages/dashboards/dashboard.less @@ -92,6 +92,13 @@ } } +.profile__image_thumb--dashboard { + width: 16px; + height: 16px; + border-radius: 100%; + margin: 3px 5px 0 0; +} + .dashboard-header, .page-header--query { .tags-control a { opacity: 0; From 69301063802d9d723847eaa11a2b92c62f5eeb9d Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Mon, 28 Jan 2019 15:59:49 +0200 Subject: [PATCH 02/27] getredash/redash#3355 Widget params: Date/Date range value empty in static param input (#3357) --- client/app/components/ParameterMappingInput.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/app/components/ParameterMappingInput.jsx b/client/app/components/ParameterMappingInput.jsx index 54487b7abc..3b1b620998 100644 --- a/client/app/components/ParameterMappingInput.jsx +++ b/client/app/components/ParameterMappingInput.jsx @@ -1,6 +1,6 @@ /* eslint react/no-multi-comp: 0 */ -import { extend, map, includes, findIndex, find, fromPairs, isNull, isUndefined } from 'lodash'; +import { extend, map, includes, findIndex, find, fromPairs } from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; import Select from 'antd/lib/select'; @@ -163,7 +163,7 @@ export class ParameterMappingInput extends React.Component { this.updateParamMapping(mapping, { value })} From 7278d4b1fcf5f8dbf451bea35a0286d3cad1d4da Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Mon, 28 Jan 2019 16:46:26 +0200 Subject: [PATCH 03/27] Refactor Policy and OrganizationStatus services (#3345) * Refactor Policy and OrganizationStatus services --- .../dashboards/edit-dashboard-dialog.js | 5 +- .../app/components/empty-state/empty-state.js | 13 +-- client/app/config/index.js | 8 +- client/app/pages/dashboards/dashboard.js | 4 +- client/app/pages/data-sources/list.js | 5 +- client/app/pages/queries/view.js | 4 +- client/app/pages/users/list.js | 5 +- client/app/services/ng.js | 2 + client/app/services/organization-status.js | 22 ----- client/app/services/organizationStatus.js | 17 ++++ client/app/services/policy.js | 84 ------------------- client/app/services/policy/DefaultPolicy.js | 49 +++++++++++ client/app/services/policy/index.js | 8 ++ 13 files changed, 99 insertions(+), 127 deletions(-) delete mode 100644 client/app/services/organization-status.js create mode 100644 client/app/services/organizationStatus.js delete mode 100644 client/app/services/policy.js create mode 100644 client/app/services/policy/DefaultPolicy.js create mode 100644 client/app/services/policy/index.js diff --git a/client/app/components/dashboards/edit-dashboard-dialog.js b/client/app/components/dashboards/edit-dashboard-dialog.js index 2253f1b067..a1c765dec7 100644 --- a/client/app/components/dashboards/edit-dashboard-dialog.js +++ b/client/app/components/dashboards/edit-dashboard-dialog.js @@ -1,4 +1,5 @@ import { isEmpty } from 'lodash'; +import { policy } from '@/services/policy'; import template from './edit-dashboard-dialog.html'; const EditDashboardDialog = { @@ -8,11 +9,11 @@ const EditDashboardDialog = { dismiss: '&', }, template, - controller($location, $http, Policy, Events) { + controller($location, $http, Events) { 'ngInject'; this.dashboard = this.resolve.dashboard; - this.policy = Policy; + this.policy = policy; this.isFormValid = () => !isEmpty(this.dashboard.name); diff --git a/client/app/components/empty-state/empty-state.js b/client/app/components/empty-state/empty-state.js index 98af5b1d7c..5db8948af4 100644 --- a/client/app/components/empty-state/empty-state.js +++ b/client/app/components/empty-state/empty-state.js @@ -1,3 +1,4 @@ +import organizationStatus from '@/services/organizationStatus'; import './empty-state.less'; import template from './empty-state.html'; @@ -15,14 +16,14 @@ const EmptyStateComponent = { showInviteStep: '<', onboardingMode: '<', }, - controller($uibModal, OrganizationStatus, currentUser) { + controller($uibModal, currentUser) { this.isAdmin = currentUser.isAdmin; - this.dataSourceStepCompleted = OrganizationStatus.objectCounters.data_sources > 0; - this.queryStepCompleted = OrganizationStatus.objectCounters.queries > 0; - this.dashboardStepCompleted = OrganizationStatus.objectCounters.dashboards > 0; - this.alertStepCompleted = OrganizationStatus.objectCounters.alerts > 0; - this.inviteStepCompleted = OrganizationStatus.objectCounters.users > 1; + this.dataSourceStepCompleted = organizationStatus.objectCounters.data_sources > 0; + this.queryStepCompleted = organizationStatus.objectCounters.queries > 0; + this.dashboardStepCompleted = organizationStatus.objectCounters.dashboards > 0; + this.alertStepCompleted = organizationStatus.objectCounters.alerts > 0; + this.inviteStepCompleted = organizationStatus.objectCounters.users > 1; this.shouldShowOnboarding = () => { if (!this.onboardingMode) { diff --git a/client/app/config/index.js b/client/app/config/index.js index ee8905009d..2ce76b5e69 100644 --- a/client/app/config/index.js +++ b/client/app/config/index.js @@ -24,6 +24,8 @@ import { each, isFunction, extend } from 'lodash'; import '@/lib/sortable'; +import organizationStatus from '@/services/organizationStatus'; + import * as filters from '@/filters'; import registerDirectives from '@/directives'; import markdownFilter from '@/filters/markdown'; @@ -110,11 +112,7 @@ function registerPages() { route.authenticated = true; route.resolve = extend( { - __organizationStatus: (OrganizationStatus) => { - 'ngInject'; - - return OrganizationStatus.refresh(); - }, + __organizationStatus: () => organizationStatus.refresh(), }, route.resolve, ); diff --git a/client/app/pages/dashboards/dashboard.js b/client/app/pages/dashboards/dashboard.js index 9e77d1e57a..4cdf8c07a8 100644 --- a/client/app/pages/dashboards/dashboard.js +++ b/client/app/pages/dashboards/dashboard.js @@ -1,6 +1,7 @@ import * as _ from 'lodash'; import PromiseRejectionError from '@/lib/promise-rejection-error'; import getTags from '@/services/getTags'; +import { policy } from '@/services/policy'; import { durationHumanize } from '@/filters'; import template from './dashboard.html'; import shareDashboardTemplate from './share-dashboard.html'; @@ -36,7 +37,6 @@ function DashboardCtrl( clientConfig, Events, toastr, - Policy, ) { this.saveInProgress = false; @@ -83,7 +83,7 @@ function DashboardCtrl( enabled: true, })); - const allowedIntervals = Policy.getDashboardRefreshIntervals(); + const allowedIntervals = policy.getDashboardRefreshIntervals(); if (_.isArray(allowedIntervals)) { _.each(this.refreshRates, (rate) => { rate.enabled = allowedIntervals.indexOf(rate.rate) >= 0; diff --git a/client/app/pages/data-sources/list.js b/client/app/pages/data-sources/list.js index f1d64e1e5d..c81bc46c0d 100644 --- a/client/app/pages/data-sources/list.js +++ b/client/app/pages/data-sources/list.js @@ -1,8 +1,9 @@ import settingsMenu from '@/services/settingsMenu'; +import { policy } from '@/services/policy'; import template from './list.html'; -function DataSourcesCtrl(Policy, DataSource) { - this.policy = Policy; +function DataSourcesCtrl(DataSource) { + this.policy = policy; this.dataSources = DataSource.query(); } diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js index 80b7942b3f..758ef9facb 100644 --- a/client/app/pages/queries/view.js +++ b/client/app/pages/queries/view.js @@ -1,6 +1,7 @@ import { pick, some, find, minBy, map, intersection, isArray, isObject } from 'lodash'; import { SCHEMA_NOT_SUPPORTED, SCHEMA_LOAD_ERROR } from '@/services/data-source'; import getTags from '@/services/getTags'; +import { policy } from '@/services/policy'; import Notifications from '@/services/notifications'; import template from './query.html'; @@ -21,7 +22,6 @@ function QueryViewCtrl( toastr, $uibModal, currentUser, - Policy, Query, DataSource, Visualization, @@ -451,7 +451,7 @@ function QueryViewCtrl( $scope.openVisualizationEditor(); } const intervals = clientConfig.queryRefreshIntervals; - const allowedIntervals = Policy.getQueryRefreshIntervals(); + const allowedIntervals = policy.getQueryRefreshIntervals(); $scope.refreshOptions = isArray(allowedIntervals) ? intersection(intervals, allowedIntervals) : intervals; $scope.updateQueryMetadata = changes => diff --git a/client/app/pages/users/list.js b/client/app/pages/users/list.js index ffdeba3541..ee1e165ac8 100644 --- a/client/app/pages/users/list.js +++ b/client/app/pages/users/list.js @@ -1,12 +1,13 @@ import { extend } from 'lodash'; +import { policy } from '@/services/policy'; import ListCtrl from '@/lib/list-ctrl'; import settingsMenu from '@/services/settingsMenu'; import template from './list.html'; class UsersListCtrl extends ListCtrl { - constructor($scope, $location, currentUser, clientConfig, Policy, User) { + constructor($scope, $location, currentUser, clientConfig, User) { super($scope, $location, currentUser, clientConfig); - this.policy = Policy; + this.policy = policy; this.enableUser = user => User.enableUser(user).then(this.update); this.disableUser = user => User.disableUser(user).then(this.update); this.deleteUser = user => User.deleteUser(user).then(this.update); diff --git a/client/app/services/ng.js b/client/app/services/ng.js index ce59976f82..de32a79f45 100644 --- a/client/app/services/ng.js +++ b/client/app/services/ng.js @@ -1,5 +1,6 @@ export let $http = null; // eslint-disable-line import/no-mutable-exports export let $sanitize = null; // eslint-disable-line import/no-mutable-exports +export let $q = null; // eslint-disable-line import/no-mutable-exports export let $uibModal = null; // eslint-disable-line import/no-mutable-exports export let toastr = null; // eslint-disable-line import/no-mutable-exports @@ -7,6 +8,7 @@ export default function init(ngModule) { ngModule.run(($injector) => { $http = $injector.get('$http'); $sanitize = $injector.get('$sanitize'); + $q = $injector.get('$q'); $uibModal = $injector.get('$uibModal'); toastr = $injector.get('toastr'); }); diff --git a/client/app/services/organization-status.js b/client/app/services/organization-status.js deleted file mode 100644 index 81d827e92e..0000000000 --- a/client/app/services/organization-status.js +++ /dev/null @@ -1,22 +0,0 @@ -export let OrganizationStatus = null; // eslint-disable-line import/no-mutable-exports - -function OrganizationStatusService($http) { - this.objectCounters = {}; - - this.refresh = () => - $http.get('api/organization/status').then(({ data }) => { - this.objectCounters = data.object_counters; - return this; - }); -} - -export default function init(ngModule) { - ngModule.service('OrganizationStatus', OrganizationStatusService); - - ngModule.run(($injector) => { - OrganizationStatus = $injector.get('OrganizationStatus'); - }); -} - -init.init = true; - diff --git a/client/app/services/organizationStatus.js b/client/app/services/organizationStatus.js new file mode 100644 index 0000000000..d849ad7c67 --- /dev/null +++ b/client/app/services/organizationStatus.js @@ -0,0 +1,17 @@ +import { $http } from '@/services/ng'; + +class OrganizationStatus { + constructor() { + this.objectCounters = {}; + } + + refresh() { + return $http.get('api/organization/status').then(({ data }) => { + this.objectCounters = data.object_counters; + return this; + }); + } +} + +export default new OrganizationStatus(); + diff --git a/client/app/services/policy.js b/client/app/services/policy.js deleted file mode 100644 index 366c06fd3e..0000000000 --- a/client/app/services/policy.js +++ /dev/null @@ -1,84 +0,0 @@ -import { isFunction, isArray } from 'lodash'; - -export class Policy { - constructor($injector) { - this.$injector = $injector; - } - - get user() { - return this.$injector.get('currentUser'); - } - - get organizationStatus() { - return this.$injector.get('OrganizationStatus'); - } - - refresh() { - const $q = this.$injector.get('$q'); - return $q.resolve(this); - } - - canCreateDataSource() { - return this.user.hasPermission('admin'); - } - - isCreateDataSourceEnabled() { - return this.user.hasPermission('admin'); - } - - canCreateDashboard() { - return this.user.hasPermission('create_dashboard'); - } - - isCreateDashboardEnabled() { - return this.user.hasPermission('create_dashboard'); - } - - // eslint-disable-next-line class-methods-use-this - canCreateAlert() { - return true; - } - - canCreateUser() { - return this.user.hasPermission('admin'); - } - - isCreateUserEnabled() { - return this.user.hasPermission('admin'); - } - - getDashboardRefreshIntervals() { - const clientConfig = this.$injector.get('clientConfig'); - const result = clientConfig.dashboardRefreshIntervals; - return isArray(result) ? result : null; - } - - getQueryRefreshIntervals() { - const clientConfig = this.$injector.get('clientConfig'); - const result = clientConfig.queryRefreshIntervals; - return isArray(result) ? result : null; - } -} - -export default function init(ngModule) { - let appInjector = null; - - ngModule.run(($injector) => { - 'ngInject'; - - appInjector = $injector; - }); - - ngModule.provider('Policy', function policyProvider() { - let PolicyClass = Policy; - - this.setPolicyClass = (policyClass) => { - PolicyClass = policyClass; - }; - - this.$get = () => (isFunction(PolicyClass) ? new PolicyClass(appInjector) : null); - }); -} - -init.init = true; - diff --git a/client/app/services/policy/DefaultPolicy.js b/client/app/services/policy/DefaultPolicy.js new file mode 100644 index 0000000000..b1101addbe --- /dev/null +++ b/client/app/services/policy/DefaultPolicy.js @@ -0,0 +1,49 @@ +import { isArray } from 'lodash'; +import { $q } from '@/services/ng'; +import { currentUser, clientConfig } from '@/services/auth'; + +/* eslint-disable class-methods-use-this */ + +export default class DefaultPolicy { + refresh() { + return $q.resolve(this); + } + + canCreateDataSource() { + return currentUser.isAdmin; + } + + isCreateDataSourceEnabled() { + return currentUser.isAdmin; + } + + canCreateDashboard() { + return currentUser.hasPermission('create_dashboard'); + } + + isCreateDashboardEnabled() { + return currentUser.hasPermission('create_dashboard'); + } + + canCreateAlert() { + return true; + } + + canCreateUser() { + return currentUser.isAdmin; + } + + isCreateUserEnabled() { + return currentUser.isAdmin; + } + + getDashboardRefreshIntervals() { + const result = clientConfig.dashboardRefreshIntervals; + return isArray(result) ? result : null; + } + + getQueryRefreshIntervals() { + const result = clientConfig.queryRefreshIntervals; + return isArray(result) ? result : null; + } +} diff --git a/client/app/services/policy/index.js b/client/app/services/policy/index.js new file mode 100644 index 0000000000..9a4ebd0b1d --- /dev/null +++ b/client/app/services/policy/index.js @@ -0,0 +1,8 @@ +import DefaultPolicy from './DefaultPolicy'; + +// eslint-disable-next-line import/no-mutable-exports +export let policy = new DefaultPolicy(); + +export function setPolicy(newPolicy) { + policy = newPolicy; +} From ff42ec2cc6700023e48c85fde387a955700b507a Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Mon, 28 Jan 2019 17:54:24 +0200 Subject: [PATCH 04/27] Cypress tests: preset the admin API key to a static value (#3358) --- .circleci/config.yml | 2 ++ cypress/cypress.js | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ac7e5bb0d1..1242d7cf18 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -73,6 +73,8 @@ jobs: command: | npm run cypress start docker-compose run cypress node ./cypress/cypress.js db-seed + # Make sure the API key is the same so Percy snapshots are consistent + docker-compose -p cypress run postgres psql -U postgres -h postgres -c "update users set api_key = 'secret' where email ='admin@redash.io';" - run: name: Execute Cypress tests command: npm run cypress run-ci diff --git a/cypress/cypress.js b/cypress/cypress.js index 3363b437e1..0dda6e4cff 100644 --- a/cypress/cypress.js +++ b/cypress/cypress.js @@ -36,7 +36,10 @@ function runCypressCI() { if (process.env.PERCY_TOKEN_ENCODED) { process.env.PERCY_TOKEN = atob(`${process.env.PERCY_TOKEN_ENCODED}`); } - execSync('docker-compose run cypress ./node_modules/.bin/percy exec -- ./node_modules/.bin/cypress run --browser chrome', { stdio: 'inherit' }); + execSync( + 'docker-compose run cypress ./node_modules/.bin/percy exec -- ./node_modules/.bin/cypress run --browser chrome', + { stdio: 'inherit' }, + ); } const command = process.argv[2] || 'all'; From 371b319e923fda457783a108d2f4fa3e1f191d0d Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Tue, 29 Jan 2019 09:18:07 +0200 Subject: [PATCH 05/27] Server-side parameter validation (#3315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * stop testing `collect_query_parameters`, it's an implementation detail * add tests for `missing_query_params` * rename SQLQuery -> ParameterizedSqlQuery * rename sql_query.py to parameterized_query.py * split to parameterized queries and parameterized SQL queries, where parameterized queries only do templating and parameterized SQL queries add tree validation on top of it * move missing parameter detection to ParameterizedQuery * get rid of some old code * fix tests * set syntax to `custom` * revert the max-age-related refactoring * 👋 tree validations 😢 * BaseQueryRunner is no longer a factory for ParameterizedQuery, for now * add an endpoint for running a query by its id and (optional) parameters without having to provide the query text * adds parameter schema to ParameterizedQuery * adds parameter schema validation (currently for strings) * validate number parameters * validate date parameters * validate parameters on POST /api/queries//results * validate enum parameters * validate date range parameters * validate query-based dropdowns by preprocessing them at the handler level and converting them to a populated enum * change _is_date_range to be a tad more succinct * a single assignment with a `map` is sufficiently explanatory * Update redash/utils/parameterized_query.py Co-Authored-By: rauchy * Update redash/utils/parameterized_query.py Co-Authored-By: rauchy * Update redash/utils/parameterized_query.py Co-Authored-By: rauchy * Update redash/utils/parameterized_query.py Co-Authored-By: rauchy * Update redash/handlers/query_results.py Co-Authored-By: rauchy * Update redash/utils/parameterized_query.py Co-Authored-By: rauchy * build error message inside the error * support all types of numbers as number parameters * check for permissions when populating query-based dropdowns * check for access to query before running it * check for empty rows when populating query-based enums * don't bother loading query results if user doesn't have access * 💥 on unexpected parameter types * parameter schema default is a list, not a dictionary * remove redundant null guards --- redash/handlers/query_results.py | 27 ++++++-- redash/utils/parameterized_query.py | 58 ++++++++++++++-- tests/utils/test_parameterized_query.py | 92 ++++++++++++++++++++++++- 3 files changed, 168 insertions(+), 9 deletions(-) diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py index 575f576653..a3e83fad52 100644 --- a/redash/handlers/query_results.py +++ b/redash/handlers/query_results.py @@ -10,7 +10,7 @@ require_permission, view_only) from redash.tasks import QueryTask from redash.tasks.queries import enqueue_query -from redash.utils import (collect_parameters_from_request, gen_query_hash, json_dumps, utcnow) +from redash.utils import (collect_parameters_from_request, gen_query_hash, json_dumps, json_loads, utcnow) from redash.utils.parameterized_query import ParameterizedQuery @@ -64,7 +64,7 @@ def run_query_sync(data_source, parameter_values, query_text, max_age=0): return None -def run_query(data_source, parameter_values, query_text, query_id, max_age=0): +def run_query(data_source, parameter_values, query_text, query_id, max_age=0, parameter_schema=None): if data_source.paused: if data_source.pause_reason: message = '{} is paused ({}). Please try later.'.format(data_source.name, data_source.pause_reason) @@ -73,7 +73,7 @@ def run_query(data_source, parameter_values, query_text, query_id, max_age=0): return error_response(message) - query = ParameterizedQuery(query_text).apply(parameter_values) + query = ParameterizedQuery(query_text, parameter_schema).apply(parameter_values) if query.missing_params: return error_response(u'Missing parameter value for: {}'.format(u", ".join(query.missing_params))) @@ -154,6 +154,23 @@ def options(self, query_id=None, query_result_id=None, filetype='json'): return make_response("", 200, headers) + def _fetch_rows(self, query_id): + query = models.Query.get_by_id_and_org(query_id, self.current_org) + require_access(query.data_source.groups, self.current_user, view_only) + + query_result = models.QueryResult.get_by_id_and_org(query.latest_query_data_id, self.current_org) + + return json_loads(query_result.data)["rows"] + + def _convert_queries_to_enums(self, definition): + if definition["type"] == "query": + definition["type"] = "enum" + + rows = self._fetch_rows(definition.pop("queryId")) + definition["enumOptions"] = [row.get('value', row.get(row.keys()[0])) for row in rows if row] + + return definition + @require_permission('execute_query') def post(self, query_id): """ @@ -171,11 +188,13 @@ def post(self, query_id): max_age = int(params.get('max_age', 0)) query = get_object_or_404(models.Query.get_by_id_and_org, query_id, self.current_org) + parameter_schema = map(self._convert_queries_to_enums, + query.options.get("parameters", [])) if not has_access(query.data_source.groups, self.current_user, not_view_only): return {'job': {'status': 4, 'error': 'You do not have permission to run queries with this data source.'}}, 403 else: - return run_query(query.data_source, parameters, query.query_text, query_id, max_age) + return run_query(query.data_source, parameters, query.query_text, query_id, max_age, parameter_schema=parameter_schema) @require_permission('view_query') def get(self, query_id=None, query_result_id=None, filetype='json'): diff --git a/redash/utils/parameterized_query.py b/redash/utils/parameterized_query.py index 4f9a873518..c1ec2d38bd 100644 --- a/redash/utils/parameterized_query.py +++ b/redash/utils/parameterized_query.py @@ -1,7 +1,8 @@ import pystache +from numbers import Number from redash.utils import mustache_render from funcy import distinct - +from dateutil.parser import parse def _collect_key_names(nodes): keys = [] @@ -33,17 +34,60 @@ def _parameter_names(parameter_values): return names +def _is_date(string): + try: + parse(string) + return True + except ValueError: + return False + + +def _is_date_range(obj): + try: + return _is_date(obj["start"]) and _is_date(obj["end"]) + except (KeyError, TypeError): + return False + + class ParameterizedQuery(object): - def __init__(self, template): + def __init__(self, template, schema=None): + self.schema = schema or [] self.template = template self.query = template self.parameters = {} def apply(self, parameters): - self.parameters.update(parameters) - self.query = mustache_render(self.template, self.parameters) + invalid_parameter_names = [key for (key, value) in parameters.iteritems() if not self._valid(key, value)] + if invalid_parameter_names: + raise InvalidParameterError(invalid_parameter_names) + else: + self.parameters.update(parameters) + self.query = mustache_render(self.template, self.parameters) + return self + def _valid(self, name, value): + definition = next((definition for definition in self.schema if definition["name"] == name), None) + + if not definition: + return True + + validators = { + "text": lambda value: isinstance(value, basestring), + "number": lambda value: isinstance(value, Number), + "enum": lambda value: value in definition["enumOptions"], + "date": _is_date, + "datetime-local": _is_date, + "datetime-with-seconds": _is_date, + "date-range": _is_date_range, + "datetime-range": _is_date_range, + "datetime-range-with-seconds": _is_date_range, + } + + validate = validators.get(definition["type"], lambda x: False) + + return validate(value) + @property def missing_params(self): query_parameters = set(_collect_query_parameters(self.template)) @@ -52,3 +96,9 @@ def missing_params(self): @property def text(self): return self.query + + +class InvalidParameterError(Exception): + def __init__(self, parameters): + message = u"The following parameter values are incompatible with their type definitions: {}".format(", ".join(parameters)) + super(InvalidParameterError, self).__init__(message) diff --git a/tests/utils/test_parameterized_query.py b/tests/utils/test_parameterized_query.py index e076dfae11..c63acc96d4 100644 --- a/tests/utils/test_parameterized_query.py +++ b/tests/utils/test_parameterized_query.py @@ -1,6 +1,7 @@ from unittest import TestCase +import pytest -from redash.utils.parameterized_query import ParameterizedQuery +from redash.utils.parameterized_query import ParameterizedQuery, InvalidParameterError class TestParameterizedQuery(TestCase): @@ -41,3 +42,92 @@ def test_handles_objects(self): } }) self.assertEqual(set([]), query.missing_params) + + def test_raises_on_invalid_text_parameters(self): + schema = [{"name": "bar", "type": "text"}] + query = ParameterizedQuery("foo", schema) + + with pytest.raises(InvalidParameterError): + query.apply({"bar": 7}) + + def test_validates_text_parameters(self): + schema = [{"name": "bar", "type": "text"}] + query = ParameterizedQuery("foo {{bar}}", schema) + + query.apply({"bar": u"baz"}) + + self.assertEquals("foo baz", query.text) + + def test_raises_on_invalid_number_parameters(self): + schema = [{"name": "bar", "type": "number"}] + query = ParameterizedQuery("foo", schema) + + with pytest.raises(InvalidParameterError): + query.apply({"bar": "baz"}) + + def test_validates_number_parameters(self): + schema = [{"name": "bar", "type": "number"}] + query = ParameterizedQuery("foo {{bar}}", schema) + + query.apply({"bar": 7}) + + self.assertEquals("foo 7", query.text) + + def test_raises_on_invalid_date_parameters(self): + schema = [{"name": "bar", "type": "date"}] + query = ParameterizedQuery("foo", schema) + + with pytest.raises(InvalidParameterError): + query.apply({"bar": "baz"}) + + def test_validates_date_parameters(self): + schema = [{"name": "bar", "type": "date"}] + query = ParameterizedQuery("foo {{bar}}", schema) + + query.apply({"bar": "2000-01-01 12:00:00"}) + + self.assertEquals("foo 2000-01-01 12:00:00", query.text) + + def test_raises_on_invalid_enum_parameters(self): + schema = [{"name": "bar", "type": "enum", "enumOptions": ["baz", "qux"]}] + query = ParameterizedQuery("foo", schema) + + with pytest.raises(InvalidParameterError): + query.apply({"bar": 7}) + + def test_raises_on_unlisted_enum_value_parameters(self): + schema = [{"name": "bar", "type": "enum", "enumOptions": ["baz", "qux"]}] + query = ParameterizedQuery("foo", schema) + + with pytest.raises(InvalidParameterError): + query.apply({"bar": "shlomo"}) + + def test_validates_enum_parameters(self): + schema = [{"name": "bar", "type": "enum", "enumOptions": ["baz", "qux"]}] + query = ParameterizedQuery("foo {{bar}}", schema) + + query.apply({"bar": "baz"}) + + self.assertEquals("foo baz", query.text) + + def test_raises_on_invalid_date_range_parameters(self): + schema = [{"name": "bar", "type": "date-range"}] + query = ParameterizedQuery("foo", schema) + + with pytest.raises(InvalidParameterError): + query.apply({"bar": "baz"}) + + def test_validates_date_range_parameters(self): + schema = [{"name": "bar", "type": "date-range"}] + query = ParameterizedQuery("foo {{bar.start}} {{bar.end}}", schema) + + query.apply({"bar": {"start": "2000-01-01 12:00:00", "end": "2000-12-31 12:00:00"}}) + + self.assertEquals("foo 2000-01-01 12:00:00 2000-12-31 12:00:00", query.text) + + def test_raises_on_unexpected_param_types(self): + schema = [{"name": "bar", "type": "burrito"}] + query = ParameterizedQuery("foo", schema) + + with pytest.raises(InvalidParameterError): + query.apply({"bar": "baz"}) From 2da511021e610567b29a1464faaaf7a6fdca9bee Mon Sep 17 00:00:00 2001 From: koooge Date: Wed, 30 Jan 2019 00:25:58 +0900 Subject: [PATCH 06/27] Frontend lint update (#3253) * client: Add lint command Signed-off-by: koooge * client: Override eslint rule object-curly-newline to keep current style Signed-off-by: koooge * client: Override eslint rule no-else-return to keep current style Signed-off-by: koooge * client: Fix eslint import/named Signed-off-by: koooge * client: eslint-5 Signed-off-by: koooge * codeclimate: Delete the old setting Signed-off-by: koooge * client: Downgrade eslint 5 to 4 in codeclimate Signed-off-by: koooge * client: npx install-peerdeps --dev eslint-config-airbnb Signed-off-by: koooge * client: Enbale .jsx lint Signed-off-by: koooge * client: Set warn Signed-off-by: koooge * client: Fix lint indent, implicit-arrow-linebreak, lines-between-class-members Signed-off-by: koooge * client: Disable eslint operator-linebreak Signed-off-by: koooge * Revert "client: Downgrade eslint 5 to 4 in codeclimate" This reverts commit f0fb0f005992a8b3c92eae505fcc2767b9d55266. * client: Fix react/button-has-type Signed-off-by: koooge * client: Disable an eslint rule react/jsx-one-expression-per-line Signed-off-by: koooge * codeclimate: Disable no-multiple-empty-lines Signed-off-by: koooge * client: Disable eslint react/destructuring-assignment Signed-off-by: koooge --- .codeclimate.yml | 6 +- client/.eslintrc.js | 11 +- client/app/components/EditInPlace.jsx | 16 +- .../app/components/ParameterMappingInput.jsx | 4 +- client/app/components/QueryEditor.jsx | 2 +- client/app/components/app-view/index.js | 7 +- .../app/components/queries/ScheduleDialog.jsx | 2 +- client/app/components/tags-list/tags-list.js | 1 + client/app/filters/datetime.js | 21 +- client/app/filters/markdown.js | 21 +- client/app/pages/data-sources/show.js | 6 +- client/app/pages/queries/view.js | 9 +- client/app/pages/settings/organization.js | 4 +- client/app/services/query-result.js | 35 +- client/app/services/query.js | 1 + client/app/visualizations/chart/index.js | 3 +- client/app/visualizations/sankey/index.js | 5 +- package-lock.json | 712 ++++++++++-------- package.json | 11 +- 19 files changed, 483 insertions(+), 394 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index c17ce2e20a..dc8e299291 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -3,12 +3,14 @@ engines: enabled: true eslint: enabled: true - channel: "eslint-3" + channel: "eslint-5" config: config: client/.eslintrc.js checks: import/no-unresolved: enabled: false + no-multiple-empty-lines: # TODO: Enable + enabled: false ratings: paths: - "redash/**/*.py" @@ -16,7 +18,5 @@ ratings: exclude_paths: - tests/**/*.py - migrations/**/*.py -- old_migrations/**/*.py - setup/**/* - bin/**/* - diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 17552fcd15..4b14b07263 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -26,15 +26,22 @@ module.exports = { "no-lonely-if": "off", "consistent-return": "off", "no-control-regex": "off", + 'no-multiple-empty-lines': 'warn', "no-script-url": "off", // some tags should have href="javascript:void(0)" + 'operator-linebreak': 'off', + 'react/destructuring-assignment': 'off', "react/jsx-filename-extension": "off", + 'react/jsx-one-expression-per-line': 'off', "react/jsx-uses-react": "error", "react/jsx-uses-vars": "error", + 'react/jsx-wrap-multilines': 'warn', + 'react/no-access-state-in-setstate': 'warn', "react/prefer-stateless-function": "warn", "react/forbid-prop-types": "warn", "react/prop-types": "warn", "jsx-a11y/anchor-is-valid": "off", "jsx-a11y/click-events-have-key-events": "off", + 'jsx-a11y/label-has-associated-control': 'warn', "jsx-a11y/label-has-for": "off", "jsx-a11y/no-static-element-interactions": "off", "max-len": ['error', 120, 2, { @@ -43,6 +50,8 @@ module.exports = { ignoreRegExpLiterals: true, ignoreStrings: true, ignoreTemplateLiterals: true, - }] + }], + "no-else-return": ["error", {"allowElseIf": true}], + "object-curly-newline": ["error", {"consistent": true}] } }; diff --git a/client/app/components/EditInPlace.jsx b/client/app/components/EditInPlace.jsx index 369f5d470b..5d06dbd87a 100644 --- a/client/app/components/EditInPlace.jsx +++ b/client/app/components/EditInPlace.jsx @@ -18,6 +18,7 @@ export class EditInPlace extends React.Component { placeholder: '', value: '', }; + constructor(props) { super(props); this.state = { @@ -67,14 +68,13 @@ export class EditInPlace extends React.Component { ); - renderEdit = () => - React.createElement(this.props.editor, { - ref: this.inputRef, - className: 'rd-form-control', - defaultValue: this.props.value, - onBlur: this.stopEditing, - onKeyDown: this.keyDown, - }); + renderEdit = () => React.createElement(this.props.editor, { + ref: this.inputRef, + className: 'rd-form-control', + defaultValue: this.props.value, + onBlur: this.stopEditing, + onKeyDown: this.keyDown, + }); render() { return ( diff --git a/client/app/components/ParameterMappingInput.jsx b/client/app/components/ParameterMappingInput.jsx index 3b1b620998..afd68647ec 100644 --- a/client/app/components/ParameterMappingInput.jsx +++ b/client/app/components/ParameterMappingInput.jsx @@ -260,8 +260,8 @@ export class ParameterMappingListInput extends React.Component {
{this.props.mappings.map((mapping, index) => { const existingParamsNames = this.props.existingParams - .filter(({ type }) => type === mapping.param.type) // exclude mismatching param types - .map(({ name }) => name); // keep names only + .filter(({ type }) => type === mapping.param.type) // exclude mismatching param types + .map(({ name }) => name); // keep names only return (
diff --git a/client/app/components/QueryEditor.jsx b/client/app/components/QueryEditor.jsx index c86b704a1c..a09aa9f406 100644 --- a/client/app/components/QueryEditor.jsx +++ b/client/app/components/QueryEditor.jsx @@ -285,7 +285,7 @@ class QueryEditor extends React.Component { {this.props.canEdit ? ( -
diff --git a/client/app/components/tags-list/tags-list.js b/client/app/components/tags-list/tags-list.js index 85dcea6b9b..d7defc102e 100644 --- a/client/app/components/tags-list/tags-list.js +++ b/client/app/components/tags-list/tags-list.js @@ -11,6 +11,7 @@ class TagsList { this.allTags = tags; }); } + toggleTag($event, tag) { if ($event.shiftKey) { // toggle tag diff --git a/client/app/filters/datetime.js b/client/app/filters/datetime.js index 5d8b8f4626..90d146509d 100644 --- a/client/app/filters/datetime.js +++ b/client/app/filters/datetime.js @@ -3,20 +3,19 @@ import moment from 'moment'; export default function init(ngModule) { ngModule.filter('toMilliseconds', () => value => value * 1000.0); - ngModule.filter('dateTime', clientConfig => - function dateTime(value) { - if (!value) { - return ''; - } + ngModule.filter('dateTime', clientConfig => function dateTime(value) { + if (!value) { + return ''; + } - const parsed = moment(value); + const parsed = moment(value); - if (!parsed.isValid()) { - return '-'; - } + if (!parsed.isValid()) { + return '-'; + } - return parsed.format(clientConfig.dateTimeFormat); - }); + return parsed.format(clientConfig.dateTimeFormat); + }); } init.init = true; diff --git a/client/app/filters/markdown.js b/client/app/filters/markdown.js index 237ba944e9..09a7df3094 100644 --- a/client/app/filters/markdown.js +++ b/client/app/filters/markdown.js @@ -1,19 +1,18 @@ import { markdown } from 'markdown'; export default function init(ngModule) { - ngModule.filter('markdown', ($sce, clientConfig) => - function parseMarkdown(text) { - if (!text) { - return ''; - } + ngModule.filter('markdown', ($sce, clientConfig) => function parseMarkdown(text) { + if (!text) { + return ''; + } - let html = markdown.toHTML(String(text)); - if (clientConfig.allowScriptsInUserInput) { - html = $sce.trustAsHtml(html); - } + let html = markdown.toHTML(String(text)); + if (clientConfig.allowScriptsInUserInput) { + html = $sce.trustAsHtml(html); + } - return html; - }); + return html; + }); } init.init = true; diff --git a/client/app/pages/data-sources/show.js b/client/app/pages/data-sources/show.js index b6446659ed..5d938a6918 100644 --- a/client/app/pages/data-sources/show.js +++ b/client/app/pages/data-sources/show.js @@ -3,12 +3,12 @@ import debug from 'debug'; import template from './show.html'; const logger = debug('redash:http'); -const deleteConfirm = { class: 'btn-warning', title: 'Delete' }; -function logAndToastrError(deleteObject, httpResponse, toastr) { +export const deleteConfirm = { class: 'btn-warning', title: 'Delete' }; +export function logAndToastrError(deleteObject, httpResponse, toastr) { logger('Failed to delete ' + deleteObject + ': ', httpResponse.status, httpResponse.statusText, httpResponse.data); toastr.error('Failed to delete ' + deleteObject + '.'); } -function toastrSuccessAndPath(deleteObject, deletePath, toastr, $location) { +export function toastrSuccessAndPath(deleteObject, deletePath, toastr, $location) { toastr.success(deleteObject + ' deleted successfully.'); $location.path('/' + deletePath + '/'); } diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js index 758ef9facb..61ed3bd234 100644 --- a/client/app/pages/queries/view.js +++ b/client/app/pages/queries/view.js @@ -454,11 +454,10 @@ function QueryViewCtrl( const allowedIntervals = policy.getQueryRefreshIntervals(); $scope.refreshOptions = isArray(allowedIntervals) ? intersection(intervals, allowedIntervals) : intervals; - $scope.updateQueryMetadata = changes => - $scope.$apply(() => { - $scope.query = Object.assign($scope.query, changes); - $scope.saveQuery(); - }); + $scope.updateQueryMetadata = changes => $scope.$apply(() => { + $scope.query = Object.assign($scope.query, changes); + $scope.saveQuery(); + }); $scope.showScheduleForm = false; $scope.openScheduleForm = () => { if (!$scope.canEdit || !$scope.canScheduleQuery) { diff --git a/client/app/pages/settings/organization.js b/client/app/pages/settings/organization.js index e826497aed..9b2ead407c 100644 --- a/client/app/pages/settings/organization.js +++ b/client/app/pages/settings/organization.js @@ -26,8 +26,8 @@ function OrganizationSettingsCtrl($http, toastr, clientConfig, Events) { this.dateFormatList = clientConfig.dateFormatList; this.googleLoginEnabled = clientConfig.googleLoginEnabled; - this.disablePasswordLoginToggle = () => - (clientConfig.googleLoginEnabled || this.settings.auth_saml_enabled) === false; + // eslint-disable-next-line max-len + this.disablePasswordLoginToggle = () => (clientConfig.googleLoginEnabled || this.settings.auth_saml_enabled) === false; } export default function init(ngModule) { diff --git a/client/app/services/query-result.js b/client/app/services/query-result.js index 22fcdfe5ed..1d096d5538 100644 --- a/client/app/services/query-result.js +++ b/client/app/services/query-result.js @@ -237,25 +237,24 @@ function QueryResultService($resource, $timeout, $q, QueryResultError) { } }); - this.filteredData = this.query_result.data.rows.filter(row => - filters.reduce((memo, filter) => { - if (!isArray(filter.current)) { - filter.current = [filter.current]; - } + this.filteredData = this.query_result.data.rows.filter(row => filters.reduce((memo, filter) => { + if (!isArray(filter.current)) { + filter.current = [filter.current]; + } - return ( - memo && - some(filter.current, (v) => { - const value = row[filter.name]; - if (moment.isMoment(value)) { - return value.isSame(v); - } - // We compare with either the value or the String representation of the value, - // because Select2 casts true/false to "true"/"false". - return v === value || String(value) === v; - }) - ); - }, true)); + return ( + memo && + some(filter.current, (v) => { + const value = row[filter.name]; + if (moment.isMoment(value)) { + return value.isSame(v); + } + // We compare with either the value or the String representation of the value, + // because Select2 casts true/false to "true"/"false". + return v === value || String(value) === v; + }) + ); + }, true)); } else { this.filteredData = this.query_result.data.rows; } diff --git a/client/app/services/query.js b/client/app/services/query.js index 57ece987dd..740e339b21 100644 --- a/client/app/services/query.js +++ b/client/app/services/query.js @@ -145,6 +145,7 @@ class Parameter { get ngModel() { return this.normalizedValue; } + set ngModel(value) { this.setValue(value); } diff --git a/client/app/visualizations/chart/index.js b/client/app/visualizations/chart/index.js index ab2f26a301..8075e2aa4c 100644 --- a/client/app/visualizations/chart/index.js +++ b/client/app/visualizations/chart/index.js @@ -231,8 +231,7 @@ function ChartEditor(ColorPalette, clientConfig) { scope.form = { yAxisColumns: [], - seriesList: sortBy(keys(scope.options.seriesOptions), name => - scope.options.seriesOptions[name].zIndex), + seriesList: sortBy(keys(scope.options.seriesOptions), name => scope.options.seriesOptions[name].zIndex), valuesList: keys(scope.options.valuesOptions), }; diff --git a/client/app/visualizations/sankey/index.js b/client/app/visualizations/sankey/index.js index b2c59cacdc..6ca87ccbc3 100644 --- a/client/app/visualizations/sankey/index.js +++ b/client/app/visualizations/sankey/index.js @@ -120,10 +120,7 @@ function createSankey(element, data) { const color = d3.scale.category20(); data = graph(data); - data.nodes = _.map(data.nodes, d => - _.extend(d, { - color: color(d.name.replace(/ .*/, '')), - })); + data.nodes = _.map(data.nodes, d => _.extend(d, { color: color(d.name.replace(/ .*/, '')) })); // append the svg canvas to the page const svg = d3 diff --git a/package-lock.json b/package-lock.json index d44c23c8f8..45a925c6f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -626,21 +626,10 @@ } }, "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", + "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==", + "dev": true }, "acorn-walk": { "version": "6.1.0", @@ -673,15 +662,29 @@ } }, "ajv": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", - "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", + "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", "dev": true, "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "json-schema-traverse": "^0.3.0", - "json-stable-stringify": "^1.0.1" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "ajv-errors": { @@ -691,9 +694,9 @@ "dev": true }, "ajv-keywords": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.0.tgz", - "integrity": "sha1-opbhf3v658HOT34N5T0pyzIWLfA=", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", "dev": true }, "align-text": { @@ -975,14 +978,6 @@ "requires": { "ast-types-flow": "0.0.7", "commander": "^2.11.0" - }, - "dependencies": { - "commander": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.16.0.tgz", - "integrity": "sha512-sVXqklSaotK9at437sFlFpyOcJonxe0yST/AG9DkQKUdIE6IqGIMv4SfAQSKaJbSdVEJYItASCrBiVQHq1HQew==", - "dev": true - } } }, "arr-diff": { @@ -1283,9 +1278,9 @@ "optional": true }, "axobject-query": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.1.tgz", - "integrity": "sha1-Bd+nBa2orZ25k/polvItOVsLCgc=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", + "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", "dev": true, "requires": { "ast-types-flow": "0.0.7" @@ -4547,21 +4542,6 @@ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - } - }, "delaunay-triangulate": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/delaunay-triangulate/-/delaunay-triangulate-1.1.6.tgz", @@ -4712,14 +4692,6 @@ "dev": true, "requires": { "esutils": "^2.0.2" - }, - "dependencies": { - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - } } }, "dom-align": { @@ -5261,123 +5233,115 @@ } }, "eslint": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", - "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.3.0.tgz", + "integrity": "sha512-N/tCqlMKkyNvAvLu+zI9AqDasnSLt00K+Hu8kdsERliC9jYEc8ck12XtjvOXrBKu8fK6RrBcN9bat6Xk++9jAg==", "dev": true, "requires": { - "ajv": "^5.3.0", - "babel-code-frame": "^6.22.0", + "ajv": "^6.5.0", + "babel-code-frame": "^6.26.0", "chalk": "^2.1.0", - "concat-stream": "^1.6.0", - "cross-spawn": "^5.1.0", + "cross-spawn": "^6.0.5", "debug": "^3.1.0", "doctrine": "^2.1.0", - "eslint-scope": "^3.7.1", + "eslint-scope": "^4.0.0", + "eslint-utils": "^1.3.1", "eslint-visitor-keys": "^1.0.0", - "espree": "^3.5.4", - "esquery": "^1.0.0", + "espree": "^4.0.0", + "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^2.0.0", "functional-red-black-tree": "^1.0.1", "glob": "^7.1.2", - "globals": "^11.0.1", - "ignore": "^3.3.3", + "globals": "^11.7.0", + "ignore": "^4.0.2", "imurmurhash": "^0.1.4", - "inquirer": "^3.0.6", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.9.1", + "inquirer": "^5.2.0", + "is-resolvable": "^1.1.0", + "js-yaml": "^3.11.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.2", + "lodash": "^4.17.5", + "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", "path-is-inside": "^1.0.2", "pluralize": "^7.0.0", "progress": "^2.0.0", - "regexpp": "^1.0.1", + "regexpp": "^2.0.0", "require-uncached": "^1.0.3", - "semver": "^5.3.0", + "semver": "^5.5.0", + "string.prototype.matchall": "^2.0.0", "strip-ansi": "^4.0.0", - "strip-json-comments": "~2.0.1", - "table": "4.0.2", - "text-table": "~0.2.0" + "strip-json-comments": "^2.0.1", + "table": "^4.0.3", + "text-table": "^0.2.0" }, "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" } }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "eslint-scope": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.0.tgz", + "integrity": "sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" } }, "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, "globals": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", - "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.10.0.tgz", + "integrity": "sha512-0GZF1RiPKU97IHUO5TORo9w1PwrH/NBPl+fS7oMLdaTRiYmYbwK4NWoZWrAdd0/abG9R2BU+OiwyQpTpE6pdfQ==", "dev": true }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", + "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" } }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -5386,34 +5350,29 @@ "requires": { "ansi-regex": "^3.0.0" } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, "eslint-config-airbnb": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-16.1.0.tgz", - "integrity": "sha512-zLyOhVWhzB/jwbz7IPSbkUuj7X2ox4PHXTcZkEmDqTvd0baJmJyuxlFPDlZOE/Y5bC+HQRaEkT3FoHo9wIdRiw==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-17.1.0.tgz", + "integrity": "sha512-R9jw28hFfEQnpPau01NO5K/JWMGLi6aymiF6RsnMURjTk+MqZKllCqGK/0tOvHkPi/NWSSOU2Ced/GX++YxLnw==", "dev": true, "requires": { - "eslint-config-airbnb-base": "^12.1.0" + "eslint-config-airbnb-base": "^13.1.0", + "object.assign": "^4.1.0", + "object.entries": "^1.0.4" }, "dependencies": { "eslint-config-airbnb-base": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz", - "integrity": "sha512-/vjm0Px5ZCpmJqnjIzcFb9TKZrKWz0gnuG/7Gfkt0Db1ELJR51xkZth+t14rYdqWgX836XbuxtArbIHlVhbLBA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz", + "integrity": "sha512-XWwQtf3U3zIoKO1BbHh6aUhJZQweOwSt4c2JrPDg9FP3Ltv3+YfEv7jIDB8275tVnO/qOHbfuYg3kzw6Je7uWw==", "dev": true, "requires": { - "eslint-restricted-globals": "^0.1.1" + "eslint-restricted-globals": "^0.1.1", + "object.assign": "^4.1.0", + "object.entries": "^1.0.4" } } } @@ -5428,13 +5387,13 @@ } }, "eslint-import-resolver-node": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz", - "integrity": "sha512-yUtXS15gIcij68NmXmP9Ni77AQuCN0itXbCc/jWd8C6/yKZaSNXicpC8cgvjnxVdmfsosIXrjpzFq7GcDryb6A==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", "dev": true, "requires": { - "debug": "^2.6.8", - "resolve": "^1.2.0" + "debug": "^2.6.9", + "resolve": "^1.5.0" }, "dependencies": { "debug": { @@ -5445,6 +5404,21 @@ "requires": { "ms": "2.0.0" } + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "resolve": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", + "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } } } }, @@ -5587,9 +5561,9 @@ } }, "eslint-module-utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", - "integrity": "sha512-jDI/X5l/6D1rRD/3T43q8Qgbls2nq5km5KSqiwlyUbGo5+04fXhMKdCPhjwbqAa6HXWaMxj8Q4hQDIh7IadJQw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz", + "integrity": "sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=", "dev": true, "requires": { "debug": "^2.6.8", @@ -5653,21 +5627,21 @@ } }, "eslint-plugin-import": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.7.0.tgz", - "integrity": "sha512-HGYmpU9f/zJaQiKNQOVfHUh2oLWW3STBrCgH0sHTX1xtsxYlH1zjLh8FlQGEIdZSdTbUMaV36WaZ6ImXkenGxQ==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz", + "integrity": "sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g==", "dev": true, "requires": { - "builtin-modules": "^1.1.1", "contains-path": "^0.1.0", "debug": "^2.6.8", "doctrine": "1.5.0", "eslint-import-resolver-node": "^0.3.1", - "eslint-module-utils": "^2.1.1", + "eslint-module-utils": "^2.2.0", "has": "^1.0.1", - "lodash.cond": "^4.3.0", + "lodash": "^4.17.4", "minimatch": "^3.0.3", - "read-pkg-up": "^2.0.0" + "read-pkg-up": "^2.0.0", + "resolve": "^1.6.0" }, "dependencies": { "debug": { @@ -5689,17 +5663,26 @@ "isarray": "^1.0.0" } }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "resolve": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", + "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } } } }, @@ -5710,9 +5693,9 @@ "dev": true }, "eslint-plugin-jsx-a11y": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.0.tgz", - "integrity": "sha512-hnhf28u7Z9zlh7Y56tETrwnPeBvXgcqlP7ntHvZsWQs/n/p/vPnfNMNFWTqJAFcbd8PrDEifX1NRGHsjnUmqMw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.1.2.tgz", + "integrity": "sha512-7gSSmwb3A+fQwtw0arguwMdOdzmKUgnUcbSNlo+GjKLAQFuC2EZxWqG9XHRI8VscBJD5a8raz3RuxQNFW+XJbw==", "dev": true, "requires": { "aria-query": "^3.0.0", @@ -5737,15 +5720,18 @@ } }, "eslint-plugin-react": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.10.0.tgz", - "integrity": "sha512-18rzWn4AtbSUxFKKM7aCVcj5LXOhOKdwBino3KKWy4psxfPW0YtIbE8WNRDUdyHFL50BeLb6qFd4vpvNYyp7hw==", + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.12.3.tgz", + "integrity": "sha512-WTIA3cS8OzkPeCi4KWuPmjR33lgG9r9Y/7RmnLTRw08MZKgAfnK/n3BO4X0S67MPkVLazdfCNT/XWqcDu4BLTA==", "dev": true, "requires": { + "array-includes": "^3.0.3", "doctrine": "^2.1.0", "has": "^1.0.3", "jsx-ast-utils": "^2.0.1", - "prop-types": "^15.6.2" + "object.fromentries": "^2.0.0", + "prop-types": "^15.6.2", + "resolve": "^1.9.0" }, "dependencies": { "has": { @@ -5757,6 +5743,12 @@ "function-bind": "^1.1.1" } }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, "prop-types": { "version": "15.6.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", @@ -5766,6 +5758,15 @@ "loose-envify": "^1.3.1", "object-assign": "^4.1.1" } + }, + "resolve": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", + "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } } } }, @@ -5785,6 +5786,12 @@ "estraverse": "^4.1.1" } }, + "eslint-utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz", + "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==", + "dev": true + }, "eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -5792,19 +5799,20 @@ "dev": true }, "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-4.1.0.tgz", + "integrity": "sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==", "dev": true, "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" + "acorn": "^6.0.2", + "acorn-jsx": "^5.0.0", + "eslint-visitor-keys": "^1.0.0" }, "dependencies": { "acorn": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", - "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.5.tgz", + "integrity": "sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg==", "dev": true } } @@ -6522,14 +6530,14 @@ } }, "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", "dev": true, "requires": { "circular-json": "^0.3.1", - "del": "^2.0.2", "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", "write": "^0.2.1" } }, @@ -6788,7 +6796,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -6806,11 +6815,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6823,15 +6834,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6934,7 +6948,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6944,6 +6959,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6956,17 +6972,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6983,6 +7002,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7055,7 +7075,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7065,6 +7086,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7140,7 +7162,8 @@ }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7170,6 +7193,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7187,6 +7211,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7225,11 +7250,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.2", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -7798,20 +7825,6 @@ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", "dev": true }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, "glsl-face-normal": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/glsl-face-normal/-/glsl-face-normal-1.0.2.tgz", @@ -8799,22 +8812,21 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", "dev": true, "requires": { "ansi-escapes": "^3.0.0", "chalk": "^2.0.0", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", - "external-editor": "^2.0.4", + "external-editor": "^2.1.0", "figures": "^2.0.0", "lodash": "^4.3.0", "mute-stream": "0.0.7", "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", + "rxjs": "^5.5.2", "string-width": "^2.1.0", "strip-ansi": "^4.0.0", "through": "^2.3.6" @@ -8826,32 +8838,6 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -8860,15 +8846,6 @@ "requires": { "ansi-regex": "^3.0.0" } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -10549,6 +10526,7 @@ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", "dev": true, + "optional": true, "requires": { "jsonify": "~0.0.0" } @@ -10598,7 +10576,8 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true + "dev": true, + "optional": true }, "jsprim": { "version": "1.4.1", @@ -11041,12 +11020,6 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, - "lodash.cond": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", - "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", - "dev": true - }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -12592,6 +12565,77 @@ } } }, + "object.fromentries": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.0.tgz", + "integrity": "sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.11.0", + "function-bind": "^1.1.1", + "has": "^1.0.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + }, + "dependencies": { + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + } + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + } + } + }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", @@ -14114,9 +14158,9 @@ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, "promise": { @@ -15320,10 +15364,19 @@ } } }, + "regexp.prototype.flags": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", + "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2" + } + }, "regexpp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", - "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, "regexpu-core": { @@ -15897,19 +15950,13 @@ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", - "dev": true - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", "dev": true, "requires": { - "rx-lite": "*" + "symbol-observable": "1.0.1" } }, "safe-buffer": { @@ -17099,6 +17146,76 @@ } } }, + "string.prototype.matchall": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-2.0.0.tgz", + "integrity": "sha512-WoZ+B2ypng1dp4iFLF2kmZlwwlE19gmjgKuhL1FJfDgCREWb3ye3SDVHSzLH6bxfnvYmkCxbzkmWcQZHA4P//Q==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.10.0", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "regexp.prototype.flags": "^1.2.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", + "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + } + } + }, "string.prototype.trim": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", @@ -17248,6 +17365,12 @@ } } }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", + "dev": true + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", @@ -17255,54 +17378,17 @@ "dev": true }, "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz", + "integrity": "sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg==", "dev": true, "requires": { - "ajv": "^5.2.3", - "ajv-keywords": "^2.1.0", + "ajv": "^6.0.1", + "ajv-keywords": "^3.0.0", "chalk": "^2.1.0", "lodash": "^4.17.4", "slice-ansi": "1.0.0", "string-width": "^2.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "tapable": { diff --git a/package.json b/package.json index 8bbcaa21f6..7fb86300d6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "watch": "webpack --watch --progress --colors -d", "analyze": "npm run clean && BUNDLE_ANALYZER=on webpack", "analyze:build": "npm run clean && NODE_ENV=production BUNDLE_ANALYZER=on webpack", + "lint": "eslint --config ./client/.eslintrc.js --ext .js --ext .jsx ./client/app", "test": "TZ=Asia/Jerusalem jest", "test:watch": "jest --watch", "cypress:install": "npm install --no-save cypress @percy/cypress", @@ -98,16 +99,16 @@ "enzyme": "^3.8.0", "enzyme-adapter-react-16": "^1.7.1", "enzyme-to-json": "^3.3.5", - "eslint": "^4.19.1", - "eslint-config-airbnb": "^16.1.0", + "eslint": "^5.3.0", + "eslint-config-airbnb": "^17.1.0", "eslint-config-airbnb-base": "^12.0.1", "eslint-import-resolver-webpack": "^0.8.3", "eslint-loader": "^2.1.1", "eslint-plugin-cypress": "^2.0.1", - "eslint-plugin-import": "^2.7.0", + "eslint-plugin-import": "^2.14.0", "eslint-plugin-jest": "^21.25.1", - "eslint-plugin-jsx-a11y": "^6.0.3", - "eslint-plugin-react": "^7.7.0", + "eslint-plugin-jsx-a11y": "^6.1.2", + "eslint-plugin-react": "^7.12.3", "file-loader": "^2.0.0", "html-webpack-plugin": "^3.2.0", "identity-obj-proxy": "^3.0.0", From 53aecdc6075c5a194a9c7e1e7c954b418d9aa342 Mon Sep 17 00:00:00 2001 From: Vladislav Denisov Date: Tue, 29 Jan 2019 19:17:13 +0300 Subject: [PATCH 07/27] yandex_metrica: changed auth from params to headers (#3360) --- redash/query_runner/yandex_metrica.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/redash/query_runner/yandex_metrica.py b/redash/query_runner/yandex_metrica.py index 814b1b8662..82f47d8565 100644 --- a/redash/query_runner/yandex_metrica.py +++ b/redash/query_runner/yandex_metrica.py @@ -115,7 +115,11 @@ def test_connection(self): def _send_query(self, path='stat/v1/data', **kwargs): token = kwargs.pop('oauth_token', self.configuration['token']) - r = requests.get('{0}/{1}'.format(self.host, path), params=dict(oauth_token=token, **kwargs)) + r = requests.get( + '{0}/{1}'.format(self.host, path), + headers={'Authorization': 'OAuth {}'.format(token)}, + params=kwargs + ) if r.status_code != 200: raise Exception(r.text) return r.json() From 61e7cdaa8107443f0cc59d9dadf575278f4aea7a Mon Sep 17 00:00:00 2001 From: Aidarbek Suleimenov Date: Tue, 29 Jan 2019 23:47:43 +0600 Subject: [PATCH 08/27] Filename set when /results called directly (#3359) * filename set when /results called directly * /results filename changed from query name to id * Long line shortened --- redash/handlers/query_results.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py index a3e83fad52..45be31301f 100644 --- a/redash/handlers/query_results.py +++ b/redash/handlers/query_results.py @@ -223,6 +223,7 @@ def get(self, query_id=None, query_result_id=None, filetype='json'): max_age = int(request.args.get('maxAge', 0)) query_result = None + query = None if query_result_id: query_result = get_object_or_404(models.QueryResult.get_by_id_and_org, query_result_id, self.current_org) @@ -276,6 +277,16 @@ def get(self, query_id=None, query_result_id=None, filetype='json'): if should_cache: response.headers.add_header('Cache-Control', 'private,max-age=%d' % ONE_YEAR) + str_date = query_result.retrieved_at.strftime("%Y_%m_%d") + str_id = None + if query is not None: + str_id = str(query.id) + else: + str_id = str(query_result.id) + filename = "{}_{}.{}".format(str_id, str_date, filetype,) + + response.headers.add_header("Content-Disposition", 'attachment; filename="{}"'.format(filename,)) + return response else: From 9e2f8e2461799ffd4c882b7b8a05601064d1fd10 Mon Sep 17 00:00:00 2001 From: Ran Byron Date: Wed, 30 Jan 2019 10:43:38 +0200 Subject: [PATCH 09/27] Fix: Escape button in tag edit modal (#3363) --- .../components/tags-control/TagsControl.jsx | 82 ++++++++------ .../tags-control/TagsEditorModal.jsx | 100 ++++++------------ 2 files changed, 82 insertions(+), 100 deletions(-) diff --git a/client/app/components/tags-control/TagsControl.jsx b/client/app/components/tags-control/TagsControl.jsx index 0a28c0f98d..f514577261 100644 --- a/client/app/components/tags-control/TagsControl.jsx +++ b/client/app/components/tags-control/TagsControl.jsx @@ -1,7 +1,7 @@ import { map, trim } from 'lodash'; -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; -import { $uibModal } from '@/services/ng'; +import TagsEditorModal from './TagsEditorModal'; export default class TagsControl extends React.Component { static propTypes = { @@ -15,27 +15,36 @@ export default class TagsControl extends React.Component { static defaultProps = { tags: [], canEdit: false, - getAvailableTags: () => {}, + getAvailableTags: () => Promise.resolve([]), onEdit: () => {}, className: '', }; - editTags() { - const { getAvailableTags, onEdit } = this.props; // eslint-disable-line react/prop-types - const tags = map(this.props.tags, trim); + constructor(props) { + super(props); + + this.state = { + showModal: false, + }; - getAvailableTags().then((availableTags) => { - $uibModal - .open({ - component: 'tagsEditorModal', - resolve: { - tags: () => tags, - availableTags: () => availableTags, - }, - }).result.then((newTags) => { - onEdit(newTags); - }); - }); + // get available tags + this.props.getAvailableTags() + .then((tags) => { + this.availableTags = tags; + }); + } + + onTagsChanged = (newTags) => { + this.props.onEdit(newTags); + this.closeEditModal(); + } + + openEditModal = () => { + this.setState({ showModal: true }); + } + + closeEditModal = () => { + this.setState({ showModal: false }); } // eslint-disable-next-line class-methods-use-this @@ -55,19 +64,32 @@ export default class TagsControl extends React.Component { } renderEditButton() { - if (this.props.canEdit) { - return (this.props.tags.length > 0) ? ( -
this.editTags()}> - - - ) : ( - this.editTags()}> - - Add tag - - ); + if (!this.props.canEdit) { + return null; } - return null; + + const tags = map(this.props.tags, trim); + + const buttonLabel = tags.length > 0 + ? + : Add tag; + + return ( + + + {buttonLabel} + + {this.state.showModal + ? + : null + } + + ); } render() { diff --git a/client/app/components/tags-control/TagsEditorModal.jsx b/client/app/components/tags-control/TagsEditorModal.jsx index 0b24ad8494..0ed0fc6ffe 100644 --- a/client/app/components/tags-control/TagsEditorModal.jsx +++ b/client/app/components/tags-control/TagsEditorModal.jsx @@ -1,12 +1,12 @@ -import { map, trim, uniq } from 'lodash'; +import { map, trim, chain } from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; -import { react2angular } from 'react2angular'; import Select from 'antd/lib/select'; +import Modal from 'antd/lib/modal'; const { Option } = Select; -class TagsEditorModal extends React.Component { +export default class TagsEditorModal extends React.Component { static propTypes = { tags: PropTypes.arrayOf(PropTypes.string), availableTags: PropTypes.arrayOf(PropTypes.string), @@ -23,81 +23,41 @@ class TagsEditorModal extends React.Component { constructor(props) { super(props); + this.state = { - result: uniq(map(this.props.tags, trim)), + result: chain(this.props.tags).map(trim).uniq().value(), }; - this.selectRef = React.createRef(); - } - componentDidMount() { - // `autoFocus` does not work on Select because its `componentDidMount` is fired before the component - // is actually visible, so it cannot get focus. This hack should be replaced with `autoFocus` prop - // when Angular will finally gone - setTimeout(() => { - if ( - this.selectRef.current && - this.selectRef.current.rcSelect && - this.selectRef.current.rcSelect.inputRef - ) { - this.selectRef.current.rcSelect.inputRef.focus(); - } - }); + this.selectOptions = + chain(this.props.availableTags) + .map(trim) + .uniq() + .map(tag => ) + .value(); } render() { - const { - availableTags, - close, - dismiss, - } = this.props; - - const uniqueAvailableTags = uniq(map(availableTags, trim)); + const { close, dismiss } = this.props; + const { result } = this.state; return ( -
-
- -

Add/Edit Tags

-
-
- -
-
- - -
-
+ close(result)} + onCancel={dismiss} + > + + ); } } - -export default function init(ngModule) { - ngModule.component('tagsEditorModal', { - template: ` - - `, - bindings: { - resolve: '<', - close: '&', - dismiss: '&', - }, - }); - ngModule.component('tagsEditorModalImpl', react2angular(TagsEditorModal)); -} - -init.init = true; From 13bc910d7c966e339296b05f1c0930588688c448 Mon Sep 17 00:00:00 2001 From: koooge Date: Wed, 30 Jan 2019 17:51:32 +0900 Subject: [PATCH 10/27] Update CodeClimate configuration format to Version 2 (#3286) * codeclimate: Update format v2 * codeclimate: Ignore generated files --- .codeclimate.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index dc8e299291..119550ee58 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,4 +1,5 @@ -engines: +version: "2" +plugins: pep8: enabled: true eslint: @@ -11,12 +12,11 @@ engines: enabled: false no-multiple-empty-lines: # TODO: Enable enabled: false -ratings: - paths: - - "redash/**/*.py" - - "client/**/*.js" -exclude_paths: -- tests/**/*.py -- migrations/**/*.py -- setup/**/* -- bin/**/* +exclude_patterns: +- "tests/**/*.py" +- "migrations/**/*.py" +- "setup/**/*" +- "bin/**/*" +- "**/node_modules/" +- "client/dist/" +- "**/*.pyc" From 0b9f575dab5a71c84aeb15ac6ae5e5aba9b92e17 Mon Sep 17 00:00:00 2001 From: Aidarbek Suleimenov Date: Wed, 30 Jan 2019 15:17:43 +0600 Subject: [PATCH 11/27] Fix: make ClickHouse password and username truly optional (#3362) * clickhouse optional password * clickhouse URL and user made optional --- redash/query_runner/clickhouse.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redash/query_runner/clickhouse.py b/redash/query_runner/clickhouse.py index a51328531a..8a07b3634a 100644 --- a/redash/query_runner/clickhouse.py +++ b/redash/query_runner/clickhouse.py @@ -68,13 +68,13 @@ def _get_tables(self, schema): def _send_query(self, data, stream=False): r = requests.post( - self.configuration['url'], + self.configuration.get('url', "http://127.0.0.1:8123"), data=data.encode("utf-8"), stream=stream, timeout=self.configuration.get('timeout', 30), params={ - 'user': self.configuration['user'], - 'password': self.configuration['password'], + 'user': self.configuration.get('user', "default"), + 'password': self.configuration.get('password', ""), 'database': self.configuration['dbname'] } ) From 0d959116d851b63ec6b887076a3623bf89d04a85 Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Wed, 30 Jan 2019 17:22:54 +0200 Subject: [PATCH 12/27] Migrate FavoritesControl component to React --- client/app/components/FavoritesControl.jsx | 89 +++++++++++++++++++ .../app/components/favorites-control/index.js | 41 --------- client/app/lib/list-ctrl.js | 5 ++ client/app/services/ng.js | 2 + 4 files changed, 96 insertions(+), 41 deletions(-) create mode 100644 client/app/components/FavoritesControl.jsx delete mode 100644 client/app/components/favorites-control/index.js diff --git a/client/app/components/FavoritesControl.jsx b/client/app/components/FavoritesControl.jsx new file mode 100644 index 0000000000..8908a3a751 --- /dev/null +++ b/client/app/components/FavoritesControl.jsx @@ -0,0 +1,89 @@ +import { isFunction } from 'lodash'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; +import { $rootScope } from '@/services/ng'; + +function toggleItem(event, item, callback) { + event.preventDefault(); + event.stopPropagation(); + + if (item) { + if (item.is_favorite) { + item.$unfavorite().then(() => { + item.is_favorite = false; + $rootScope.$broadcast('reloadFavorites'); + if (isFunction(callback)) { + callback(); + } + }); + } else { + item.$favorite().then(() => { + item.is_favorite = true; + $rootScope.$broadcast('reloadFavorites'); + if (isFunction(callback)) { + callback(); + } + }); + } + } +} + +export function FavoritesControl({ item, onChange }) { + const icon = item.is_favorite ? 'fa-star' : 'fa-star-o'; + const title = item.is_favorite ? 'Remove from favorites' : 'Add to favorites'; + return ( + toggleItem(event, item, onChange)} + > +