chore: Convert QueryAutoRefresh to TypeScript functional React component [sc-48362] (#20179)
* git commit -m 'Convert QueryAutoRefresh to functional component [sc-48362]' * addressing PR comments [sc-48362] Removes unneeded props and state tracking of offline, adds finally block to simplify clearing pending request, simplifies value comparison in array by using includes in place of indexOf * Address PR comment to use enum for QueryState [sc-48362] Original implementation had string literals used in multiple places representing Query.state value options. This commit creates a formal TypeScript enum for QueryState so we can remove string literals and ensure better consistency * Address PR comments for object type validation [sc-48362] This commit resolves an issue why the TypeScript typing for queryList was marked as a Query[] but was actually a Dictionary (associative array) or Queries. A new type QueryDictionary has been added and the QueryAutoRefresh code was adjusted to use QueryDictionary instead of Query[] in appropriate places as well as unit tests. Commit also removes QueryAutoRefreshContainer by making the once component using QueryAutoRefresh (which is already redux connected) pass the needed values on props. this simplifies the code base and reduce files that need unit testing while keeping QueryAutoRefresh out of needing a redux connection directly. * Addresses PR comment to add QueryState.SCHEDULED to runningQueryStateList [sc-48362] In previous implementation 'scheduled' was not included int he list of Query States. Further investigation shows it should be added to as a running state. * Fix prettier lint error [sc-48362] * Adjust unit tests for props update hoisting callbacks out of actions wrapper object [sc-48362] * Update with changes from master [sc-48362] Merges in updates from master and resolves conflicts from relocation of some of the Query TypeScript definitions into core * Removes logic setting user offline and relying on results panel error message [sc-48362] * Fixes bad import after some TypeScript definitions were relocated to core [sc-48362] * Fixes TypeScript errors [sc-48362]
This commit is contained in:
parent
daded10992
commit
9f9aae49c9
|
|
@ -254,15 +254,35 @@ export type QueryColumn = {
|
|||
is_dttm: boolean;
|
||||
};
|
||||
|
||||
export type QueryState =
|
||||
| 'stopped'
|
||||
| 'failed'
|
||||
| 'pending'
|
||||
| 'running'
|
||||
| 'scheduled'
|
||||
| 'success'
|
||||
| 'fetching'
|
||||
| 'timed_out';
|
||||
// Possible states of a query object for processing on the server
|
||||
export enum QueryState {
|
||||
STARTED = 'started',
|
||||
STOPPED = 'stopped',
|
||||
FAILED = 'failed',
|
||||
PENDING = 'pending',
|
||||
RUNNING = 'running',
|
||||
SCHEDULED = 'scheduled',
|
||||
SUCCESS = 'success',
|
||||
FETCHING = 'fetching',
|
||||
TIMED_OUT = 'timed_out',
|
||||
}
|
||||
|
||||
// Inidcates a Query's state is still processing
|
||||
export const runningQueryStateList: QueryState[] = [
|
||||
QueryState.RUNNING,
|
||||
QueryState.STARTED,
|
||||
QueryState.PENDING,
|
||||
QueryState.FETCHING,
|
||||
QueryState.SCHEDULED,
|
||||
];
|
||||
|
||||
// Indicates a Query's state has completed processing regardless of success / failure
|
||||
export const concludedQueryStateList: QueryState[] = [
|
||||
QueryState.STOPPED,
|
||||
QueryState.FAILED,
|
||||
QueryState.SUCCESS,
|
||||
QueryState.TIMED_OUT,
|
||||
];
|
||||
|
||||
export type Query = {
|
||||
cached: boolean;
|
||||
|
|
@ -304,7 +324,7 @@ export type Query = {
|
|||
executedSql: string;
|
||||
output: string | Record<string, any>;
|
||||
actions: Record<string, any>;
|
||||
type: DatasourceType.Query;
|
||||
type: DatasourceType;
|
||||
columns: QueryColumn[];
|
||||
};
|
||||
|
||||
|
|
@ -335,7 +355,7 @@ export const testQuery: Query = {
|
|||
isDataPreview: false,
|
||||
progress: 0,
|
||||
resultsKey: null,
|
||||
state: 'success',
|
||||
state: QueryState.SUCCESS,
|
||||
tempSchema: null,
|
||||
trackingUrl: null,
|
||||
templateParams: null,
|
||||
|
|
|
|||
|
|
@ -94,12 +94,17 @@ class App extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { queries, actions, queriesLastUpdate } = this.props;
|
||||
if (this.state.hash && this.state.hash === '#search') {
|
||||
return window.location.replace('/superset/sqllab/history/');
|
||||
}
|
||||
return (
|
||||
<div className="App SqlLab">
|
||||
<QueryAutoRefresh />
|
||||
<QueryAutoRefresh
|
||||
queries={queries}
|
||||
refreshQueries={actions?.refreshQueries}
|
||||
queriesLastUpdate={queriesLastUpdate}
|
||||
/>
|
||||
<TabbedSqlEditors />
|
||||
<ToastContainer />
|
||||
</div>
|
||||
|
|
@ -114,10 +119,12 @@ App.propTypes = {
|
|||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const { common, localStorageUsageInKilobytes } = state;
|
||||
const { common, localStorageUsageInKilobytes, sqlLab } = state;
|
||||
return {
|
||||
common,
|
||||
localStorageUsageInKilobytes,
|
||||
queries: sqlLab?.queries,
|
||||
queriesLastUpdate: sqlLab?.queriesLastUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,68 +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 { 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';
|
||||
|
||||
describe('QueryAutoRefresh', () => {
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
const sqlLab = {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
ryhMUZCGb: runningQuery,
|
||||
},
|
||||
};
|
||||
const state = {
|
||||
...initialState,
|
||||
sqlLab,
|
||||
};
|
||||
const store = mockStore(state);
|
||||
const getWrapper = () =>
|
||||
shallow(<QueryAutoRefresh store={store} />)
|
||||
.dive()
|
||||
.dive();
|
||||
let wrapper;
|
||||
|
||||
it('shouldCheckForQueries', () => {
|
||||
wrapper = getWrapper();
|
||||
expect(wrapper.instance().shouldCheckForQueries()).toBe(true);
|
||||
});
|
||||
|
||||
it('setUserOffline', () => {
|
||||
wrapper = getWrapper();
|
||||
const spy = sinon.spy(wrapper.instance().props.actions, 'setUserOffline');
|
||||
|
||||
// state not changed
|
||||
wrapper.setState({
|
||||
offline: false,
|
||||
});
|
||||
expect(spy.called).toBe(false);
|
||||
|
||||
// state is changed
|
||||
wrapper.setState({
|
||||
offline: true,
|
||||
});
|
||||
expect(spy.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import QueryAutoRefresh, {
|
||||
isQueryRunning,
|
||||
shouldCheckForQueries,
|
||||
} from 'src/SqlLab/components/QueryAutoRefresh';
|
||||
import { successfulQuery, runningQuery } from 'src/SqlLab/fixtures';
|
||||
import { QueryDictionary } from 'src/SqlLab/types';
|
||||
|
||||
// NOTE: The uses of @ts-ignore in this file is to enable testing of bad inputs to verify the
|
||||
// function / component handles bad data elegantly
|
||||
describe('QueryAutoRefresh', () => {
|
||||
const runningQueries: QueryDictionary = {};
|
||||
runningQueries[runningQuery.id] = runningQuery;
|
||||
|
||||
const successfulQueries: QueryDictionary = {};
|
||||
successfulQueries[successfulQuery.id] = successfulQuery;
|
||||
|
||||
const refreshQueries = jest.fn();
|
||||
|
||||
const queriesLastUpdate = Date.now();
|
||||
|
||||
it('isQueryRunning returns true for valid running query', () => {
|
||||
const running = isQueryRunning(runningQuery);
|
||||
expect(running).toBe(true);
|
||||
});
|
||||
|
||||
it('isQueryRunning returns false for valid not-running query', () => {
|
||||
const running = isQueryRunning(successfulQuery);
|
||||
expect(running).toBe(false);
|
||||
});
|
||||
|
||||
it('isQueryRunning returns false for invalid query', () => {
|
||||
// @ts-ignore
|
||||
let running = isQueryRunning(null);
|
||||
expect(running).toBe(false);
|
||||
// @ts-ignore
|
||||
running = isQueryRunning(undefined);
|
||||
expect(running).toBe(false);
|
||||
// @ts-ignore
|
||||
running = isQueryRunning('I Should Be An Object');
|
||||
expect(running).toBe(false);
|
||||
// @ts-ignore
|
||||
running = isQueryRunning({ state: { badFormat: true } });
|
||||
expect(running).toBe(false);
|
||||
});
|
||||
|
||||
it('shouldCheckForQueries is true for valid running query', () => {
|
||||
expect(shouldCheckForQueries(runningQueries)).toBe(true);
|
||||
});
|
||||
|
||||
it('shouldCheckForQueries is false for valid completed query', () => {
|
||||
expect(shouldCheckForQueries(successfulQueries)).toBe(false);
|
||||
});
|
||||
|
||||
it('shouldCheckForQueries is false for invalid inputs', () => {
|
||||
// @ts-ignore
|
||||
expect(shouldCheckForQueries(null)).toBe(false);
|
||||
// @ts-ignore
|
||||
expect(shouldCheckForQueries(undefined)).toBe(false);
|
||||
expect(
|
||||
// @ts-ignore
|
||||
shouldCheckForQueries({
|
||||
// @ts-ignore
|
||||
'1234': null,
|
||||
// @ts-ignore
|
||||
'23425': 'hello world',
|
||||
// @ts-ignore
|
||||
'345': [],
|
||||
// @ts-ignore
|
||||
'57346': undefined,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('Attempts to refresh when given pending query', () => {
|
||||
render(
|
||||
<QueryAutoRefresh
|
||||
queries={runningQueries}
|
||||
refreshQueries={refreshQueries}
|
||||
queriesLastUpdate={queriesLastUpdate}
|
||||
/>,
|
||||
);
|
||||
setTimeout(() => {
|
||||
expect(refreshQueries).toHaveBeenCalled();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('Does not fail and attempts to refresh when given pending query and invlaid query', () => {
|
||||
render(
|
||||
<QueryAutoRefresh
|
||||
// @ts-ignore
|
||||
queries={{ ...runningQueries, g324t: null }}
|
||||
refreshQueries={refreshQueries}
|
||||
queriesLastUpdate={queriesLastUpdate}
|
||||
/>,
|
||||
);
|
||||
setTimeout(() => {
|
||||
expect(refreshQueries).toHaveBeenCalled();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('Does NOT Attempt to refresh when given only completed queries', () => {
|
||||
render(
|
||||
<QueryAutoRefresh
|
||||
queries={successfulQueries}
|
||||
refreshQueries={refreshQueries}
|
||||
queriesLastUpdate={queriesLastUpdate}
|
||||
/>,
|
||||
);
|
||||
setTimeout(() => {
|
||||
expect(refreshQueries).not.toHaveBeenCalled();
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,124 +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 { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import * as Actions from 'src/SqlLab/actions/sqlLab';
|
||||
|
||||
const QUERY_UPDATE_FREQ = 2000;
|
||||
const QUERY_UPDATE_BUFFER_MS = 5000;
|
||||
const MAX_QUERY_AGE_TO_POLL = 21600000;
|
||||
const QUERY_TIMEOUT_LIMIT = 10000;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
stopTimer() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
stopwatch() {
|
||||
// only poll /superset/queries/ if there are started or running queries
|
||||
if (this.shouldCheckForQueries()) {
|
||||
SupersetClient.get({
|
||||
endpoint: `/superset/queries/${
|
||||
this.props.queriesLastUpdate - QUERY_UPDATE_BUFFER_MS
|
||||
}`,
|
||||
timeout: QUERY_TIMEOUT_LIMIT,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
if (Object.keys(json).length > 0) {
|
||||
this.props.actions.refreshQueries(json);
|
||||
}
|
||||
this.setState({ offline: false });
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ offline: true });
|
||||
});
|
||||
} else {
|
||||
this.setState({ offline: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
QueryAutoRefresh.propTypes = {
|
||||
offline: PropTypes.bool.isRequired,
|
||||
queries: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
queriesLastUpdate: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps({ sqlLab }) {
|
||||
return {
|
||||
offline: sqlLab.offline,
|
||||
queries: sqlLab.queries,
|
||||
queriesLastUpdate: sqlLab.queriesLastUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators(Actions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QueryAutoRefresh);
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* 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 { useState } from 'react';
|
||||
import { isObject } from 'lodash';
|
||||
import {
|
||||
SupersetClient,
|
||||
Query,
|
||||
runningQueryStateList,
|
||||
} from '@superset-ui/core';
|
||||
import { QueryDictionary } from 'src/SqlLab/types';
|
||||
import useInterval from 'src/SqlLab/utils/useInterval';
|
||||
|
||||
const QUERY_UPDATE_FREQ = 2000;
|
||||
const QUERY_UPDATE_BUFFER_MS = 5000;
|
||||
const MAX_QUERY_AGE_TO_POLL = 21600000;
|
||||
const QUERY_TIMEOUT_LIMIT = 10000;
|
||||
|
||||
interface RefreshQueriesFunc {
|
||||
(alteredQueries: any): any;
|
||||
}
|
||||
|
||||
export interface QueryAutoRefreshProps {
|
||||
queries: QueryDictionary;
|
||||
refreshQueries: RefreshQueriesFunc;
|
||||
queriesLastUpdate: number;
|
||||
}
|
||||
|
||||
// returns true if the Query.state matches one of the specifc values indicating the query is still processing on server
|
||||
export const isQueryRunning = (q: Query): boolean =>
|
||||
runningQueryStateList.includes(q?.state);
|
||||
|
||||
// returns true if at least one query is running and within the max age to poll timeframe
|
||||
export const shouldCheckForQueries = (queryList: QueryDictionary): boolean => {
|
||||
let shouldCheck = false;
|
||||
const now = Date.now();
|
||||
if (isObject(queryList)) {
|
||||
shouldCheck = Object.values(queryList).some(
|
||||
q => isQueryRunning(q) && now - q?.startDttm < MAX_QUERY_AGE_TO_POLL,
|
||||
);
|
||||
}
|
||||
return shouldCheck;
|
||||
};
|
||||
|
||||
function QueryAutoRefresh({
|
||||
queries,
|
||||
refreshQueries,
|
||||
queriesLastUpdate,
|
||||
}: QueryAutoRefreshProps) {
|
||||
// We do not want to spam requests in the case of slow connections and potentially recieve responses out of order
|
||||
// pendingRequest check ensures we only have one active http call to check for query statuses
|
||||
const [pendingRequest, setPendingRequest] = useState(false);
|
||||
|
||||
const checkForRefresh = () => {
|
||||
if (!pendingRequest && shouldCheckForQueries(queries)) {
|
||||
setPendingRequest(true);
|
||||
SupersetClient.get({
|
||||
endpoint: `/superset/queries/${
|
||||
queriesLastUpdate - QUERY_UPDATE_BUFFER_MS
|
||||
}`,
|
||||
timeout: QUERY_TIMEOUT_LIMIT,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
if (json) {
|
||||
refreshQueries?.(json);
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
setPendingRequest(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Solves issue where direct usage of setInterval in function components
|
||||
// uses stale props / state from closure
|
||||
// See comments in the useInterval.ts file for more information
|
||||
useInterval(() => {
|
||||
checkForRefresh();
|
||||
}, QUERY_UPDATE_FREQ);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default QueryAutoRefresh;
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
import sinon from 'sinon';
|
||||
import * as actions from 'src/SqlLab/actions/sqlLab';
|
||||
import { ColumnKeyTypeType } from 'src/SqlLab/components/ColumnElement';
|
||||
import { DatasourceType, QueryResponse, QueryState } from '@superset-ui/core';
|
||||
|
||||
export const mockedActions = sinon.stub({ ...actions });
|
||||
|
||||
|
|
@ -189,6 +190,7 @@ export const defaultQueryEditor = {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const queries = [
|
||||
{
|
||||
dbId: 1,
|
||||
|
|
@ -201,7 +203,7 @@ export const queries = [
|
|||
id: 'BkA1CLrJg',
|
||||
progress: 100,
|
||||
startDttm: 1476910566092.96,
|
||||
state: 'success',
|
||||
state: QueryState.SUCCESS,
|
||||
changedOn: 1476910566000,
|
||||
tempTable: null,
|
||||
userId: 1,
|
||||
|
|
@ -260,7 +262,7 @@ export const queries = [
|
|||
id: 'S1zeAISkx',
|
||||
progress: 100,
|
||||
startDttm: 1476910570802.2,
|
||||
state: 'success',
|
||||
state: QueryState.SUCCESS,
|
||||
changedOn: 1476910572000,
|
||||
tempTable: null,
|
||||
userId: 1,
|
||||
|
|
@ -294,7 +296,7 @@ export const queryWithNoQueryLimit = {
|
|||
id: 'BkA1CLrJg',
|
||||
progress: 100,
|
||||
startDttm: 1476910566092.96,
|
||||
state: 'success',
|
||||
state: QueryState.SUCCESS,
|
||||
changedOn: 1476910566000,
|
||||
tempTable: null,
|
||||
userId: 1,
|
||||
|
|
@ -344,6 +346,7 @@ export const queryWithNoQueryLimit = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const queryWithBadColumns = {
|
||||
...queries[0],
|
||||
results: {
|
||||
|
|
@ -407,6 +410,7 @@ export const queryWithBadColumns = {
|
|||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const databases = {
|
||||
result: [
|
||||
{
|
||||
|
|
@ -429,6 +433,7 @@ export const databases = {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const tables = {
|
||||
options: [
|
||||
{
|
||||
|
|
@ -464,7 +469,7 @@ export const stoppedQuery = {
|
|||
sql: 'SELECT ...',
|
||||
sqlEditorId: 'rJaf5u9WZ',
|
||||
startDttm: 1497400851936,
|
||||
state: 'stopped',
|
||||
state: QueryState.STOPPED,
|
||||
tab: 'Untitled Query 2',
|
||||
tempTable: '',
|
||||
};
|
||||
|
|
@ -482,7 +487,7 @@ export const failedQueryWithErrorMessage = {
|
|||
sql: 'SELECT ...',
|
||||
sqlEditorId: 'rJaf5u9WZ',
|
||||
startDttm: 1497400851936,
|
||||
state: 'failed',
|
||||
state: QueryState.FAILED,
|
||||
tab: 'Untitled Query 2',
|
||||
tempTable: '',
|
||||
};
|
||||
|
|
@ -507,20 +512,113 @@ export const failedQueryWithErrors = {
|
|||
sql: 'SELECT ...',
|
||||
sqlEditorId: 'rJaf5u9WZ',
|
||||
startDttm: 1497400851936,
|
||||
state: 'failed',
|
||||
state: QueryState.FAILED,
|
||||
tab: 'Untitled Query 2',
|
||||
tempTable: '',
|
||||
};
|
||||
|
||||
export const runningQuery = {
|
||||
const baseQuery: QueryResponse = {
|
||||
queryId: 567,
|
||||
dbId: 1,
|
||||
sql: 'SELECT * FROM superset.slices',
|
||||
sqlEditorId: 'SJ8YO72R',
|
||||
tab: 'Demo',
|
||||
ctas: false,
|
||||
cached: false,
|
||||
id: 'BkA1CLrJg',
|
||||
progress: 100,
|
||||
startDttm: 1476910566092.96,
|
||||
state: QueryState.SUCCESS,
|
||||
tempSchema: null,
|
||||
tempTable: 'temp',
|
||||
userId: 1,
|
||||
executedSql: 'SELECT * FROM superset.slices',
|
||||
rows: 42,
|
||||
started: 'started',
|
||||
queryLimit: 100,
|
||||
endDttm: 1476910566798,
|
||||
schema: 'test_schema',
|
||||
errorMessage: null,
|
||||
db: { key: 'main' },
|
||||
user: { key: 'admin' },
|
||||
isDataPreview: false,
|
||||
resultsKey: null,
|
||||
trackingUrl: null,
|
||||
templateParams: null,
|
||||
limitingFactor: 'capacity',
|
||||
duration: '2334645675467',
|
||||
time: { key: 'value' },
|
||||
querylink: { key: 'value' },
|
||||
output: { key: 'value' },
|
||||
actions: { key: 'value' },
|
||||
extra: {
|
||||
progress: null,
|
||||
},
|
||||
columns: [],
|
||||
type: DatasourceType.Query,
|
||||
results: {
|
||||
displayLimitReached: false,
|
||||
query: { limit: 6 },
|
||||
columns: [
|
||||
{
|
||||
is_dttm: true,
|
||||
name: 'ds',
|
||||
type: 'STRING',
|
||||
},
|
||||
{
|
||||
is_dttm: false,
|
||||
name: 'gender',
|
||||
type: 'STRING',
|
||||
},
|
||||
],
|
||||
selected_columns: [
|
||||
{
|
||||
is_dttm: true,
|
||||
name: 'ds',
|
||||
type: 'STRING',
|
||||
},
|
||||
{
|
||||
is_dttm: false,
|
||||
name: 'gender',
|
||||
type: 'STRING',
|
||||
},
|
||||
],
|
||||
expanded_columns: [
|
||||
{
|
||||
is_dttm: true,
|
||||
name: 'ds',
|
||||
type: 'STRING',
|
||||
},
|
||||
],
|
||||
data: [
|
||||
{ col1: '0', col2: '1' },
|
||||
{ col1: '2', col2: '3' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const runningQuery: QueryResponse = {
|
||||
...baseQuery,
|
||||
dbId: 1,
|
||||
cached: false,
|
||||
ctas: false,
|
||||
id: 'ryhMUZCGb',
|
||||
progress: 90,
|
||||
state: 'running',
|
||||
state: QueryState.RUNNING,
|
||||
startDttm: Date.now() - 500,
|
||||
};
|
||||
|
||||
export const successfulQuery: QueryResponse = {
|
||||
...baseQuery,
|
||||
dbId: 1,
|
||||
cached: false,
|
||||
ctas: false,
|
||||
id: 'ryhMUZCGb',
|
||||
progress: 100,
|
||||
state: QueryState.SUCCESS,
|
||||
startDttm: Date.now() - 500,
|
||||
};
|
||||
|
||||
export const cachedQuery = { ...queries[0], cached: true };
|
||||
|
||||
export const user = {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ import { ExploreRootState } from 'src/explore/types';
|
|||
|
||||
export type ExploreDatasource = Dataset | QueryResponse;
|
||||
|
||||
// Object as Dictionary (associative array) with Query id as the key and type Query as the value
|
||||
export type QueryDictionary = {
|
||||
[id: string]: QueryResponse;
|
||||
};
|
||||
|
||||
export interface QueryEditor {
|
||||
dbId?: number;
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* 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 { useEffect, useRef } from 'react';
|
||||
|
||||
/*
|
||||
* Functional components and setTimeout with useState do not play well
|
||||
* and the setTimeout callback typically has stale state from a closure
|
||||
* The useInterval function solves this issue.
|
||||
* more info: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
||||
*/
|
||||
function useInterval(callback: Function, delay: number | null): void {
|
||||
const savedCallback = useRef<Function>(callback);
|
||||
// Remember the latest function.
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// Set up the interval.
|
||||
useEffect(() => {
|
||||
function tick() {
|
||||
savedCallback?.current?.();
|
||||
}
|
||||
if (delay !== null) {
|
||||
const id = setInterval(tick, delay);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
return () => {};
|
||||
}, [delay]);
|
||||
}
|
||||
|
||||
export default useInterval;
|
||||
Loading…
Reference in New Issue