perf(sqllab): Rendering perf improvement using immutable state (#20877)

* perf(sqllab): Rendering perf improvement using immutable state

- keep queryEditors immutable during active state
- add unsavedQueryEditor to store all active changes
- refactor each component to subscribe the related unsaved editor state only

* revert ISaveableDatasource type cast

* missing trigger prop

* a default of an empty object and optional operator
This commit is contained in:
JUST.in DO IT
2022-08-23 08:17:19 -07:00
committed by GitHub
parent 4ca4a5c7cb
commit f77b910e2c
32 changed files with 1929 additions and 606 deletions

View File

@@ -40,7 +40,12 @@ import {
} from 'src/SqlLab/actions/sqlLab';
import { EmptyStateBig } from 'src/components/EmptyState';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { initialState, queries, table } from 'src/SqlLab/fixtures';
import {
initialState,
queries,
table,
defaultQueryEditor,
} from 'src/SqlLab/fixtures';
const MOCKED_SQL_EDITOR_HEIGHT = 500;
@@ -48,7 +53,31 @@ fetchMock.get('glob:*/api/v1/database/*', { result: [] });
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const store = mockStore(initialState);
const store = mockStore({
...initialState,
sqlLab: {
...initialState.sqlLab,
databases: {
dbid1: {
allow_ctas: false,
allow_cvas: false,
allow_dml: false,
allow_file_upload: false,
allow_multi_schema_metadata_fetch: false,
allow_run_async: false,
backend: 'postgresql',
database_name: 'examples',
expose_in_sqllab: true,
force_ctas_schema: null,
id: 1,
},
},
unsavedQueryEditor: {
id: defaultQueryEditor.id,
dbId: 'dbid1',
},
},
});
describe('SqlEditor', () => {
const mockedProps = {
@@ -57,21 +86,9 @@ describe('SqlEditor', () => {
queryEditorSetSelectedText,
queryEditorSetSchemaOptions,
addDangerToast: jest.fn(),
removeDataPreview: jest.fn(),
},
database: {
allow_ctas: false,
allow_cvas: false,
allow_dml: false,
allow_file_upload: false,
allow_multi_schema_metadata_fetch: false,
allow_run_async: false,
backend: 'postgresql',
database_name: 'examples',
expose_in_sqllab: true,
force_ctas_schema: null,
id: 1,
},
queryEditorId: initialState.sqlLab.queryEditors[0].id,
queryEditor: initialState.sqlLab.queryEditors[0],
latestQuery: queries[0],
tables: [table],
getHeight: () => '100px',
@@ -94,8 +111,8 @@ describe('SqlEditor', () => {
);
it('does not render SqlEditor if no db selected', () => {
const database = {};
const updatedProps = { ...mockedProps, database };
const queryEditor = initialState.sqlLab.queryEditors[1];
const updatedProps = { ...mockedProps, queryEditor };
const wrapper = buildWrapper(updatedProps);
expect(wrapper.find(EmptyStateBig)).toExist();
});

View File

@@ -43,10 +43,10 @@ import {
persistEditorHeight,
postStopQuery,
queryEditorSetAutorun,
queryEditorSetQueryLimit,
queryEditorSetSql,
queryEditorSetAndSaveSql,
queryEditorSetTemplateParams,
runQueryFromSqlEditor,
runQuery,
saveQuery,
addSavedQueryToTabState,
@@ -79,8 +79,8 @@ import SqlEditorLeftBar from '../SqlEditorLeftBar';
import AceEditorWrapper from '../AceEditorWrapper';
import RunQueryActionButton from '../RunQueryActionButton';
import { newQueryTabName } from '../../utils/newQueryTabName';
import QueryLimitSelect from '../QueryLimitSelect';
const LIMIT_DROPDOWN = [10, 100, 1000, 10000, 100000];
const SQL_EDITOR_PADDING = 10;
const INITIAL_NORTH_PERCENT = 30;
const INITIAL_SOUTH_PERCENT = 70;
@@ -96,26 +96,6 @@ const validatorMap =
bootstrapData?.common?.conf?.SQL_VALIDATORS_BY_ENGINE || {};
const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
const LimitSelectStyled = styled.span`
${({ theme }) => `
.ant-dropdown-trigger {
align-items: center;
color: ${theme.colors.grayscale.dark2};
display: flex;
font-size: 12px;
margin-right: ${theme.gridUnit * 2}px;
text-decoration: none;
span {
display: inline-block;
margin-right: ${theme.gridUnit * 2}px;
&:last-of-type: {
margin-right: ${theme.gridUnit * 4}px;
}
}
}
`}
`;
const StyledToolbar = styled.div`
padding: ${({ theme }) => theme.gridUnit * 2}px;
background: ${({ theme }) => theme.colors.grayscale.light5};
@@ -154,7 +134,7 @@ const propTypes = {
tables: PropTypes.array.isRequired,
editorQueries: PropTypes.array.isRequired,
dataPreviewQueries: PropTypes.array.isRequired,
queryEditorId: PropTypes.string.isRequired,
queryEditor: PropTypes.object.isRequired,
hideLeftBar: PropTypes.bool,
defaultQueryLimit: PropTypes.number.isRequired,
maxRow: PropTypes.number.isRequired,
@@ -205,7 +185,6 @@ class SqlEditor extends React.PureComponent {
);
this.queryPane = this.queryPane.bind(this);
this.getHotkeyConfig = this.getHotkeyConfig.bind(this);
this.renderQueryLimit = this.renderQueryLimit.bind(this);
this.getAceEditorAndSouthPaneHeights =
this.getAceEditorAndSouthPaneHeights.bind(this);
this.getSqlEditorHeight = this.getSqlEditorHeight.bind(this);
@@ -382,21 +361,10 @@ class SqlEditor extends React.PureComponent {
this.props.queryEditorSetAndSaveSql(this.props.queryEditor, sql);
}
setQueryLimit(queryLimit) {
this.props.queryEditorSetQueryLimit(this.props.queryEditor, queryLimit);
}
getQueryCostEstimate() {
if (this.props.database) {
const qe = this.props.queryEditor;
const query = {
dbId: qe.dbId,
sql: qe.selectedText ? qe.selectedText : this.props.queryEditor.sql,
sqlEditorId: qe.id,
schema: qe.schema,
templateParams: qe.templateParams,
};
this.props.estimateQueryCost(query);
this.props.estimateQueryCost(qe);
}
}
@@ -425,16 +393,9 @@ class SqlEditor extends React.PureComponent {
}
requestValidation(sql) {
if (this.props.database) {
const qe = this.props.queryEditor;
const query = {
dbId: qe.dbId,
sql,
sqlEditorId: qe.id,
schema: qe.schema,
templateParams: qe.templateParams,
};
this.props.validateQuery(query);
const { database, queryEditor, validateQuery } = this.props;
if (database) {
validateQuery(queryEditor, sql);
}
}
@@ -458,25 +419,22 @@ class SqlEditor extends React.PureComponent {
}
startQuery(ctas = false, ctas_method = CtasEnum.TABLE) {
const qe = this.props.queryEditor;
const query = {
dbId: qe.dbId,
sql: qe.selectedText ? qe.selectedText : qe.sql,
sqlEditorId: qe.id,
tab: qe.name,
schema: qe.schema,
tempTable: ctas ? this.state.ctas : '',
templateParams: qe.templateParams,
queryLimit: qe.queryLimit || this.props.defaultQueryLimit,
runAsync: this.props.database
? this.props.database.allow_run_async
: false,
const {
database,
runQueryFromSqlEditor,
setActiveSouthPaneTab,
queryEditor,
defaultQueryLimit,
} = this.props;
runQueryFromSqlEditor(
database,
queryEditor,
defaultQueryLimit,
ctas ? this.state.ctas : '',
ctas,
ctas_method,
updateTabState: !qe.selectedText,
};
this.props.runQuery(query);
this.props.setActiveSouthPaneTab('Results');
);
setActiveSouthPaneTab('Results');
}
stopQuery() {
@@ -529,11 +487,7 @@ class SqlEditor extends React.PureComponent {
onBlur={this.setQueryEditorSql}
onChange={this.onSqlChanged}
queryEditor={this.props.queryEditor}
sql={this.props.queryEditor.sql}
database={this.props.database}
schemas={this.props.queryEditor.schemaOptions}
tables={this.props.queryEditor.tableOptions}
functionNames={this.props.queryEditor.functionNames}
extendedTables={this.props.tables}
height={`${aceEditorHeight}px`}
hotkeys={hotkeys}
@@ -577,7 +531,7 @@ class SqlEditor extends React.PureComponent {
onChange={params => {
this.props.actions.queryEditorSetTemplateParams(qe, params);
}}
code={qe.templateParams}
queryEditor={qe}
/>
</Menu.Item>
)}
@@ -599,25 +553,6 @@ class SqlEditor extends React.PureComponent {
);
}
renderQueryLimit() {
// Adding SQL_MAX_ROW value to dropdown
const { maxRow } = this.props;
LIMIT_DROPDOWN.push(maxRow);
return (
<Menu>
{[...new Set(LIMIT_DROPDOWN)].map(limit => (
<Menu.Item key={`${limit}`} onClick={() => this.setQueryLimit(limit)}>
{/* // eslint-disable-line no-use-before-define */}
<a role="button" styling="link">
{this.convertToNumWithSpaces(limit)}
</a>{' '}
</Menu.Item>
))}
</Menu>
);
}
async saveQuery(query) {
const { queryEditor: qe, actions } = this.props;
const savedQuery = await actions.saveQuery(query);
@@ -673,11 +608,10 @@ class SqlEditor extends React.PureComponent {
? this.props.database.allow_run_async
: false
}
queryEditor={qe}
queryState={this.props.latestQuery?.state}
runQuery={this.runQuery}
selectedText={qe.selectedText}
stopQuery={this.stopQuery}
sql={this.props.queryEditor.sql}
overlayCreateAsMenu={showMenu ? runMenuBtn : null}
/>
</span>
@@ -687,27 +621,17 @@ class SqlEditor extends React.PureComponent {
<span>
<EstimateQueryCostButton
getEstimate={this.getQueryCostEstimate}
queryCostEstimate={qe.queryCostEstimate}
selectedText={qe.selectedText}
queryEditor={qe}
tooltip={t('Estimate the cost before running a query')}
/>
</span>
)}
<span>
<LimitSelectStyled>
<AntdDropdown overlay={this.renderQueryLimit()} trigger="click">
<a onClick={e => e.preventDefault()}>
<span>LIMIT:</span>
<span className="limitDropdown">
{this.convertToNumWithSpaces(
this.props.queryEditor.queryLimit ||
this.props.defaultQueryLimit,
)}
</span>
<Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
</a>
</AntdDropdown>
</LimitSelectStyled>
<QueryLimitSelect
queryEditor={this.props.queryEditor}
maxRow={this.props.maxRow}
defaultQueryLimit={this.props.defaultQueryLimit}
/>
</span>
{this.props.latestQuery && (
<Timer
@@ -721,11 +645,8 @@ class SqlEditor extends React.PureComponent {
<div className="rightItems">
<span>
<SaveQuery
query={{
...qe,
columns: this.props.latestQuery?.results?.columns || [],
}}
defaultLabel={qe.name || qe.description}
queryEditor={qe}
columns={this.props.latestQuery?.results?.columns || []}
onSave={this.saveQuery}
onUpdate={this.props.actions.updateSavedQuery}
saveQueryWarning={this.props.saveQueryWarning}
@@ -832,12 +753,22 @@ class SqlEditor extends React.PureComponent {
SqlEditor.defaultProps = defaultProps;
SqlEditor.propTypes = propTypes;
function mapStateToProps({ sqlLab }, props) {
const queryEditor = sqlLab.queryEditors.find(
editor => editor.id === props.queryEditorId,
);
function mapStateToProps({ sqlLab }, { queryEditor }) {
let { latestQueryId, dbId } = queryEditor;
if (sqlLab.unsavedQueryEditor.id === queryEditor.id) {
const { latestQueryId: unsavedQID, dbId: unsavedDBID } =
sqlLab.unsavedQueryEditor;
latestQueryId = unsavedQID || latestQueryId;
dbId = unsavedDBID || dbId;
}
const database = sqlLab.databases[dbId];
const latestQuery = sqlLab.queries[latestQueryId];
return { sqlLab, ...props, queryEditor, queryEditors: sqlLab.queryEditors };
return {
queryEditors: sqlLab.queryEditors,
latestQuery,
database,
};
}
function mapDispatchToProps(dispatch) {
@@ -848,10 +779,10 @@ function mapDispatchToProps(dispatch) {
persistEditorHeight,
postStopQuery,
queryEditorSetAutorun,
queryEditorSetQueryLimit,
queryEditorSetSql,
queryEditorSetAndSaveSql,
queryEditorSetTemplateParams,
runQueryFromSqlEditor,
runQuery,
saveQuery,
addSavedQueryToTabState,