mirror of
https://github.com/apache/superset.git
synced 2026-04-19 08:04:53 +00:00
@@ -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')
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user