mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
* Migrate Query Source View page to React: skeleton
* Sync QueryView and QuerySource (#4430)
* Migrate schema browser to react (#4432)
* Restyle code with Prettier
* Migrate Query page to React: Save changes (#4452)
* Migrate query source to React: Set of updates (#4457)
* Migrate Query page to React: Visualization Tabs (#4453)
Co-Authored-By: Levko Kravets <levko.ne@gmail.com>
* Migrate Query Source page to React: Visualizations area (#4463)
* Migrate Query page to React: Delete visualization button (#4461)
* Migrate Query Source page to React: Visualization actions (#4467)
* Migrate Query pages to React: Execute query hook (#4470)
* Migrate Query Source page to React: Editor area (#4468)
* Migrate Query Source page to React: metadata, schedule and description blocks (#4476)
* Migrate Query page to React: Cancel query execution (#4496)
* Migrate Query Source page to React: refine code (#4499)
* Migrate Query Source page to React: alerts (#4504)
* Migrate Query Source page to React: unsaved changes alert (#4505)
* Migrate Query Source to React: resizable areas (v2) (#4503)
* Migrate Query page to React: Query View (#4455)
Co-authored-by: Levko Kravets <levko.ne@gmail.com>
* Switch React and Angular versions of pages (until Angular version removed)
* Migrate Query pages to React: fix permissions (#4506)
* Migrate Query Source page to React: don't reload when saving new query (#4507)
* Migrate Query pages to React: fix tests (#4509)
* Use skipParametersDirtyFlag in executeQuery
* Fix: cannot fork query from Query View page
* Optimize query editor: handle query text changes faster
* Revert "Optimize query editor: handle query text changes faster"
This reverts commit 2934e53be6.
* Reduce debounced time to 100
* Migrate Query pages to React: cleanup (#4512)
* Migrate Query pages to React: cleanup
* Further cleanup
* Remove unused dependencies
* Fix embed pages
* Attempt to fix flaky test
* Cleanup: explicitly register the last Angular component
* Move contents of /filters folder to /lib
* Remove unnecessary import
* Remove cy.wait from Parameters spec
Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
Co-authored-by: Levko Kravets <levko.ne@gmail.com>
593 lines
17 KiB
JavaScript
593 lines
17 KiB
JavaScript
import { createQuery } from "../../support/redash-api";
|
|
|
|
describe("Parameter", () => {
|
|
const expectDirtyStateChange = edit => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find(".parameter-input")
|
|
.should($el => {
|
|
assert.isUndefined($el.data("dirty"));
|
|
});
|
|
|
|
edit();
|
|
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find(".parameter-input")
|
|
.should($el => {
|
|
assert.isTrue($el.data("dirty"));
|
|
});
|
|
};
|
|
|
|
beforeEach(() => {
|
|
cy.login();
|
|
});
|
|
|
|
describe("Text Parameter", () => {
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Text Parameter",
|
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
|
options: {
|
|
parameters: [{ name: "test-parameter", title: "Test Parameter", type: "text" }],
|
|
},
|
|
};
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}`));
|
|
});
|
|
|
|
it("updates the results after clicking Apply", () => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.type("Redash");
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", "Redash");
|
|
});
|
|
|
|
it("sets dirty state when edited", () => {
|
|
expectDirtyStateChange(() => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.type("Redash");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Number Parameter", () => {
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Number Parameter",
|
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
|
options: {
|
|
parameters: [{ name: "test-parameter", title: "Test Parameter", type: "number" }],
|
|
},
|
|
};
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}`));
|
|
});
|
|
|
|
it("updates the results after clicking Apply", () => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.type("{selectall}42");
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", 42);
|
|
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.type("{selectall}31415");
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", 31415);
|
|
});
|
|
|
|
it("sets dirty state when edited", () => {
|
|
expectDirtyStateChange(() => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.type("{selectall}42");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Dropdown Parameter", () => {
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Dropdown Parameter",
|
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
|
options: {
|
|
parameters: [
|
|
{ name: "test-parameter", title: "Test Parameter", type: "enum", enumOptions: "value1\nvalue2\nvalue3" },
|
|
],
|
|
},
|
|
};
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
|
});
|
|
|
|
it("updates the results after selecting a value", () => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find(".ant-select")
|
|
.click();
|
|
|
|
cy.contains("li.ant-select-dropdown-menu-item", "value2").click();
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", "value2");
|
|
});
|
|
|
|
it("supports multi-selection", () => {
|
|
cy.clickThrough(`
|
|
ParameterSettings-test-parameter
|
|
AllowMultipleValuesCheckbox
|
|
QuotationSelect
|
|
DoubleQuotationMarkOption
|
|
SaveParameterSettings
|
|
`);
|
|
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find(".ant-select")
|
|
.click();
|
|
|
|
// select all unselected options
|
|
cy.get("li.ant-select-dropdown-menu-item").each($option => {
|
|
if (!$option.hasClass("ant-select-dropdown-menu-item-selected")) {
|
|
cy.wrap($option).click();
|
|
}
|
|
});
|
|
|
|
cy.getByTestId("QueryEditor").click(); // just to close the select menu
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", '"value1","value2","value3"');
|
|
});
|
|
|
|
it("sets dirty state when edited", () => {
|
|
expectDirtyStateChange(() => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find(".ant-select")
|
|
.click();
|
|
|
|
cy.contains("li.ant-select-dropdown-menu-item", "value2").click();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Query Based Dropdown Parameter", () => {
|
|
beforeEach(() => {
|
|
const dropdownQueryData = {
|
|
name: "Dropdown Query",
|
|
query: `SELECT 'value1' AS name, 1 AS value UNION ALL
|
|
SELECT 'value2' AS name, 2 AS value UNION ALL
|
|
SELECT 'value3' AS name, 3 AS value`,
|
|
};
|
|
createQuery(dropdownQueryData, true).then(dropdownQuery => {
|
|
const queryData = {
|
|
name: "Query Based Dropdown Parameter",
|
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
|
options: {
|
|
parameters: [{ name: "test-parameter", title: "Test Parameter", type: "query", queryId: dropdownQuery.id }],
|
|
},
|
|
};
|
|
|
|
cy.visit(`/queries/${dropdownQuery.id}`);
|
|
cy.getByTestId("ExecuteButton").click();
|
|
cy.getByTestId("TableVisualization")
|
|
.should("contain", "value1")
|
|
.and("contain", "value2")
|
|
.and("contain", "value3");
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
|
});
|
|
});
|
|
|
|
it("supports multi-selection", () => {
|
|
cy.clickThrough(`
|
|
ParameterSettings-test-parameter
|
|
AllowMultipleValuesCheckbox
|
|
QuotationSelect
|
|
DoubleQuotationMarkOption
|
|
SaveParameterSettings
|
|
`);
|
|
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find(".ant-select")
|
|
.click();
|
|
|
|
// make sure all options are unselected and select all
|
|
cy.get("li.ant-select-dropdown-menu-item").each($option => {
|
|
expect($option).not.to.have.class("ant-select-dropdown-menu-item-selected");
|
|
cy.wrap($option).click();
|
|
});
|
|
|
|
cy.getByTestId("QueryEditor").click(); // just to close the select menu
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", '"1","2","3"');
|
|
});
|
|
});
|
|
|
|
describe("Date Parameter", () => {
|
|
const selectCalendarDate = date => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.click();
|
|
|
|
cy.get(".ant-calendar-date-panel")
|
|
.contains(".ant-calendar-date", date)
|
|
.click();
|
|
};
|
|
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Date Parameter",
|
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
|
options: {
|
|
parameters: [{ name: "test-parameter", title: "Test Parameter", type: "date", value: null }],
|
|
},
|
|
};
|
|
|
|
const now = new Date();
|
|
now.setDate(1);
|
|
cy.wrap(now.getTime()).as("now");
|
|
cy.clock(now.getTime(), ["Date"]);
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}`));
|
|
});
|
|
|
|
afterEach(() => {
|
|
cy.clock().then(clock => clock.restore());
|
|
});
|
|
|
|
it("updates the results after selecting a date", function() {
|
|
selectCalendarDate("15");
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", Cypress.moment(this.now).format("15/MM/YY"));
|
|
});
|
|
|
|
it("allows picking a dynamic date", function() {
|
|
cy.getByTestId("DynamicButton").click();
|
|
|
|
cy.getByTestId("DynamicButtonMenu")
|
|
.contains("Today/Now")
|
|
.click();
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", Cypress.moment(this.now).format("DD/MM/YY"));
|
|
});
|
|
|
|
it("sets dirty state when edited", () => {
|
|
expectDirtyStateChange(() => selectCalendarDate("15"));
|
|
});
|
|
});
|
|
|
|
describe("Date and Time Parameter", () => {
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Date and Time Parameter",
|
|
query: "SELECT '{{test-parameter}}' AS parameter",
|
|
options: {
|
|
parameters: [{ name: "test-parameter", title: "Test Parameter", type: "datetime-local", value: null }],
|
|
},
|
|
};
|
|
|
|
const now = new Date();
|
|
now.setDate(1);
|
|
cy.wrap(now.getTime()).as("now");
|
|
cy.clock(now.getTime(), ["Date"]);
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}`));
|
|
});
|
|
|
|
afterEach(() => {
|
|
cy.clock().then(clock => clock.restore());
|
|
});
|
|
|
|
it("updates the results after selecting a date and clicking in ok", function() {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.as("Input")
|
|
.click();
|
|
|
|
cy.get(".ant-calendar-date-panel")
|
|
.contains(".ant-calendar-date", "15")
|
|
.click();
|
|
|
|
cy.get(".ant-calendar-ok-btn").click();
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", Cypress.moment(this.now).format("YYYY-MM-15 HH:mm"));
|
|
});
|
|
|
|
it("shows the current datetime after clicking in Now", function() {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.as("Input")
|
|
.click();
|
|
|
|
cy.get(".ant-calendar-date-panel")
|
|
.contains("Now")
|
|
.click();
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", Cypress.moment(this.now).format("YYYY-MM-DD HH:mm"));
|
|
});
|
|
|
|
it("allows picking a dynamic date", function() {
|
|
cy.getByTestId("DynamicButton").click();
|
|
|
|
cy.getByTestId("DynamicButtonMenu")
|
|
.contains("Today/Now")
|
|
.click();
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
cy.getByTestId("TableVisualization").should("contain", Cypress.moment(this.now).format("YYYY-MM-DD HH:mm"));
|
|
});
|
|
|
|
it("sets dirty state when edited", () => {
|
|
expectDirtyStateChange(() => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.click();
|
|
|
|
cy.get(".ant-calendar-date-panel")
|
|
.contains("Now")
|
|
.click();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Date Range Parameter", () => {
|
|
const selectCalendarDateRange = (startDate, endDate) => {
|
|
cy.getByTestId("ParameterName-test-parameter")
|
|
.find("input")
|
|
.first()
|
|
.click();
|
|
|
|
cy.get(".ant-calendar-date-panel")
|
|
.contains(".ant-calendar-date", startDate)
|
|
.click();
|
|
|
|
cy.get(".ant-calendar-date-panel")
|
|
.contains(".ant-calendar-date", endDate)
|
|
.click();
|
|
};
|
|
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Date Range Parameter",
|
|
query: "SELECT '{{test-parameter.start}} - {{test-parameter.end}}' AS parameter",
|
|
options: {
|
|
parameters: [{ name: "test-parameter", title: "Test Parameter", type: "date-range" }],
|
|
},
|
|
};
|
|
|
|
const now = new Date();
|
|
now.setDate(1);
|
|
cy.wrap(now.getTime()).as("now");
|
|
cy.clock(now.getTime(), ["Date"]);
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
|
});
|
|
|
|
afterEach(() => {
|
|
cy.clock().then(clock => clock.restore());
|
|
});
|
|
|
|
it("updates the results after selecting a date range", function() {
|
|
selectCalendarDateRange("15", "20");
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
const now = Cypress.moment(this.now);
|
|
cy.getByTestId("TableVisualization").should(
|
|
"contain",
|
|
now.format("YYYY-MM-15") + " - " + now.format("YYYY-MM-20")
|
|
);
|
|
});
|
|
|
|
it("allows picking a dynamic date range", function() {
|
|
cy.getByTestId("DynamicButton").click();
|
|
|
|
cy.getByTestId("DynamicButtonMenu")
|
|
.contains("Last month")
|
|
.click();
|
|
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
|
|
const lastMonth = Cypress.moment(this.now).subtract(1, "month");
|
|
cy.getByTestId("TableVisualization").should(
|
|
"contain",
|
|
lastMonth.startOf("month").format("YYYY-MM-DD") + " - " + lastMonth.endOf("month").format("YYYY-MM-DD")
|
|
);
|
|
});
|
|
|
|
it("sets dirty state when edited", () => {
|
|
expectDirtyStateChange(() => selectCalendarDateRange("15", "20"));
|
|
});
|
|
});
|
|
|
|
describe("Apply Changes", () => {
|
|
const expectAppliedChanges = apply => {
|
|
cy.getByTestId("ParameterName-test-parameter-1")
|
|
.find("input")
|
|
.as("Input")
|
|
.type("Redash");
|
|
|
|
cy.getByTestId("ParameterName-test-parameter-2")
|
|
.find("input")
|
|
.type("Redash");
|
|
|
|
cy.location("search").should("not.contain", "Redash");
|
|
|
|
cy.server();
|
|
cy.route("POST", "api/queries/*/results").as("Results");
|
|
|
|
apply(cy.get("@Input"));
|
|
|
|
cy.location("search").should("contain", "Redash");
|
|
cy.wait("@Results");
|
|
};
|
|
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Testing Apply Button",
|
|
query: "SELECT '{{test-parameter-1}} {{ test-parameter-2 }}'",
|
|
options: {
|
|
parameters: [
|
|
{ name: "test-parameter-1", title: "Test Parameter 1", type: "text" },
|
|
{ name: "test-parameter-2", title: "Test Parameter 2", type: "text" },
|
|
],
|
|
},
|
|
};
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
|
});
|
|
|
|
it("shows and hides according to parameter dirty state", () => {
|
|
cy.getByTestId("ParameterApplyButton").should("not.be", "visible");
|
|
|
|
cy.getByTestId("ParameterName-test-parameter-1")
|
|
.find("input")
|
|
.as("Param")
|
|
.type("Redash");
|
|
|
|
cy.getByTestId("ParameterApplyButton").should("be", "visible");
|
|
|
|
cy.get("@Param").clear();
|
|
|
|
cy.getByTestId("ParameterApplyButton").should("not.be", "visible");
|
|
});
|
|
|
|
it("updates dirty counter", () => {
|
|
cy.getByTestId("ParameterName-test-parameter-1")
|
|
.find("input")
|
|
.type("Redash");
|
|
|
|
cy.getByTestId("ParameterApplyButton")
|
|
.find(".ant-badge-count p.current")
|
|
.should("contain", "1");
|
|
|
|
cy.getByTestId("ParameterName-test-parameter-2")
|
|
.find("input")
|
|
.type("Redash");
|
|
|
|
cy.getByTestId("ParameterApplyButton")
|
|
.find(".ant-badge-count p.current")
|
|
.should("contain", "2");
|
|
});
|
|
|
|
it('applies changes from "Apply Changes" button', () => {
|
|
expectAppliedChanges(() => {
|
|
cy.getByTestId("ParameterApplyButton").click();
|
|
});
|
|
});
|
|
|
|
it('applies changes from "alt+enter" keyboard shortcut', () => {
|
|
expectAppliedChanges(input => {
|
|
input.type("{alt}{enter}");
|
|
});
|
|
});
|
|
|
|
it('disables "Execute" button', () => {
|
|
cy.getByTestId("ParameterName-test-parameter-1")
|
|
.find("input")
|
|
.as("Input")
|
|
.type("Redash");
|
|
cy.getByTestId("ExecuteButton").should("be.disabled");
|
|
|
|
cy.get("@Input").clear();
|
|
cy.getByTestId("ExecuteButton").should("be.enabled");
|
|
});
|
|
});
|
|
|
|
describe("Draggable", () => {
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Draggable",
|
|
query: "SELECT '{{param1}}', '{{param2}}', '{{param3}}', '{{param4}}' AS parameter",
|
|
options: {
|
|
parameters: [
|
|
{ name: "param1", title: "Parameter 1", type: "text" },
|
|
{ name: "param2", title: "Parameter 2", type: "text" },
|
|
{ name: "param3", title: "Parameter 3", type: "text" },
|
|
{ name: "param4", title: "Parameter 4", type: "text" },
|
|
],
|
|
},
|
|
};
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
|
|
|
cy.get(".parameter-block")
|
|
.first()
|
|
.invoke("width")
|
|
.as("paramWidth");
|
|
|
|
cy.get("body").type("{alt}D"); // hide schema browser
|
|
});
|
|
|
|
const dragParam = (paramName, offsetLeft, offsetTop) => {
|
|
cy.getByTestId(`DragHandle-${paramName}`)
|
|
.trigger("mouseover")
|
|
.trigger("mousedown");
|
|
|
|
cy.get(".parameter-dragged .drag-handle")
|
|
.trigger("mousemove", offsetLeft, offsetTop, { force: true })
|
|
.trigger("mouseup", { force: true });
|
|
};
|
|
|
|
it("is possible to rearrange parameters", function() {
|
|
cy.server();
|
|
cy.route("POST", "api/queries/*").as("QuerySave");
|
|
|
|
dragParam("param1", this.paramWidth, 1);
|
|
cy.wait("@QuerySave");
|
|
dragParam("param4", -this.paramWidth, 1);
|
|
cy.wait("@QuerySave");
|
|
|
|
cy.reload();
|
|
|
|
const expectedOrder = ["Parameter 2", "Parameter 1", "Parameter 4", "Parameter 3"];
|
|
cy.get(".parameter-container label").each(($label, index) => expect($label).to.have.text(expectedOrder[index]));
|
|
});
|
|
});
|
|
|
|
describe("Parameter Settings", () => {
|
|
beforeEach(() => {
|
|
const queryData = {
|
|
name: "Draggable",
|
|
query: "SELECT '{{parameter}}' AS parameter",
|
|
options: {
|
|
parameters: [{ name: "parameter", title: "Parameter", type: "text" }],
|
|
},
|
|
};
|
|
|
|
createQuery(queryData, false).then(({ id }) => cy.visit(`/queries/${id}/source`));
|
|
|
|
cy.getByTestId("ParameterSettings-parameter").click();
|
|
});
|
|
|
|
it("changes the parameter title", () => {
|
|
cy.getByTestId("ParameterTitleInput").type("{selectall}New Parameter Name");
|
|
cy.getByTestId("SaveParameterSettings").click();
|
|
|
|
cy.contains("Query saved");
|
|
cy.reload();
|
|
|
|
cy.getByTestId("ParameterName-parameter").contains("label", "New Parameter Name");
|
|
});
|
|
});
|
|
});
|