Skip to content

Commit 45efcb3

Browse files
authored
Added time filter to query search page (#1329)
* Added time filter to query search page * Added start date * Updated python endpoint test * changed spec * Added specs and tests * Modified python/js tests and some function/file names based on code review comments * Resolved conflicts in DashboardSelect_spec and QuerySearch_spec * Break python tests for separate functions, Move sql queries to setUp() * Get around eslint error for spec * Small changes based on comments
1 parent 07a7736 commit 45efcb3

File tree

11 files changed

+300
-55
lines changed

11 files changed

+300
-55
lines changed

caravel/assets/javascripts/SqlLab/common.js

-10
This file was deleted.

caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx

+68-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
const $ = window.$ = require('jquery');
22
import React from 'react';
3-
43
import { Button } from 'react-bootstrap';
54
import Select from 'react-select';
65
import QueryTable from './QueryTable';
76
import DatabaseSelect from './DatabaseSelect';
8-
import { STATUS_OPTIONS } from '../common';
7+
import { now, epochTimeXHoursAgo,
8+
epochTimeXDaysAgo, epochTimeXYearsAgo } from '../../modules/dates';
9+
import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
910

1011
const propTypes = {
1112
actions: React.PropTypes.object.isRequired,
@@ -20,6 +21,8 @@ class QuerySearch extends React.PureComponent {
2021
databaseId: null,
2122
userId: null,
2223
searchText: null,
24+
from: null,
25+
to: null,
2326
status: 'success',
2427
queriesArray: [],
2528
};
@@ -38,13 +41,44 @@ class QuerySearch extends React.PureComponent {
3841
const val = (db) ? db.value : null;
3942
this.setState({ databaseId: val });
4043
}
41-
insertParams(baseUrl, params) {
42-
return baseUrl + '?' + params.join('&');
44+
getTimeFromSelection(selection) {
45+
switch (selection) {
46+
case 'now':
47+
return now();
48+
case '1 hour ago':
49+
return epochTimeXHoursAgo(1);
50+
case '1 day ago':
51+
return epochTimeXDaysAgo(1);
52+
case '7 days ago':
53+
return epochTimeXDaysAgo(7);
54+
case '28 days ago':
55+
return epochTimeXDaysAgo(28);
56+
case '90 days ago':
57+
return epochTimeXDaysAgo(90);
58+
case '1 year ago':
59+
return epochTimeXYearsAgo(1);
60+
default:
61+
return null;
62+
}
63+
}
64+
changeFrom(user) {
65+
const val = (user) ? user.value : null;
66+
this.setState({ from: val });
67+
}
68+
changeTo(status) {
69+
const val = (status) ? status.value : null;
70+
this.setState({ to: val });
4371
}
4472
changeUser(user) {
4573
const val = (user) ? user.value : null;
4674
this.setState({ userId: val });
4775
}
76+
insertParams(baseUrl, params) {
77+
const validParams = params.filter(
78+
function (p) { return p !== ''; }
79+
);
80+
return baseUrl + '?' + validParams.join('&');
81+
}
4882
changeStatus(status) {
4983
const val = (status) ? status.value : null;
5084
this.setState({ status: val });
@@ -67,10 +101,12 @@ class QuerySearch extends React.PureComponent {
67101
}
68102
refreshQueries() {
69103
const params = [
70-
`userId=${this.state.userId}`,
71-
`databaseId=${this.state.databaseId}`,
72-
`searchText=${this.state.searchText}`,
73-
`status=${this.state.status}`,
104+
this.state.userId ? `user_id=${this.state.userId}` : '',
105+
this.state.databaseId ? `database_id=${this.state.databaseId}` : '',
106+
this.state.searchText ? `search_text=${this.state.searchText}` : '',
107+
this.state.status ? `status=${this.state.status}` : '',
108+
this.state.from ? `from=${this.getTimeFromSelection(this.state.from)}` : '',
109+
this.state.to ? `to=${this.getTimeFromSelection(this.state.to)}` : '',
74110
];
75111

76112
const url = this.insertParams('/caravel/search_queries', params);
@@ -113,9 +149,30 @@ class QuerySearch extends React.PureComponent {
113149
placeholder="Search Results"
114150
/>
115151
</div>
116-
<div className="col-sm-2">
152+
<div className="col-sm-1">
153+
<Select
154+
name="select-from"
155+
placeholder="[From]-"
156+
options={TIME_OPTIONS.
157+
slice(1, TIME_OPTIONS.length).map((t) => ({ value: t, label: t }))}
158+
value={this.state.from}
159+
autosize={false}
160+
onChange={this.changeFrom.bind(this)}
161+
/>
162+
</div>
163+
<div className="col-sm-1">
164+
<Select
165+
name="select-to"
166+
placeholder="[To]-"
167+
options={TIME_OPTIONS.map((t) => ({ value: t, label: t }))}
168+
value={this.state.to}
169+
autosize={false}
170+
onChange={this.changeTo.bind(this)}
171+
/>
172+
</div>
173+
<div className="col-sm-1">
117174
<Select
118-
name="select-state"
175+
name="select-status"
119176
placeholder="[Query Status]"
120177
options={STATUS_OPTIONS.map((s) => ({ value: s, label: s }))}
121178
value={this.state.status}
@@ -130,7 +187,7 @@ class QuerySearch extends React.PureComponent {
130187
</div>
131188
<QueryTable
132189
columns={[
133-
'state', 'db', 'user',
190+
'state', 'db', 'user', 'date',
134191
'progress', 'rows', 'sql', 'querylink',
135192
]}
136193
onUserClicked={this.onUserClicked.bind(this)}

caravel/assets/javascripts/SqlLab/components/QueryTable.jsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import VisualizeModal from './VisualizeModal';
88
import ResultSet from './ResultSet';
99
import ModalTrigger from '../../components/ModalTrigger';
1010
import HighlightedSql from './HighlightedSql';
11-
import { STATE_BSSTYLE_MAP } from '../common';
11+
import { STATE_BSSTYLE_MAP } from '../constants';
1212
import { fDuration } from '../../modules/dates';
1313
import { getLink } from '../../../utils/common';
1414

@@ -73,6 +73,7 @@ class QueryTable extends React.PureComponent {
7373
if (q.endDttm) {
7474
q.duration = fDuration(q.startDttm, q.endDttm);
7575
}
76+
q.date = moment(q.startDttm).format('MMM Do YYYY');
7677
q.user = (
7778
<button
7879
className="btn btn-link btn-xs"

caravel/assets/javascripts/SqlLab/components/Timer.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { now, fDuration } from '../../modules/dates';
33

4-
import { STATE_BSSTYLE_MAP } from '../common.js';
4+
import { STATE_BSSTYLE_MAP } from '../constants.js';
55

66
class Timer extends React.PureComponent {
77
constructor(props) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export const STATE_BSSTYLE_MAP = {
2+
failed: 'danger',
3+
pending: 'info',
4+
fetching: 'info',
5+
running: 'warning',
6+
stopped: 'danger',
7+
success: 'success',
8+
};
9+
10+
export const STATUS_OPTIONS = [
11+
'success',
12+
'failed',
13+
'running',
14+
];
15+
16+
export const TIME_OPTIONS = [
17+
'now',
18+
'1 hour ago',
19+
'1 day ago',
20+
'7 days ago',
21+
'28 days ago',
22+
'90 days ago',
23+
'1 year ago',
24+
];

caravel/assets/javascripts/modules/dates.js

+21
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,24 @@ export const now = function () {
8585
return moment().utc().valueOf();
8686
};
8787

88+
export const epochTimeXHoursAgo = function (h) {
89+
return moment()
90+
.subtract(h, 'hours')
91+
.utc()
92+
.valueOf();
93+
};
94+
95+
export const epochTimeXDaysAgo = function (d) {
96+
return moment()
97+
.subtract(d, 'days')
98+
.utc()
99+
.valueOf();
100+
};
101+
102+
export const epochTimeXYearsAgo = function (y) {
103+
return moment()
104+
.subtract(y, 'years')
105+
.utc()
106+
.valueOf();
107+
};
108+

caravel/assets/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"less-loader": "^2.2.2",
117117
"mocha": "^2.4.5",
118118
"react-addons-test-utils": "^15.3.2",
119+
"sinon": "^1.17.6",
119120
"style-loader": "^0.13.0",
120121
"transform-loader": "^0.2.3",
121122
"url-loader": "^0.5.7",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import Select from 'react-select';
3+
import DatabaseSelect from '../../../javascripts/SqlLab/components/DatabaseSelect';
4+
import { shallow } from 'enzyme';
5+
import { describe, it } from 'mocha';
6+
import { expect } from 'chai';
7+
import sinon from 'sinon';
8+
9+
describe('DatabaseSelect', () => {
10+
const mockedProps = {
11+
actions: {},
12+
};
13+
it('is valid element', () => {
14+
expect(
15+
React.isValidElement(<DatabaseSelect {...mockedProps} />)
16+
).to.equal(true);
17+
});
18+
19+
it('has one select', () => {
20+
const wrapper = shallow(
21+
<DatabaseSelect {...mockedProps} />
22+
);
23+
expect(wrapper.find(Select)).to.have.length(1);
24+
});
25+
26+
it('calls onChange on select change', () => {
27+
const onChange = sinon.spy();
28+
const wrapper = shallow(
29+
<DatabaseSelect onChange={onChange} />
30+
);
31+
wrapper.find(Select).simulate('change', { value: 1 });
32+
expect(onChange).to.have.property('callCount', 1);
33+
});
34+
});

caravel/assets/spec/javascripts/sqllab/QuerySearch_spec.jsx

+40-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import QuerySearch from '../../../javascripts/SqlLab/components/QuerySearch';
55
import { shallow } from 'enzyme';
66
import { describe, it } from 'mocha';
77
import { expect } from 'chai';
8+
import sinon from 'sinon';
89

910
describe('QuerySearch', () => {
1011
const mockedProps = {
@@ -15,19 +16,53 @@ describe('QuerySearch', () => {
1516
React.isValidElement(<QuerySearch {...mockedProps} />)
1617
).to.equal(true);
1718
});
19+
const wrapper = shallow(<QuerySearch {...mockedProps} />);
1820

19-
it('should have two Select', () => {
20-
const wrapper = shallow(<QuerySearch {...mockedProps} />);
21-
expect(wrapper.find(Select)).to.have.length(2);
21+
it('should have four Select', () => {
22+
expect(wrapper.find(Select)).to.have.length(4);
23+
});
24+
25+
it('updates userId on user selects change', () => {
26+
wrapper.find('[name="select-user"]')
27+
.simulate('change', { value: 1 });
28+
expect(wrapper.state().userId).to.equal(1);
29+
});
30+
31+
it('updates fromTime on user selects from time', () => {
32+
wrapper.find('[name="select-from"]')
33+
.simulate('change', { value: 0 });
34+
expect(wrapper.state().from).to.equal(0);
35+
});
36+
37+
it('updates toTime on user selects to time', () => {
38+
wrapper.find('[name="select-to"]')
39+
.simulate('change', { value: 0 });
40+
expect(wrapper.state().to).to.equal(0);
41+
});
42+
43+
it('updates status on user selects status', () => {
44+
wrapper.find('[name="select-status"]')
45+
.simulate('change', { value: 'success' });
46+
expect(wrapper.state().status).to.equal('success');
2247
});
2348

2449
it('should have one input for searchText', () => {
25-
const wrapper = shallow(<QuerySearch {...mockedProps} />);
2650
expect(wrapper.find('input')).to.have.length(1);
2751
});
2852

53+
it('updates search text on user inputs search text', () => {
54+
wrapper.find('input').simulate('change', { target: { value: 'text' } });
55+
expect(wrapper.state().searchText).to.equal('text');
56+
});
57+
2958
it('should have one Button', () => {
30-
const wrapper = shallow(<QuerySearch {...mockedProps} />);
3159
expect(wrapper.find(Button)).to.have.length(1);
3260
});
61+
62+
it('refreshes queries when clicked', () => {
63+
const search = sinon.spy(QuerySearch.prototype, 'refreshQueries');
64+
wrapper.find(Button).simulate('click');
65+
/* eslint-disable no-unused-expressions */
66+
expect(search).to.have.been.called;
67+
});
3368
});

caravel/views.py

+22-11
Original file line numberDiff line numberDiff line change
@@ -2226,30 +2226,41 @@ def queries(self, last_updated_ms):
22262226
def search_queries(self):
22272227
"""Search for queries."""
22282228
query = db.session.query(models.Query)
2229-
userId = request.args.get('userId')
2230-
databaseId = request.args.get('databaseId')
2231-
searchText = request.args.get('searchText')
2229+
search_user_id = request.args.get('user_id')
2230+
database_id = request.args.get('database_id')
2231+
search_text = request.args.get('search_text')
22322232
status = request.args.get('status')
2233+
# From and To time stamp should be Epoch timestamp in seconds
2234+
from_time = request.args.get('from')
2235+
to_time = request.args.get('to')
22332236

2234-
if userId != 'null':
2237+
if search_user_id:
22352238
# Filter on db Id
2236-
query = query.filter(models.Query.user_id == userId)
2239+
query = query.filter(models.Query.user_id == search_user_id)
22372240

2238-
if databaseId != 'null':
2241+
if database_id:
22392242
# Filter on db Id
2240-
query = query.filter(models.Query.database_id == databaseId)
2243+
query = query.filter(models.Query.database_id == database_id)
22412244

2242-
if status != 'null':
2245+
if status:
22432246
# Filter on status
22442247
query = query.filter(models.Query.status == status)
22452248

2246-
if searchText != 'null':
2249+
if search_text:
22472250
# Filter on search text
2248-
query = query.filter(models.Query.sql.like('%{}%'.format(searchText)))
2251+
query = query \
2252+
.filter(models.Query.sql.like('%{}%'.format(search_text)))
22492253

2250-
sql_queries = query.limit(config.get("QUERY_SEARCH_LIMIT")).all()
2254+
if from_time:
2255+
query = query.filter(models.Query.start_time > int(from_time))
22512256

2257+
if to_time:
2258+
query = query.filter(models.Query.start_time < int(to_time))
2259+
2260+
query_limit = config.get('QUERY_SEARCH_LIMIT', 5000)
2261+
sql_queries = query.limit(query_limit).all()
22522262
dict_queries = {q.client_id: q.to_dict() for q in sql_queries}
2263+
22532264
return Response(
22542265
json.dumps(dict_queries, default=utils.json_int_dttm_ser),
22552266
status=200,

0 commit comments

Comments
 (0)