fix: timer component, fixes #10849, closes #11002 (#11004)

This commit is contained in:
Jesse Yang
2020-09-23 10:53:24 -07:00
committed by GitHub
parent af1e8e8839
commit 7549dad12d
8 changed files with 99 additions and 77 deletions

View File

@@ -16,43 +16,80 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import shortid from 'shortid'; import * as shortid from 'shortid';
import { selectResultsTab, assertSQLLabResultsAreEqual } from './sqllab.helper'; import { selectResultsTab, assertSQLLabResultsAreEqual } from './sqllab.helper';
function parseClockStr(node: JQuery) {
return Number.parseFloat(node.text().replace(/:/g, ''));
}
describe('SqlLab query panel', () => { describe('SqlLab query panel', () => {
beforeEach(() => { beforeEach(() => {
cy.login(); cy.login();
cy.server(); cy.server();
cy.visit('/superset/sqllab'); cy.visit('/superset/sqllab');
cy.route('POST', '/superset/sql_json/').as('sqlLabQuery');
}); });
it.skip('supports entering and running a query', () => { it.skip('supports entering and running a query', () => {
// row limit has to be < ~10 for us to be able to determine how many rows // row limit has to be < ~10 for us to be able to determine how many rows
// are fetched below (because React _Virtualized_ does not render all rows) // are fetched below (because React _Virtualized_ does not render all rows)
const rowLimit = 3; let clockTime = 0;
const sampleResponse = {
status: 'success',
data: [{ '?column?': 1 }],
columns: [{ name: '?column?', type: 'INT', is_date: false }],
selected_columns: [{ name: '?column?', type: 'INT', is_date: false }],
expanded_columns: [],
};
cy.route({
method: 'POST',
url: '/superset/sql_json/',
delay: 1000,
response: () => sampleResponse,
}).as('mockSQLResponse');
cy.get('.TableSelector .Select:eq(0)').click();
cy.get('.TableSelector .Select:eq(0) input[type=text]')
.focus()
.type('{enter}');
cy.get('#brace-editor textarea') cy.get('#brace-editor textarea')
.clear({ force: true }) .focus()
.type( .clear()
`{selectall}{backspace}SELECT ds, gender, name, num FROM main.birth_names LIMIT ${rowLimit}`, .type(`{selectall}{backspace}SELECT 1`);
{ force: true },
);
cy.get('#js-sql-toolbar button').eq(0).click();
cy.wait('@sqlLabQuery'); cy.get('#js-sql-toolbar button:eq(0)').eq(0).click();
selectResultsTab() // wait for 300 milliseconds
.eq(0) // ensures results tab in case preview tab exists cy.wait(300);
.then(tableNodes => {
const [header, bodyWrapper] = tableNodes[0].childNodes; // started timer
const body = bodyWrapper.childNodes[0]; cy.get('.sql-toolbar .label-success').then(node => {
const expectedColCount = header.childNodes.length; clockTime = parseClockStr(node);
const expectedRowCount = body.childNodes.length; // should be longer than 0.2s
expect(expectedColCount).to.equal(4); expect(clockTime).greaterThan(0.2);
expect(expectedRowCount).to.equal(rowLimit); });
});
cy.wait('@mockSQLResponse');
// timer is increasing
cy.get('.sql-toolbar .label-success').then(node => {
const newClockTime = parseClockStr(node);
expect(newClockTime).greaterThan(0.9);
clockTime = newClockTime;
});
// rerun the query
cy.get('#js-sql-toolbar button:eq(0)').eq(0).click();
// should restart the timer
cy.get('.sql-toolbar .label-success').contains('00:00:00');
cy.wait('@mockSQLResponse');
cy.get('.sql-toolbar .label-success').then(node => {
expect(parseClockStr(node)).greaterThan(0.9);
});
}); });
it.skip('successfully saves a query', () => { it.skip('successfully saves a query', () => {
@@ -64,7 +101,7 @@ describe('SqlLab query panel', () => {
const savedQueryTitle = `CYPRESS TEST QUERY ${shortid.generate()}`; const savedQueryTitle = `CYPRESS TEST QUERY ${shortid.generate()}`;
// we will assert that the results of the query we save, and the saved query are the same // we will assert that the results of the query we save, and the saved query are the same
let initialResultsTable = null; let initialResultsTable: HTMLElement | null = null;
let savedQueryResultsTable = null; let savedQueryResultsTable = null;
cy.get('#brace-editor textarea') cy.get('#brace-editor textarea')

View File

@@ -39,6 +39,7 @@ describe('SqlLab query tabs', () => {
.contains(`Untitled Query ${initialTabCount + 2}`); .contains(`Untitled Query ${initialTabCount + 2}`);
}); });
}); });
it('allows you to close a tab', () => { it('allows you to close a tab', () => {
cy.get('[data-test="sql-editor-tabs"]') cy.get('[data-test="sql-editor-tabs"]')
.children() .children()

View File

@@ -30,7 +30,7 @@ declare namespace Cypress {
*/ */
login(): void; login(): void;
visitChartByParams(params: string | object): cy; visitChartByParams(params: string | Record<string, unknown>): cy;
visitChartByName(name: string): cy; visitChartByName(name: string): cy;
visitChartById(id: number): cy; visitChartById(id: number): cy;

View File

@@ -5,7 +5,7 @@
"lib": ["ES5", "ES2015", "DOM"], "lib": ["ES5", "ES2015", "DOM"],
"types": ["cypress"], "types": ["cypress"],
"allowJs": true, "allowJs": true,
"noEmit": true "noEmit": true,
}, },
"files": ["cypress/support/index.d.ts"], "files": ["cypress/support/index.d.ts"],
"include": ["node_modules/cypress", "cypress/**/*.ts"] "include": ["node_modules/cypress", "cypress/**/*.ts"]

View File

@@ -34,17 +34,15 @@ describe('Timer', () => {
wrapper = mount(<Timer {...mockedProps} />); wrapper = mount(<Timer {...mockedProps} />);
}); });
it('is a valid element', () => { it('renders correctly', () => {
expect(React.isValidElement(<Timer {...mockedProps} />)).toBe(true); expect(React.isValidElement(<Timer {...mockedProps} />)).toBe(true);
expect(wrapper.find('span').hasClass('label-warning')).toBe(true);
}); });
it('useEffect starts timer after 30ms and sets state of clockStr', async () => { it('should start timer and sets clockStr', async () => {
expect.assertions(2);
expect(wrapper.find('span').text()).toBe(''); expect(wrapper.find('span').text()).toBe('');
await new Promise(r => setTimeout(r, 35)); await new Promise(r => setTimeout(r, 35));
expect(wrapper.find('span').text()).not.toBe(''); expect(wrapper.find('span').text()).not.toBe('');
}); });
it('renders a span with the correct class', () => {
expect(wrapper.find('span').hasClass('label-warning')).toBe(true);
});
}); });

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React from 'react'; import React, { CSSProperties } from 'react';
import { Label as BootstrapLabel } from 'react-bootstrap'; import { Label as BootstrapLabel } from 'react-bootstrap';
import { styled } from '@superset-ui/core'; import { styled } from '@superset-ui/core';
import cx from 'classnames'; import cx from 'classnames';
@@ -31,7 +31,7 @@ export interface LabelProps {
placement?: string; placement?: string;
onClick?: OnClickHandler; onClick?: OnClickHandler;
bsStyle?: string; bsStyle?: string;
style?: BootstrapLabel.LabelProps['style']; style?: CSSProperties;
children?: React.ReactNode; children?: React.ReactNode;
} }

