From 7847cf7d63ad680d7fb3915dc8b41005305161c0 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Thu, 17 Jan 2019 11:56:16 +0200 Subject: [PATCH 001/177] Fix invitation pending for older invitations (#3298) * explicitly look for a False under details['is_invitation_pending'] and not any falsey result, to avoid locking out invitations which were created before the Pending Invitation feature was introduced. Solves https://github.com/getredash/redash/issues/3297 * test that old invites (that do not have any is_invitation_pending flag set in their details object) are still acceptable --- redash/handlers/authentication.py | 2 +- tests/handlers/test_authentication.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/redash/handlers/authentication.py b/redash/handlers/authentication.py index d6c0380d6a..77b5de0ec5 100644 --- a/redash/handlers/authentication.py +++ b/redash/handlers/authentication.py @@ -38,7 +38,7 @@ def render_token_login_page(template, org_slug, token): return render_template("error.html", error_message="Your invite link has expired. Please ask for a new one."), 400 - if not user.is_invitation_pending: + if user.details.get('is_invitation_pending') is False: return render_template("error.html", error_message=("This invitation has already been accepted. " "Please try resetting your password instead.")), 400 diff --git a/tests/handlers/test_authentication.py b/tests/handlers/test_authentication.py index c0fb797d9f..1559921d14 100644 --- a/tests/handlers/test_authentication.py +++ b/tests/handlers/test_authentication.py @@ -50,6 +50,12 @@ def test_bad_token(self): response = self.post_request('/invite/{}'.format('jdsnfkjdsnfkj'), data={'password': '1234'}, org=self.factory.org) self.assertEqual(response.status_code, 400) + def test_user_invited_before_invitation_pending_check(self): + user = self.factory.create_user(details={}) + token = invite_token(user) + response = self.post_request('/invite/{}'.format(token), data={'password': 'test1234'}, org=self.factory.org) + self.assertEqual(response.status_code, 302) + def test_already_active_user(self): token = invite_token(self.factory.user) self.post_request('/invite/{}'.format(token), data={'password': 'test1234'}, org=self.factory.org) From 06887f6ff183792f7a56d030f9512db8a6bb105b Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Thu, 17 Jan 2019 15:14:46 +0200 Subject: [PATCH 002/177] Multifilter's dropdown cropped when visualization container is too small --- client/app/components/filters.html | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/client/app/components/filters.html b/client/app/components/filters.html index a770cdee78..9393e923e8 100644 --- a/client/app/components/filters.html +++ b/client/app/components/filters.html @@ -3,16 +3,29 @@
- + {{$select.selected | filterValue:filter}} {{value | filterValue:filter }} - + {{$item | filterValue:filter}} From e8120c5f79cca01b5a011e8197c4cd604bf718d9 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Fri, 18 Jan 2019 11:30:45 +0200 Subject: [PATCH 003/177] Use None as "not scheduled" default value of a query (#3277) * Use null as the default scheduled value. * Don't serialize None to json, so we can use SQL is not null predicate. * Fix warning about unicode in tests * Handling empty query.schedule in UI (#3283) * Add migration to convert empty schedules to null and drop the not null contraint. --- client/app/components/proptypes.js | 14 +++++ .../app/components/queries/ScheduleDialog.jsx | 20 +++++-- .../components/queries/ScheduleDialog.test.js | 15 ++--- .../app/components/queries/SchedulePhrase.jsx | 7 ++- .../__snapshots__/ScheduleDialog.test.js.snap | 47 ++++------------ client/app/pages/queries/query.html | 2 +- client/app/services/query.js | 7 +-- .../73beceabb948_bring_back_null_schedule.py | 56 +++++++++++++++++++ redash/models/__init__.py | 12 ++-- redash/models/types.py | 3 + tests/factories.py | 2 +- tests/models/test_dashboards.py | 8 +-- tests/test_models.py | 5 +- 13 files changed, 123 insertions(+), 75 deletions(-) create mode 100644 migrations/versions/73beceabb948_bring_back_null_schedule.py diff --git a/client/app/components/proptypes.js b/client/app/components/proptypes.js index ff18c69de8..19b7f6c5aa 100644 --- a/client/app/components/proptypes.js +++ b/client/app/components/proptypes.js @@ -15,6 +15,20 @@ export const Table = PropTypes.shape({ export const Schema = PropTypes.arrayOf(Table); +export const RefreshScheduleType = PropTypes.shape({ + interval: PropTypes.number, + time: PropTypes.string, + day_of_week: PropTypes.string, + until: PropTypes.string, +}); + +export const RefreshScheduleDefault = { + interval: null, + time: null, + day_of_week: null, + until: null, +}; + export const Field = PropTypes.shape({ name: PropTypes.string.isRequired, title: PropTypes.string, diff --git a/client/app/components/queries/ScheduleDialog.jsx b/client/app/components/queries/ScheduleDialog.jsx index d33b961e76..5557aa5da7 100644 --- a/client/app/components/queries/ScheduleDialog.jsx +++ b/client/app/components/queries/ScheduleDialog.jsx @@ -9,6 +9,7 @@ import Radio from 'antd/lib/radio'; import { capitalize, clone, isEqual } from 'lodash'; import moment from 'moment'; import { secondsToInterval, durationHumanize, pluralize, IntervalEnum, localizeTime } from '@/filters'; +import { RefreshScheduleType, RefreshScheduleDefault } from '../proptypes'; import './ScheduleDialog.css'; @@ -21,13 +22,16 @@ const { Option, OptGroup } = Select; export class ScheduleDialog extends React.Component { static propTypes = { show: PropTypes.bool.isRequired, - // eslint-disable-next-line react/forbid-prop-types - query: PropTypes.object.isRequired, + schedule: RefreshScheduleType, refreshOptions: PropTypes.arrayOf(PropTypes.number).isRequired, updateQuery: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, }; + static defaultProps = { + schedule: RefreshScheduleDefault, + }; + constructor(props) { super(props); this.state = this.initState; @@ -35,7 +39,7 @@ export class ScheduleDialog extends React.Component { } get initState() { - const newSchedule = clone(this.props.query.schedule); + const newSchedule = clone(this.props.schedule || ScheduleDialog.defaultProps.schedule); const { time, interval: seconds, day_of_week: day } = newSchedule; const { interval } = secondsToInterval(seconds); const [hour, minute] = time ? localizeTime(time).split(':') : [null, null]; @@ -144,9 +148,15 @@ export class ScheduleDialog extends React.Component { } save() { + const { newSchedule } = this.state; + // save if changed - if (!isEqual(this.state.newSchedule, this.props.query.schedule)) { - this.props.updateQuery({ schedule: clone(this.state.newSchedule) }); + if (!isEqual(newSchedule, this.props.schedule)) { + if (newSchedule.interval) { + this.props.updateQuery({ schedule: clone(newSchedule) }); + } else { + this.props.updateQuery({ schedule: null }); + } } this.props.onClose(); } diff --git a/client/app/components/queries/ScheduleDialog.test.js b/client/app/components/queries/ScheduleDialog.test.js index cb0fc8ee2b..9227173344 100644 --- a/client/app/components/queries/ScheduleDialog.test.js +++ b/client/app/components/queries/ScheduleDialog.test.js @@ -1,17 +1,11 @@ import React from 'react'; import { mount } from 'enzyme'; import { ScheduleDialog } from './ScheduleDialog'; +import RefreshScheduleDefault from '../proptypes'; const defaultProps = { show: true, - query: { - schedule: { - time: null, - until: null, - interval: null, - day_of_week: null, - }, - }, + schedule: RefreshScheduleDefault, refreshOptions: [ 60, 300, 600, // 1, 5 ,10 mins 3600, 36000, 82800, // 1, 10, 23 hours @@ -23,12 +17,11 @@ const defaultProps = { }; function getWrapper(schedule = {}, props = {}) { - const defaultSchedule = defaultProps.query.schedule; props = Object.assign( {}, defaultProps, props, - { query: { schedule: Object.assign({}, defaultSchedule, schedule) } }, + { schedule: Object.assign({}, RefreshScheduleDefault, schedule) }, ); return [mount(), props]; } @@ -78,7 +71,7 @@ describe('ScheduleDialog', () => { const [wrapper] = getWrapper({ interval: 1209600, time: '22:15', - day_of_week: 2, + day_of_week: 'Monday', }); test('Sets to correct interval', () => { diff --git a/client/app/components/queries/SchedulePhrase.jsx b/client/app/components/queries/SchedulePhrase.jsx index 665b0ae1ab..9858d7b4f0 100644 --- a/client/app/components/queries/SchedulePhrase.jsx +++ b/client/app/components/queries/SchedulePhrase.jsx @@ -3,23 +3,24 @@ import React from 'react'; import PropTypes from 'prop-types'; import Tooltip from 'antd/lib/tooltip'; import { localizeTime, durationHumanize } from '@/filters'; +import { RefreshScheduleType, RefreshScheduleDefault } from '../proptypes'; import './ScheduleDialog.css'; class SchedulePhrase extends React.Component { static propTypes = { - // eslint-disable-next-line react/forbid-prop-types - schedule: PropTypes.object.isRequired, + schedule: RefreshScheduleType, isNew: PropTypes.bool.isRequired, isLink: PropTypes.bool, }; static defaultProps = { + schedule: RefreshScheduleDefault, isLink: false, }; get content() { - const { interval: seconds } = this.props.schedule; + const { interval: seconds } = this.props.schedule || SchedulePhrase.defaultProps.schedule; if (!seconds) { return ['Never']; } diff --git a/client/app/components/queries/__snapshots__/ScheduleDialog.test.js.snap b/client/app/components/queries/__snapshots__/ScheduleDialog.test.js.snap index 899d1525ca..65a8d28ff2 100644 --- a/client/app/components/queries/__snapshots__/ScheduleDialog.test.js.snap +++ b/client/app/components/queries/__snapshots__/ScheduleDialog.test.js.snap @@ -1632,6 +1632,7 @@ exports[`ScheduleDialog Sets correct schedule settings Sets to "2 Weeks 22:15 Tu >
+
+ +

diff --git a/client/app/pages/users/show.js b/client/app/pages/users/show.js index 1be8b39d02..318b197960 100644 --- a/client/app/pages/users/show.js +++ b/client/app/pages/users/show.js @@ -6,7 +6,7 @@ import './settings.less'; function UserCtrl( $scope, $routeParams, $http, $location, toastr, - clientConfig, currentUser, User, + clientConfig, currentUser, User, AlertDialog, ) { $scope.userId = $routeParams.userId; $scope.currentUser = currentUser; @@ -122,6 +122,34 @@ function UserCtrl( $scope.disableUser = (user) => { User.disableUser(user); }; + + $scope.regenerateUserApiKey = (user) => { + const doRegenerate = () => { + $scope.disableRegenerateApiKeyButton = true; + $http + .post(`api/users/${$scope.user.id}/regenerate_api_key`) + .success((data) => { + toastr.success('The API Key has been updated.'); + user.api_key = data.api_key; + $scope.disableRegenerateApiKeyButton = false; + }) + .error((response) => { + const message = + response.message + ? response.message + : `Failed regenerating API Key: ${response.statusText}`; + + toastr.error(message); + $scope.disableRegenerateApiKeyButton = false; + }); + }; + + const title = 'Regenerate API Key'; + const message = 'Are you sure you want to regenerate?'; + + AlertDialog.open(title, message, { class: 'btn-warning', title: 'Regenerate' }) + .then(doRegenerate); + }; } export default function init(ngModule) { diff --git a/redash/handlers/api.py b/redash/handlers/api.py index f8ef199857..23225a7b5d 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -11,7 +11,7 @@ from redash.handlers.events import EventsResource from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource from redash.handlers.query_results import QueryResultListResource, QueryResultResource, JobResource -from redash.handlers.users import UserResource, UserListResource, UserInviteResource, UserResetPasswordResource, UserDisableResource +from redash.handlers.users import UserResource, UserListResource, UserInviteResource, UserResetPasswordResource, UserDisableResource, UserRegenerateApiKeyResource from redash.handlers.visualizations import VisualizationListResource from redash.handlers.visualizations import VisualizationResource from redash.handlers.widgets import WidgetResource, WidgetListResource @@ -101,6 +101,9 @@ def json_representation(data, code, headers=None): api.add_org_resource(UserResource, '/api/users/', endpoint='user') api.add_org_resource(UserInviteResource, '/api/users//invite', endpoint='user_invite') api.add_org_resource(UserResetPasswordResource, '/api/users//reset_password', endpoint='user_reset_password') +api.add_org_resource(UserRegenerateApiKeyResource, + '/api/users//regenerate_api_key', + endpoint='user_regenerate_api_key') api.add_org_resource(UserDisableResource, '/api/users//disable', endpoint='user_disable') api.add_org_resource(VisualizationListResource, '/api/visualizations', endpoint='visualizations') diff --git a/redash/handlers/users.py b/redash/handlers/users.py index 4b4b45a4e4..0e373cc864 100644 --- a/redash/handlers/users.py +++ b/redash/handlers/users.py @@ -149,6 +149,26 @@ def post(self, user_id): } +class UserRegenerateApiKeyResource(BaseResource): + def post(self, user_id): + user = models.User.get_by_id_and_org(user_id, self.current_org) + if user.is_disabled: + abort(404, message='Not found') + if not is_admin_or_owner(user_id): + abort(403) + + user.regenerate_api_key() + models.db.session.commit() + + self.record_event({ + 'action': 'regnerate_api_key', + 'object_id': user.id, + 'object_type': 'user' + }) + + return user.to_dict(with_api_key=True) + + class UserResource(BaseResource): def get(self, user_id): require_permission_or_owner('list_users', user_id) diff --git a/redash/models/users.py b/redash/models/users.py index f175b3b5c5..fb9a58cd80 100644 --- a/redash/models/users.py +++ b/redash/models/users.py @@ -122,6 +122,9 @@ def disable(self): def enable(self): self.disabled_at = None + def regenerate_api_key(self): + self.api_key = generate_token(40) + def to_dict(self, with_api_key=False): profile_image_url = self.profile_image_url if self.is_disabled: diff --git a/tests/handlers/test_users.py b/tests/handlers/test_users.py index 5b88ce7ec4..c12409d56a 100644 --- a/tests/handlers/test_users.py +++ b/tests/handlers/test_users.py @@ -328,3 +328,47 @@ def test_disabled_user_should_not_receive_restore_password_email(self): rv = self.make_request('post', '/api/users/{}/reset_password'.format(user.id), user=admin_user) self.assertEqual(rv.status_code, 404) send_password_reset_email_mock.assert_not_called() + + +class TestUserRegenerateApiKey(BaseTestCase): + def test_non_admin_cannot_regenerate_other_user_api_key(self): + admin_user = self.factory.create_admin() + other_user = self.factory.create_user() + orig_api_key = other_user.api_key + + rv = self.make_request('post', "/api/users/{}/regenerate_api_key".format(other_user.id), user=admin_user) + self.assertEqual(rv.status_code, 200) + + other_user = models.User.query.get(other_user.id) + self.assertNotEquals(orig_api_key, other_user.api_key) + + def test_admin_can_regenerate_other_user_api_key(self): + user1 = self.factory.create_user() + user2 = self.factory.create_user() + orig_user2_api_key = user2.api_key + + rv = self.make_request('post', "/api/users/{}/regenerate_api_key".format(user2.id), user=user1) + self.assertEqual(rv.status_code, 403) + + user = models.User.query.get(user2.id) + self.assertEquals(orig_user2_api_key, user.api_key) + + def test_admin_can_regenerate_api_key_myself(self): + admin_user = self.factory.create_admin() + orig_api_key = admin_user.api_key + + rv = self.make_request('post', "/api/users/{}/regenerate_api_key".format(admin_user.id), user=admin_user) + self.assertEqual(rv.status_code, 200) + + user = models.User.query.get(admin_user.id) + self.assertNotEquals(orig_api_key, user.api_key) + + def test_user_can_regenerate_api_key_myself(self): + user = self.factory.create_user() + orig_api_key = user.api_key + + rv = self.make_request('post', "/api/users/{}/regenerate_api_key".format(user.id), user=user) + self.assertEqual(rv.status_code, 200) + + user = models.User.query.get(user.id) + self.assertNotEquals(orig_api_key, user.api_key) diff --git a/tests/models/test_users.py b/tests/models/test_users.py index 7a51318ca2..f687c454ac 100644 --- a/tests/models/test_users.py +++ b/tests/models/test_users.py @@ -62,6 +62,17 @@ def test_non_unicode_search_string(self): assert user in User.search(User.all(user.org), term=u'א') +class TestUserRegenerateApiKey(BaseTestCase): + def test_regenerate_api_key(self): + user = self.factory.user + before_api_key = user.api_key + user.regenerate_api_key() + + # check committed by research + user = User.query.get(user.id) + self.assertNotEquals(user.api_key, before_api_key) + + class TestUserDetail(BaseTestCase): # def setUp(self): # super(TestUserDetail, self).setUp() @@ -94,4 +105,4 @@ def test_sync(self): user_reloaded = User.query.filter(User.id==user.id).first() self.assertIn('active_at', user_reloaded.details) - self.assertEqual(user_reloaded.active_at, timestamp) \ No newline at end of file + self.assertEqual(user_reloaded.active_at, timestamp) From 8bdcfb06c5fcdfb24acc2eb757ffe91bf23f4f45 Mon Sep 17 00:00:00 2001 From: Gabriel Dutra Date: Tue, 22 Jan 2019 04:52:23 -0200 Subject: [PATCH 008/177] add wait time before percy data source page snapshot (#3320) --- cypress/integration/data-source/create_data_source_spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cypress/integration/data-source/create_data_source_spec.js b/cypress/integration/data-source/create_data_source_spec.js index 1ae4be039a..e286a334e6 100644 --- a/cypress/integration/data-source/create_data_source_spec.js +++ b/cypress/integration/data-source/create_data_source_spec.js @@ -14,6 +14,8 @@ describe('Create Data Source', () => { cy.getByTestId('Database Name').type('postgres{enter}'); cy.contains('Saved.'); + + cy.wait(1000); cy.percySnapshot('Create Data Source page'); }); }); From c4bf44677a5e9957141d797e9fc6361513a7bb0f Mon Sep 17 00:00:00 2001 From: YOSHIDA Katsuhiko Date: Tue, 22 Jan 2019 21:43:52 +0900 Subject: [PATCH 009/177] Fix an error of exporting dict value as Excel (#3323) --- redash/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redash/models/__init__.py b/redash/models/__init__.py index a59bb25f28..73ff622bd3 100644 --- a/redash/models/__init__.py +++ b/redash/models/__init__.py @@ -351,7 +351,7 @@ def make_excel_content(self): for (r, row) in enumerate(query_data['rows']): for (c, name) in enumerate(column_names): v = row.get(name) - if isinstance(v, list): + if isinstance(v, list) or isinstance(v, dict): v = str(v).encode('utf-8') sheet.write(r + 1, c, v) From ff6b20b69c0fc0dedde7c74ad58e2c578f682149 Mon Sep 17 00:00:00 2001 From: Miles Maddox Date: Tue, 22 Jan 2019 07:52:13 -0600 Subject: [PATCH 010/177] support for fetching all JQL results by way of pagination (#3304) --- redash/query_runner/jql.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/redash/query_runner/jql.py b/redash/query_runner/jql.py index 2022f8f13a..de10508ad5 100644 --- a/redash/query_runner/jql.py +++ b/redash/query_runner/jql.py @@ -24,6 +24,8 @@ def add_column(self, column, column_type=TYPE_STRING): def to_json(self): return json_dumps({'rows': self.rows, 'columns': self.columns.values()}) + def merge(self, set): + self.rows = self.rows + set.rows def parse_issue(issue, field_mapping): result = OrderedDict() @@ -179,6 +181,19 @@ def run_query(self, query, user): results = parse_count(data) else: results = parse_issues(data, field_mapping) + index = data['startAt'] + data['maxResults'] + + while data['total'] > index: + query['startAt'] = index + response, error = self.get_response(jql_url, params=query) + if error is not None: + return None, error + + data = response.json() + index = data['startAt'] + data['maxResults'] + + addl_results = parse_issues(data, field_mapping) + results.merge(addl_results) return results.to_json(), None except KeyboardInterrupt: From 7fa66654451c197e5b5bf264edb1adc96bd0bff6 Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Tue, 22 Jan 2019 17:26:11 +0200 Subject: [PATCH 011/177] Use Ant's Paginator component; migrate SortIcon to React (#3317) --- client/app/assets/less/ant.less | 88 +++++++++++++++++++ client/app/assets/less/inc/ant-variables.less | 23 +++++ .../assets/less/inc/visualizations/misc.less | 7 +- client/app/assets/less/redash/query.less | 7 +- client/app/components/Paginator.jsx | 60 +++++++++++++ client/app/components/SortIcon.jsx | 31 +++++++ .../dynamic-table/dynamic-table.html | 13 +-- client/app/components/dynamic-table/index.js | 30 ++++++- client/app/components/paginator.js | 30 ------- client/app/components/sort-icon.js | 29 ------ client/app/lib/pagination/live-paginator.js | 1 + 11 files changed, 242 insertions(+), 77 deletions(-) create mode 100644 client/app/components/Paginator.jsx create mode 100644 client/app/components/SortIcon.jsx delete mode 100644 client/app/components/paginator.js delete mode 100644 client/app/components/sort-icon.js diff --git a/client/app/assets/less/ant.less b/client/app/assets/less/ant.less index e5c1311699..c9985fa36d 100644 --- a/client/app/assets/less/ant.less +++ b/client/app/assets/less/ant.less @@ -12,6 +12,7 @@ @import '~antd/lib/button/style/index'; @import '~antd/lib/radio/style/index'; @import '~antd/lib/time-picker/style/index'; +@import '~antd/lib/pagination/style/index'; @import 'inc/ant-variables'; // Remove bold in labels for Ant checkboxes and radio buttons @@ -47,3 +48,90 @@ -webkit-appearance: none; margin: 0; } + +// Pagination overrides (based on existing Bootstrap overrides) +.@{pagination-prefix-cls} { + display: inline-block; + margin-top: 18px; + margin-bottom: 18px; + vertical-align: top; + + &-item { + background-color: @pagination-bg; + border-color: transparent; + color: @pagination-color; + font-size: 14px; + margin-right: 5px; + + a { + color: inherit; + } + + &:focus, + &:hover { + background-color: @pagination-hover-bg; + border-color: transparent; + color: @pagination-hover-color; + a { + color: inherit; + } + } + + &-active { + &, + &:hover, + &:focus { + background-color: @pagination-active-bg; + color: @pagination-active-color; + border-color: transparent; + pointer-events: none; + cursor: default; + + a { + color: inherit; + } + } + } + } + + &-disabled { + &, + &:hover, + &:focus { + opacity: 0.5; + pointer-events: none; + } + } + + &-prev, + &-next { + .@{pagination-prefix-cls}-item-link { + background-color: @pagination-bg; + border-color: transparent; + color: @pagination-color; + line-height: @pagination-item-size - 2px; + } + + &:focus .@{pagination-prefix-cls}-item-link, + &:hover .@{pagination-prefix-cls}-item-link { + background-color: @pagination-hover-bg; + border-color: transparent; + color: @pagination-hover-color; + } + } + + &-prev, + &-jump-prev, + &-jump-next { + margin-right: 5px; + } + + &-jump-prev, + &-jump-next { + .@{pagination-prefix-cls}-item-container { + .@{pagination-prefix-cls}-item-link-icon { + color: @pagination-color; + } + } + } +} diff --git a/client/app/assets/less/inc/ant-variables.less b/client/app/assets/less/inc/ant-variables.less index adebc76f9f..44cbbcd430 100644 --- a/client/app/assets/less/inc/ant-variables.less +++ b/client/app/assets/less/inc/ant-variables.less @@ -1,8 +1,14 @@ /* -------------------------------------------------------- Colors -----------------------------------------------------------*/ + +@lightblue: #03A9F4; @primary-color: #2196F3; +@redash-gray: rgba(102, 136, 153, 1); +@redash-orange: rgba(255, 120, 100, 1); +@redash-black: rgba(0, 0, 0, 1); +@redash-yellow: rgba(252, 252, 161, 0.75); /* -------------------------------------------------------- Font @@ -27,3 +33,20 @@ @input-color: #595959; @border-radius-base: 2px; @border-color-base: #E8E8E8; + + +/* -------------------------------------------------------- + Pagination +-----------------------------------------------------------*/ + +@pagination-item-size: 33px; +@pagination-font-family: @redash-font; +@pagination-font-weight-active: normal; + +@pagination-bg: fade(@redash-gray, 15%); +@pagination-color: #7E7E7E; +@pagination-active-bg: @lightblue; +@pagination-active-color: #FFF; +@pagination-disabled-bg: fade(@redash-gray, 15%); +@pagination-hover-color: #333; +@pagination-hover-bg: fade(@redash-gray, 25%); diff --git a/client/app/assets/less/inc/visualizations/misc.less b/client/app/assets/less/inc/visualizations/misc.less index a01778d59c..d439837db0 100644 --- a/client/app/assets/less/inc/visualizations/misc.less +++ b/client/app/assets/less/inc/visualizations/misc.less @@ -1,3 +1,6 @@ -visualization-renderer .pagination { - margin: 0; +visualization-renderer { + .pagination, + .ant-pagination { + margin: 0; + } } diff --git a/client/app/assets/less/redash/query.less b/client/app/assets/less/redash/query.less index 20791a8c47..1cb76bff2e 100644 --- a/client/app/assets/less/redash/query.less +++ b/client/app/assets/less/redash/query.less @@ -202,8 +202,11 @@ edit-in-place p.editable:hover { } } -.visualization-renderer .pagination { - margin-top: 10px; +.visualization-renderer { + .pagination, + .ant-pagination { + margin-top: 10px; + } } .embed__vis { diff --git a/client/app/components/Paginator.jsx b/client/app/components/Paginator.jsx new file mode 100644 index 0000000000..be1a767364 --- /dev/null +++ b/client/app/components/Paginator.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; +import Pagination from 'antd/lib/pagination'; + +export function Paginator({ + page, + itemsPerPage, + totalCount, + onChange, +}) { + if (totalCount <= itemsPerPage) { + return null; + } + return ( +
+ +
+ ); +} + +Paginator.propTypes = { + page: PropTypes.number.isRequired, + itemsPerPage: PropTypes.number.isRequired, + totalCount: PropTypes.number.isRequired, + onChange: PropTypes.func, +}; + +Paginator.defaultProps = { + onChange: () => {}, +}; + +export default function init(ngModule) { + ngModule.component('paginatorImpl', react2angular(Paginator)); + ngModule.component('paginator', { + template: ` + `, + bindings: { + paginator: '<', + }, + controller($scope) { + this.onPageChanged = (page) => { + this.paginator.setPage(page); + $scope.$applyAsync(); + }; + }, + }); +} + +init.init = true; diff --git a/client/app/components/SortIcon.jsx b/client/app/components/SortIcon.jsx new file mode 100644 index 0000000000..ad16b5426e --- /dev/null +++ b/client/app/components/SortIcon.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; + +export function SortIcon({ column, sortColumn, reverse }) { + if (column !== sortColumn) { + return null; + } + + return ( + + ); +} + +SortIcon.propTypes = { + column: PropTypes.string, + sortColumn: PropTypes.string, + reverse: PropTypes.bool, +}; + +SortIcon.defaultProps = { + column: null, + sortColumn: null, + reverse: false, +}; + +export default function init(ngModule) { + ngModule.component('sortIcon', react2angular(SortIcon)); +} + +init.init = true; diff --git a/client/app/components/dynamic-table/dynamic-table.html b/client/app/components/dynamic-table/dynamic-table.html index fcc7d1afa8..3ee4f2ff9c 100644 --- a/client/app/components/dynamic-table/dynamic-table.html +++ b/client/app/components/dynamic-table/dynamic-table.html @@ -32,15 +32,4 @@ -
-
    -
    + diff --git a/client/app/components/dynamic-table/index.js b/client/app/components/dynamic-table/index.js index f29b0d1b1b..a51f661e52 100644 --- a/client/app/components/dynamic-table/index.js +++ b/client/app/components/dynamic-table/index.js @@ -82,9 +82,33 @@ function createRowRenderTemplate(columns, $compile) { return $compile(rowTemplate); } -function DynamicTable($compile) { +class DynamicTablePaginatorAdapter { + constructor($ctrl) { + this.$ctrl = $ctrl; + } + + get page() { + return this.$ctrl.currentPage; + } + + get itemsPerPage() { + return this.$ctrl.itemsPerPage; + } + + get totalCount() { + return this.$ctrl.preparedRows.length; + } + + setPage(page) { + this.$ctrl.onPageChanged(page); + } +} + +function DynamicTable($scope, $compile) { 'ngInject'; + this.paginatorAdapter = new DynamicTablePaginatorAdapter(this); + this.itemsPerPage = validateItemsPerPage(this.itemsPerPage); this.currentPage = 1; this.searchTerm = ''; @@ -180,8 +204,10 @@ function DynamicTable($compile) { } }; - this.onPageChanged = () => { + this.onPageChanged = (page) => { + this.currentPage = page; updateRowsToDisplay(false); + $scope.$applyAsync(); }; this.onSearchTermChanged = () => { diff --git a/client/app/components/paginator.js b/client/app/components/paginator.js deleted file mode 100644 index 63c0062e3e..0000000000 --- a/client/app/components/paginator.js +++ /dev/null @@ -1,30 +0,0 @@ -class PaginatorCtrl { - pageChanged() { - this.paginator.setPage(this.paginator.page); - } -} - -export default function init(ngModule) { - ngModule.component('paginator', { - template: ` -
    -
      -
      - `, - bindings: { - paginator: '<', - }, - controller: PaginatorCtrl, - }); -} - -init.init = true; diff --git a/client/app/components/sort-icon.js b/client/app/components/sort-icon.js deleted file mode 100644 index 314233156e..0000000000 --- a/client/app/components/sort-icon.js +++ /dev/null @@ -1,29 +0,0 @@ -export default function init(ngModule) { - ngModule.component('sortIcon', { - template: '', - bindings: { - column: '<', - sortColumn: '<', - reverse: '<', - }, - controller() { - this.$onChanges = (changes) => { - ['column', 'sortColumn', 'reverse'].forEach((v) => { - if (v in changes) { - this[v] = changes[v].currentValue; - } - }); - - this.showIcon = false; - - if (this.column === this.sortColumn) { - this.showIcon = true; - this.icon = this.reverse ? 'desc' : 'asc'; - } - }; - }, - }); -} - -init.init = true; - diff --git a/client/app/lib/pagination/live-paginator.js b/client/app/lib/pagination/live-paginator.js index 0ed8391dce..da3ae2bd80 100644 --- a/client/app/lib/pagination/live-paginator.js +++ b/client/app/lib/pagination/live-paginator.js @@ -4,6 +4,7 @@ export default class LivePaginator { } = {}) { this.page = page; this.itemsPerPage = itemsPerPage; + this.totalCount = 0; this.orderByField = orderByField; this.orderByReverse = orderByReverse; this.rowsFetcher = rowsFetcher; From a9c514aaf7d5d9a244988011d8fda05f30fbc064 Mon Sep 17 00:00:00 2001 From: Omer Lachish Date: Wed, 23 Jan 2019 11:10:04 +0200 Subject: [PATCH 012/177] Textless query result endpoint (#3311) * add an endpoint for running a query by its id and (optional) parameters without having to provide the query text * check for access to query before running it --- redash/handlers/api.py | 3 ++- redash/handlers/query_results.py | 23 +++++++++++++++++++++++ tests/handlers/test_query_results.py | 8 ++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/redash/handlers/api.py b/redash/handlers/api.py index 23225a7b5d..140c37ce83 100644 --- a/redash/handlers/api.py +++ b/redash/handlers/api.py @@ -6,7 +6,7 @@ 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, DashboardResource, DashboardShareResource, PublicDashboardResource +from redash.handlers.dashboards import DashboardListResource, DashboardResource, DashboardShareResource, PublicDashboardResource from redash.handlers.data_sources import DataSourceTypeListResource, DataSourceListResource, DataSourceSchemaResource, DataSourceResource, DataSourcePauseResource, DataSourceTestResource from redash.handlers.events import EventsResource from redash.handlers.queries import QueryForkResource, QueryRefreshResource, QueryListResource, QueryRecentResource, QuerySearchResource, QueryResource, MyQueriesResource @@ -92,6 +92,7 @@ def json_representation(data, code, headers=None): api.add_org_resource(QueryResultResource, '/api/query_results/.', '/api/query_results/', + '/api/queries//results', '/api/queries//results.', '/api/queries//results/.', endpoint='query_result') diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py index d7cca6318e..575f576653 100644 --- a/redash/handlers/query_results.py +++ b/redash/handlers/query_results.py @@ -154,6 +154,29 @@ def options(self, query_id=None, query_result_id=None, filetype='json'): return make_response("", 200, headers) + @require_permission('execute_query') + def post(self, query_id): + """ + Execute a saved query. + + :param number query_id: The ID of the query whose results should be fetched. + :param object parameters: The parameter values to apply to the query. + :qparam number max_age: If query results less than `max_age` seconds old are available, + return them, otherwise execute the query; if omitted or -1, returns + any cached result, or executes if not available. Set to zero to + always execute. + """ + params = request.get_json(force=True) + parameters = params.get('parameters', {}) + 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) + + 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) + @require_permission('view_query') def get(self, query_id=None, query_result_id=None, filetype='json'): """ diff --git a/tests/handlers/test_query_results.py b/tests/handlers/test_query_results.py index 4517cc6256..8a65233ef6 100644 --- a/tests/handlers/test_query_results.py +++ b/tests/handlers/test_query_results.py @@ -128,6 +128,14 @@ def test_has_full_access_to_data_source(self): rv = self.make_request('get', '/api/query_results/{}'.format(query_result.id)) self.assertEquals(rv.status_code, 200) + def test_execute_new_query(self): + query = self.factory.create_query() + + rv = self.make_request('post', '/api/queries/{}/results'.format(query.id), data={'parameters': {}}) + + self.assertEquals(rv.status_code, 200) + self.assertIn('job', rv.json) + def test_access_with_query_api_key(self): ds = self.factory.create_data_source(group=self.factory.org.default_group, view_only=False) query = self.factory.create_query() From bfeb015d713df6757094ab8fa3f4c641aab5e261 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Wed, 23 Jan 2019 13:38:08 +0200 Subject: [PATCH 013/177] Add configuration for the Support probot. (#3327) --- .github/support.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/support.yml diff --git a/.github/support.yml b/.github/support.yml new file mode 100644 index 0000000000..164b588b36 --- /dev/null +++ b/.github/support.yml @@ -0,0 +1,23 @@ +# Configuration for Support Requests - https://github.com/dessant/support-requests + +# Label used to mark issues as support requests +supportLabel: Support Question + +# Comment to post on issues marked as support requests, `{issue-author}` is an +# optional placeholder. Set to `false` to disable +supportComment: > + :wave: @{issue-author}, we use the issue tracker exclusively for bug reports + and planned work. However, this issue appears to be a support request. + Please use [our forum](https://discuss.redash.io) to get help. + +# Close issues marked as support requests +close: true + +# Lock issues marked as support requests +lock: false + +# Assign `off-topic` as the reason for locking. Set to `false` to disable +setLockReason: true + +# Repository to extend settings from +# _extends: repo From 87667676e6eab79d04c2389781e4e6a629497c05 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Wed, 23 Jan 2019 13:48:11 +0200 Subject: [PATCH 014/177] Remove link to roadmap (#3329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's no longer maintained 😢 --- CONTRIBUTING.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1288782f5f..54e5d58d13 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,6 @@ The following is a set of guidelines for contributing to Redash. These are guide ## Quick Links: -- [Feature Roadmap](https://trello.com/b/b2LUHU7A/redash-roadmap) - [Feature Requests](https://discuss.redash.io/c/feature-requests) - [Documentation](https://redash.io/help/) - [Blog](https://blog.redash.io/) From d5afa1815e7575b194ea18c4e01129ce9429a3cc Mon Sep 17 00:00:00 2001 From: Ran Byron Date: Wed, 23 Jan 2019 16:27:49 +0200 Subject: [PATCH 015/177] Filtering out incompatible dashboard params (#3330) --- .../app/components/ParameterMappingInput.jsx | 35 ++++++++++++------- .../components/dashboards/AddWidgetDialog.jsx | 6 ++-- .../EditParameterMappingsDialog.jsx | 6 ++-- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/client/app/components/ParameterMappingInput.jsx b/client/app/components/ParameterMappingInput.jsx index c5ebcf0013..54487b7abc 100644 --- a/client/app/components/ParameterMappingInput.jsx +++ b/client/app/components/ParameterMappingInput.jsx @@ -223,7 +223,10 @@ export class ParameterMappingInput extends React.Component { export class ParameterMappingListInput extends React.Component { static propTypes = { mappings: PropTypes.arrayOf(PropTypes.object), - existingParamNames: PropTypes.arrayOf(PropTypes.string), + existingParams: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + type: PropTypes.string, + })), onChange: PropTypes.func, clientConfig: PropTypes.any, // eslint-disable-line react/forbid-prop-types Query: PropTypes.any, // eslint-disable-line react/forbid-prop-types @@ -231,7 +234,7 @@ export class ParameterMappingListInput extends React.Component { static defaultProps = { mappings: [], - existingParamNames: [], + existingParams: [], onChange: () => {}, clientConfig: null, Query: null, @@ -255,17 +258,23 @@ export class ParameterMappingListInput extends React.Component { return (
      - {this.props.mappings.map((mapping, index) => ( -
      - this.updateParamMapping(mapping, newMapping)} - clientConfig={clientConfig} - Query={Query} - /> -
      - ))} + {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 + + return ( +
      + this.updateParamMapping(mapping, newMapping)} + clientConfig={clientConfig} + Query={Query} + /> +
      + ); + })}
      ); } diff --git a/client/app/components/dashboards/AddWidgetDialog.jsx b/client/app/components/dashboards/AddWidgetDialog.jsx index a6922840e5..6d51a35e90 100644 --- a/client/app/components/dashboards/AddWidgetDialog.jsx +++ b/client/app/components/dashboards/AddWidgetDialog.jsx @@ -281,9 +281,9 @@ class AddWidgetDialog extends React.Component { const clientConfig = this.props.clientConfig; // eslint-disable-line react/prop-types const Query = this.props.Query; // eslint-disable-line react/prop-types - const existingParamNames = map( + const existingParams = map( this.props.dashboard.getParametersDefs(), - param => param.name, + ({ name, type }) => ({ name, type }), ); return ( @@ -311,7 +311,7 @@ class AddWidgetDialog extends React.Component { this.updateParamMappings(mappings)} clientConfig={clientConfig} Query={Query} diff --git a/client/app/components/dashboards/EditParameterMappingsDialog.jsx b/client/app/components/dashboards/EditParameterMappingsDialog.jsx index 3b1ba76ac7..2230224933 100644 --- a/client/app/components/dashboards/EditParameterMappingsDialog.jsx +++ b/client/app/components/dashboards/EditParameterMappingsDialog.jsx @@ -63,9 +63,9 @@ class EditParameterMappingsDialog extends React.Component { const clientConfig = this.props.clientConfig; // eslint-disable-line react/prop-types const Query = this.props.Query; // eslint-disable-line react/prop-types - const existingParamNames = map( + const existingParams = map( this.props.dashboard.getParametersDefs(), - param => param.name, + ({ name, type }) => ({ name, type }), ); return ( @@ -87,7 +87,7 @@ class EditParameterMappingsDialog extends React.Component { (this.state.parameterMappings.length > 0) && this.updateParamMappings(mappings)} clientConfig={clientConfig} Query={Query} From 1a61ee3ec0711e911bdaf25ed3f4130265991c4d Mon Sep 17 00:00:00 2001 From: Vibhor Kumar Date: Wed, 23 Jan 2019 13:07:40 -0500 Subject: [PATCH 016/177] Add: Uptycs query runner (#3319) * adding uptycs query_runner in redash * as per comment from Arik comment fixed the code * fixed function_name * fixed some indentation issues * fixed the indentation issue and taken out customer_id from secret * fixed the dependency of urllib3 * fixed the indententaton issue * remved the urllib3 from requirements * fixed the indentation issues * added the new square image for Uptycs. Removed unnecessary variable and made ssl as an option * fixed indentation issue * Renamed SSL to verify_ssl and also added verify_ssl validate in verify in missing places --- client/app/assets/images/db-logos/uptycs.png | Bin 0 -> 2629 bytes redash/query_runner/uptycs.py | 140 +++++++++++++++++++ redash/settings/__init__.py | 1 + 3 files changed, 141 insertions(+) create mode 100644 client/app/assets/images/db-logos/uptycs.png create mode 100644 redash/query_runner/uptycs.py diff --git a/client/app/assets/images/db-logos/uptycs.png b/client/app/assets/images/db-logos/uptycs.png new file mode 100644 index 0000000000000000000000000000000000000000..3f9ead878531696c74e07bc41df07bf791643ddd GIT binary patch literal 2629 zcmbW3c{tPy7sr2cOR1YBOK+~h7|JpXvJFEqni*^MWh}|wB)c$3*E%6%CnD-bmYM8Z zj7mtBY%{K9Y=guQS&G4{_rLd#_mB6U=Q+0&+0M%F&W_!) z2Y@qN802+}5J%?J%I#t}U)(HPbaC;*Avu`l*D(Bq-^v)mHoc@>yNBsN^WfJd{~$!| zXd=C^KTM8<;@{@94U^tUhbiTo6o0=J#=PSiq<+p{;OHDjL55EAO6gx4fspiVqlS5@ z_5#uWVndeZnby zu+3XO1Dm^5Fm-+ueT0Z!CPfUek;a{(7sVUrtG=M85g2OMJXy(M_qV_Aq!eDMFJADn zw>@QtHI*cjW_%G_(}*Mz#Q{Nr4@nSw2XWt@YjOptd$Pat;5`#;-Znh!i@g#Gmq=F< zmwK2{lr~t}NBgu1MMGlDBF;rlFS=&wBPjTpBCM>>sUtSxhwCujjXImB932~GEZNn%qQTqne`SlJ-XeBoMB;%q!R>{fVdJfrQW7v1X~4Yauyx8eK#z z1;wluCOG2NaQ7c85pat;ZC_BE?d%^|<06tx3g}3!Tiky8&$Me~Jy$vd{Z3*J^ zTix9|HGsbB-R2KEdo<>k(}#*H>XrOid6K_xy!ArUsY{fk-S>xPgIcxN9>%lFDY0cG zv#-l_dSy~JYGPmq*?CRp-^1i9dHOoZ!9u&{Ri&XLa{S?W^PqAoRcG}SmxG~eYa)*p z%P9GpS>x6&Lh)M(tvvDV+i6T&8}o^Nj{x~s+_m3*x_dkh<3?C;R*#XY@yom> zZR=?~Dqz}pR0|_g*0-mtxGl1{Ht5t#al8v1VjptdzeDKSWc5M7tmgVAHeHQ;7x6;< zoE>$ag$CE{<7AWucxM(;~4klJ~c|bZshX!b>boazgsf0gVn= zl}48OhtmTgPgGN`%*V)3d0S6*S#l{N0EVq-YS22eY^?hTpp6spYcC9AEffC`ict zfOHq1S1*#aYJvx)St^aJ8rtyaUu)=O6a<#ymng$xo#yw7rM@r$&HE}hEYWMHUT&c5 zBkdwjTTV(2r{< zs&}g?+{`rlj?vW#+dFRr@Nump@)r4Qxzh-)q7wTn+#77Ub>;LZ%Syo~*lZBdf7|+f zMbmpllZ36PHxUOLw1sV))c4BM5$_(EcN>LD)o}6);V0Ho2Mz8&GbBhPH4E-Z+-)ak z`4rV%3&!Q0SSb{lHhv|g+oCVOWiS>;?z8-GV|!Isv1h=tzEZW5FTbG1Ko&$STU8O@ zLL2RDKC*534Oh)UUyinLvlRQ#$r7uk@(+yj+l=Bs5=+U>HJwW36;hq5`cd zfO(P()vb>(Xhi;Gm!c>CLQaNg=JJGS>#}~%b#(hl7p4VR0TVmlEN+0-A9;`AL^U+C zixSqpseNEQp+mq#*|vR}?p$}eGD1(3ClbV!9+n{W(DfV6SW1EC1ZKB-2nJ21UGsE{ z#ZOW4GrDo2Te^Rq(U|7t6(hR78~+l6tf;E!aGwxLdz?Tpi5aB3&gCKo2{sQiv^L%M zSK$mX6&@VteJZJ4=4Z!`Ze#`XZY@4qy+0z+phFJGE#h1Z^9eQt2eWm*2!qk`9G>%5 z1&jBN^S@>ocEB5wK2UiSC2#wfdHDIngcHUR@&EdkoGQYWAy0|RJ$gN9 z34!Od3CNz*1hr=-;8cGI`1}_U_IptxiV!db>p}Km**U}^88oZHH9oBA(1ld42S;z% z6K@v3xC~D6L?mZm6^>1M_dXJG-1tq-tQ(Q&_upnSkoKe9!#U3TlX)0p*Pn>ELe)#X zB&W69sjmdhq-ceR0p3t~Jzk9Bc}oW)XHu4#24RMPw3~m5Zuq>XcY{taeC!N$U8JNj zZd6&u90BO1vGu459jZ7e1kL%`?1z_siZWOb8Z!---mUZEc>_+%^kN&66o+!_fDW1T zLah)(DzAoy)|C6Wz05`Pmx^J!G>GbS(S?#v&XSqoto`N0=0kVVd)UnciJlCUUv`_c z_X+*^RYxD?l;i-T*))_eV^Cnz^K58stme25$XhI>o`}MBKZpKbwgL$%#4wEykGT?R z;nLcfs9Bcg)T$2}?UoP-wGcn01KLJEB%<_mgmplcwo^Krr7PRsA`H`s^{I~x(+7P2>j3nvY G`t#pvmd)b; literal 0 HcmV?d00001 diff --git a/redash/query_runner/uptycs.py b/redash/query_runner/uptycs.py new file mode 100644 index 0000000000..d8e8c59fb1 --- /dev/null +++ b/redash/query_runner/uptycs.py @@ -0,0 +1,140 @@ +from redash.query_runner import * +from redash.utils import json_dumps + +import json +import jwt +import datetime +import requests +import logging + +logger = logging.getLogger(__name__) + + +class Uptycs(BaseSQLQueryRunner): + noop_query = "SELECT 1" + + @classmethod + def configuration_schema(cls): + return { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "customer_id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "verify_ssl": { + "type": "boolean", + "default": True, + "title": "Verify SSL Certificates", + }, + "secret": { + "type": "string", + }, + }, + "order": ['url', 'customer_id', 'key', 'secret'], + "required": ["url", "customer_id", "key", "secret"], + "secret": ["secret", "key"] + } + + @classmethod + def annotate_query(cls): + return False + + def generate_header(self, key, secret): + header = {} + utcnow = datetime.datetime.utcnow() + date = utcnow.strftime("%a, %d %b %Y %H:%M:%S GMT") + auth_var = jwt.encode({'iss': key}, secret, algorithm='HS256') + authorization = "Bearer %s" % (auth_var) + header['date'] = date + header['Authorization'] = authorization + return header + + def transformed_to_redash_json(self, data): + transformed_columns = [] + rows = [] + # convert all type to JSON string + # In future we correct data type mapping later + if 'columns' in data: + for json_each in data['columns']: + name = json_each['name'] + new_json = {"name": name, + "type": "string", + "friendly_name": name} + transformed_columns.append(new_json) + # Transfored items into rows. + if 'items' in data: + rows = data['items'] + + redash_json_data = {"columns": transformed_columns, + "rows": rows} + return redash_json_data + + def api_call(self, sql): + # JWT encoded header + header = self.generate_header(self.configuration.get('key'), + self.configuration.get('secret')) + + # URL form using API key file based on GLOBAL + url = ("%s/public/api/customers/%s/query" % + (self.configuration.get('url'), + self.configuration.get('customer_id'))) + + # post data base sql + post_data_json = {"query": sql} + + response = requests.post(url, headers=header, json=post_data_json, + verify=self.configuration.get('verify_ssl', + True)) + + if response.status_code == 200: + response_output = json.loads(response.content) + else: + error = 'status_code ' + str(response.status_code) + '\n' + error = error + "failed to connect" + json_data = {} + return json_data, error + # if we get right status code then call transfored_to_redash + json_data = self.transformed_to_redash_json(response_output) + error = None + # if we got error from Uptycs include error information + if 'error' in response_output: + error = response_output['error']['message']['brief'] + error = error + '\n' + response_output['error']['message']['detail'] + return json_data, error + + def run_query(self, query, user): + data, error = self.api_call(query) + json_data = json_dumps(data) + logger.debug("%s", json_data) + return json_data, error + + def get_schema(self, get_stats=False): + header = self.generate_header(self.configuration.get('key'), + self.configuration.get('secret')) + url = ("%s/public/api/customers/%s/schema/global" % + (self.configuration.get('url'), + self.configuration.get('customer_id'))) + response = requests.get(url, headers=header, + verify=self.configuration.get('verify_ssl', + True)) + redash_json = [] + schema = json.loads(response.content) + for each_def in schema['tables']: + table_name = each_def['name'] + columns = [] + for col in each_def['columns']: + columns.append(col['name']) + table_json = {"name": table_name, "columns": columns} + redash_json.append(table_json) + + logger.debug("%s", schema.values()) + return redash_json + + +register(Uptycs) diff --git a/redash/settings/__init__.py b/redash/settings/__init__.py index f1608e9a87..a995b382fb 100644 --- a/redash/settings/__init__.py +++ b/redash/settings/__init__.py @@ -190,6 +190,7 @@ def all_settings(): 'redash.query_runner.druid', 'redash.query_runner.kylin', 'redash.query_runner.drill', + 'redash.query_runner.uptycs', ] enabled_query_runners = array_from_string(os.environ.get("REDASH_ENABLED_QUERY_RUNNERS", ",".join(default_query_runners))) From c2c722e12e5753fc21f93a3053b97bef705362e5 Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Wed, 23 Jan 2019 20:10:52 +0200 Subject: [PATCH 017/177] Migrate PageHeader component to React (#3324) * Migrate PageHeader component to React * CR1 --- .../assets/less/redash/redash-newstyle.less | 2 +- client/app/components/PageHeader.jsx | 23 +++++++++++++++++++ client/app/components/page-header/index.js | 16 ------------- .../components/page-header/page-header.html | 9 -------- client/app/components/settings-screen.html | 3 +-- .../outdated-queries/outdated-queries.html | 3 +-- client/app/pages/admin/status/status.html | 5 ++-- client/app/pages/admin/tasks/tasks.html | 3 +-- client/app/pages/alert/alert.html | 4 +--- client/app/pages/alerts-list/alerts-list.html | 2 +- .../app/pages/dashboards/dashboard-list.html | 3 +-- .../dashboards/public-dashboard-page.html | 3 +-- .../app/pages/queries-list/queries-list.html | 2 +- 13 files changed, 34 insertions(+), 44 deletions(-) create mode 100644 client/app/components/PageHeader.jsx delete mode 100644 client/app/components/page-header/index.js delete mode 100644 client/app/components/page-header/page-header.html diff --git a/client/app/assets/less/redash/redash-newstyle.less b/client/app/assets/less/redash/redash-newstyle.less index 711d546f38..efb824d138 100644 --- a/client/app/assets/less/redash/redash-newstyle.less +++ b/client/app/assets/less/redash/redash-newstyle.less @@ -370,7 +370,7 @@ body { padding: 20px; } -page-header, .page-header--new { +.page-header-wrapper, .page-header--new { h3 { margin: 0.2em 0; line-height: 1.3; diff --git a/client/app/components/PageHeader.jsx b/client/app/components/PageHeader.jsx new file mode 100644 index 0000000000..748f8f7e91 --- /dev/null +++ b/client/app/components/PageHeader.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { react2angular } from 'react2angular'; + +export function PageHeader({ title }) { + return ( +
      +
      +

      { title }

      +
      +
      + ); +} + +PageHeader.propTypes = { + title: PropTypes.string.isRequired, +}; + +export default function init(ngModule) { + ngModule.component('pageHeader', react2angular(PageHeader)); +} + +init.init = true; diff --git a/client/app/components/page-header/index.js b/client/app/components/page-header/index.js deleted file mode 100644 index 149c72943e..0000000000 --- a/client/app/components/page-header/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import template from './page-header.html'; - -function controller() {} - -export default function init(ngModule) { - ngModule.component('pageHeader', { - template, - controller, - transclude: true, - bindings: { - title: '@', - }, - }); -} - -init.init = true; diff --git a/client/app/components/page-header/page-header.html b/client/app/components/page-header/page-header.html deleted file mode 100644 index 056ca2558d..0000000000 --- a/client/app/components/page-header/page-header.html +++ /dev/null @@ -1,9 +0,0 @@ -
      -
      -

      {{$ctrl.title}}

      -
      -
      -

      -

      -
      -
      diff --git a/client/app/components/settings-screen.html b/client/app/components/settings-screen.html index 809867f1fd..fb0510dc5e 100644 --- a/client/app/components/settings-screen.html +++ b/client/app/components/settings-screen.html @@ -1,6 +1,5 @@
      - - +
        diff --git a/client/app/pages/admin/outdated-queries/outdated-queries.html b/client/app/pages/admin/outdated-queries/outdated-queries.html index 68036ec98f..605b574709 100644 --- a/client/app/pages/admin/outdated-queries/outdated-queries.html +++ b/client/app/pages/admin/outdated-queries/outdated-queries.html @@ -1,6 +1,5 @@
        - - +
          diff --git a/client/app/pages/admin/status/status.html b/client/app/pages/admin/status/status.html index 99435059d2..eb5191df0e 100644 --- a/client/app/pages/admin/status/status.html +++ b/client/app/pages/admin/status/status.html @@ -1,6 +1,5 @@
          - - +
            @@ -62,4 +61,4 @@
          -
        \ No newline at end of file +
        diff --git a/client/app/pages/admin/tasks/tasks.html b/client/app/pages/admin/tasks/tasks.html index ffd96b5056..f0b04d913d 100644 --- a/client/app/pages/admin/tasks/tasks.html +++ b/client/app/pages/admin/tasks/tasks.html @@ -1,6 +1,5 @@
        - - +
          diff --git a/client/app/pages/alert/alert.html b/client/app/pages/alert/alert.html index 737f384624..54ff300f76 100644 --- a/client/app/pages/alert/alert.html +++ b/client/app/pages/alert/alert.html @@ -1,7 +1,5 @@
          - - - + diff --git a/client/app/pages/alerts-list/alerts-list.html b/client/app/pages/alerts-list/alerts-list.html index 1c8e7d8e8a..b4d96d0250 100644 --- a/client/app/pages/alerts-list/alerts-list.html +++ b/client/app/pages/alerts-list/alerts-list.html @@ -1,5 +1,5 @@
          - + - - +
          diff --git a/client/app/pages/dashboards/public-dashboard-page.html b/client/app/pages/dashboards/public-dashboard-page.html index e25016c9f3..4377410336 100644 --- a/client/app/pages/dashboards/public-dashboard-page.html +++ b/client/app/pages/dashboards/public-dashboard-page.html @@ -1,6 +1,5 @@
          - - +
          diff --git a/client/app/pages/queries-list/queries-list.html b/client/app/pages/queries-list/queries-list.html index a20f96d9ac..acc005bbff 100644 --- a/client/app/pages/queries-list/queries-list.html +++ b/client/app/pages/queries-list/queries-list.html @@ -1,5 +1,5 @@
          - +
          From b0b4d5e26a5b88933e8d76ad63507d37d1899d8c Mon Sep 17 00:00:00 2001 From: Levko Kravets Date: Thu, 24 Jan 2019 16:24:58 +0200 Subject: [PATCH 018/177] Convert Angular services to CommonJS-style and use them in React components instead of injecting (#3331) * Refine Auth service: remove dead code and fix race condition * Export services in CommonJS style * Refine Users, Events and OfflineListener services * Refactor Notifications service - rewrite to CommonJS * Replace Angular service injection with imports in React components * Fix Footer tests * Events service -> recordEvent function * CR1 --- client/app/components/AutocompleteToggle.jsx | 2 +- client/app/components/DateInput.jsx | 5 +- client/app/components/DateRangeInput.jsx | 5 +- client/app/components/DateTimeInput.jsx | 5 +- client/app/components/DateTimeRangeInput.jsx | 5 +- client/app/components/Footer.jsx | 17 +-- client/app/components/Footer.test.js | 15 +-- client/app/components/ParameterValueInput.jsx | 55 ++-------- .../components/QueryBasedParameterInput.jsx | 4 +- client/app/components/QueryEditor.jsx | 20 ++-- .../dashboards/AddTextboxDialog.jsx | 6 +- .../components/dashboards/AddWidgetDialog.jsx | 17 +-- .../EditParameterMappingsDialog.jsx | 8 +- .../components/dynamic-form/DynamicForm.jsx | 2 +- .../tags-control/DashboardTagsControl.jsx | 2 +- .../tags-control/QueryTagsControl.jsx | 2 +- .../components/tags-control/TagsControl.jsx | 3 +- client/app/index.js | 3 +- client/app/pages/queries/view.js | 2 +- client/app/services/alert-dialog.js | 15 +-- client/app/services/alert-subscription.js | 14 ++- client/app/services/alert.js | 15 +-- client/app/services/auth.js | 98 ++++++++--------- client/app/services/dashboard.js | 11 +- client/app/services/data-source.js | 11 +- client/app/services/destination.js | 16 +-- client/app/services/events.js | 6 +- client/app/services/getTags.js | 2 +- client/app/services/group.js | 14 ++- client/app/services/http.js | 11 -- client/app/services/keyboard-shortcuts.js | 10 +- client/app/services/ng.js | 16 +++ client/app/services/notifications.js | 102 ++++++++---------- client/app/services/offline-listener.js | 36 +++---- client/app/services/organization-status.js | 10 +- client/app/services/query-snippet.js | 10 +- client/app/services/query.js | 39 ++++--- client/app/{lib => services}/recordEvent.js | 6 +- client/app/services/toastr.js | 10 -- client/app/services/user.js | 24 +++-- client/app/services/widget.js | 20 ++-- 41 files changed, 319 insertions(+), 355 deletions(-) delete mode 100644 client/app/services/http.js create mode 100644 client/app/services/ng.js rename client/app/{lib => services}/recordEvent.js (79%) delete mode 100644 client/app/services/toastr.js diff --git a/client/app/components/AutocompleteToggle.jsx b/client/app/components/AutocompleteToggle.jsx index e74e0a9047..5a4c9bff20 100644 --- a/client/app/components/AutocompleteToggle.jsx +++ b/client/app/components/AutocompleteToggle.jsx @@ -2,7 +2,7 @@ import React from 'react'; import Tooltip from 'antd/lib/tooltip'; import PropTypes from 'prop-types'; import '@/redash-font/style.less'; -import recordEvent from '@/lib/recordEvent'; +import recordEvent from '@/services/recordEvent'; export default function AutocompleteToggle({ state, disabled, onToggle }) { let tooltipMessage = 'Live Autocomplete Enabled'; diff --git a/client/app/components/DateInput.jsx b/client/app/components/DateInput.jsx index 695a2a1997..ff7812ed8e 100644 --- a/client/app/components/DateInput.jsx +++ b/client/app/components/DateInput.jsx @@ -3,12 +3,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; import DatePicker from 'antd/lib/date-picker'; +import { clientConfig } from '@/services/auth'; export function DateInput({ value, onSelect, - // eslint-disable-next-line react/prop-types - clientConfig, className, }) { const format = clientConfig.dateFormat || 'YYYY-MM-DD'; @@ -46,7 +45,7 @@ DateInput.defaultProps = { }; export default function init(ngModule) { - ngModule.component('dateInput', react2angular(DateInput, null, ['clientConfig'])); + ngModule.component('dateInput', react2angular(DateInput)); } init.init = true; diff --git a/client/app/components/DateRangeInput.jsx b/client/app/components/DateRangeInput.jsx index 1b1454bbb5..d8ccef48f7 100644 --- a/client/app/components/DateRangeInput.jsx +++ b/client/app/components/DateRangeInput.jsx @@ -4,14 +4,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; import DatePicker from 'antd/lib/date-picker'; +import { clientConfig } from '@/services/auth'; const { RangePicker } = DatePicker; export function DateRangeInput({ value, onSelect, - // eslint-disable-next-line react/prop-types - clientConfig, className, }) { const format = clientConfig.dateFormat || 'YYYY-MM-DD'; @@ -53,7 +52,7 @@ DateRangeInput.defaultProps = { }; export default function init(ngModule) { - ngModule.component('dateRangeInput', react2angular(DateRangeInput, null, ['clientConfig'])); + ngModule.component('dateRangeInput', react2angular(DateRangeInput)); } init.init = true; diff --git a/client/app/components/DateTimeInput.jsx b/client/app/components/DateTimeInput.jsx index d95fd60884..92fb81f104 100644 --- a/client/app/components/DateTimeInput.jsx +++ b/client/app/components/DateTimeInput.jsx @@ -3,13 +3,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; import DatePicker from 'antd/lib/date-picker'; +import { clientConfig } from '@/services/auth'; export function DateTimeInput({ value, withSeconds, onSelect, - // eslint-disable-next-line react/prop-types - clientConfig, className, }) { const format = (clientConfig.dateFormat || 'YYYY-MM-DD') + @@ -51,7 +50,7 @@ DateTimeInput.defaultProps = { }; export default function init(ngModule) { - ngModule.component('dateTimeInput', react2angular(DateTimeInput, null, ['clientConfig'])); + ngModule.component('dateTimeInput', react2angular(DateTimeInput)); } init.init = true; diff --git a/client/app/components/DateTimeRangeInput.jsx b/client/app/components/DateTimeRangeInput.jsx index d0fa5ecb39..51f6c62ddc 100644 --- a/client/app/components/DateTimeRangeInput.jsx +++ b/client/app/components/DateTimeRangeInput.jsx @@ -4,6 +4,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; import DatePicker from 'antd/lib/date-picker'; +import { clientConfig } from '@/services/auth'; const { RangePicker } = DatePicker; @@ -11,8 +12,6 @@ export function DateTimeRangeInput({ value, withSeconds, onSelect, - // eslint-disable-next-line react/prop-types - clientConfig, className, }) { const format = (clientConfig.dateFormat || 'YYYY-MM-DD') + @@ -58,7 +57,7 @@ DateTimeRangeInput.defaultProps = { }; export default function init(ngModule) { - ngModule.component('dateTimeRangeInput', react2angular(DateTimeRangeInput, null, ['clientConfig'])); + ngModule.component('dateTimeRangeInput', react2angular(DateTimeRangeInput)); } init.init = true; diff --git a/client/app/components/Footer.jsx b/client/app/components/Footer.jsx index bd1f06efda..7e45562fe1 100644 --- a/client/app/components/Footer.jsx +++ b/client/app/components/Footer.jsx @@ -1,11 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; - import { react2angular } from 'react2angular'; +import { clientConfig, currentUser } from '@/services/auth'; import frontendVersion from '../version.json'; -export function Footer({ clientConfig, currentUser }) { +export function Footer() { const backendVersion = clientConfig.version; const newVersionAvailable = clientConfig.newVersionAvailable && currentUser.isAdmin; const separator = ' \u2022 '; @@ -31,18 +30,8 @@ export function Footer({ clientConfig, currentUser }) { ); } -Footer.propTypes = { - clientConfig: PropTypes.shape({ - version: PropTypes.string, - newVersionAvailable: PropTypes.bool, - }).isRequired, - currentUser: PropTypes.shape({ - isAdmin: PropTypes.bool, - }).isRequired, -}; - export default function init(ngModule) { - ngModule.component('footer', react2angular(Footer, [], ['clientConfig', 'currentUser'])); + ngModule.component('footer', react2angular(Footer)); } init.init = true; diff --git a/client/app/components/Footer.test.js b/client/app/components/Footer.test.js index 81a157ebfc..519a9e5732 100644 --- a/client/app/components/Footer.test.js +++ b/client/app/components/Footer.test.js @@ -1,16 +1,19 @@ +import { extend } from 'lodash'; import React from 'react'; import renderer from 'react-test-renderer'; +import { clientConfig, currentUser } from '../services/auth'; import { Footer } from './Footer'; test('Footer renders', () => { - const clientConfig = { + // TODO: Properly mock this + extend(clientConfig, { version: '5.0.1', newVersionAvailable: true, - }; - const currentUser = { - isAdmin: true, - }; - const component = renderer.create(
          ); + }); + extend(currentUser, { + permissions: ['admin'], + }); + const component = renderer.create(
          ); const tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/client/app/components/ParameterValueInput.jsx b/client/app/components/ParameterValueInput.jsx index c16edcc7af..301e83fc91 100644 --- a/client/app/components/ParameterValueInput.jsx +++ b/client/app/components/ParameterValueInput.jsx @@ -31,99 +31,69 @@ export class ParameterValueInput extends React.Component { }; renderDateTimeWithSecondsInput() { - const { - value, - onSelect, - clientConfig, // eslint-disable-line react/prop-types - } = this.props; + const { value, onSelect } = this.props; return ( ); } renderDateTimeInput() { - const { - value, - onSelect, - clientConfig, // eslint-disable-line react/prop-types - } = this.props; + const { value, onSelect } = this.props; return ( ); } renderDateInput() { - const { - value, - onSelect, - clientConfig, // eslint-disable-line react/prop-types - } = this.props; + const { value, onSelect } = this.props; return ( ); } renderDateTimeRangeWithSecondsInput() { - const { - value, - onSelect, - clientConfig, // eslint-disable-line react/prop-types - } = this.props; + const { value, onSelect } = this.props; return ( ); } renderDateTimeRangeInput() { - const { - value, - onSelect, - clientConfig, // eslint-disable-line react/prop-types - } = this.props; + const { value, onSelect } = this.props; return ( ); } renderDateRangeInput() { - const { - value, - onSelect, - clientConfig, // eslint-disable-line react/prop-types - } = this.props; + const { value, onSelect } = this.props; return ( ); } @@ -146,19 +116,13 @@ export class ParameterValueInput extends React.Component { } renderQueryBasedInput() { - const { - value, - onSelect, - queryId, - Query, // eslint-disable-line react/prop-types - } = this.props; + const { value, onSelect, queryId } = this.props; return ( ); } @@ -212,10 +176,7 @@ export default function init(ngModule) { }; }, }); - ngModule.component( - 'parameterValueInputImpl', - react2angular(ParameterValueInput, null, ['clientConfig', 'Query']), - ); + ngModule.component('parameterValueInputImpl', react2angular(ParameterValueInput)); } init.init = true; diff --git a/client/app/components/QueryBasedParameterInput.jsx b/client/app/components/QueryBasedParameterInput.jsx index f9ef4ebace..8c82bda54e 100644 --- a/client/app/components/QueryBasedParameterInput.jsx +++ b/client/app/components/QueryBasedParameterInput.jsx @@ -3,6 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; import Select from 'antd/lib/select'; +import { Query } from '@/services/query'; const { Option } = Select; @@ -79,7 +80,6 @@ export class QueryBasedParameterInput extends React.Component { _loadOptions(queryId) { if (queryId && (queryId !== this.state.queryId)) { - const Query = this.props.Query; // eslint-disable-line react/prop-types this.setState({ loading: true }); Query.resultById({ id: queryId }, (result) => { if (this.props.queryId === queryId) { @@ -117,7 +117,7 @@ export class QueryBasedParameterInput extends React.Component { } export default function init(ngModule) { - ngModule.component('queryBasedParameterInput', react2angular(QueryBasedParameterInput, null, ['Query'])); + ngModule.component('queryBasedParameterInput', react2angular(QueryBasedParameterInput)); } init.init = true; diff --git a/client/app/components/QueryEditor.jsx b/client/app/components/QueryEditor.jsx index f9e2499f8f..c86b704a1c 100644 --- a/client/app/components/QueryEditor.jsx +++ b/client/app/components/QueryEditor.jsx @@ -14,6 +14,10 @@ import 'brace/mode/sql'; import 'brace/theme/textmate'; import 'brace/ext/searchbox'; +import { Query } from '@/services/query'; +import { QuerySnippet } from '@/services/query-snippet'; +import { KeyboardShortcuts } from '@/services/keyboard-shortcuts'; + import localOptions from '@/lib/localOptions'; import AutocompleteToggle from '@/components/AutocompleteToggle'; import keywordBuilder from './keywordBuilder'; @@ -152,8 +156,7 @@ class QueryEditor extends React.Component { } }); - // eslint-disable-next-line react/prop-types - this.props.QuerySnippet.query((snippets) => { + QuerySnippet.query((snippets) => { const snippetManager = snippetsModule.snippetManager; const m = { snippetText: '', @@ -194,7 +197,7 @@ class QueryEditor extends React.Component { const selectedQueryText = (rawSelectedQueryText.length > 1) ? rawSelectedQueryText : null; this.setState({ selectedQueryText }); this.props.updateSelectedQuery(selectedQueryText); - } + }; updateQuery = (queryText) => { this.props.updateQuery(queryText); @@ -202,9 +205,7 @@ class QueryEditor extends React.Component { }; formatQuery = () => { - // eslint-disable-next-line react/prop-types - const format = this.props.Query.format; - format(this.props.dataSource.syntax || 'sql', this.props.queryText) + Query.format(this.props.dataSource.syntax || 'sql', this.props.queryText) .then(this.updateQuery) .catch(error => toastr.error(error)); }; @@ -212,11 +213,10 @@ class QueryEditor extends React.Component { toggleAutocomplete = (state) => { this.setState({ autocompleteQuery: state }); localOptions.set('liveAutocomplete', state); - } + }; render() { - // eslint-disable-next-line react/prop-types - const modKey = this.props.KeyboardShortcuts.modKey; + const modKey = KeyboardShortcuts.modKey; const isExecuteDisabled = this.props.queryExecuting || !this.props.canExecuteQuery(); @@ -320,7 +320,7 @@ class QueryEditor extends React.Component { } export default function init(ngModule) { - ngModule.component('queryEditor', react2angular(QueryEditor, null, ['QuerySnippet', 'Query', 'KeyboardShortcuts'])); + ngModule.component('queryEditor', react2angular(QueryEditor)); } init.init = true; diff --git a/client/app/components/dashboards/AddTextboxDialog.jsx b/client/app/components/dashboards/AddTextboxDialog.jsx index 97fead0cc8..6d9510ae51 100644 --- a/client/app/components/dashboards/AddTextboxDialog.jsx +++ b/client/app/components/dashboards/AddTextboxDialog.jsx @@ -3,6 +3,8 @@ import { debounce } from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; import { react2angular } from 'react2angular'; +import { toastr } from '@/services/ng'; +import { Widget } from '@/services/widget'; class AddTextboxDialog extends React.Component { static propTypes = { @@ -38,8 +40,6 @@ class AddTextboxDialog extends React.Component { } saveWidget() { - const Widget = this.props.Widget; // eslint-disable-line react/prop-types - const toastr = this.props.toastr; // eslint-disable-line react/prop-types const dashboard = this.props.dashboard; this.setState({ saveInProgress: true }); @@ -148,7 +148,7 @@ export default function init(ngModule) { dismiss: '&', }, }); - ngModule.component('addTextboxDialogImpl', react2angular(AddTextboxDialog, null, ['toastr', 'Widget'])); + ngModule.component('addTextboxDialogImpl', react2angular(AddTextboxDialog)); } init.init = true; diff --git a/client/app/components/dashboards/AddWidgetDialog.jsx b/client/app/components/dashboards/AddWidgetDialog.jsx index 6d51a35e90..ea351575f4 100644 --- a/client/app/components/dashboards/AddWidgetDialog.jsx +++ b/client/app/components/dashboards/AddWidgetDialog.jsx @@ -10,6 +10,10 @@ import { editableMappingsToParameterMappings, } from '@/components/ParameterMappingInput'; +import { toastr } from '@/services/ng'; +import { Widget } from '@/services/widget'; +import { Query } from '@/services/query'; + const { Option, OptGroup } = Select; class AddWidgetDialog extends React.Component { @@ -38,7 +42,6 @@ class AddWidgetDialog extends React.Component { }; // Don't show draft (unpublished) queries - const Query = this.props.Query; // eslint-disable-line react/prop-types Query.recent().$promise.then((items) => { this.setState({ recentQueries: items.filter(item => !item.is_draft), @@ -62,7 +65,6 @@ class AddWidgetDialog extends React.Component { }); if (queryId) { - const Query = this.props.Query; // eslint-disable-line react/prop-types Query.get({ id: queryId }, (query) => { if (query) { const existingParamNames = map( @@ -95,7 +97,6 @@ class AddWidgetDialog extends React.Component { return; } - const Query = this.props.Query; // eslint-disable-line react/prop-types Query.query({ q: term }, (results) => { // If user will type too quick - it's possible that there will be // several requests running simultaneously. So we need to check @@ -117,8 +118,6 @@ class AddWidgetDialog extends React.Component { } saveWidget() { - const Widget = this.props.Widget; // eslint-disable-line react/prop-types - const toastr = this.props.toastr; // eslint-disable-line react/prop-types const dashboard = this.props.dashboard; this.setState({ saveInProgress: true }); @@ -278,9 +277,6 @@ class AddWidgetDialog extends React.Component { } render() { - const clientConfig = this.props.clientConfig; // eslint-disable-line react/prop-types - const Query = this.props.Query; // eslint-disable-line react/prop-types - const existingParams = map( this.props.dashboard.getParametersDefs(), ({ name, type }) => ({ name, type }), @@ -313,8 +309,6 @@ class AddWidgetDialog extends React.Component { mappings={this.state.parameterMappings} existingParams={existingParams} onChange={mappings => this.updateParamMappings(mappings)} - clientConfig={clientConfig} - Query={Query} />, ] } @@ -358,8 +352,7 @@ export default function init(ngModule) { dismiss: '&', }, }); - ngModule.component('addWidgetDialogImpl', react2angular(AddWidgetDialog, null, [ - 'toastr', 'Widget', 'Query', 'clientConfig'])); + ngModule.component('addWidgetDialogImpl', react2angular(AddWidgetDialog)); } init.init = true; diff --git a/client/app/components/dashboards/EditParameterMappingsDialog.jsx b/client/app/components/dashboards/EditParameterMappingsDialog.jsx index 2230224933..776be76f6c 100644 --- a/client/app/components/dashboards/EditParameterMappingsDialog.jsx +++ b/client/app/components/dashboards/EditParameterMappingsDialog.jsx @@ -60,9 +60,6 @@ class EditParameterMappingsDialog extends React.Component { } render() { - const clientConfig = this.props.clientConfig; // eslint-disable-line react/prop-types - const Query = this.props.Query; // eslint-disable-line react/prop-types - const existingParams = map( this.props.dashboard.getParametersDefs(), ({ name, type }) => ({ name, type }), @@ -89,8 +86,6 @@ class EditParameterMappingsDialog extends React.Component { mappings={this.state.parameterMappings} existingParams={existingParams} onChange={mappings => this.updateParamMappings(mappings)} - clientConfig={clientConfig} - Query={Query} /> }
          @@ -134,8 +129,7 @@ export default function init(ngModule) { dismiss: '&', }, }); - ngModule.component('editParameterMappingsDialogImpl', react2angular(EditParameterMappingsDialog, null, [ - 'Query', 'clientConfig'])); + ngModule.component('editParameterMappingsDialogImpl', react2angular(EditParameterMappingsDialog)); } init.init = true; diff --git a/client/app/components/dynamic-form/DynamicForm.jsx b/client/app/components/dynamic-form/DynamicForm.jsx index a35ede9107..329e2493a7 100644 --- a/client/app/components/dynamic-form/DynamicForm.jsx +++ b/client/app/components/dynamic-form/DynamicForm.jsx @@ -8,7 +8,7 @@ import Button from 'antd/lib/button'; import Upload from 'antd/lib/upload'; import Icon from 'antd/lib/icon'; import { react2angular } from 'react2angular'; -import { toastr } from '@/services/toastr'; +import { toastr } from '@/services/ng'; import { Field, Action, AntdForm } from '../proptypes'; import helper from './dynamicFormHelper'; diff --git a/client/app/components/tags-control/DashboardTagsControl.jsx b/client/app/components/tags-control/DashboardTagsControl.jsx index 1a535d4994..7337964308 100644 --- a/client/app/components/tags-control/DashboardTagsControl.jsx +++ b/client/app/components/tags-control/DashboardTagsControl.jsx @@ -6,7 +6,7 @@ export class DashboardTagsControl extends ModelTagsControl { } export default function init(ngModule) { - ngModule.component('dashboardTagsControl', react2angular(DashboardTagsControl, null, ['$uibModal'])); + ngModule.component('dashboardTagsControl', react2angular(DashboardTagsControl)); } init.init = true; diff --git a/client/app/components/tags-control/QueryTagsControl.jsx b/client/app/components/tags-control/QueryTagsControl.jsx index cd071e2308..9b56c26700 100644 --- a/client/app/components/tags-control/QueryTagsControl.jsx +++ b/client/app/components/tags-control/QueryTagsControl.jsx @@ -6,7 +6,7 @@ export class QueryTagsControl extends ModelTagsControl { } export default function init(ngModule) { - ngModule.component('queryTagsControl', react2angular(QueryTagsControl, null, ['$uibModal'])); + ngModule.component('queryTagsControl', react2angular(QueryTagsControl)); } init.init = true; diff --git a/client/app/components/tags-control/TagsControl.jsx b/client/app/components/tags-control/TagsControl.jsx index c486ac78af..0a28c0f98d 100644 --- a/client/app/components/tags-control/TagsControl.jsx +++ b/client/app/components/tags-control/TagsControl.jsx @@ -1,6 +1,7 @@ import { map, trim } from 'lodash'; import React from 'react'; import PropTypes from 'prop-types'; +import { $uibModal } from '@/services/ng'; export default class TagsControl extends React.Component { static propTypes = { @@ -20,7 +21,7 @@ export default class TagsControl extends React.Component { }; editTags() { - const { getAvailableTags, onEdit, $uibModal } = this.props; // eslint-disable-line react/prop-types + const { getAvailableTags, onEdit } = this.props; // eslint-disable-line react/prop-types const tags = map(this.props.tags, trim); getAvailableTags().then((availableTags) => { diff --git a/client/app/index.js b/client/app/index.js index 11699ee39c..5b24c90750 100644 --- a/client/app/index.js +++ b/client/app/index.js @@ -13,8 +13,7 @@ ngModule.config(($locationProvider, $compileProvider, uiSelectConfig, toastrConf }); // Update ui-select's template to use Font-Awesome instead of glyphicon. -// eslint-disable-next-line no-unused-vars -ngModule.run(($templateCache, OfflineListener) => { +ngModule.run(($templateCache) => { const templateName = 'bootstrap/match.tpl.html'; let template = $templateCache.get(templateName); template = template.replace('glyphicon glyphicon-remove', 'fa fa-remove'); diff --git a/client/app/pages/queries/view.js b/client/app/pages/queries/view.js index 76fc36b991..d8c4f1aacb 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 Notifications from '@/services/notifications'; import template from './query.html'; const DEFAULT_TAB = 'table'; @@ -16,7 +17,6 @@ function QueryViewCtrl( KeyboardShortcuts, Title, AlertDialog, - Notifications, clientConfig, toastr, $uibModal, diff --git a/client/app/services/alert-dialog.js b/client/app/services/alert-dialog.js index c64b48c30c..84b1c410a2 100644 --- a/client/app/services/alert-dialog.js +++ b/client/app/services/alert-dialog.js @@ -1,3 +1,5 @@ +export let AlertDialog = null; // eslint-disable-line import/no-mutable-exports + const AlertDialogComponent = { template: `