diff --git a/caravel/assets/javascripts/SqlLab/common.js b/caravel/assets/javascripts/SqlLab/common.js
deleted file mode 100644
index ed5726fa0..000000000
--- a/caravel/assets/javascripts/SqlLab/common.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export const STATE_BSSTYLE_MAP = {
- failed: 'danger',
- pending: 'info',
- fetching: 'info',
- running: 'warning',
- stopped: 'danger',
- success: 'success',
-};
-
-export const STATUS_OPTIONS = ['success', 'failed', 'running'];
diff --git a/caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx b/caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx
index 36d834d35..b7b442137 100644
--- a/caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx
@@ -1,11 +1,12 @@
const $ = window.$ = require('jquery');
import React from 'react';
-
import { Button } from 'react-bootstrap';
import Select from 'react-select';
import QueryTable from './QueryTable';
import DatabaseSelect from './DatabaseSelect';
-import { STATUS_OPTIONS } from '../common';
+import { now, epochTimeXHoursAgo,
+ epochTimeXDaysAgo, epochTimeXYearsAgo } from '../../modules/dates';
+import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
const propTypes = {
actions: React.PropTypes.object.isRequired,
@@ -20,6 +21,8 @@ class QuerySearch extends React.PureComponent {
databaseId: null,
userId: null,
searchText: null,
+ from: null,
+ to: null,
status: 'success',
queriesArray: [],
};
@@ -38,13 +41,44 @@ class QuerySearch extends React.PureComponent {
const val = (db) ? db.value : null;
this.setState({ databaseId: val });
}
- insertParams(baseUrl, params) {
- return baseUrl + '?' + params.join('&');
+ getTimeFromSelection(selection) {
+ switch (selection) {
+ case 'now':
+ return now();
+ case '1 hour ago':
+ return epochTimeXHoursAgo(1);
+ case '1 day ago':
+ return epochTimeXDaysAgo(1);
+ case '7 days ago':
+ return epochTimeXDaysAgo(7);
+ case '28 days ago':
+ return epochTimeXDaysAgo(28);
+ case '90 days ago':
+ return epochTimeXDaysAgo(90);
+ case '1 year ago':
+ return epochTimeXYearsAgo(1);
+ default:
+ return null;
+ }
+ }
+ changeFrom(user) {
+ const val = (user) ? user.value : null;
+ this.setState({ from: val });
+ }
+ changeTo(status) {
+ const val = (status) ? status.value : null;
+ this.setState({ to: val });
}
changeUser(user) {
const val = (user) ? user.value : null;
this.setState({ userId: val });
}
+ insertParams(baseUrl, params) {
+ const validParams = params.filter(
+ function (p) { return p !== ''; }
+ );
+ return baseUrl + '?' + validParams.join('&');
+ }
changeStatus(status) {
const val = (status) ? status.value : null;
this.setState({ status: val });
@@ -67,10 +101,12 @@ class QuerySearch extends React.PureComponent {
}
refreshQueries() {
const params = [
- `userId=${this.state.userId}`,
- `databaseId=${this.state.databaseId}`,
- `searchText=${this.state.searchText}`,
- `status=${this.state.status}`,
+ this.state.userId ? `user_id=${this.state.userId}` : '',
+ this.state.databaseId ? `database_id=${this.state.databaseId}` : '',
+ this.state.searchText ? `search_text=${this.state.searchText}` : '',
+ this.state.status ? `status=${this.state.status}` : '',
+ this.state.from ? `from=${this.getTimeFromSelection(this.state.from)}` : '',
+ this.state.to ? `to=${this.getTimeFromSelection(this.state.to)}` : '',
];
const url = this.insertParams('/caravel/search_queries', params);
@@ -113,9 +149,30 @@ class QuerySearch extends React.PureComponent {
placeholder="Search Results"
/>
-
+
+
+
+
+
{
+ const mockedProps = {
+ actions: {},
+ };
+ it('is valid element', () => {
+ expect(
+ React.isValidElement()
+ ).to.equal(true);
+ });
+
+ it('has one select', () => {
+ const wrapper = shallow(
+
+ );
+ expect(wrapper.find(Select)).to.have.length(1);
+ });
+
+ it('calls onChange on select change', () => {
+ const onChange = sinon.spy();
+ const wrapper = shallow(
+
+ );
+ wrapper.find(Select).simulate('change', { value: 1 });
+ expect(onChange).to.have.property('callCount', 1);
+ });
+});
diff --git a/caravel/assets/spec/javascripts/sqllab/QuerySearch_spec.jsx b/caravel/assets/spec/javascripts/sqllab/QuerySearch_spec.jsx
index 07086e474..4027ec4ec 100644
--- a/caravel/assets/spec/javascripts/sqllab/QuerySearch_spec.jsx
+++ b/caravel/assets/spec/javascripts/sqllab/QuerySearch_spec.jsx
@@ -5,6 +5,7 @@ import QuerySearch from '../../../javascripts/SqlLab/components/QuerySearch';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
+import sinon from 'sinon';
describe('QuerySearch', () => {
const mockedProps = {
@@ -15,19 +16,53 @@ describe('QuerySearch', () => {
React.isValidElement()
).to.equal(true);
});
+ const wrapper = shallow();
- it('should have two Select', () => {
- const wrapper = shallow();
- expect(wrapper.find(Select)).to.have.length(2);
+ it('should have four Select', () => {
+ expect(wrapper.find(Select)).to.have.length(4);
+ });
+
+ it('updates userId on user selects change', () => {
+ wrapper.find('[name="select-user"]')
+ .simulate('change', { value: 1 });
+ expect(wrapper.state().userId).to.equal(1);
+ });
+
+ it('updates fromTime on user selects from time', () => {
+ wrapper.find('[name="select-from"]')
+ .simulate('change', { value: 0 });
+ expect(wrapper.state().from).to.equal(0);
+ });
+
+ it('updates toTime on user selects to time', () => {
+ wrapper.find('[name="select-to"]')
+ .simulate('change', { value: 0 });
+ expect(wrapper.state().to).to.equal(0);
+ });
+
+ it('updates status on user selects status', () => {
+ wrapper.find('[name="select-status"]')
+ .simulate('change', { value: 'success' });
+ expect(wrapper.state().status).to.equal('success');
});
it('should have one input for searchText', () => {
- const wrapper = shallow();
expect(wrapper.find('input')).to.have.length(1);
});
+ it('updates search text on user inputs search text', () => {
+ wrapper.find('input').simulate('change', { target: { value: 'text' } });
+ expect(wrapper.state().searchText).to.equal('text');
+ });
+
it('should have one Button', () => {
- const wrapper = shallow();
expect(wrapper.find(Button)).to.have.length(1);
});
+
+ it('refreshes queries when clicked', () => {
+ const search = sinon.spy(QuerySearch.prototype, 'refreshQueries');
+ wrapper.find(Button).simulate('click');
+ /* eslint-disable no-unused-expressions */
+ expect(search).to.have.been.called;
+ });
});
diff --git a/caravel/views.py b/caravel/views.py
index d2cd5e969..5e120c44e 100755
--- a/caravel/views.py
+++ b/caravel/views.py
@@ -2226,30 +2226,41 @@ class Caravel(BaseCaravelView):
def search_queries(self):
"""Search for queries."""
query = db.session.query(models.Query)
- userId = request.args.get('userId')
- databaseId = request.args.get('databaseId')
- searchText = request.args.get('searchText')
+ search_user_id = request.args.get('user_id')
+ database_id = request.args.get('database_id')
+ search_text = request.args.get('search_text')
status = request.args.get('status')
+ # From and To time stamp should be Epoch timestamp in seconds
+ from_time = request.args.get('from')
+ to_time = request.args.get('to')
- if userId != 'null':
+ if search_user_id:
# Filter on db Id
- query = query.filter(models.Query.user_id == userId)
+ query = query.filter(models.Query.user_id == search_user_id)
- if databaseId != 'null':
+ if database_id:
# Filter on db Id
- query = query.filter(models.Query.database_id == databaseId)
+ query = query.filter(models.Query.database_id == database_id)
- if status != 'null':
+ if status:
# Filter on status
query = query.filter(models.Query.status == status)
- if searchText != 'null':
+ if search_text:
# Filter on search text
- query = query.filter(models.Query.sql.like('%{}%'.format(searchText)))
+ query = query \
+ .filter(models.Query.sql.like('%{}%'.format(search_text)))
- sql_queries = query.limit(config.get("QUERY_SEARCH_LIMIT")).all()
+ if from_time:
+ query = query.filter(models.Query.start_time > int(from_time))
+ if to_time:
+ query = query.filter(models.Query.start_time < int(to_time))
+
+ query_limit = config.get('QUERY_SEARCH_LIMIT', 5000)
+ sql_queries = query.limit(query_limit).all()
dict_queries = {q.client_id: q.to_dict() for q in sql_queries}
+
return Response(
json.dumps(dict_queries, default=utils.json_int_dttm_ser),
status=200,
diff --git a/tests/core_tests.py b/tests/core_tests.py
index ccb5e2106..704ada0b9 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -10,7 +10,7 @@ import json
import io
import random
import unittest
-
+from datetime import datetime
from flask import escape
from flask_appbuilder.security.sqla import models as ab_models
@@ -41,6 +41,14 @@ class CoreTests(CaravelTestCase):
def setUp(self):
db.session.query(models.Query).delete()
db.session.query(models.DatasourceAccessRequest).delete()
+ self.login('admin')
+ self.run_sql("SELECT * FROM ab_user", 'admin', client_id='client_id_1')
+ self.run_sql("SELECT * FROM NO_TABLE", 'admin', client_id='client_id_3')
+ self.logout()
+
+ self.login('gamma')
+ self.run_sql("SELECT * FROM ab_permission", 'gamma', client_id='client_id_2')
+ self.logout()
def tearDown(self):
pass
@@ -332,35 +340,98 @@ class CoreTests(CaravelTestCase):
self.login('admin')
data = self.get_json_resp('/caravel/queries/{}'.format(0))
- self.assertEquals(0, len(data))
+ self.assertEquals(2, len(data))
self.logout()
- self.run_sql("SELECT * FROM ab_user", 'admin', client_id='client_id_1')
- self.run_sql("SELECT * FROM ab_user1", 'admin', client_id='client_id_2')
+ self.run_sql("SELECT * FROM ab_user1", 'admin', client_id='client_id_4')
+ self.run_sql("SELECT * FROM ab_user2", 'admin', client_id='client_id_5')
self.login('admin')
data = self.get_json_resp('/caravel/queries/{}'.format(0))
- self.assertEquals(2, len(data))
+ self.assertEquals(4, len(data))
query = db.session.query(models.Query).filter_by(
- sql='SELECT * FROM ab_user').first()
+ sql='SELECT * FROM ab_user1').first()
query.changed_on = utils.EPOCH
db.session.commit()
data = self.get_json_resp('/caravel/queries/{}'.format(123456000))
- self.assertEquals(1, len(data))
+ self.assertEquals(3, len(data))
self.logout()
resp = self.client.get('/caravel/queries/{}'.format(0))
self.assertEquals(403, resp.status_code)
- def test_search_query_endpoint(self):
- userId = 'userId=null'
- databaseId = 'databaseId=null'
- searchText = 'searchText=null'
- status = 'status=success'
- params = [userId, databaseId, searchText, status]
- resp = self.client.get('/caravel/search_queries?'+'&'.join(params))
- self.assertEquals(200, resp.status_code)
+ def test_search_query_on_db_id(self):
+ self.login('admin')
+ # Test search queries on database Id
+ resp = self.get_resp('/caravel/search_queries?database_id=1')
+ data = json.loads(resp)
+ self.assertEquals(3, len(data))
+ db_ids = [data[k]['dbId'] for k in data]
+ self.assertEquals([1, 1, 1], db_ids)
+
+ resp = self.get_resp('/caravel/search_queries?database_id=-1')
+ data = json.loads(resp)
+ self.assertEquals(0, len(data))
+ self.logout()
+
+ def test_search_query_on_user(self):
+ self.login('admin')
+ # Test search queries on user Id
+ user = appbuilder.sm.find_user('admin')
+ resp = self.get_resp('/caravel/search_queries?user_id={}'.format(user.id))
+ data = json.loads(resp)
+ self.assertEquals(2, len(data))
+ user_ids = [data[k]['userId'] for k in data]
+ self.assertEquals([user.id, user.id], user_ids)
+
+ user = appbuilder.sm.find_user('gamma')
+ resp = self.get_resp('/caravel/search_queries?user_id={}'.format(user.id))
+ data = json.loads(resp)
+ self.assertEquals(1, len(data))
+ self.assertEquals(list(data.values())[0]['userId'] , user.id)
+ self.logout()
+
+ def test_search_query_on_status(self):
+ self.login('admin')
+ # Test search queries on status
+ resp = self.get_resp('/caravel/search_queries?status=success')
+ data = json.loads(resp)
+ self.assertEquals(2, len(data))
+ states = [data[k]['state'] for k in data]
+ self.assertEquals(['success', 'success'], states)
+
+ resp = self.get_resp('/caravel/search_queries?status=failed')
+ data = json.loads(resp)
+ self.assertEquals(1, len(data))
+ self.assertEquals(list(data.values())[0]['state'], 'failed')
+ self.logout()
+
+ def test_search_query_on_text(self):
+ self.login('admin')
+ resp = self.get_resp('/caravel/search_queries?search_text=permission')
+ data = json.loads(resp)
+ self.assertEquals(1, len(data))
+ self.assertIn('permission', list(data.values())[0]['sql'])
+ self.logout()
+
+ def test_search_query_on_time(self):
+ self.login('admin')
+ first_query_time = db.session.query(models.Query).filter_by(
+ sql='SELECT * FROM ab_user').first().start_time
+ second_query_time = db.session.query(models.Query).filter_by(
+ sql='SELECT * FROM ab_permission').first().start_time
+ # Test search queries on time filter
+ from_time = 'from={}'.format(int(first_query_time))
+ to_time = 'to={}'.format(int(second_query_time))
+ params = [from_time, to_time]
+ resp = self.get_resp('/caravel/search_queries?'+'&'.join(params))
+ data = json.loads(resp)
+ self.assertEquals(2, len(data))
+ for _, v in data.items():
+ self.assertLess(int(first_query_time), v['startDttm'])
+ self.assertLess(v['startDttm'], int(second_query_time))
+ self.logout()
def test_public_user_dashboard_access(self):
# Try access before adding appropriate permissions.
@@ -448,6 +519,6 @@ class CoreTests(CaravelTestCase):
data = self.run_sql(sql, "admin", "fdaklj3ws")
self.assertEqual(data['data'][0]['test'], "2017-01-01T00:00:00")
-
if __name__ == '__main__':
unittest.main()
+