chore: Migrate /superset/search_queries to API v1 (#22579)

This commit is contained in:
Diego Medina
2023-01-11 13:22:41 -03:00
committed by GitHub
parent 1fe0290a60
commit 44c9cf4de5
18 changed files with 1 additions and 881 deletions

View File

@@ -1,139 +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 thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
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';
import { user } from 'src/SqlLab/fixtures';
const mockStore = configureStore([thunk]);
const store = mockStore({
sqlLab: user,
});
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 mockedProps = {
displayLimit: 50,
};
it('is valid', () => {
expect(
React.isValidElement(
<ThemeProvider theme={supersetTheme}>
<Provider store={store}>
<QuerySearch {...mockedProps} />
</Provider>
</ThemeProvider>,
),
).toBe(true);
});
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', () => {
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 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', () => {
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(
screen.getByPlaceholderText(/Query search string/i),
).toBeInTheDocument();
});
it('updates search text on user inputs search text', () => {
const search = screen.getByPlaceholderText(/Query search string/i);
userEvent.type(search, 'text');
expect(search.value).toBe('text');
});
it('should have one Button', () => {
const button = screen.getAllByRole('button');
expect(button.length).toEqual(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);
});
});

View File

@@ -1,289 +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, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { setDatabases, addDangerToast } from 'src/SqlLab/actions/sqlLab';
import Button from 'src/components/Button';
import Select from 'src/components/DeprecatedSelect';
import { styled, t, SupersetClient, QueryResponse } from '@superset-ui/core';
import { debounce } from 'lodash';
import Loading from 'src/components/Loading';
import {
now,
epochTimeXHoursAgo,
epochTimeXDaysAgo,
epochTimeXYearsAgo,
} from 'src/utils/dates';
import AsyncSelect from 'src/components/AsyncSelect';
import { STATUS_OPTIONS, TIME_OPTIONS } from 'src/SqlLab/constants';
import QueryTable from '../QueryTable';
interface QuerySearchProps {
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;
`;
const QuerySearch = ({ displayLimit }: QuerySearchProps) => {
const dispatch = useDispatch();
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<QueryResponse[]>([]);
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) {
dispatch(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,
}));
dispatch(setDatabases(result));
if (result.length === 0) {
dispatch(
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={{ value: from, label: from }}
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={{ value: to, label: to }}
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={{ value: status, label: status }}
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}
displayLimit={displayLimit}
/>
</TableStyles>
)}
</StyledTableStylesContainer>
</TableWrapper>
);
};
export default QuerySearch;