View File

@@ -16,10 +16,11 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { styled } from '@superset-ui/core';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
import { now, fDuration } from '../modules/dates'; import { now, fDuration } from 'src/modules/dates';
interface TimerProps { interface TimerProps {
endTime?: number; endTime?: number;
@@ -28,6 +29,11 @@ interface TimerProps {
status?: string; status?: string;
} }
const TimerLabel = styled(Label)`
width: 80px;
text-align: right;
`;
export default function Timer({ export default function Timer({
endTime, endTime,
isRunning, isRunning,
@@ -35,46 +41,31 @@ export default function Timer({
status = 'success', status = 'success',
}: TimerProps) { }: TimerProps) {
const [clockStr, setClockStr] = useState(''); const [clockStr, setClockStr] = useState('');
const [timer, setTimer] = useState<NodeJS.Timeout>(); const timer = useRef<NodeJS.Timeout>();
const stopTimer = () => {
if (timer) {
clearInterval(timer);
setTimer(undefined);
}
};
const stopwatch = () => {
if (startTime) {
const endDttm = endTime || now();
if (startTime < endDttm) {
setClockStr(fDuration(startTime, endDttm));
}
if (!isRunning) {
stopTimer();
}
}
};
const startTimer = () => {
setTimer(setInterval(stopwatch, 30));
};
useEffect(() => { useEffect(() => {
if (isRunning) { const stopTimer = () => {
startTimer(); if (timer.current) {
} clearInterval(timer.current);
}, [isRunning]); timer.current = undefined;
}
useEffect(() => {
return () => {
stopTimer();
}; };
});
return ( if (isRunning) {
<Label id="timer" bsStyle={status}> timer.current = setInterval(() => {
{clockStr} if (startTime) {
</Label> const endDttm = endTime || now();
); if (startTime < endDttm) {
setClockStr(fDuration(startTime, endDttm));
}
if (!isRunning) {
stopTimer();
}
}
}, 30);
}
return stopTimer;
}, [endTime, isRunning, startTime]);
return <TimerLabel bsStyle={status}>{clockStr}</TimerLabel>;
} }

View File

@@ -93,11 +93,6 @@ input[type='checkbox'] {
margin-right: 5px; margin-right: 5px;
} }
#timer {
width: 80px;
text-align: right;
}
.notbtn { .notbtn {
cursor: default; cursor: default;
box-shadow: none; box-shadow: none;