Skip to content

Commit

Permalink
Added time filter to query search page (#1329)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
vera-liu authored Oct 28, 2016
1 parent 07a7736 commit 45efcb3
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 55 deletions.
10 changes: 0 additions & 10 deletions caravel/assets/javascripts/SqlLab/common.js

This file was deleted.

79 changes: 68 additions & 11 deletions caravel/assets/javascripts/SqlLab/components/QuerySearch.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,6 +21,8 @@ class QuerySearch extends React.PureComponent {
databaseId: null,
userId: null,
searchText: null,
from: null,
to: null,
status: 'success',
queriesArray: [],
};
Expand All @@ -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 });
Expand All @@ -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);
Expand Down Expand Up @@ -113,9 +149,30 @@ class QuerySearch extends React.PureComponent {
placeholder="Search Results"
/>
</div>
<div className="col-sm-2">
<div className="col-sm-1">
<Select
name="select-from"
placeholder="[From]-"
options={TIME_OPTIONS.
slice(1, TIME_OPTIONS.length).map((t) => ({ value: t, label: t }))}
value={this.state.from}
autosize={false}
onChange={this.changeFrom.bind(this)}
/>
</div>
<div className="col-sm-1">
<Select
name="select-to"
placeholder="[To]-"
options={TIME_OPTIONS.map((t) => ({ value: t, label: t }))}
value={this.state.to}
autosize={false}
onChange={this.changeTo.bind(this)}
/>
</div>
<div className="col-sm-1">
<Select
name="select-state"
name="select-status"
placeholder="[Query Status]"
options={STATUS_OPTIONS.map((s) => ({ value: s, label: s }))}
value={this.state.status}
Expand All @@ -130,7 +187,7 @@ class QuerySearch extends React.PureComponent {
</div>
<QueryTable
columns={[
'state', 'db', 'user',
'state', 'db', 'user', 'date',
'progress', 'rows', 'sql', 'querylink',
]}
onUserClicked={this.onUserClicked.bind(this)}
Expand Down
3 changes: 2 additions & 1 deletion caravel/assets/javascripts/SqlLab/components/QueryTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import VisualizeModal from './VisualizeModal';
import ResultSet from './ResultSet';
import ModalTrigger from '../../components/ModalTrigger';
import HighlightedSql from './HighlightedSql';
import { STATE_BSSTYLE_MAP } from '../common';
import { STATE_BSSTYLE_MAP } from '../constants';
import { fDuration } from '../../modules/dates';
import { getLink } from '../../../utils/common';

Expand Down Expand Up @@ -73,6 +73,7 @@ class QueryTable extends React.PureComponent {
if (q.endDttm) {
q.duration = fDuration(q.startDttm, q.endDttm);
}
q.date = moment(q.startDttm).format('MMM Do YYYY');
q.user = (
<button
className="btn btn-link btn-xs"
Expand Down
2 changes: 1 addition & 1 deletion caravel/assets/javascripts/SqlLab/components/Timer.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { now, fDuration } from '../../modules/dates';

import { STATE_BSSTYLE_MAP } from '../common.js';
import { STATE_BSSTYLE_MAP } from '../constants.js';

class Timer extends React.PureComponent {
constructor(props) {
Expand Down
24 changes: 24 additions & 0 deletions caravel/assets/javascripts/SqlLab/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const STATE_BSSTYLE_MAP = {
failed: 'danger',
pending: 'info',
fetching: 'info',
running: 'warning',
stopped: 'danger',
success: 'success',
};

export const STATUS_OPTIONS = [
'success',
'failed',
'running',
];

export const TIME_OPTIONS = [
'now',
'1 hour ago',
'1 day ago',
'7 days ago',
'28 days ago',
'90 days ago',
'1 year ago',
];
21 changes: 21 additions & 0 deletions caravel/assets/javascripts/modules/dates.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,24 @@ export const now = function () {
return moment().utc().valueOf();
};

export const epochTimeXHoursAgo = function (h) {
return moment()
.subtract(h, 'hours')
.utc()
.valueOf();
};

export const epochTimeXDaysAgo = function (d) {
return moment()
.subtract(d, 'days')
.utc()
.valueOf();
};

export const epochTimeXYearsAgo = function (y) {
return moment()
.subtract(y, 'years')
.utc()
.valueOf();
};

1 change: 1 addition & 0 deletions caravel/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"less-loader": "^2.2.2",
"mocha": "^2.4.5",
"react-addons-test-utils": "^15.3.2",
"sinon": "^1.17.6",
"style-loader": "^0.13.0",
"transform-loader": "^0.2.3",
"url-loader": "^0.5.7",
Expand Down
34 changes: 34 additions & 0 deletions caravel/assets/spec/javascripts/sqllab/DatabaseSelect_spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import Select from 'react-select';
import DatabaseSelect from '../../../javascripts/SqlLab/components/DatabaseSelect';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import { expect } from 'chai';
import sinon from 'sinon';

describe('DatabaseSelect', () => {
const mockedProps = {
actions: {},
};
it('is valid element', () => {
expect(
React.isValidElement(<DatabaseSelect {...mockedProps} />)
).to.equal(true);
});

it('has one select', () => {
const wrapper = shallow(
<DatabaseSelect {...mockedProps} />
);
expect(wrapper.find(Select)).to.have.length(1);
});

it('calls onChange on select change', () => {
const onChange = sinon.spy();
const wrapper = shallow(
<DatabaseSelect onChange={onChange} />
);
wrapper.find(Select).simulate('change', { value: 1 });
expect(onChange).to.have.property('callCount', 1);
});
});
45 changes: 40 additions & 5 deletions caravel/assets/spec/javascripts/sqllab/QuerySearch_spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -15,19 +16,53 @@ describe('QuerySearch', () => {
React.isValidElement(<QuerySearch {...mockedProps} />)
).to.equal(true);
});
const wrapper = shallow(<QuerySearch {...mockedProps} />);

it('should have two Select', () => {
const wrapper = shallow(<QuerySearch {...mockedProps} />);
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(<QuerySearch {...mockedProps} />);
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(<QuerySearch {...mockedProps} />);
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;
});
});
33 changes: 22 additions & 11 deletions caravel/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2226,30 +2226,41 @@ def queries(self, last_updated_ms):
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,
Expand Down
Loading

0 comments on commit 45efcb3

Please sign in to comment.