mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Merge branch 'master' into dependabot/npm_and_yarn/tar-fs-2.1.3
This commit is contained in:
2
.github/workflows/preview-image.yml
vendored
2
.github/workflows/preview-image.yml
vendored
@@ -121,7 +121,7 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
build-args: |
|
build-args: |
|
||||||
test_all_deps=true
|
test_all_deps=true
|
||||||
outputs: type=image,push-by-digest=true,push=true
|
outputs: type=image,push-by-digest=false,push=true
|
||||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
|
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
|
||||||
linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent);
|
linear-gradient(to left, #b3babf, #b3babf 1px, transparent 1px, transparent);
|
||||||
background-size: calc((100% + 15px) / 6) 5px;
|
background-size: calc((100% + 15px) / 12) 5px;
|
||||||
background-position: -7px 1px;
|
background-position: -7px 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function wrapComponentWithSettings(WrappedComponent) {
|
|||||||
"dateTimeFormat",
|
"dateTimeFormat",
|
||||||
"integerFormat",
|
"integerFormat",
|
||||||
"floatFormat",
|
"floatFormat",
|
||||||
|
"nullValue",
|
||||||
"booleanValues",
|
"booleanValues",
|
||||||
"tableCellMaxJSONSize",
|
"tableCellMaxJSONSize",
|
||||||
"allowCustomJSVisualizations",
|
"allowCustomJSVisualizations",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
export default {
|
export default {
|
||||||
columns: 6, // grid columns count
|
columns: 12, // grid columns count
|
||||||
rowHeight: 50, // grid row height (incl. bottom padding)
|
rowHeight: 50, // grid row height (incl. bottom padding)
|
||||||
margins: 15, // widget margins
|
margins: 15, // widget margins
|
||||||
mobileBreakPoint: 800,
|
mobileBreakPoint: 800,
|
||||||
// defaults for widgets
|
// defaults for widgets
|
||||||
defaultSizeX: 3,
|
defaultSizeX: 6,
|
||||||
defaultSizeY: 3,
|
defaultSizeY: 3,
|
||||||
minSizeX: 1,
|
minSizeX: 2,
|
||||||
maxSizeX: 6,
|
maxSizeX: 12,
|
||||||
minSizeY: 1,
|
minSizeY: 2,
|
||||||
maxSizeY: 1000,
|
maxSizeY: 1000,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe("Dashboard", () => {
|
|||||||
cy.getByTestId("DashboardSaveButton").click();
|
cy.getByTestId("DashboardSaveButton").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.wait("@NewDashboard").then(xhr => {
|
cy.wait("@NewDashboard").then((xhr) => {
|
||||||
const id = Cypress._.get(xhr, "response.body.id");
|
const id = Cypress._.get(xhr, "response.body.id");
|
||||||
assert.isDefined(id, "Dashboard api call returns id");
|
assert.isDefined(id, "Dashboard api call returns id");
|
||||||
|
|
||||||
@@ -40,13 +40,9 @@ describe("Dashboard", () => {
|
|||||||
|
|
||||||
cy.getByTestId("DashboardMoreButton").click();
|
cy.getByTestId("DashboardMoreButton").click();
|
||||||
|
|
||||||
cy.getByTestId("DashboardMoreButtonMenu")
|
cy.getByTestId("DashboardMoreButtonMenu").contains("Archive").click();
|
||||||
.contains("Archive")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.get(".ant-modal .ant-btn")
|
cy.get(".ant-modal .ant-btn").contains("Archive").click({ force: true });
|
||||||
.contains("Archive")
|
|
||||||
.click({ force: true });
|
|
||||||
cy.get(".label-tag-archived").should("exist");
|
cy.get(".label-tag-archived").should("exist");
|
||||||
|
|
||||||
cy.visit("/dashboards");
|
cy.visit("/dashboards");
|
||||||
@@ -60,7 +56,7 @@ describe("Dashboard", () => {
|
|||||||
cy.server();
|
cy.server();
|
||||||
cy.route("GET", "**/api/dashboards/*").as("LoadDashboard");
|
cy.route("GET", "**/api/dashboards/*").as("LoadDashboard");
|
||||||
cy.createDashboard("Dashboard multiple urls").then(({ id, slug }) => {
|
cy.createDashboard("Dashboard multiple urls").then(({ id, slug }) => {
|
||||||
[`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach(url => {
|
[`/dashboards/${id}`, `/dashboards/${id}-anything-here`, `/dashboard/${slug}`].forEach((url) => {
|
||||||
cy.visit(url);
|
cy.visit(url);
|
||||||
cy.wait("@LoadDashboard");
|
cy.wait("@LoadDashboard");
|
||||||
cy.getByTestId(`DashboardId${id}Container`).should("exist");
|
cy.getByTestId(`DashboardId${id}Container`).should("exist");
|
||||||
@@ -72,7 +68,7 @@ describe("Dashboard", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
context("viewport width is at 800px", () => {
|
context("viewport width is at 800px", () => {
|
||||||
before(function() {
|
before(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar")
|
cy.createDashboard("Foo Bar")
|
||||||
.then(({ id }) => {
|
.then(({ id }) => {
|
||||||
@@ -80,49 +76,42 @@ describe("Dashboard", () => {
|
|||||||
this.dashboardEditUrl = `/dashboards/${id}?edit`;
|
this.dashboardEditUrl = `/dashboards/${id}?edit`;
|
||||||
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).as("textboxEl");
|
cy.getByTestId(elTestId).as("textboxEl");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.viewport(800 + menuWidth, 800);
|
cy.viewport(800 + menuWidth, 800);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows widgets with full width", () => {
|
it("shows widgets with full width", () => {
|
||||||
cy.get("@textboxEl").should($el => {
|
cy.get("@textboxEl").should(($el) => {
|
||||||
expect($el.width()).to.eq(770);
|
expect($el.width()).to.eq(770);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.get("@textboxEl").should($el => {
|
cy.get("@textboxEl").should(($el) => {
|
||||||
expect($el.width()).to.eq(378);
|
expect($el.width()).to.eq(182);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides edit option", () => {
|
it("hides edit option", () => {
|
||||||
cy.getByTestId("DashboardMoreButton")
|
cy.getByTestId("DashboardMoreButton").click().should("be.visible");
|
||||||
.click()
|
|
||||||
.should("be.visible");
|
|
||||||
|
|
||||||
cy.getByTestId("DashboardMoreButtonMenu")
|
cy.getByTestId("DashboardMoreButtonMenu").contains("Edit").as("editButton").should("not.be.visible");
|
||||||
.contains("Edit")
|
|
||||||
.as("editButton")
|
|
||||||
.should("not.be.visible");
|
|
||||||
|
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.get("@editButton").should("be.visible");
|
cy.get("@editButton").should("be.visible");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("disables edit mode", function() {
|
it("disables edit mode", function () {
|
||||||
cy.viewport(801 + menuWidth, 800);
|
cy.viewport(801 + menuWidth, 800);
|
||||||
cy.visit(this.dashboardEditUrl);
|
cy.visit(this.dashboardEditUrl);
|
||||||
cy.contains("button", "Done Editing")
|
cy.contains("button", "Done Editing").as("saveButton").should("exist");
|
||||||
.as("saveButton")
|
|
||||||
.should("exist");
|
|
||||||
|
|
||||||
cy.viewport(800 + menuWidth, 800);
|
cy.viewport(800 + menuWidth, 800);
|
||||||
cy.contains("button", "Done Editing").should("not.exist");
|
cy.contains("button", "Done Editing").should("not.exist");
|
||||||
@@ -130,14 +119,14 @@ describe("Dashboard", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
context("viewport width is at 767px", () => {
|
context("viewport width is at 767px", () => {
|
||||||
before(function() {
|
before(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar").then(({ id }) => {
|
cy.createDashboard("Foo Bar").then(({ id }) => {
|
||||||
this.dashboardUrl = `/dashboards/${id}`;
|
this.dashboardUrl = `/dashboards/${id}`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.viewport(767, 800);
|
cy.viewport(767, 800);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getWidgetTestId, editDashboard, resizeBy } from "../../support/dashboar
|
|||||||
const menuWidth = 80;
|
const menuWidth = 80;
|
||||||
|
|
||||||
describe("Grid compliant widgets", () => {
|
describe("Grid compliant widgets", () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.viewport(1215 + menuWidth, 800);
|
cy.viewport(1215 + menuWidth, 800);
|
||||||
cy.createDashboard("Foo Bar")
|
cy.createDashboard("Foo Bar")
|
||||||
@@ -13,7 +13,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
this.dashboardUrl = `/dashboards/${id}`;
|
this.dashboardUrl = `/dashboards/${id}`;
|
||||||
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).as("textboxEl");
|
cy.getByTestId(elTestId).as("textboxEl");
|
||||||
});
|
});
|
||||||
@@ -27,7 +27,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
|
|
||||||
it("stays put when dragged under snap threshold", () => {
|
it("stays put when dragged under snap threshold", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(90)
|
.dragBy(30)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 15 + menuWidth); // no change, 15 -> 15
|
.should("have.property", "left", 15 + menuWidth); // no change, 15 -> 15
|
||||||
});
|
});
|
||||||
@@ -36,14 +36,14 @@ describe("Grid compliant widgets", () => {
|
|||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(110)
|
.dragBy(110)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 215 + menuWidth); // moved by 200, 15 -> 215
|
.should("have.property", "left", 115 + menuWidth); // moved by 100, 15 -> 115
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves two columns when dragged over snap threshold", () => {
|
it("moves two columns when dragged over snap threshold", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.dragBy(330)
|
.dragBy(200)
|
||||||
.invoke("offset")
|
.invoke("offset")
|
||||||
.should("have.property", "left", 415 + menuWidth); // moved by 400, 15 -> 415
|
.should("have.property", "left", 215 + menuWidth); // moved by 200, 15 -> 215
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ describe("Grid compliant widgets", () => {
|
|||||||
cy.route("POST", "**/api/widgets/*").as("WidgetSave");
|
cy.route("POST", "**/api/widgets/*").as("WidgetSave");
|
||||||
|
|
||||||
editDashboard();
|
editDashboard();
|
||||||
cy.get("@textboxEl").dragBy(330);
|
cy.get("@textboxEl").dragBy(100);
|
||||||
cy.wait("@WidgetSave");
|
cy.wait("@WidgetSave");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -64,24 +64,24 @@ describe("Grid compliant widgets", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("stays put when dragged under snap threshold", () => {
|
it("stays put when dragged under snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 90)
|
resizeBy(cy.get("@textboxEl"), 30)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 585); // no change, 585 -> 585
|
.should("eq", 285); // no change, 285 -> 285
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves one column when dragged over snap threshold", () => {
|
it("moves one column when dragged over snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 110)
|
resizeBy(cy.get("@textboxEl"), 110)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 785); // resized by 200, 585 -> 785
|
.should("eq", 385); // resized by 200, 185 -> 385
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves two columns when dragged over snap threshold", () => {
|
it("moves two columns when dragged over snap threshold", () => {
|
||||||
resizeBy(cy.get("@textboxEl"), 400)
|
resizeBy(cy.get("@textboxEl"), 400)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("width")
|
.invoke("width")
|
||||||
.should("eq", 985); // resized by 400, 585 -> 985
|
.should("eq", 685); // resized by 400, 285 -> 685
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,16 +101,16 @@ describe("Grid compliant widgets", () => {
|
|||||||
resizeBy(cy.get("@textboxEl"), 0, 30)
|
resizeBy(cy.get("@textboxEl"), 0, 30)
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.invoke("height")
|
.invoke("height")
|
||||||
.should("eq", 185); // resized by 50, , 135 -> 185
|
.should("eq", 185);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shrinks to minimum", () => {
|
it("shrinks to minimum", () => {
|
||||||
cy.get("@textboxEl")
|
cy.get("@textboxEl")
|
||||||
.then($el => resizeBy(cy.get("@textboxEl"), -$el.width(), -$el.height())) // resize to 0,0
|
.then(($el) => resizeBy(cy.get("@textboxEl"), -$el.width(), -$el.height())) // resize to 0,0
|
||||||
.then(() => cy.get("@textboxEl"))
|
.then(() => cy.get("@textboxEl"))
|
||||||
.should($el => {
|
.should(($el) => {
|
||||||
expect($el.width()).to.eq(185); // min textbox width
|
expect($el.width()).to.eq(185); // min textbox width
|
||||||
expect($el.height()).to.eq(35); // min textbox height
|
expect($el.height()).to.eq(85); // min textbox height
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { getWidgetTestId, editDashboard } from "../../support/dashboard";
|
import { getWidgetTestId, editDashboard } from "../../support/dashboard";
|
||||||
|
|
||||||
describe("Textbox", () => {
|
describe("Textbox", () => {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.createDashboard("Foo Bar").then(({ id }) => {
|
cy.createDashboard("Foo Bar").then(({ id }) => {
|
||||||
this.dashboardId = id;
|
this.dashboardId = id;
|
||||||
@@ -12,12 +12,10 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const confirmDeletionInModal = () => {
|
const confirmDeletionInModal = () => {
|
||||||
cy.get(".ant-modal .ant-btn")
|
cy.get(".ant-modal .ant-btn").contains("Delete").click({ force: true });
|
||||||
.contains("Delete")
|
|
||||||
.click({ force: true });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
it("adds textbox", function() {
|
it("adds textbox", function () {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
cy.getByTestId("AddTextboxButton").click();
|
cy.getByTestId("AddTextboxButton").click();
|
||||||
@@ -29,10 +27,10 @@ describe("Textbox", () => {
|
|||||||
cy.get(".widget-text").should("exist");
|
cy.get(".widget-text").should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes textbox by X button", function() {
|
it("removes textbox by X button", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
|
|
||||||
@@ -45,32 +43,30 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes textbox by menu", function() {
|
it("removes textbox by menu", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId).within(() => {
|
cy.getByTestId(elTestId).within(() => {
|
||||||
cy.getByTestId("WidgetDropdownButton").click();
|
cy.getByTestId("WidgetDropdownButton").click();
|
||||||
});
|
});
|
||||||
cy.getByTestId("WidgetDropdownButtonMenu")
|
cy.getByTestId("WidgetDropdownButtonMenu").contains("Remove from Dashboard").click();
|
||||||
.contains("Remove from Dashboard")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
confirmDeletionInModal();
|
confirmDeletionInModal();
|
||||||
cy.getByTestId(elTestId).should("not.exist");
|
cy.getByTestId(elTestId).should("not.exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows opening menu after removal", function() {
|
it("allows opening menu after removal", function () {
|
||||||
let elTestId1;
|
let elTestId1;
|
||||||
cy.addTextbox(this.dashboardId, "txb 1")
|
cy.addTextbox(this.dashboardId, "txb 1")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
elTestId1 = elTestId;
|
elTestId1 = elTestId;
|
||||||
return cy.addTextbox(this.dashboardId, "txb 2").then(getWidgetTestId);
|
return cy.addTextbox(this.dashboardId, "txb 2").then(getWidgetTestId);
|
||||||
})
|
})
|
||||||
.then(elTestId2 => {
|
.then((elTestId2) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
editDashboard();
|
editDashboard();
|
||||||
|
|
||||||
@@ -97,10 +93,10 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("edits textbox", function() {
|
it("edits textbox", function () {
|
||||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
cy.getByTestId(elTestId)
|
cy.getByTestId(elTestId)
|
||||||
.as("textboxEl")
|
.as("textboxEl")
|
||||||
@@ -108,17 +104,13 @@ describe("Textbox", () => {
|
|||||||
cy.getByTestId("WidgetDropdownButton").click();
|
cy.getByTestId("WidgetDropdownButton").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getByTestId("WidgetDropdownButtonMenu")
|
cy.getByTestId("WidgetDropdownButtonMenu").contains("Edit").click();
|
||||||
.contains("Edit")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const newContent = "[edited]";
|
const newContent = "[edited]";
|
||||||
cy.getByTestId("TextboxDialog")
|
cy.getByTestId("TextboxDialog")
|
||||||
.should("exist")
|
.should("exist")
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("textarea")
|
cy.get("textarea").clear().type(newContent);
|
||||||
.clear()
|
|
||||||
.type(newContent);
|
|
||||||
cy.contains("button", "Save").click();
|
cy.contains("button", "Save").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -126,7 +118,7 @@ describe("Textbox", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders textbox according to position configuration", function() {
|
it("renders textbox according to position configuration", function () {
|
||||||
const id = this.dashboardId;
|
const id = this.dashboardId;
|
||||||
const txb1Pos = { col: 0, row: 0, sizeX: 3, sizeY: 2 };
|
const txb1Pos = { col: 0, row: 0, sizeX: 3, sizeY: 2 };
|
||||||
const txb2Pos = { col: 1, row: 1, sizeX: 3, sizeY: 4 };
|
const txb2Pos = { col: 1, row: 1, sizeX: 3, sizeY: 4 };
|
||||||
@@ -135,15 +127,15 @@ describe("Textbox", () => {
|
|||||||
cy.addTextbox(id, "x", { position: txb1Pos })
|
cy.addTextbox(id, "x", { position: txb1Pos })
|
||||||
.then(() => cy.addTextbox(id, "x", { position: txb2Pos }))
|
.then(() => cy.addTextbox(id, "x", { position: txb2Pos }))
|
||||||
.then(getWidgetTestId)
|
.then(getWidgetTestId)
|
||||||
.then(elTestId => {
|
.then((elTestId) => {
|
||||||
cy.visit(this.dashboardUrl);
|
cy.visit(this.dashboardUrl);
|
||||||
return cy.getByTestId(elTestId);
|
return cy.getByTestId(elTestId);
|
||||||
})
|
})
|
||||||
.should($el => {
|
.should(($el) => {
|
||||||
const { top, left } = $el.offset();
|
const { top, left } = $el.offset();
|
||||||
expect(top).to.be.oneOf([162, 162.015625]);
|
expect(top).to.be.oneOf([162, 162.015625]);
|
||||||
expect(left).to.eq(282);
|
expect(left).to.eq(188);
|
||||||
expect($el.width()).to.eq(545);
|
expect($el.width()).to.eq(265);
|
||||||
expect($el.height()).to.eq(185);
|
expect($el.height()).to.eq(185);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
26
migrations/versions/1655999df5e3_default_alert_selector.py
Normal file
26
migrations/versions/1655999df5e3_default_alert_selector.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""set default alert selector
|
||||||
|
|
||||||
|
Revision ID: 1655999df5e3
|
||||||
|
Revises: 9e8c841d1a30
|
||||||
|
Create Date: 2025-07-09 14:44:00
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1655999df5e3'
|
||||||
|
down_revision = '9e8c841d1a30'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.execute("""
|
||||||
|
UPDATE alerts
|
||||||
|
SET options = jsonb_set(options, '{selector}', '"first"')
|
||||||
|
WHERE options->>'selector' IS NULL;
|
||||||
|
""")
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
pass
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""12-column dashboard layout
|
||||||
|
|
||||||
|
Revision ID: db0aca1ebd32
|
||||||
|
Revises: 1655999df5e3
|
||||||
|
Create Date: 2025-03-31 13:45:43.160893
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'db0aca1ebd32'
|
||||||
|
down_revision = '1655999df5e3'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.execute("""
|
||||||
|
UPDATE widgets
|
||||||
|
SET options = jsonb_set(options, '{position,col}', to_json((options->'position'->>'col')::int * 2)::jsonb);
|
||||||
|
UPDATE widgets
|
||||||
|
SET options = jsonb_set(options, '{position,sizeX}', to_json((options->'position'->>'sizeX')::int * 2)::jsonb);
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.execute("""
|
||||||
|
UPDATE widgets
|
||||||
|
SET options = jsonb_set(options, '{position,col}', to_json((options->'position'->>'col')::int / 2)::jsonb);
|
||||||
|
UPDATE widgets
|
||||||
|
SET options = jsonb_set(options, '{position,sizeX}', to_json((options->'position'->>'sizeX')::int / 2)::jsonb);
|
||||||
|
""")
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
"mini-css-extract-plugin": "^1.6.2",
|
"mini-css-extract-plugin": "^1.6.2",
|
||||||
"mockdate": "^2.0.2",
|
"mockdate": "^2.0.2",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "3.3.2",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"react-refresh": "^0.14.0",
|
"react-refresh": "^0.14.0",
|
||||||
"react-test-renderer": "^16.14.0",
|
"react-test-renderer": "^16.14.0",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import requests
|
|||||||
from authlib.integrations.flask_client import OAuth
|
from authlib.integrations.flask_client import OAuth
|
||||||
from flask import Blueprint, flash, redirect, request, session, url_for
|
from flask import Blueprint, flash, redirect, request, session, url_for
|
||||||
|
|
||||||
from redash import models
|
from redash import models, settings
|
||||||
from redash.authentication import (
|
from redash.authentication import (
|
||||||
create_and_login_user,
|
create_and_login_user,
|
||||||
get_next_path,
|
get_next_path,
|
||||||
@@ -29,6 +29,41 @@ def verify_profile(org, profile):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_profile(access_token, logger):
|
||||||
|
headers = {"Authorization": f"OAuth {access_token}"}
|
||||||
|
response = requests.get("https://www.googleapis.com/oauth2/v1/userinfo", headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 401:
|
||||||
|
logger.warning("Failed getting user profile (response code 401).")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def build_redirect_uri():
|
||||||
|
scheme = settings.GOOGLE_OAUTH_SCHEME_OVERRIDE or None
|
||||||
|
return url_for(".callback", _external=True, _scheme=scheme)
|
||||||
|
|
||||||
|
|
||||||
|
def build_next_path(org_slug=None):
|
||||||
|
next_path = request.args.get("next")
|
||||||
|
if not next_path:
|
||||||
|
if org_slug is None:
|
||||||
|
org_slug = session.get("org_slug")
|
||||||
|
|
||||||
|
scheme = None
|
||||||
|
if settings.GOOGLE_OAUTH_SCHEME_OVERRIDE:
|
||||||
|
scheme = settings.GOOGLE_OAUTH_SCHEME_OVERRIDE
|
||||||
|
|
||||||
|
next_path = url_for(
|
||||||
|
"redash.index",
|
||||||
|
org_slug=org_slug,
|
||||||
|
_external=True,
|
||||||
|
_scheme=scheme,
|
||||||
|
)
|
||||||
|
return next_path
|
||||||
|
|
||||||
|
|
||||||
def create_google_oauth_blueprint(app):
|
def create_google_oauth_blueprint(app):
|
||||||
oauth = OAuth(app)
|
oauth = OAuth(app)
|
||||||
|
|
||||||
@@ -36,23 +71,12 @@ def create_google_oauth_blueprint(app):
|
|||||||
blueprint = Blueprint("google_oauth", __name__)
|
blueprint = Blueprint("google_oauth", __name__)
|
||||||
|
|
||||||
CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
|
CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
|
||||||
oauth = OAuth(app)
|
|
||||||
oauth.register(
|
oauth.register(
|
||||||
name="google",
|
name="google",
|
||||||
server_metadata_url=CONF_URL,
|
server_metadata_url=CONF_URL,
|
||||||
client_kwargs={"scope": "openid email profile"},
|
client_kwargs={"scope": "openid email profile"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_user_profile(access_token):
|
|
||||||
headers = {"Authorization": "OAuth {}".format(access_token)}
|
|
||||||
response = requests.get("https://www.googleapis.com/oauth2/v1/userinfo", headers=headers)
|
|
||||||
|
|
||||||
if response.status_code == 401:
|
|
||||||
logger.warning("Failed getting user profile (response code 401).")
|
|
||||||
return None
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
|
@blueprint.route("/<org_slug>/oauth/google", endpoint="authorize_org")
|
||||||
def org_login(org_slug):
|
def org_login(org_slug):
|
||||||
session["org_slug"] = current_org.slug
|
session["org_slug"] = current_org.slug
|
||||||
@@ -60,9 +84,9 @@ def create_google_oauth_blueprint(app):
|
|||||||
|
|
||||||
@blueprint.route("/oauth/google", endpoint="authorize")
|
@blueprint.route("/oauth/google", endpoint="authorize")
|
||||||
def login():
|
def login():
|
||||||
redirect_uri = url_for(".callback", _external=True)
|
redirect_uri = build_redirect_uri()
|
||||||
|
|
||||||
next_path = request.args.get("next", url_for("redash.index", org_slug=session.get("org_slug")))
|
next_path = build_next_path()
|
||||||
logger.debug("Callback url: %s", redirect_uri)
|
logger.debug("Callback url: %s", redirect_uri)
|
||||||
logger.debug("Next is: %s", next_path)
|
logger.debug("Next is: %s", next_path)
|
||||||
|
|
||||||
@@ -86,7 +110,7 @@ def create_google_oauth_blueprint(app):
|
|||||||
flash("Validation error. Please retry.")
|
flash("Validation error. Please retry.")
|
||||||
return redirect(url_for("redash.login"))
|
return redirect(url_for("redash.login"))
|
||||||
|
|
||||||
profile = get_user_profile(access_token)
|
profile = get_user_profile(access_token, logger)
|
||||||
if profile is None:
|
if profile is None:
|
||||||
flash("Validation error. Please retry.")
|
flash("Validation error. Please retry.")
|
||||||
return redirect(url_for("redash.login"))
|
return redirect(url_for("redash.login"))
|
||||||
@@ -110,7 +134,9 @@ def create_google_oauth_blueprint(app):
|
|||||||
if user is None:
|
if user is None:
|
||||||
return logout_and_redirect_to_index()
|
return logout_and_redirect_to_index()
|
||||||
|
|
||||||
unsafe_next_path = session.get("next_url") or url_for("redash.index", org_slug=org.slug)
|
unsafe_next_path = session.get("next_url")
|
||||||
|
if not unsafe_next_path:
|
||||||
|
unsafe_next_path = build_next_path(org.slug)
|
||||||
next_path = get_next_path(unsafe_next_path)
|
next_path = get_next_path(unsafe_next_path)
|
||||||
|
|
||||||
return redirect(next_path)
|
return redirect(next_path)
|
||||||
|
|||||||
@@ -255,6 +255,12 @@ def number_format_config():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def null_value_config():
|
||||||
|
return {
|
||||||
|
"nullValue": current_org.get_setting("null_value"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def client_config():
|
def client_config():
|
||||||
if not current_user.is_api_user() and current_user.is_authenticated:
|
if not current_user.is_api_user() and current_user.is_authenticated:
|
||||||
client_config = {
|
client_config = {
|
||||||
@@ -289,6 +295,7 @@ def client_config():
|
|||||||
client_config.update({"basePath": base_href()})
|
client_config.update({"basePath": base_href()})
|
||||||
client_config.update(date_time_format_config())
|
client_config.update(date_time_format_config())
|
||||||
client_config.update(number_format_config())
|
client_config.update(number_format_config())
|
||||||
|
client_config.update(null_value_config())
|
||||||
|
|
||||||
return client_config
|
return client_config
|
||||||
|
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
|||||||
db.session.query(tag_column, usage_count)
|
db.session.query(tag_column, usage_count)
|
||||||
.group_by(tag_column)
|
.group_by(tag_column)
|
||||||
.filter(Query.id.in_(queries.options(load_only("id"))))
|
.filter(Query.id.in_(queries.options(load_only("id"))))
|
||||||
.order_by(usage_count.desc())
|
.order_by(tag_column)
|
||||||
)
|
)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
@@ -1137,7 +1137,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
|
|||||||
db.session.query(tag_column, usage_count)
|
db.session.query(tag_column, usage_count)
|
||||||
.group_by(tag_column)
|
.group_by(tag_column)
|
||||||
.filter(Dashboard.id.in_(dashboards.options(load_only("id"))))
|
.filter(Dashboard.id.in_(dashboards.options(load_only("id"))))
|
||||||
.order_by(usage_count.desc())
|
.order_by(tag_column)
|
||||||
)
|
)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|||||||
@@ -313,6 +313,10 @@ class BigQuery(BaseSQLQueryRunner):
|
|||||||
queries = []
|
queries = []
|
||||||
for dataset in datasets:
|
for dataset in datasets:
|
||||||
dataset_id = dataset["datasetReference"]["datasetId"]
|
dataset_id = dataset["datasetReference"]["datasetId"]
|
||||||
|
location = dataset["location"]
|
||||||
|
if self._get_location() and location != self._get_location():
|
||||||
|
logger.debug("dataset location is different: %s", location)
|
||||||
|
continue
|
||||||
query = query_base.format(dataset_id=dataset_id)
|
query = query_base.format(dataset_id=dataset_id)
|
||||||
queries.append(query)
|
queries.append(query)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ def init_app(app):
|
|||||||
app.config["WTF_CSRF_CHECK_DEFAULT"] = False
|
app.config["WTF_CSRF_CHECK_DEFAULT"] = False
|
||||||
app.config["WTF_CSRF_SSL_STRICT"] = False
|
app.config["WTF_CSRF_SSL_STRICT"] = False
|
||||||
app.config["WTF_CSRF_TIME_LIMIT"] = settings.CSRF_TIME_LIMIT
|
app.config["WTF_CSRF_TIME_LIMIT"] = settings.CSRF_TIME_LIMIT
|
||||||
|
app.config["SESSION_COOKIE_NAME"] = settings.SESSION_COOKIE_NAME
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def inject_csrf_token(response):
|
def inject_csrf_token(response):
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ SESSION_COOKIE_SECURE = parse_boolean(os.environ.get("REDASH_SESSION_COOKIE_SECU
|
|||||||
# Whether the session cookie is set HttpOnly.
|
# Whether the session cookie is set HttpOnly.
|
||||||
SESSION_COOKIE_HTTPONLY = parse_boolean(os.environ.get("REDASH_SESSION_COOKIE_HTTPONLY", "true"))
|
SESSION_COOKIE_HTTPONLY = parse_boolean(os.environ.get("REDASH_SESSION_COOKIE_HTTPONLY", "true"))
|
||||||
SESSION_EXPIRY_TIME = int(os.environ.get("REDASH_SESSION_EXPIRY_TIME", 60 * 60 * 6))
|
SESSION_EXPIRY_TIME = int(os.environ.get("REDASH_SESSION_EXPIRY_TIME", 60 * 60 * 6))
|
||||||
|
SESSION_COOKIE_NAME = os.environ.get("REDASH_SESSION_COOKIE_NAME", "session")
|
||||||
|
|
||||||
# Whether the session cookie is set to secure.
|
# Whether the session cookie is set to secure.
|
||||||
REMEMBER_COOKIE_SECURE = parse_boolean(os.environ.get("REDASH_REMEMBER_COOKIE_SECURE") or str(COOKIES_SECURE))
|
REMEMBER_COOKIE_SECURE = parse_boolean(os.environ.get("REDASH_REMEMBER_COOKIE_SECURE") or str(COOKIES_SECURE))
|
||||||
@@ -135,6 +136,13 @@ FEATURE_POLICY = os.environ.get("REDASH_FEATURE_POLICY", "")
|
|||||||
|
|
||||||
MULTI_ORG = parse_boolean(os.environ.get("REDASH_MULTI_ORG", "false"))
|
MULTI_ORG = parse_boolean(os.environ.get("REDASH_MULTI_ORG", "false"))
|
||||||
|
|
||||||
|
# If Redash is behind a proxy it might sometimes receive a X-Forwarded-Proto of HTTP
|
||||||
|
# even if your actual Redash URL scheme is HTTPS. This will cause Flask to build
|
||||||
|
# the OAuth redirect URL incorrectly thus failing auth. This is especially common if
|
||||||
|
# you're behind a SSL/TCP configured AWS ELB or similar.
|
||||||
|
# This setting will force the URL scheme.
|
||||||
|
GOOGLE_OAUTH_SCHEME_OVERRIDE = os.environ.get("REDASH_GOOGLE_OAUTH_SCHEME_OVERRIDE", "")
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID = os.environ.get("REDASH_GOOGLE_CLIENT_ID", "")
|
GOOGLE_CLIENT_ID = os.environ.get("REDASH_GOOGLE_CLIENT_ID", "")
|
||||||
GOOGLE_CLIENT_SECRET = os.environ.get("REDASH_GOOGLE_CLIENT_SECRET", "")
|
GOOGLE_CLIENT_SECRET = os.environ.get("REDASH_GOOGLE_CLIENT_SECRET", "")
|
||||||
GOOGLE_OAUTH_ENABLED = bool(GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET)
|
GOOGLE_OAUTH_ENABLED = bool(GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ DATE_FORMAT = os.environ.get("REDASH_DATE_FORMAT", "DD/MM/YY")
|
|||||||
TIME_FORMAT = os.environ.get("REDASH_TIME_FORMAT", "HH:mm")
|
TIME_FORMAT = os.environ.get("REDASH_TIME_FORMAT", "HH:mm")
|
||||||
INTEGER_FORMAT = os.environ.get("REDASH_INTEGER_FORMAT", "0,0")
|
INTEGER_FORMAT = os.environ.get("REDASH_INTEGER_FORMAT", "0,0")
|
||||||
FLOAT_FORMAT = os.environ.get("REDASH_FLOAT_FORMAT", "0,0.00")
|
FLOAT_FORMAT = os.environ.get("REDASH_FLOAT_FORMAT", "0,0.00")
|
||||||
|
NULL_VALUE = os.environ.get("REDASH_NULL_VALUE", "null")
|
||||||
MULTI_BYTE_SEARCH_ENABLED = parse_boolean(os.environ.get("MULTI_BYTE_SEARCH_ENABLED", "false"))
|
MULTI_BYTE_SEARCH_ENABLED = parse_boolean(os.environ.get("MULTI_BYTE_SEARCH_ENABLED", "false"))
|
||||||
|
|
||||||
JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
|
JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
|
||||||
@@ -59,6 +60,7 @@ settings = {
|
|||||||
"time_format": TIME_FORMAT,
|
"time_format": TIME_FORMAT,
|
||||||
"integer_format": INTEGER_FORMAT,
|
"integer_format": INTEGER_FORMAT,
|
||||||
"float_format": FLOAT_FORMAT,
|
"float_format": FLOAT_FORMAT,
|
||||||
|
"null_value": NULL_VALUE,
|
||||||
"multi_byte_search_enabled": MULTI_BYTE_SEARCH_ENABLED,
|
"multi_byte_search_enabled": MULTI_BYTE_SEARCH_ENABLED,
|
||||||
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
|
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
|
||||||
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,
|
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"less-loader": "^11.1.3",
|
"less-loader": "^11.1.3",
|
||||||
"less-plugin-autoprefix": "^2.0.0",
|
"less-plugin-autoprefix": "^2.0.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "3.3.2",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"style-loader": "^3.3.3",
|
"style-loader": "^3.3.3",
|
||||||
"ts-migrate": "^0.1.35",
|
"ts-migrate": "^0.1.35",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import numeral from "numeral";
|
|||||||
import { isString, isArray, isUndefined, isFinite, isNil, toString } from "lodash";
|
import { isString, isArray, isUndefined, isFinite, isNil, toString } from "lodash";
|
||||||
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
|
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
|
||||||
|
|
||||||
|
|
||||||
numeral.options.scalePercentBy100 = false;
|
numeral.options.scalePercentBy100 = false;
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -12,9 +13,16 @@ const urlPattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019
|
|||||||
|
|
||||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||||
|
|
||||||
|
function NullValueComponent() {
|
||||||
|
return <span className="display-as-null">{visualizationsSettings.nullValue}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
export function createTextFormatter(highlightLinks: any) {
|
export function createTextFormatter(highlightLinks: any) {
|
||||||
if (highlightLinks) {
|
if (highlightLinks) {
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return <NullValueComponent/>
|
||||||
|
}
|
||||||
if (isString(value)) {
|
if (isString(value)) {
|
||||||
const Link = visualizationsSettings.LinkComponent;
|
const Link = visualizationsSettings.LinkComponent;
|
||||||
value = value.replace(urlPattern, (unused, prefix, href) => {
|
value = value.replace(urlPattern, (unused, prefix, href) => {
|
||||||
@@ -29,7 +37,7 @@ export function createTextFormatter(highlightLinks: any) {
|
|||||||
return toString(value);
|
return toString(value);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (value: any) => toString(value);
|
return (value: any) => value === null ? <NullValueComponent/> : toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMoment(value: any) {
|
function toMoment(value: any) {
|
||||||
@@ -46,11 +54,14 @@ function toMoment(value: any) {
|
|||||||
export function createDateTimeFormatter(format: any) {
|
export function createDateTimeFormatter(format: any) {
|
||||||
if (isString(format) && format !== "") {
|
if (isString(format) && format !== "") {
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return <NullValueComponent/>;
|
||||||
|
}
|
||||||
const wrapped = toMoment(value);
|
const wrapped = toMoment(value);
|
||||||
return wrapped.isValid() ? wrapped.format(format) : toString(value);
|
return wrapped.isValid() ? wrapped.format(format) : toString(value);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (value: any) => toString(value);
|
return (value: any) => value === null ? <NullValueComponent/> : toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBooleanFormatter(values: any) {
|
export function createBooleanFormatter(values: any) {
|
||||||
@@ -58,6 +69,9 @@ export function createBooleanFormatter(values: any) {
|
|||||||
if (values.length >= 2) {
|
if (values.length >= 2) {
|
||||||
// Both `true` and `false` specified
|
// Both `true` and `false` specified
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return <NullValueComponent/>;
|
||||||
|
}
|
||||||
if (isNil(value)) {
|
if (isNil(value)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -69,6 +83,9 @@ export function createBooleanFormatter(values: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (value: any) => {
|
return (value: any) => {
|
||||||
|
if (value === null) {
|
||||||
|
return <NullValueComponent/>;
|
||||||
|
}
|
||||||
if (isNil(value)) {
|
if (isNil(value)) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -76,12 +93,20 @@ export function createBooleanFormatter(values: any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNumberFormatter(format: any) {
|
export function createNumberFormatter(format: any, canReturnHTMLElement: boolean = false) {
|
||||||
if (isString(format) && format !== "") {
|
if (isString(format) && format !== "") {
|
||||||
const n = numeral(0); // cache `numeral` instance
|
const n = numeral(0); // cache `numeral` instance
|
||||||
return (value: any) => (value === null || value === "" ? "" : n.set(value).format(format));
|
return (value: any) => {
|
||||||
|
if (canReturnHTMLElement && value === null) {
|
||||||
|
return <NullValueComponent/>;
|
||||||
|
}
|
||||||
|
if (value === "" || value === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return n.set(value).format(format);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (value: any) => toString(value);
|
return (value: any) => (canReturnHTMLElement && value === null) ? <NullValueComponent/> : toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSimpleTemplate(str: any, data: any) {
|
export function formatSimpleTemplate(str: any, data: any) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default {
|
|||||||
Renderer,
|
Renderer,
|
||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
defaultRows: 8,
|
defaultRows: 8,
|
||||||
minColumns: 1,
|
minColumns: 1,
|
||||||
minRows: 5,
|
minRows: 5,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default {
|
|||||||
Renderer,
|
Renderer,
|
||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
defaultRows: 8,
|
defaultRows: 8,
|
||||||
minColumns: 2,
|
minColumns: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ export default {
|
|||||||
Renderer,
|
Renderer,
|
||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultColumns: 2,
|
defaultColumns: 4,
|
||||||
defaultRows: 5,
|
defaultRows: 5,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ export default {
|
|||||||
...options,
|
...options,
|
||||||
}),
|
}),
|
||||||
Renderer: DetailsRenderer,
|
Renderer: DetailsRenderer,
|
||||||
defaultColumns: 2,
|
defaultColumns: 4,
|
||||||
defaultRows: 2,
|
defaultRows: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default {
|
|||||||
Renderer,
|
Renderer,
|
||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
defaultRows: 8,
|
defaultRows: 8,
|
||||||
minColumns: 2,
|
minColumns: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ export default {
|
|||||||
Editor,
|
Editor,
|
||||||
|
|
||||||
defaultRows: 10,
|
defaultRows: 10,
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
minColumns: 2,
|
minColumns: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "a",
|
"title": "a",
|
||||||
@@ -56,6 +57,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "a",
|
"title": "a",
|
||||||
@@ -89,6 +91,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "test",
|
"title": "test",
|
||||||
@@ -122,6 +125,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "a",
|
"title": "a",
|
||||||
@@ -155,6 +159,7 @@ Object {
|
|||||||
"linkTitleTemplate": "{{ @ }}",
|
"linkTitleTemplate": "{{ @ }}",
|
||||||
"linkUrlTemplate": "{{ @ }}",
|
"linkUrlTemplate": "{{ @ }}",
|
||||||
"name": "a",
|
"name": "a",
|
||||||
|
"nullValue": "null",
|
||||||
"numberFormat": undefined,
|
"numberFormat": undefined,
|
||||||
"order": 100000,
|
"order": 100000,
|
||||||
"title": "a",
|
"title": "a",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function Editor({ column, onChange }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function initNumberColumn(column: any) {
|
export default function initNumberColumn(column: any) {
|
||||||
const format = createNumberFormatter(column.numberFormat);
|
const format = createNumberFormatter(column.numberFormat, true);
|
||||||
|
|
||||||
function prepareData(row: any) {
|
function prepareData(row: any) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ function getDefaultFormatOptions(column: any) {
|
|||||||
dateTimeFormat: dateTimeFormat[column.type],
|
dateTimeFormat: dateTimeFormat[column.type],
|
||||||
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
|
||||||
numberFormat: numberFormat[column.type],
|
numberFormat: numberFormat[column.type],
|
||||||
|
nullValue: visualizationsSettings.nullValue,
|
||||||
booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
|
booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
|
||||||
// `image` cell options
|
// `image` cell options
|
||||||
imageUrlTemplate: "{{ @ }}",
|
imageUrlTemplate: "{{ @ }}",
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export default {
|
|||||||
|
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
defaultRows: 14,
|
defaultRows: 14,
|
||||||
defaultColumns: 3,
|
defaultColumns: 6,
|
||||||
minColumns: 2,
|
minColumns: 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,11 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.display-as-null {
|
||||||
|
font-style: italic;
|
||||||
|
color: @text-muted;
|
||||||
|
}
|
||||||
|
|
||||||
.table-visualization-spacer {
|
.table-visualization-spacer {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const visualizationsSettings = {
|
|||||||
dateTimeFormat: "DD/MM/YYYY HH:mm",
|
dateTimeFormat: "DD/MM/YYYY HH:mm",
|
||||||
integerFormat: "0,0",
|
integerFormat: "0,0",
|
||||||
floatFormat: "0,0.00",
|
floatFormat: "0,0.00",
|
||||||
|
nullValue: "null",
|
||||||
booleanValues: ["false", "true"],
|
booleanValues: ["false", "true"],
|
||||||
tableCellMaxJSONSize: 50000,
|
tableCellMaxJSONSize: 50000,
|
||||||
allowCustomJSVisualizations: false,
|
allowCustomJSVisualizations: false,
|
||||||
|
|||||||
@@ -7723,10 +7723,10 @@ prelude-ls@~1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||||
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
||||||
|
|
||||||
prettier@^1.19.1:
|
prettier@3.3.2:
|
||||||
version "1.19.1"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a"
|
||||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==
|
||||||
|
|
||||||
pretty-format@^24.9.0:
|
pretty-format@^24.9.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
|
|||||||
@@ -11156,10 +11156,10 @@ prelude-ls@~1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||||
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
||||||
|
|
||||||
prettier@^1.19.1:
|
prettier@3.3.2:
|
||||||
version "1.19.1"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a"
|
||||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==
|
||||||
|
|
||||||
pretty-bytes@^5.6.0:
|
pretty-bytes@^5.6.0:
|
||||||
version "5.6.0"
|
version "5.6.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user