mirror of
https://github.com/apache/superset.git
synced 2024-09-16 02:29:39 -04:00
refactor: Query search into functional component (#13102)
This commit is contained in:
parent
9c9862ff5b
commit
66a7318fa2
@ -17,77 +17,121 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import Button from 'src/components/Button';
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import Select from 'src/components/Select';
|
||||
import QuerySearch from 'src/SqlLab/components/QuerySearch';
|
||||
import { Provider } from 'react-redux';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { fireEvent, render, screen, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore({});
|
||||
|
||||
const SEARCH_ENDPOINT = 'glob:*/superset/search_queries?*';
|
||||
const USER_ENDPOINT = 'glob:*/api/v1/query/related/user';
|
||||
const DATABASE_ENDPOINT = 'glob:*/api/v1/database/?*';
|
||||
|
||||
fetchMock.get(SEARCH_ENDPOINT, []);
|
||||
fetchMock.get(USER_ENDPOINT, []);
|
||||
fetchMock.get(DATABASE_ENDPOINT, []);
|
||||
|
||||
describe('QuerySearch', () => {
|
||||
const search = sinon.spy(QuerySearch.prototype, 'refreshQueries');
|
||||
const mockedProps = {
|
||||
actions: { addDangerToast: jest.fn() },
|
||||
height: 0,
|
||||
displayLimit: 50,
|
||||
};
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(<QuerySearch {...mockedProps} />)).toBe(true);
|
||||
});
|
||||
let wrapper;
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<QuerySearch {...mockedProps} />);
|
||||
expect(
|
||||
React.isValidElement(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<Provider store={store}>
|
||||
<QuerySearch {...mockedProps} />
|
||||
</Provider>
|
||||
</ThemeProvider>,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should have three Select', () => {
|
||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(3);
|
||||
beforeEach(async () => {
|
||||
// You need this await function in order to change state in the app. In fact you need it everytime you re-render.
|
||||
await act(async () => {
|
||||
render(
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<Provider store={store}>
|
||||
<QuerySearch {...mockedProps} />
|
||||
</Provider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have three Selects', () => {
|
||||
expect(screen.getByText(/28 days ago/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/now/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/success/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates fromTime on user selects from time', () => {
|
||||
wrapper.find('[name="select-from"]').simulate('change', { value: 0 });
|
||||
expect(wrapper.state().from).toBe(0);
|
||||
const role = screen.getByText(/28 days ago/i);
|
||||
fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 });
|
||||
userEvent.click(screen.getByText(/1 hour ago/i));
|
||||
expect(screen.getByText(/1 hour ago/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates toTime on user selects to time', () => {
|
||||
wrapper.find('[name="select-to"]').simulate('change', { value: 0 });
|
||||
expect(wrapper.state().to).toBe(0);
|
||||
it('updates toTime on user selects on time', () => {
|
||||
const role = screen.getByText(/now/i);
|
||||
fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 });
|
||||
userEvent.click(screen.getByText(/1 hour ago/i));
|
||||
expect(screen.getByText(/1 hour ago/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates status on user selects status', () => {
|
||||
wrapper
|
||||
.find('[name="select-status"]')
|
||||
.simulate('change', { value: 'success' });
|
||||
expect(wrapper.state().status).toBe('success');
|
||||
const role = screen.getByText(/success/i);
|
||||
fireEvent.keyDown(role, { key: 'ArrowDown', keyCode: 40 });
|
||||
userEvent.click(screen.getByText(/failed/i));
|
||||
expect(screen.getByText(/failed/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have one input for searchText', () => {
|
||||
expect(wrapper.find('input')).toExist();
|
||||
expect(
|
||||
screen.getByPlaceholderText(/Query search string/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates search text on user inputs search text', () => {
|
||||
wrapper.find('input').simulate('change', { target: { value: 'text' } });
|
||||
expect(wrapper.state().searchText).toBe('text');
|
||||
});
|
||||
|
||||
it('refreshes queries when enter (only) is pressed on the input', () => {
|
||||
const { callCount } = search;
|
||||
wrapper.find('input').simulate('keyDown', { keyCode: 'a'.charCodeAt(0) });
|
||||
expect(search.callCount).toBe(callCount);
|
||||
wrapper.find('input').simulate('keyDown', { keyCode: '\r'.charCodeAt(0) });
|
||||
expect(search.callCount).toBe(callCount + 1);
|
||||
const search = screen.getByPlaceholderText(/Query search string/i);
|
||||
userEvent.type(search, 'text');
|
||||
expect(search.value).toBe('text');
|
||||
});
|
||||
|
||||
it('should have one Button', () => {
|
||||
expect(wrapper.find(Button)).toExist();
|
||||
const button = screen.getAllByRole('button');
|
||||
expect(button.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('refreshes queries when clicked', () => {
|
||||
const { callCount } = search;
|
||||
wrapper.find(Button).simulate('click');
|
||||
expect(search.callCount).toBe(callCount + 1);
|
||||
it('should call API when search button is pressed', async () => {
|
||||
fetchMock.resetHistory();
|
||||
const button = screen.getByRole('button');
|
||||
await act(async () => {
|
||||
userEvent.click(button);
|
||||
});
|
||||
expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call API when (only)enter key is pressed', async () => {
|
||||
fetchMock.resetHistory();
|
||||
const search = screen.getByPlaceholderText(/Query search string/i);
|
||||
await act(async () => {
|
||||
userEvent.type(search, 'a');
|
||||
});
|
||||
expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(0);
|
||||
await act(async () => {
|
||||
userEvent.type(search, '{enter}');
|
||||
});
|
||||
expect(fetchMock.calls(SEARCH_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
@ -1,330 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from 'src/components/Button';
|
||||
import Select from 'src/components/Select';
|
||||
import { styled, t, SupersetClient } from '@superset-ui/core';
|
||||
|
||||
import Loading from '../../components/Loading';
|
||||
import QueryTable from './QueryTable';
|
||||
import {
|
||||
now,
|
||||
epochTimeXHoursAgo,
|
||||
epochTimeXDaysAgo,
|
||||
epochTimeXYearsAgo,
|
||||
} from '../../modules/dates';
|
||||
import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
|
||||
import AsyncSelect from '../../components/AsyncSelect';
|
||||
|
||||
const propTypes = {
|
||||
actions: PropTypes.object.isRequired,
|
||||
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
displayLimit: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const TableWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const TableStyles = styled.div`
|
||||
table {
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light4};
|
||||
}
|
||||
|
||||
.table > thead > tr > th {
|
||||
border-bottom: ${({ theme }) => theme.gridUnit / 2}px solid
|
||||
${({ theme }) => theme.colors.grayscale.light2};
|
||||
background: ${({ theme }) => theme.colors.grayscale.light4};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTableStylesContainer = styled.div`
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
class QuerySearch extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
databaseId: null,
|
||||
userId: null,
|
||||
searchText: null,
|
||||
from: '28 days ago',
|
||||
to: 'now',
|
||||
status: 'success',
|
||||
queriesArray: [],
|
||||
queriesLoading: true,
|
||||
};
|
||||
this.userMutator = this.userMutator.bind(this);
|
||||
this.changeUser = this.changeUser.bind(this);
|
||||
this.dbMutator = this.dbMutator.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.changeSearch = this.changeSearch.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.changeFrom = this.changeFrom.bind(this);
|
||||
this.changeTo = this.changeTo.bind(this);
|
||||
this.changeStatus = this.changeStatus.bind(this);
|
||||
this.refreshQueries = this.refreshQueries.bind(this);
|
||||
this.onUserClicked = this.onUserClicked.bind(this);
|
||||
this.onDbClicked = this.onDbClicked.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refreshQueries();
|
||||
}
|
||||
|
||||
onUserClicked(userId) {
|
||||
this.setState({ userId }, () => {
|
||||
this.refreshQueries();
|
||||
});
|
||||
}
|
||||
|
||||
onDbClicked(dbId) {
|
||||
this.setState({ databaseId: dbId }, () => {
|
||||
this.refreshQueries();
|
||||
});
|
||||
}
|
||||
|
||||
onChange(db) {
|
||||
const val = db ? db.value : null;
|
||||
this.setState({ databaseId: val });
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.keyCode === 13) {
|
||||
this.refreshQueries();
|
||||
}
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
changeSearch(event) {
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
|
||||
userLabel(user) {
|
||||
if (user.first_name && user.last_name) {
|
||||
return `${user.first_name} ${user.last_name}`;
|
||||
}
|
||||
return user.username;
|
||||
}
|
||||
|
||||
userMutator(data) {
|
||||
return data.result.map(({ value, text }) => ({
|
||||
label: text,
|
||||
value,
|
||||
}));
|
||||
}
|
||||
|
||||
dbMutator(data) {
|
||||
const options = data.result.map(db => ({
|
||||
value: db.id,
|
||||
label: db.database_name,
|
||||
}));
|
||||
this.props.actions.setDatabases(data.result);
|
||||
if (data.result.length === 0) {
|
||||
this.props.actions.addDangerToast(
|
||||
t("It seems you don't have access to any database"),
|
||||
);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
refreshQueries() {
|
||||
this.setState({ queriesLoading: true });
|
||||
const params = [
|
||||
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)}` : '',
|
||||
];
|
||||
|
||||
SupersetClient.get({
|
||||
endpoint: this.insertParams('/superset/search_queries', params),
|
||||
})
|
||||
.then(({ json }) => {
|
||||
this.setState({ queriesArray: json, queriesLoading: false });
|
||||
})
|
||||
.catch(() => {
|
||||
this.props.actions.addDangerToast(
|
||||
t('An error occurred when refreshing queries'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TableWrapper>
|
||||
<div id="search-header" className="row space-1">
|
||||
<div className="col-sm-2">
|
||||
<AsyncSelect
|
||||
dataEndpoint="api/v1/query/related/user"
|
||||
mutator={this.userMutator}
|
||||
value={this.state.userId}
|
||||
onChange={this.changeUser}
|
||||
placeholder={t('Filter by user')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<AsyncSelect
|
||||
onChange={this.onChange}
|
||||
dataEndpoint="/api/v1/database/?q=(filters:!((col:expose_in_sqllab,opr:eq,value:!t)))"
|
||||
value={this.state.databaseId}
|
||||
mutator={this.dbMutator}
|
||||
placeholder={t('Filter by database')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<input
|
||||
type="text"
|
||||
onChange={this.changeSearch}
|
||||
onKeyDown={this.onKeyDown}
|
||||
className="form-control input-sm"
|
||||
placeholder={t('Query search string')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-4 search-date-filter-container">
|
||||
<Select
|
||||
name="select-from"
|
||||
placeholder={t('[From]-')}
|
||||
options={TIME_OPTIONS.slice(1, TIME_OPTIONS.length).map(xt => ({
|
||||
value: xt,
|
||||
label: xt,
|
||||
}))}
|
||||
value={this.state.from}
|
||||
autosize={false}
|
||||
onChange={this.changeFrom}
|
||||
/>
|
||||
|
||||
<Select
|
||||
name="select-to"
|
||||
placeholder={t('[To]-')}
|
||||
options={TIME_OPTIONS.map(xt => ({ value: xt, label: xt }))}
|
||||
value={this.state.to}
|
||||
autosize={false}
|
||||
onChange={this.changeTo}
|
||||
/>
|
||||
|
||||
<Select
|
||||
name="select-status"
|
||||
placeholder={t('Filter by status')}
|
||||
options={Object.keys(STATUS_OPTIONS).map(s => ({
|
||||
value: s,
|
||||
label: s,
|
||||
}))}
|
||||
value={this.state.status}
|
||||
isLoading={false}
|
||||
autosize={false}
|
||||
onChange={this.changeStatus}
|
||||
/>
|
||||
|
||||
<Button
|
||||
buttonSize="small"
|
||||
buttonStyle="success"
|
||||
onClick={this.refreshQueries}
|
||||
>
|
||||
{t('Search')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<StyledTableStylesContainer>
|
||||
{this.state.queriesLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<TableStyles>
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state',
|
||||
'db',
|
||||
'user',
|
||||
'time',
|
||||
'progress',
|
||||
'rows',
|
||||
'sql',
|
||||
'querylink',
|
||||
]}
|
||||
onUserClicked={this.onUserClicked}
|
||||
onDbClicked={this.onDbClicked}
|
||||
queries={this.state.queriesArray}
|
||||
actions={this.props.actions}
|
||||
displayLimit={this.props.displayLimit}
|
||||
/>
|
||||
</TableStyles>
|
||||
)}
|
||||
</StyledTableStylesContainer>
|
||||
</TableWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
QuerySearch.propTypes = propTypes;
|
||||
export default QuerySearch;
|
288
superset-frontend/src/SqlLab/components/QuerySearch.tsx
Normal file
288
superset-frontend/src/SqlLab/components/QuerySearch.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Button from 'src/components/Button';
|
||||
import Select from 'src/components/Select';
|
||||
import { styled, t, SupersetClient } from '@superset-ui/core';
|
||||
import { debounce } from 'lodash';
|
||||
import Loading from '../../components/Loading';
|
||||
import QueryTable from './QueryTable';
|
||||
import {
|
||||
now,
|
||||
epochTimeXHoursAgo,
|
||||
epochTimeXDaysAgo,
|
||||
epochTimeXYearsAgo,
|
||||
} from '../../modules/dates';
|
||||
import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
|
||||
import AsyncSelect from '../../components/AsyncSelect';
|
||||
import { Query } from '../types';
|
||||
|
||||
interface QuerySearchProps {
|
||||
actions: {
|
||||
addDangerToast: (msg: string) => void;
|
||||
setDatabases: (data: Record<string, any>) => Record<string, any>;
|
||||
};
|
||||
displayLimit: number;
|
||||
}
|
||||
|
||||
interface UserMutatorProps {
|
||||
value: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface DbMutatorProps {
|
||||
id: number;
|
||||
database_name: string;
|
||||
}
|
||||
|
||||
const TableWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const TableStyles = styled.div`
|
||||
table {
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light4};
|
||||
}
|
||||
|
||||
.table > thead > tr > th {
|
||||
border-bottom: ${({ theme }) => theme.gridUnit / 2}px solid
|
||||
${({ theme }) => theme.colors.grayscale.light2};
|
||||
background: ${({ theme }) => theme.colors.grayscale.light4};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTableStylesContainer = styled.div`
|
||||
overflow: auto;
|
||||
`;
|
||||
function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
|
||||
const [databaseId, setDatabaseId] = useState<string>('');
|
||||
const [userId, setUserId] = useState<string>('');
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [from, setFrom] = useState<string>('28 days ago');
|
||||
const [to, setTo] = useState<string>('now');
|
||||
const [status, setStatus] = useState<string>('success');
|
||||
const [queriesArray, setQueriesArray] = useState<Query[]>([]);
|
||||
const [queriesLoading, setQueriesLoading] = useState<boolean>(true);
|
||||
|
||||
const getTimeFromSelection = (selection: string) => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const insertParams = (baseUrl: string, params: string[]) => {
|
||||
const validParams = params.filter(function (p) {
|
||||
return p !== '';
|
||||
});
|
||||
return `${baseUrl}?${validParams.join('&')}`;
|
||||
};
|
||||
|
||||
const refreshQueries = async () => {
|
||||
setQueriesLoading(true);
|
||||
const params = [
|
||||
userId && `user_id=${userId}`,
|
||||
databaseId && `database_id=${databaseId}`,
|
||||
searchText && `search_text=${searchText}`,
|
||||
status && `status=${status}`,
|
||||
from && `from=${getTimeFromSelection(from)}`,
|
||||
to && `to=${getTimeFromSelection(to)}`,
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: insertParams('/superset/search_queries', params),
|
||||
});
|
||||
const queries = Object.values(response.json);
|
||||
setQueriesArray(queries);
|
||||
} catch (err) {
|
||||
actions.addDangerToast(t('An error occurred when refreshing queries'));
|
||||
} finally {
|
||||
setQueriesLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
refreshQueries();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onUserClicked = (userId: string) => {
|
||||
setUserId(userId);
|
||||
refreshQueries();
|
||||
};
|
||||
|
||||
const onDbClicked = (dbId: string) => {
|
||||
setDatabaseId(dbId);
|
||||
refreshQueries();
|
||||
};
|
||||
|
||||
const onKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.keyCode === 13) {
|
||||
refreshQueries();
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (e: React.ChangeEvent) => {
|
||||
e.persist();
|
||||
const handleChange = debounce(e => {
|
||||
setSearchText(e.target.value);
|
||||
}, 200);
|
||||
handleChange(e);
|
||||
};
|
||||
|
||||
const userMutator = ({ result }: { result: UserMutatorProps[] }) =>
|
||||
result.map(({ value, text }: UserMutatorProps) => ({
|
||||
label: text,
|
||||
value,
|
||||
}));
|
||||
|
||||
const dbMutator = ({ result }: { result: DbMutatorProps[] }) => {
|
||||
const options = result.map(({ id, database_name }: DbMutatorProps) => ({
|
||||
value: id,
|
||||
label: database_name,
|
||||
}));
|
||||
actions.setDatabases(result);
|
||||
if (result.length === 0) {
|
||||
actions.addDangerToast(
|
||||
t("It seems you don't have access to any database"),
|
||||
);
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
return (
|
||||
<TableWrapper>
|
||||
<div id="search-header" className="row space-1">
|
||||
<div className="col-sm-2">
|
||||
<AsyncSelect
|
||||
dataEndpoint="api/v1/query/related/user"
|
||||
mutator={userMutator}
|
||||
value={userId}
|
||||
onChange={(selected: any) => setUserId(selected?.value)}
|
||||
placeholder={t('Filter by user')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-2">
|
||||
<AsyncSelect
|
||||
onChange={(db: any) => setDatabaseId(db?.value)}
|
||||
dataEndpoint="/api/v1/database/?q=(filters:!((col:expose_in_sqllab,opr:eq,value:!t)))"
|
||||
value={databaseId}
|
||||
mutator={dbMutator}
|
||||
placeholder={t('Filter by database')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-4">
|
||||
<input
|
||||
type="text"
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
className="form-control input-sm"
|
||||
placeholder={t('Query search string')}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-sm-4 search-date-filter-container">
|
||||
<Select
|
||||
name="select-from"
|
||||
placeholder={t('[From]-')}
|
||||
options={TIME_OPTIONS.slice(1, TIME_OPTIONS.length).map(xt => ({
|
||||
value: xt,
|
||||
label: xt,
|
||||
}))}
|
||||
value={(from as unknown) as undefined}
|
||||
autosize={false}
|
||||
onChange={(selected: any) => setFrom(selected?.value)}
|
||||
/>
|
||||
|
||||
<Select
|
||||
name="select-to"
|
||||
placeholder={t('[To]-')}
|
||||
options={TIME_OPTIONS.map(xt => ({ value: xt, label: xt }))}
|
||||
value={(to as unknown) as undefined}
|
||||
autosize={false}
|
||||
onChange={(selected: any) => setTo(selected?.value)}
|
||||
/>
|
||||
|
||||
<Select
|
||||
name="select-status"
|
||||
placeholder={t('Filter by status')}
|
||||
options={Object.keys(STATUS_OPTIONS).map(s => ({
|
||||
value: s,
|
||||
label: s,
|
||||
}))}
|
||||
value={(status as unknown) as undefined}
|
||||
isLoading={false}
|
||||
autosize={false}
|
||||
onChange={(selected: any) => setStatus(selected?.value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
buttonSize="small"
|
||||
buttonStyle="success"
|
||||
onClick={refreshQueries}
|
||||
>
|
||||
{t('Search')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<StyledTableStylesContainer>
|
||||
{queriesLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<TableStyles>
|
||||
<QueryTable
|
||||
columns={[
|
||||
'state',
|
||||
'db',
|
||||
'user',
|
||||
'time',
|
||||
'progress',
|
||||
'rows',
|
||||
'sql',
|
||||
'querylink',
|
||||
]}
|
||||
onUserClicked={onUserClicked}
|
||||
onDbClicked={onDbClicked}
|
||||
queries={queriesArray}
|
||||
actions={actions}
|
||||
displayLimit={displayLimit}
|
||||
/>
|
||||
</TableStyles>
|
||||
)}
|
||||
</StyledTableStylesContainer>
|
||||
</TableWrapper>
|
||||
);
|
||||
}
|
||||
export default QuerySearch;
|
Loading…
Reference in New Issue
Block a user