Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/apache/superset into hxgh…
Browse files Browse the repository at this point in the history
…/sc-39227/2-0-remove-sqlalchemy-docs-url-and-sqlalchemy
  • Loading branch information
hughhhh committed Mar 16, 2022
2 parents 364a3ef + d01fdad commit 2a1283e
Show file tree
Hide file tree
Showing 23 changed files with 370 additions and 186 deletions.
2 changes: 1 addition & 1 deletion superset-frontend/cypress-base/cypress.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"baseUrl": "http://localhost:8088",
"chromeWebSecurity": false,
"defaultCommandTimeout": 5000,
"defaultCommandTimeout": 8000,
"numTestsKeptInMemory": 0,
"experimentalFetchPolyfill": true,
"requestTimeout": 10000,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,29 @@ interface QueryString {
native_filters_key: string;
}

describe('nativefiler url param key', () => {
xdescribe('nativefiler url param key', () => {
// const urlParams = { param1: '123', param2: 'abc' };
before(() => {
cy.login();
cy.visit(WORLD_HEALTH_DASHBOARD);
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
cy.wait(1000); // wait for key to be published (debounced)
});

let initialFilterKey: string;
it('should have cachekey in nativefilter param', () => {
// things in `before` will not retry and the `waitForChartLoad` check is
// especically flaky and may need more retries
cy.visit(WORLD_HEALTH_DASHBOARD);
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
cy.wait(1000); // wait for key to be published (debounced)
cy.location().then(loc => {
const queryParams = qs.parse(loc.search) as QueryString;
expect(typeof queryParams.native_filters_key).eq('string');
});
});

it('should have different key when page reloads', () => {
cy.visit(WORLD_HEALTH_DASHBOARD);
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
cy.wait(1000); // wait for key to be published (debounced)
cy.location().then(loc => {
const queryParams = qs.parse(loc.search) as QueryString;
expect(queryParams.native_filters_key).not.equal(initialFilterKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@
* under the License.
*/
import React from 'react';
import { render } from 'spec/helpers/testing-library';
import { ThemeProvider, supersetTheme } from '@superset-ui/core';
import { shallow } from 'enzyme';
import sinon from 'sinon';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import QueryAutoRefresh from 'src/SqlLab/components/QueryAutoRefresh';
import { initialState, runningQuery } from 'src/SqlLab/fixtures';
import fetchMock from 'fetch-mock';
import * as actions from 'src/SqlLab/actions/sqlLab';

describe('QueryAutoRefresh', () => {
const middlewares = [thunk];
Expand All @@ -40,29 +38,31 @@ describe('QueryAutoRefresh', () => {
sqlLab,
};
const store = mockStore(state);
const setup = (overrides = {}) => (
<ThemeProvider theme={supersetTheme}>
<QueryAutoRefresh store={store} {...overrides} />
</ThemeProvider>
);

const mockFetch = fetchMock.get('glob:*/superset/queries/*', {});
const getWrapper = () =>
shallow(<QueryAutoRefresh store={store} />)
.dive()
.dive();
let wrapper;

it('shouldCheckForQueries', () => {
render(setup(), {
useRedux: true,
});

expect(mockFetch.called()).toBe(true);
wrapper = getWrapper();
expect(wrapper.instance().shouldCheckForQueries()).toBe(true);
});

it('setUserOffline', () => {
const spy = jest.spyOn(actions, 'setUserOffline');
wrapper = getWrapper();
const spy = sinon.spy(wrapper.instance().props.actions, 'setUserOffline');

render(setup(), {
useRedux: true,
// state not changed
wrapper.setState({
offline: false,
});
expect(spy.called).toBe(false);

expect(spy).toHaveBeenCalled();
// state is changed
wrapper.setState({
offline: true,
});
expect(spy.callCount).toBe(1);
});
});
86 changes: 47 additions & 39 deletions superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useState, useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
Expand All @@ -28,70 +28,78 @@ const QUERY_UPDATE_BUFFER_MS = 5000;
const MAX_QUERY_AGE_TO_POLL = 21600000;
const QUERY_TIMEOUT_LIMIT = 10000;

function QueryAutoRefresh({ offline, queries, queriesLastUpdate, actions }) {
const [offlineState, setOfflineState] = useState(offline);
let timer = null;
class QueryAutoRefresh extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
offline: props.offline,
};
}

UNSAFE_componentWillMount() {
this.startTimer();
}

componentDidUpdate(prevProps) {
if (prevProps.offline !== this.state.offline) {
this.props.actions.setUserOffline(this.state.offline);
}
}

componentWillUnmount() {
this.stopTimer();
}

const shouldCheckForQueries = () => {
shouldCheckForQueries() {
// if there are started or running queries, this method should return true
const { queries } = this.props;
const now = new Date().getTime();
const isQueryRunning = q =>
['running', 'started', 'pending', 'fetching'].indexOf(q.state) >= 0;

return Object.values(queries).some(
q => isQueryRunning(q) && now - q.startDttm < MAX_QUERY_AGE_TO_POLL,
);
};
}

startTimer() {
if (!this.timer) {
this.timer = setInterval(this.stopwatch.bind(this), QUERY_UPDATE_FREQ);
}
}

const stopwatch = () => {
stopTimer() {
clearInterval(this.timer);
this.timer = null;
}

stopwatch() {
// only poll /superset/queries/ if there are started or running queries
if (shouldCheckForQueries()) {
if (this.shouldCheckForQueries()) {
SupersetClient.get({
endpoint: `/superset/queries/${
queriesLastUpdate - QUERY_UPDATE_BUFFER_MS
this.props.queriesLastUpdate - QUERY_UPDATE_BUFFER_MS
}`,
timeout: QUERY_TIMEOUT_LIMIT,
})
.then(({ json }) => {
if (Object.keys(json).length > 0) {
actions.refreshQueries(json);
this.props.actions.refreshQueries(json);
}

setOfflineState(false);
this.setState({ offline: false });
})
.catch(() => {
setOfflineState(true);
this.setState({ offline: true });
});
} else {
setOfflineState(false);
this.setState({ offline: false });
}
};

const startTimer = () => {
if (!timer) {
timer = setInterval(stopwatch(), QUERY_UPDATE_FREQ);
}
};

const stopTimer = () => {
clearInterval(timer);
timer = null;
};

useEffect(() => {
startTimer();
return () => {
stopTimer();
};
}, []);
}

useEffect(() => {
actions.setUserOffline(offlineState);
}, [offlineState]);

return null;
render() {
return null;
}
}

QueryAutoRefresh.propTypes = {
offline: PropTypes.bool.isRequired,
queries: PropTypes.object.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@
*/
import React from 'react';
import { useArgs } from '@storybook/client-api';
import TimezoneSelector, { TimezoneProps } from './index';
import TimezoneSelector, { TimezoneSelectorProps } from './index';

export default {
title: 'TimezoneSelector',
component: TimezoneSelector,
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const InteractiveTimezoneSelector = (args: TimezoneProps) => {
export const InteractiveTimezoneSelector = (args: TimezoneSelectorProps) => {
const [{ timezone }, updateArgs] = useArgs();
const onTimezoneChange = (value: string) => {
updateArgs({ timezone: value });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,42 @@ import React from 'react';
import moment from 'moment-timezone';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import TimezoneSelector from './index';
import type { TimezoneSelectorProps } from './index';

jest.spyOn(moment.tz, 'guess').mockReturnValue('America/New_York');
const loadComponent = (mockCurrentTime?: string) => {
if (mockCurrentTime) {
jest.useFakeTimers('modern');
jest.setSystemTime(new Date(mockCurrentTime));
}
return new Promise<React.FC<TimezoneSelectorProps>>(resolve => {
jest.isolateModules(() => {
const { default: TimezoneSelector } = module.require('./index');
resolve(TimezoneSelector);
jest.useRealTimers();
});
});
};

const getSelectOptions = () =>
waitFor(() => document.querySelectorAll('.ant-select-item-option-content'));

it('use the timezone from `moment` if no timezone provided', () => {
const openSelectMenu = async () => {
const searchInput = screen.getByRole('combobox');
userEvent.click(searchInput);
};

jest.spyOn(moment.tz, 'guess').mockReturnValue('America/New_York');

test('use the timezone from `moment` if no timezone provided', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = jest.fn();
render(<TimezoneSelector onTimezoneChange={onTimezoneChange} />);
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenCalledWith('America/Nassau');
});

it('update to closest deduped timezone when timezone is provided', async () => {
test('update to closest deduped timezone when timezone is provided', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
Expand All @@ -46,7 +67,8 @@ it('update to closest deduped timezone when timezone is provided', async () => {
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Vancouver');
});

it('use the default timezone when an invalid timezone is provided', async () => {
test('use the default timezone when an invalid timezone is provided', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector onTimezoneChange={onTimezoneChange} timezone="UTC" />,
Expand All @@ -55,49 +77,65 @@ it('use the default timezone when an invalid timezone is provided', async () =>
expect(onTimezoneChange).toHaveBeenLastCalledWith('Africa/Abidjan');
});

it.skip('can select a timezone values and returns canonical value', async () => {
test('render timezones in correct oder for standard time', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="America/Nassau"
/>,
);

const searchInput = screen.getByRole('combobox', {
name: 'Timezone selector',
});
expect(searchInput).toBeInTheDocument();
userEvent.click(searchInput);
const isDaylight = moment(moment.now()).isDST();

const selectedTimezone = isDaylight
? 'GMT -04:00 (Eastern Daylight Time)'
: 'GMT -05:00 (Eastern Standard Time)';

// selected option ranks first
await openSelectMenu();
const options = await getSelectOptions();
expect(options[0]).toHaveTextContent(selectedTimezone);

// others are ranked by offset
expect(options[0]).toHaveTextContent('GMT -05:00 (Eastern Standard Time)');
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)');
expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)');
expect(options[3]).toHaveTextContent('GMT -10:00 (America/Adak)');
});

test('render timezones in correct order for daylight saving time', async () => {
const TimezoneSelector = await loadComponent('2022-07-01');
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="America/Nassau"
/>,
);
await openSelectMenu();
const options = await getSelectOptions();
// first option is always current timezone
expect(options[0]).toHaveTextContent('GMT -04:00 (Eastern Daylight Time)');
expect(options[1]).toHaveTextContent('GMT -11:00 (Pacific/Pago_Pago)');
expect(options[2]).toHaveTextContent('GMT -10:00 (Hawaii Standard Time)');
expect(options[3]).toHaveTextContent('GMT -09:30 (Pacific/Marquesas)');
});

test('can select a timezone values and returns canonical timezone name', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = jest.fn();
render(
<TimezoneSelector
onTimezoneChange={onTimezoneChange}
timezone="Africa/Abidjan"
/>,
);

await openSelectMenu();

const searchInput = screen.getByRole('combobox');
// search for mountain time
await userEvent.type(searchInput, 'mou', { delay: 10 });

const findTitle = isDaylight
? 'GMT -06:00 (Mountain Daylight Time)'
: 'GMT -07:00 (Mountain Standard Time)';
const findTitle = 'GMT -07:00 (Mountain Standard Time)';
const selectOption = await screen.findByTitle(findTitle);
expect(selectOption).toBeInTheDocument();
userEvent.click(selectOption);
expect(onTimezoneChange).toHaveBeenCalledTimes(1);
expect(onTimezoneChange).toHaveBeenLastCalledWith('America/Cambridge_Bay');
});

it('can update props and rerender with different values', async () => {
test('can update props and rerender with different values', async () => {
const TimezoneSelector = await loadComponent('2022-01-01');
const onTimezoneChange = jest.fn();
const { rerender } = render(
<TimezoneSelector
Expand Down
Loading

0 comments on commit 2a1283e

Please sign in to comment.