mirror of
https://github.com/getredash/redash.git
synced 2025-12-25 01:03:20 -05:00
Compare commits
24 Commits
25.05.0-de
...
v25.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67a95e9dcc | ||
|
|
35afe880a1 | ||
|
|
a6298f2753 | ||
|
|
e69283f488 | ||
|
|
09ed3c4b81 | ||
|
|
f5e2a4c0fc | ||
|
|
4e200b4a08 | ||
|
|
5ae1f70d9e | ||
|
|
3f781d262b | ||
|
|
a34c1591e3 | ||
|
|
9f76fda18c | ||
|
|
d8ae679937 | ||
|
|
f3b0b60abd | ||
|
|
df8be91a07 | ||
|
|
c9ddd2a7d6 | ||
|
|
6b1e910126 | ||
|
|
14550a9a6c | ||
|
|
b80c5f6a7c | ||
|
|
e46d44f208 | ||
|
|
a1a4bc9d3e | ||
|
|
0900178d24 | ||
|
|
5d31429ca8 | ||
|
|
2f35ceb803 | ||
|
|
8e6c02ecde |
3
.github/workflows/periodic-snapshot.yml
vendored
3
.github/workflows/periodic-snapshot.yml
vendored
@@ -2,7 +2,7 @@ name: Periodic Snapshot
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '10 0 1 * *' # 10 minutes after midnight on the first of every month
|
||||
- cron: '10 0 1 * *' # 10 minutes after midnight on the first day of every month
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
@@ -24,6 +24,7 @@ permissions:
|
||||
jobs:
|
||||
bump-version-and-tag:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
8
.github/workflows/preview-image.yml
vendored
8
.github/workflows/preview-image.yml
vendored
@@ -100,8 +100,8 @@ jobs:
|
||||
if: ${{ github.event.inputs.dockerRepository == 'preview' || !github.event.workflow_run }}
|
||||
with:
|
||||
tags: |
|
||||
${{ vars.DOCKER_USER }}/redash
|
||||
${{ vars.DOCKER_USER }}/preview
|
||||
${{ vars.DOCKER_REPOSITORY }}/redash
|
||||
${{ vars.DOCKER_REPOSITORY }}/preview
|
||||
context: .
|
||||
build-args: |
|
||||
test_all_deps=true
|
||||
@@ -117,11 +117,11 @@ jobs:
|
||||
if: ${{ github.event.inputs.dockerRepository == 'redash' }}
|
||||
with:
|
||||
tags: |
|
||||
${{ vars.DOCKER_USER }}/redash:${{ steps.version.outputs.VERSION_TAG }}
|
||||
${{ vars.DOCKER_REPOSITORY }}/redash:${{ steps.version.outputs.VERSION_TAG }}
|
||||
context: .
|
||||
build-args: |
|
||||
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-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
env:
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
right: 0;
|
||||
background: linear-gradient(to bottom, transparent, transparent 2px, #f6f8f9 2px, #f6f8f9 5px),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ function wrapComponentWithSettings(WrappedComponent) {
|
||||
"dateTimeFormat",
|
||||
"integerFormat",
|
||||
"floatFormat",
|
||||
"nullValue",
|
||||
"booleanValues",
|
||||
"tableCellMaxJSONSize",
|
||||
"allowCustomJSVisualizations",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export default {
|
||||
columns: 6, // grid columns count
|
||||
columns: 12, // grid columns count
|
||||
rowHeight: 50, // grid row height (incl. bottom padding)
|
||||
margins: 15, // widget margins
|
||||
mobileBreakPoint: 800,
|
||||
// defaults for widgets
|
||||
defaultSizeX: 3,
|
||||
defaultSizeX: 6,
|
||||
defaultSizeY: 3,
|
||||
minSizeX: 1,
|
||||
maxSizeX: 6,
|
||||
minSizeY: 1,
|
||||
minSizeX: 2,
|
||||
maxSizeX: 12,
|
||||
minSizeY: 2,
|
||||
maxSizeY: 1000,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const logger = debug("redash:services:QueryResult");
|
||||
const filterTypes = ["filter", "multi-filter", "multiFilter"];
|
||||
|
||||
function defer() {
|
||||
const result = { onStatusChange: status => {} };
|
||||
const result = { onStatusChange: (status) => {} };
|
||||
result.promise = new Promise((resolve, reject) => {
|
||||
result.resolve = resolve;
|
||||
result.reject = reject;
|
||||
@@ -40,13 +40,13 @@ function getColumnNameWithoutType(column) {
|
||||
}
|
||||
|
||||
function getColumnFriendlyName(column) {
|
||||
return getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, a => a.toUpperCase());
|
||||
return getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, (a) => a.toUpperCase());
|
||||
}
|
||||
|
||||
const createOrSaveUrl = data => (data.id ? `api/query_results/${data.id}` : "api/query_results");
|
||||
const createOrSaveUrl = (data) => (data.id ? `api/query_results/${data.id}` : "api/query_results");
|
||||
const QueryResultResource = {
|
||||
get: ({ id }) => axios.get(`api/query_results/${id}`),
|
||||
post: data => axios.post(createOrSaveUrl(data), data),
|
||||
post: (data) => axios.post(createOrSaveUrl(data), data),
|
||||
};
|
||||
|
||||
export const ExecutionStatus = {
|
||||
@@ -97,11 +97,11 @@ function handleErrorResponse(queryResult, error) {
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function fetchDataFromJob(jobId, interval = 1000) {
|
||||
return axios.get(`api/jobs/${jobId}`).then(data => {
|
||||
return axios.get(`api/jobs/${jobId}`).then((data) => {
|
||||
const status = statuses[data.job.status];
|
||||
if (status === ExecutionStatus.WAITING || status === ExecutionStatus.PROCESSING) {
|
||||
return sleep(interval).then(() => fetchDataFromJob(data.job.id));
|
||||
@@ -146,7 +146,7 @@ class QueryResult {
|
||||
// TODO: we should stop manipulating incoming data, and switch to relaying
|
||||
// on the column type set by the backend. This logic is prone to errors,
|
||||
// and better be removed. Kept for now, for backward compatability.
|
||||
each(this.query_result.data.rows, row => {
|
||||
each(this.query_result.data.rows, (row) => {
|
||||
forOwn(row, (v, k) => {
|
||||
let newType = null;
|
||||
if (isNumber(v)) {
|
||||
@@ -173,7 +173,7 @@ class QueryResult {
|
||||
});
|
||||
});
|
||||
|
||||
each(this.query_result.data.columns, column => {
|
||||
each(this.query_result.data.columns, (column) => {
|
||||
column.name = "" + column.name;
|
||||
if (columnTypes[column.name]) {
|
||||
if (column.type == null || column.type === "string") {
|
||||
@@ -265,14 +265,14 @@ class QueryResult {
|
||||
|
||||
getColumnNames() {
|
||||
if (this.columnNames === undefined && this.query_result.data) {
|
||||
this.columnNames = this.query_result.data.columns.map(v => v.name);
|
||||
this.columnNames = this.query_result.data.columns.map((v) => v.name);
|
||||
}
|
||||
|
||||
return this.columnNames;
|
||||
}
|
||||
|
||||
getColumnFriendlyNames() {
|
||||
return this.getColumnNames().map(col => getColumnFriendlyName(col));
|
||||
return this.getColumnNames().map((col) => getColumnFriendlyName(col));
|
||||
}
|
||||
|
||||
getTruncated() {
|
||||
@@ -286,7 +286,7 @@ class QueryResult {
|
||||
|
||||
const filters = [];
|
||||
|
||||
this.getColumns().forEach(col => {
|
||||
this.getColumns().forEach((col) => {
|
||||
const name = col.name;
|
||||
const type = name.split("::")[1] || name.split("__")[1];
|
||||
if (includes(filterTypes, type)) {
|
||||
@@ -302,8 +302,8 @@ class QueryResult {
|
||||
}
|
||||
}, this);
|
||||
|
||||
this.getRawData().forEach(row => {
|
||||
filters.forEach(filter => {
|
||||
this.getRawData().forEach((row) => {
|
||||
filters.forEach((filter) => {
|
||||
filter.values.push(row[filter.name]);
|
||||
if (filter.values.length === 1) {
|
||||
if (filter.multiple) {
|
||||
@@ -315,8 +315,8 @@ class QueryResult {
|
||||
});
|
||||
});
|
||||
|
||||
filters.forEach(filter => {
|
||||
filter.values = uniqBy(filter.values, v => {
|
||||
filters.forEach((filter) => {
|
||||
filter.values = uniqBy(filter.values, (v) => {
|
||||
if (moment.isMoment(v)) {
|
||||
return v.unix();
|
||||
}
|
||||
@@ -345,12 +345,12 @@ class QueryResult {
|
||||
|
||||
axios
|
||||
.get(`api/queries/${queryId}/results/${id}.json`)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
// Success handler
|
||||
queryResult.isLoadingResult = false;
|
||||
queryResult.update(response);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
// Error handler
|
||||
queryResult.isLoadingResult = false;
|
||||
handleErrorResponse(queryResult, error);
|
||||
@@ -362,10 +362,10 @@ class QueryResult {
|
||||
loadLatestCachedResult(queryId, parameters) {
|
||||
axios
|
||||
.post(`api/queries/${queryId}/results`, { queryId, parameters })
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
this.update(response);
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
handleErrorResponse(this, error);
|
||||
});
|
||||
}
|
||||
@@ -375,11 +375,11 @@ class QueryResult {
|
||||
this.deferred.onStatusChange(ExecutionStatus.LOADING_RESULT);
|
||||
|
||||
QueryResultResource.get({ id: this.job.query_result_id })
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
this.update(response);
|
||||
this.isLoadingResult = false;
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
if (tryCount === undefined) {
|
||||
tryCount = 0;
|
||||
}
|
||||
@@ -394,9 +394,12 @@ class QueryResult {
|
||||
});
|
||||
this.isLoadingResult = false;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.loadResult(tryCount + 1);
|
||||
}, 1000 * Math.pow(2, tryCount));
|
||||
setTimeout(
|
||||
() => {
|
||||
this.loadResult(tryCount + 1);
|
||||
},
|
||||
1000 * Math.pow(2, tryCount)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -410,19 +413,26 @@ class QueryResult {
|
||||
: axios.get(`api/queries/${query}/jobs/${this.job.id}`);
|
||||
|
||||
request
|
||||
.then(jobResponse => {
|
||||
.then((jobResponse) => {
|
||||
this.update(jobResponse);
|
||||
|
||||
if (this.getStatus() === "processing" && this.job.query_result_id && this.job.query_result_id !== "None") {
|
||||
loadResult();
|
||||
} else if (this.getStatus() !== "failed") {
|
||||
const waitTime = tryNumber > 10 ? 3000 : 500;
|
||||
let waitTime;
|
||||
if (tryNumber <= 10) {
|
||||
waitTime = 500;
|
||||
} else if (tryNumber <= 50) {
|
||||
waitTime = 1000;
|
||||
} else {
|
||||
waitTime = 3000;
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.refreshStatus(query, parameters, tryNumber + 1);
|
||||
}, waitTime);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
logger("Connection error", error);
|
||||
// TODO: use QueryResultError, or better yet: exception/reject of promise.
|
||||
this.update({
|
||||
@@ -451,14 +461,14 @@ class QueryResult {
|
||||
|
||||
axios
|
||||
.post(`api/queries/${id}/results`, { id, parameters, apply_auto_limit: applyAutoLimit, max_age: maxAge })
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
queryResult.update(response);
|
||||
|
||||
if ("job" in response) {
|
||||
queryResult.refreshStatus(id, parameters);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
handleErrorResponse(queryResult, error);
|
||||
});
|
||||
|
||||
@@ -481,14 +491,14 @@ class QueryResult {
|
||||
}
|
||||
|
||||
QueryResultResource.post(params)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
queryResult.update(response);
|
||||
|
||||
if ("job" in response) {
|
||||
queryResult.refreshStatus(query, parameters);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
handleErrorResponse(queryResult, error);
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("Dashboard", () => {
|
||||
cy.getByTestId("DashboardSaveButton").click();
|
||||
});
|
||||
|
||||
cy.wait("@NewDashboard").then(xhr => {
|
||||
cy.wait("@NewDashboard").then((xhr) => {
|
||||
const id = Cypress._.get(xhr, "response.body.id");
|
||||
assert.isDefined(id, "Dashboard api call returns id");
|
||||
|
||||
@@ -40,13 +40,9 @@ describe("Dashboard", () => {
|
||||
|
||||
cy.getByTestId("DashboardMoreButton").click();
|
||||
|
||||
cy.getByTestId("DashboardMoreButtonMenu")
|
||||
.contains("Archive")
|
||||
.click();
|
||||
cy.getByTestId("DashboardMoreButtonMenu").contains("Archive").click();
|
||||
|
||||
cy.get(".ant-modal .ant-btn")
|
||||
.contains("Archive")
|
||||
.click({ force: true });
|
||||
cy.get(".ant-modal .ant-btn").contains("Archive").click({ force: true });
|
||||
cy.get(".label-tag-archived").should("exist");
|
||||
|
||||
cy.visit("/dashboards");
|
||||
@@ -60,7 +56,7 @@ describe("Dashboard", () => {
|
||||
cy.server();
|
||||
cy.route("GET", "**/api/dashboards/*").as("LoadDashboard");
|
||||
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.wait("@LoadDashboard");
|
||||
cy.getByTestId(`DashboardId${id}Container`).should("exist");
|
||||
@@ -72,7 +68,7 @@ describe("Dashboard", () => {
|
||||
});
|
||||
|
||||
context("viewport width is at 800px", () => {
|
||||
before(function() {
|
||||
before(function () {
|
||||
cy.login();
|
||||
cy.createDashboard("Foo Bar")
|
||||
.then(({ id }) => {
|
||||
@@ -80,49 +76,42 @@ describe("Dashboard", () => {
|
||||
this.dashboardEditUrl = `/dashboards/${id}?edit`;
|
||||
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
||||
})
|
||||
.then(elTestId => {
|
||||
.then((elTestId) => {
|
||||
cy.visit(this.dashboardUrl);
|
||||
cy.getByTestId(elTestId).as("textboxEl");
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
cy.login();
|
||||
cy.visit(this.dashboardUrl);
|
||||
cy.viewport(800 + menuWidth, 800);
|
||||
});
|
||||
|
||||
it("shows widgets with full width", () => {
|
||||
cy.get("@textboxEl").should($el => {
|
||||
cy.get("@textboxEl").should(($el) => {
|
||||
expect($el.width()).to.eq(770);
|
||||
});
|
||||
|
||||
cy.viewport(801 + menuWidth, 800);
|
||||
cy.get("@textboxEl").should($el => {
|
||||
expect($el.width()).to.eq(378);
|
||||
cy.get("@textboxEl").should(($el) => {
|
||||
expect($el.width()).to.eq(182);
|
||||
});
|
||||
});
|
||||
|
||||
it("hides edit option", () => {
|
||||
cy.getByTestId("DashboardMoreButton")
|
||||
.click()
|
||||
.should("be.visible");
|
||||
cy.getByTestId("DashboardMoreButton").click().should("be.visible");
|
||||
|
||||
cy.getByTestId("DashboardMoreButtonMenu")
|
||||
.contains("Edit")
|
||||
.as("editButton")
|
||||
.should("not.be.visible");
|
||||
cy.getByTestId("DashboardMoreButtonMenu").contains("Edit").as("editButton").should("not.be.visible");
|
||||
|
||||
cy.viewport(801 + menuWidth, 800);
|
||||
cy.get("@editButton").should("be.visible");
|
||||
});
|
||||
|
||||
it("disables edit mode", function() {
|
||||
it("disables edit mode", function () {
|
||||
cy.viewport(801 + menuWidth, 800);
|
||||
cy.visit(this.dashboardEditUrl);
|
||||
cy.contains("button", "Done Editing")
|
||||
.as("saveButton")
|
||||
.should("exist");
|
||||
cy.contains("button", "Done Editing").as("saveButton").should("exist");
|
||||
|
||||
cy.viewport(800 + menuWidth, 800);
|
||||
cy.contains("button", "Done Editing").should("not.exist");
|
||||
@@ -130,14 +119,14 @@ describe("Dashboard", () => {
|
||||
});
|
||||
|
||||
context("viewport width is at 767px", () => {
|
||||
before(function() {
|
||||
before(function () {
|
||||
cy.login();
|
||||
cy.createDashboard("Foo Bar").then(({ id }) => {
|
||||
this.dashboardUrl = `/dashboards/${id}`;
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
cy.visit(this.dashboardUrl);
|
||||
cy.viewport(767, 800);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getWidgetTestId, editDashboard, resizeBy } from "../../support/dashboar
|
||||
const menuWidth = 80;
|
||||
|
||||
describe("Grid compliant widgets", () => {
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
cy.login();
|
||||
cy.viewport(1215 + menuWidth, 800);
|
||||
cy.createDashboard("Foo Bar")
|
||||
@@ -13,7 +13,7 @@ describe("Grid compliant widgets", () => {
|
||||
this.dashboardUrl = `/dashboards/${id}`;
|
||||
return cy.addTextbox(id, "Hello World!").then(getWidgetTestId);
|
||||
})
|
||||
.then(elTestId => {
|
||||
.then((elTestId) => {
|
||||
cy.visit(this.dashboardUrl);
|
||||
cy.getByTestId(elTestId).as("textboxEl");
|
||||
});
|
||||
@@ -27,7 +27,7 @@ describe("Grid compliant widgets", () => {
|
||||
|
||||
it("stays put when dragged under snap threshold", () => {
|
||||
cy.get("@textboxEl")
|
||||
.dragBy(90)
|
||||
.dragBy(30)
|
||||
.invoke("offset")
|
||||
.should("have.property", "left", 15 + menuWidth); // no change, 15 -> 15
|
||||
});
|
||||
@@ -36,14 +36,14 @@ describe("Grid compliant widgets", () => {
|
||||
cy.get("@textboxEl")
|
||||
.dragBy(110)
|
||||
.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", () => {
|
||||
cy.get("@textboxEl")
|
||||
.dragBy(330)
|
||||
.dragBy(200)
|
||||
.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");
|
||||
|
||||
editDashboard();
|
||||
cy.get("@textboxEl").dragBy(330);
|
||||
cy.get("@textboxEl").dragBy(100);
|
||||
cy.wait("@WidgetSave");
|
||||
});
|
||||
});
|
||||
@@ -64,24 +64,24 @@ describe("Grid compliant widgets", () => {
|
||||
});
|
||||
|
||||
it("stays put when dragged under snap threshold", () => {
|
||||
resizeBy(cy.get("@textboxEl"), 90)
|
||||
resizeBy(cy.get("@textboxEl"), 30)
|
||||
.then(() => cy.get("@textboxEl"))
|
||||
.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", () => {
|
||||
resizeBy(cy.get("@textboxEl"), 110)
|
||||
.then(() => cy.get("@textboxEl"))
|
||||
.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", () => {
|
||||
resizeBy(cy.get("@textboxEl"), 400)
|
||||
.then(() => cy.get("@textboxEl"))
|
||||
.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)
|
||||
.then(() => cy.get("@textboxEl"))
|
||||
.invoke("height")
|
||||
.should("eq", 185); // resized by 50, , 135 -> 185
|
||||
.should("eq", 185);
|
||||
});
|
||||
|
||||
it("shrinks to minimum", () => {
|
||||
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"))
|
||||
.should($el => {
|
||||
.should(($el) => {
|
||||
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";
|
||||
|
||||
describe("Textbox", () => {
|
||||
beforeEach(function() {
|
||||
beforeEach(function () {
|
||||
cy.login();
|
||||
cy.createDashboard("Foo Bar").then(({ id }) => {
|
||||
this.dashboardId = id;
|
||||
@@ -12,12 +12,10 @@ describe("Textbox", () => {
|
||||
});
|
||||
|
||||
const confirmDeletionInModal = () => {
|
||||
cy.get(".ant-modal .ant-btn")
|
||||
.contains("Delete")
|
||||
.click({ force: true });
|
||||
cy.get(".ant-modal .ant-btn").contains("Delete").click({ force: true });
|
||||
};
|
||||
|
||||
it("adds textbox", function() {
|
||||
it("adds textbox", function () {
|
||||
cy.visit(this.dashboardUrl);
|
||||
editDashboard();
|
||||
cy.getByTestId("AddTextboxButton").click();
|
||||
@@ -29,10 +27,10 @@ describe("Textbox", () => {
|
||||
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!")
|
||||
.then(getWidgetTestId)
|
||||
.then(elTestId => {
|
||||
.then((elTestId) => {
|
||||
cy.visit(this.dashboardUrl);
|
||||
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!")
|
||||
.then(getWidgetTestId)
|
||||
.then(elTestId => {
|
||||
.then((elTestId) => {
|
||||
cy.visit(this.dashboardUrl);
|
||||
cy.getByTestId(elTestId).within(() => {
|
||||
cy.getByTestId("WidgetDropdownButton").click();
|
||||
});
|
||||
cy.getByTestId("WidgetDropdownButtonMenu")
|
||||
.contains("Remove from Dashboard")
|
||||
.click();
|
||||
cy.getByTestId("WidgetDropdownButtonMenu").contains("Remove from Dashboard").click();
|
||||
|
||||
confirmDeletionInModal();
|
||||
cy.getByTestId(elTestId).should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("allows opening menu after removal", function() {
|
||||
it("allows opening menu after removal", function () {
|
||||
let elTestId1;
|
||||
cy.addTextbox(this.dashboardId, "txb 1")
|
||||
.then(getWidgetTestId)
|
||||
.then(elTestId => {
|
||||
.then((elTestId) => {
|
||||
elTestId1 = elTestId;
|
||||
return cy.addTextbox(this.dashboardId, "txb 2").then(getWidgetTestId);
|
||||
})
|
||||
.then(elTestId2 => {
|
||||
.then((elTestId2) => {
|
||||
cy.visit(this.dashboardUrl);
|
||||
editDashboard();
|
||||
|
||||
@@ -97,10 +93,10 @@ describe("Textbox", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("edits textbox", function() {
|
||||
it("edits textbox", function () {
|
||||
cy.addTextbox(this.dashboardId, "Hello World!")
|
||||
.then(getWidgetTestId)
|
||||
.then(elTestId => {
|
||||
.then((elTestId) => {
|
||||
cy.visit(this.dashboardUrl);
|
||||
cy.getByTestId(elTestId)
|
||||
.as("textboxEl")
|
||||
@@ -108,17 +104,13 @@ describe("Textbox", () => {
|
||||
cy.getByTestId("WidgetDropdownButton").click();
|
||||
});
|
||||
|
||||
cy.getByTestId("WidgetDropdownButtonMenu")
|
||||
.contains("Edit")
|
||||
.click();
|
||||
cy.getByTestId("WidgetDropdownButtonMenu").contains("Edit").click();
|
||||
|
||||
const newContent = "[edited]";
|
||||
cy.getByTestId("TextboxDialog")
|
||||
.should("exist")
|
||||
.within(() => {
|
||||
cy.get("textarea")
|
||||
.clear()
|
||||
.type(newContent);
|
||||
cy.get("textarea").clear().type(newContent);
|
||||
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 txb1Pos = { col: 0, row: 0, sizeX: 3, sizeY: 2 };
|
||||
const txb2Pos = { col: 1, row: 1, sizeX: 3, sizeY: 4 };
|
||||
@@ -135,15 +127,15 @@ describe("Textbox", () => {
|
||||
cy.addTextbox(id, "x", { position: txb1Pos })
|
||||
.then(() => cy.addTextbox(id, "x", { position: txb2Pos }))
|
||||
.then(getWidgetTestId)
|
||||
.then(elTestId => {
|
||||
.then((elTestId) => {
|
||||
cy.visit(this.dashboardUrl);
|
||||
return cy.getByTestId(elTestId);
|
||||
})
|
||||
.should($el => {
|
||||
.should(($el) => {
|
||||
const { top, left } = $el.offset();
|
||||
expect(top).to.be.oneOf([162, 162.015625]);
|
||||
expect(left).to.eq(282);
|
||||
expect($el.width()).to.eq(545);
|
||||
expect(left).to.eq(188);
|
||||
expect($el.width()).to.eq(265);
|
||||
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);
|
||||
""")
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "redash-client",
|
||||
"version": "25.05.0-dev",
|
||||
"version": "25.8.0",
|
||||
"description": "The frontend part of Redash.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -138,11 +138,12 @@
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"mockdate": "^2.0.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier": "3.3.2",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-test-renderer": "^16.14.0",
|
||||
"request-cookies": "^1.1.0",
|
||||
"source-map-loader": "^1.1.3",
|
||||
"style-loader": "^2.0.0",
|
||||
"typescript": "^4.1.2",
|
||||
"url-loader": "^4.1.1",
|
||||
|
||||
2753
poetry.lock
generated
2753
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ force-exclude = '''
|
||||
|
||||
[tool.poetry]
|
||||
name = "redash"
|
||||
version = "25.05.0-dev"
|
||||
version = "25.8.0"
|
||||
description = "Make Your Company Data Driven. Connect to any data source, easily visualize, dashboard and share your data."
|
||||
authors = ["Arik Fraimovich <arik@redash.io>"]
|
||||
# to be added to/removed from the mailing list, please reach out to Arik via the above email or Discord
|
||||
@@ -95,7 +95,7 @@ optional = true
|
||||
|
||||
[tool.poetry.group.all_ds.dependencies]
|
||||
atsd-client = "3.0.5"
|
||||
azure-kusto-data = "0.0.35"
|
||||
azure-kusto-data = "5.0.1"
|
||||
boto3 = "1.28.8"
|
||||
botocore = "1.31.8"
|
||||
cassandra-driver = "3.21.0"
|
||||
@@ -110,6 +110,7 @@ influxdb = "5.2.3"
|
||||
influxdb-client = "1.38.0"
|
||||
memsql = "3.2.0"
|
||||
mysqlclient = "2.1.1"
|
||||
numpy = "1.24.4"
|
||||
nzalchemy = "^11.0.2"
|
||||
nzpy = ">=1.15"
|
||||
oauth2client = "4.1.3"
|
||||
|
||||
@@ -14,7 +14,7 @@ from redash.app import create_app # noqa
|
||||
from redash.destinations import import_destinations
|
||||
from redash.query_runner import import_query_runners
|
||||
|
||||
__version__ = "25.05.0-dev"
|
||||
__version__ = "25.8.0"
|
||||
|
||||
|
||||
if os.environ.get("REMOTE_DEBUG"):
|
||||
|
||||
@@ -4,7 +4,7 @@ import requests
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
from flask import Blueprint, flash, redirect, request, session, url_for
|
||||
|
||||
from redash import models
|
||||
from redash import models, settings
|
||||
from redash.authentication import (
|
||||
create_and_login_user,
|
||||
get_next_path,
|
||||
@@ -29,6 +29,41 @@ def verify_profile(org, profile):
|
||||
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):
|
||||
oauth = OAuth(app)
|
||||
|
||||
@@ -36,23 +71,12 @@ def create_google_oauth_blueprint(app):
|
||||
blueprint = Blueprint("google_oauth", __name__)
|
||||
|
||||
CONF_URL = "https://accounts.google.com/.well-known/openid-configuration"
|
||||
oauth = OAuth(app)
|
||||
oauth.register(
|
||||
name="google",
|
||||
server_metadata_url=CONF_URL,
|
||||
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")
|
||||
def org_login(org_slug):
|
||||
session["org_slug"] = current_org.slug
|
||||
@@ -60,9 +84,9 @@ def create_google_oauth_blueprint(app):
|
||||
|
||||
@blueprint.route("/oauth/google", endpoint="authorize")
|
||||
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("Next is: %s", next_path)
|
||||
|
||||
@@ -86,7 +110,7 @@ def create_google_oauth_blueprint(app):
|
||||
flash("Validation error. Please retry.")
|
||||
return redirect(url_for("redash.login"))
|
||||
|
||||
profile = get_user_profile(access_token)
|
||||
profile = get_user_profile(access_token, logger)
|
||||
if profile is None:
|
||||
flash("Validation error. Please retry.")
|
||||
return redirect(url_for("redash.login"))
|
||||
@@ -110,7 +134,9 @@ def create_google_oauth_blueprint(app):
|
||||
if user is None:
|
||||
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)
|
||||
|
||||
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():
|
||||
if not current_user.is_api_user() and current_user.is_authenticated:
|
||||
client_config = {
|
||||
@@ -289,6 +295,7 @@ def client_config():
|
||||
client_config.update({"basePath": base_href()})
|
||||
client_config.update(date_time_format_config())
|
||||
client_config.update(number_format_config())
|
||||
client_config.update(null_value_config())
|
||||
|
||||
return client_config
|
||||
|
||||
|
||||
@@ -564,7 +564,7 @@ class Query(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model):
|
||||
db.session.query(tag_column, usage_count)
|
||||
.group_by(tag_column)
|
||||
.filter(Query.id.in_(queries.options(load_only("id"))))
|
||||
.order_by(usage_count.desc())
|
||||
.order_by(tag_column)
|
||||
)
|
||||
return query
|
||||
|
||||
@@ -1137,7 +1137,7 @@ class Dashboard(ChangeTrackingMixin, TimestampMixin, BelongsToOrgMixin, db.Model
|
||||
db.session.query(tag_column, usage_count)
|
||||
.group_by(tag_column)
|
||||
.filter(Dashboard.id.in_(dashboards.options(load_only("id"))))
|
||||
.order_by(usage_count.desc())
|
||||
.order_by(tag_column)
|
||||
)
|
||||
return query
|
||||
|
||||
|
||||
@@ -288,7 +288,10 @@ class BaseSQLQueryRunner(BaseQueryRunner):
|
||||
return True
|
||||
|
||||
def query_is_select_no_limit(self, query):
|
||||
parsed_query = sqlparse.parse(query)[0]
|
||||
parsed_query_list = sqlparse.parse(query)
|
||||
if len(parsed_query_list) == 0:
|
||||
return False
|
||||
parsed_query = parsed_query_list[0]
|
||||
last_keyword_idx = find_last_keyword_idx(parsed_query)
|
||||
# Either invalid query or query that is not select
|
||||
if last_keyword_idx == -1 or parsed_query.tokens[0].value.upper() != "SELECT":
|
||||
|
||||
@@ -11,12 +11,12 @@ from redash.query_runner import (
|
||||
from redash.utils import json_loads
|
||||
|
||||
try:
|
||||
from azure.kusto.data.exceptions import KustoServiceError
|
||||
from azure.kusto.data.request import (
|
||||
from azure.kusto.data import (
|
||||
ClientRequestProperties,
|
||||
KustoClient,
|
||||
KustoConnectionStringBuilder,
|
||||
)
|
||||
from azure.kusto.data.exceptions import KustoServiceError
|
||||
|
||||
enabled = True
|
||||
except ImportError:
|
||||
@@ -37,6 +37,34 @@ TYPES_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def _get_data_scanned(kusto_response):
|
||||
try:
|
||||
metadata_table = next(
|
||||
(table for table in kusto_response.tables if table.table_name == "QueryCompletionInformation"),
|
||||
None,
|
||||
)
|
||||
|
||||
if metadata_table:
|
||||
resource_usage_json = next(
|
||||
(row["Payload"] for row in metadata_table.rows if row["EventTypeName"] == "QueryResourceConsumption"),
|
||||
"{}",
|
||||
)
|
||||
resource_usage = json_loads(resource_usage_json).get("resource_usage", {})
|
||||
|
||||
data_scanned = (
|
||||
resource_usage["cache"]["shards"]["cold"]["hitbytes"]
|
||||
+ resource_usage["cache"]["shards"]["cold"]["missbytes"]
|
||||
+ resource_usage["cache"]["shards"]["hot"]["hitbytes"]
|
||||
+ resource_usage["cache"]["shards"]["hot"]["missbytes"]
|
||||
+ resource_usage["cache"]["shards"]["bypassbytes"]
|
||||
)
|
||||
|
||||
except Exception:
|
||||
data_scanned = 0
|
||||
|
||||
return int(data_scanned)
|
||||
|
||||
|
||||
class AzureKusto(BaseQueryRunner):
|
||||
should_annotate_query = False
|
||||
noop_query = "let noop = datatable (Noop:string)[1]; noop"
|
||||
@@ -44,8 +72,6 @@ class AzureKusto(BaseQueryRunner):
|
||||
def __init__(self, configuration):
|
||||
super(AzureKusto, self).__init__(configuration)
|
||||
self.syntax = "custom"
|
||||
self.client_request_properties = ClientRequestProperties()
|
||||
self.client_request_properties.application = "redash"
|
||||
|
||||
@classmethod
|
||||
def configuration_schema(cls):
|
||||
@@ -60,12 +86,14 @@ class AzureKusto(BaseQueryRunner):
|
||||
},
|
||||
"azure_ad_tenant_id": {"type": "string", "title": "Azure AD Tenant Id"},
|
||||
"database": {"type": "string"},
|
||||
"msi": {"type": "boolean", "title": "Use Managed Service Identity"},
|
||||
"user_msi": {
|
||||
"type": "string",
|
||||
"title": "User-assigned managed identity client ID",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"cluster",
|
||||
"azure_ad_client_id",
|
||||
"azure_ad_client_secret",
|
||||
"azure_ad_tenant_id",
|
||||
"database",
|
||||
],
|
||||
"order": [
|
||||
@@ -91,18 +119,48 @@ class AzureKusto(BaseQueryRunner):
|
||||
return "Azure Data Explorer (Kusto)"
|
||||
|
||||
def run_query(self, query, user):
|
||||
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
connection_string=self.configuration["cluster"],
|
||||
aad_app_id=self.configuration["azure_ad_client_id"],
|
||||
app_key=self.configuration["azure_ad_client_secret"],
|
||||
authority_id=self.configuration["azure_ad_tenant_id"],
|
||||
)
|
||||
cluster = self.configuration["cluster"]
|
||||
msi = self.configuration.get("msi", False)
|
||||
# Managed Service Identity(MSI)
|
||||
if msi:
|
||||
# If user-assigned managed identity is used, the client ID must be provided
|
||||
if self.configuration.get("user_msi"):
|
||||
kcsb = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(
|
||||
cluster,
|
||||
client_id=self.configuration["user_msi"],
|
||||
)
|
||||
else:
|
||||
kcsb = KustoConnectionStringBuilder.with_aad_managed_service_identity_authentication(cluster)
|
||||
# Service Principal auth
|
||||
else:
|
||||
aad_app_id = self.configuration.get("azure_ad_client_id")
|
||||
app_key = self.configuration.get("azure_ad_client_secret")
|
||||
authority_id = self.configuration.get("azure_ad_tenant_id")
|
||||
|
||||
if not (aad_app_id and app_key and authority_id):
|
||||
raise ValueError(
|
||||
"Azure AD Client ID, Client Secret, and Tenant ID are required for Service Principal authentication."
|
||||
)
|
||||
|
||||
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
connection_string=cluster,
|
||||
aad_app_id=aad_app_id,
|
||||
app_key=app_key,
|
||||
authority_id=authority_id,
|
||||
)
|
||||
|
||||
client = KustoClient(kcsb)
|
||||
|
||||
request_properties = ClientRequestProperties()
|
||||
request_properties.application = "redash"
|
||||
|
||||
if user:
|
||||
request_properties.user = user.email
|
||||
request_properties.set_option("request_description", user.email)
|
||||
|
||||
db = self.configuration["database"]
|
||||
try:
|
||||
response = client.execute(db, query, self.client_request_properties)
|
||||
response = client.execute(db, query, request_properties)
|
||||
|
||||
result_cols = response.primary_results[0].columns
|
||||
result_rows = response.primary_results[0].rows
|
||||
@@ -123,14 +181,15 @@ class AzureKusto(BaseQueryRunner):
|
||||
rows.append(row.to_dict())
|
||||
|
||||
error = None
|
||||
data = {"columns": columns, "rows": rows}
|
||||
data = {
|
||||
"columns": columns,
|
||||
"rows": rows,
|
||||
"metadata": {"data_scanned": _get_data_scanned(response)},
|
||||
}
|
||||
|
||||
except KustoServiceError as err:
|
||||
data = None
|
||||
try:
|
||||
error = err.args[1][0]["error"]["@message"]
|
||||
except (IndexError, KeyError):
|
||||
error = err.args[1]
|
||||
error = str(err)
|
||||
|
||||
return data, error
|
||||
|
||||
@@ -143,7 +202,10 @@ class AzureKusto(BaseQueryRunner):
|
||||
self._handle_run_query_error(error)
|
||||
|
||||
schema_as_json = json_loads(results["rows"][0]["DatabaseSchema"])
|
||||
tables_list = schema_as_json["Databases"][self.configuration["database"]]["Tables"].values()
|
||||
tables_list = [
|
||||
*(schema_as_json["Databases"][self.configuration["database"]]["Tables"].values()),
|
||||
*(schema_as_json["Databases"][self.configuration["database"]]["MaterializedViews"].values()),
|
||||
]
|
||||
|
||||
schema = {}
|
||||
|
||||
@@ -154,7 +216,9 @@ class AzureKusto(BaseQueryRunner):
|
||||
schema[table_name] = {"name": table_name, "columns": []}
|
||||
|
||||
for column in table["OrderedColumns"]:
|
||||
schema[table_name]["columns"].append(column["Name"])
|
||||
schema[table_name]["columns"].append(
|
||||
{"name": column["Name"], "type": TYPES_MAP.get(column["CslType"], None)}
|
||||
)
|
||||
|
||||
return list(schema.values())
|
||||
|
||||
|
||||
@@ -313,6 +313,10 @@ class BigQuery(BaseSQLQueryRunner):
|
||||
queries = []
|
||||
for dataset in datasets:
|
||||
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)
|
||||
queries.append(query)
|
||||
|
||||
|
||||
@@ -77,7 +77,11 @@ class ClickHouse(BaseSQLQueryRunner):
|
||||
self._url = self._url._replace(netloc="{}:{}".format(self._url.hostname, port))
|
||||
|
||||
def _get_tables(self, schema):
|
||||
query = "SELECT database, table, name FROM system.columns WHERE database NOT IN ('system')"
|
||||
query = """
|
||||
SELECT database, table, name, type as data_type
|
||||
FROM system.columns
|
||||
WHERE database NOT IN ('system', 'information_schema', 'INFORMATION_SCHEMA')
|
||||
"""
|
||||
|
||||
results, error = self.run_query(query, None)
|
||||
|
||||
@@ -90,7 +94,7 @@ class ClickHouse(BaseSQLQueryRunner):
|
||||
if table_name not in schema:
|
||||
schema[table_name] = {"name": table_name, "columns": []}
|
||||
|
||||
schema[table_name]["columns"].append(row["name"])
|
||||
schema[table_name]["columns"].append({"name": row["name"], "type": row["data_type"]})
|
||||
|
||||
return list(schema.values())
|
||||
|
||||
|
||||
@@ -215,10 +215,10 @@ class MongoDB(BaseQueryRunner):
|
||||
if readPreference:
|
||||
kwargs["readPreference"] = readPreference
|
||||
|
||||
if "username" in self.configuration:
|
||||
if self.configuration.get("username"):
|
||||
kwargs["username"] = self.configuration["username"]
|
||||
|
||||
if "password" in self.configuration:
|
||||
if self.configuration.get("password"):
|
||||
kwargs["password"] = self.configuration["password"]
|
||||
|
||||
db_connection = pymongo.MongoClient(self.configuration["connectionString"], **kwargs)
|
||||
|
||||
@@ -25,6 +25,7 @@ def init_app(app):
|
||||
app.config["WTF_CSRF_CHECK_DEFAULT"] = False
|
||||
app.config["WTF_CSRF_SSL_STRICT"] = False
|
||||
app.config["WTF_CSRF_TIME_LIMIT"] = settings.CSRF_TIME_LIMIT
|
||||
app.config["SESSION_COOKIE_NAME"] = settings.SESSION_COOKIE_NAME
|
||||
|
||||
@app.after_request
|
||||
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.
|
||||
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_COOKIE_NAME = os.environ.get("REDASH_SESSION_COOKIE_NAME", "session")
|
||||
|
||||
# Whether the session cookie is set to 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"))
|
||||
|
||||
# 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_SECRET = os.environ.get("REDASH_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")
|
||||
INTEGER_FORMAT = os.environ.get("REDASH_INTEGER_FORMAT", "0,0")
|
||||
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"))
|
||||
|
||||
JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
|
||||
@@ -59,6 +60,7 @@ settings = {
|
||||
"time_format": TIME_FORMAT,
|
||||
"integer_format": INTEGER_FORMAT,
|
||||
"float_format": FLOAT_FORMAT,
|
||||
"null_value": NULL_VALUE,
|
||||
"multi_byte_search_enabled": MULTI_BYTE_SEARCH_ENABLED,
|
||||
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
|
||||
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,
|
||||
|
||||
42
tests/query_runner/test_azure_kusto.py
Normal file
42
tests/query_runner/test_azure_kusto.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from redash.query_runner.azure_kusto import AzureKusto
|
||||
|
||||
|
||||
class TestAzureKusto(TestCase):
|
||||
def setUp(self):
|
||||
self.configuration = {
|
||||
"cluster": "https://example.kusto.windows.net",
|
||||
"database": "sample_db",
|
||||
"azure_ad_client_id": "client_id",
|
||||
"azure_ad_client_secret": "client_secret",
|
||||
"azure_ad_tenant_id": "tenant_id",
|
||||
}
|
||||
self.kusto = AzureKusto(self.configuration)
|
||||
|
||||
@patch.object(AzureKusto, "run_query")
|
||||
def test_get_schema(self, mock_run_query):
|
||||
mock_response = {
|
||||
"rows": [
|
||||
{
|
||||
"DatabaseSchema": '{"Databases":{"sample_db":{"Tables":{"Table1":{"Name":"Table1","OrderedColumns":[{"Name":"Column1","Type":"System.String","CslType":"string"},{"Name":"Column2","Type":"System.DateTime","CslType":"datetime"}]}},"MaterializedViews":{"View1":{"Name":"View1","OrderedColumns":[{"Name":"Column1","Type":"System.String","CslType":"string"},{"Name":"Column2","Type":"System.DateTime","CslType":"datetime"}]}}}}}'
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_run_query.return_value = (mock_response, None)
|
||||
|
||||
expected_schema = [
|
||||
{
|
||||
"name": "Table1",
|
||||
"columns": [{"name": "Column1", "type": "string"}, {"name": "Column2", "type": "datetime"}],
|
||||
},
|
||||
{
|
||||
"name": "View1",
|
||||
"columns": [{"name": "Column1", "type": "string"}, {"name": "Column2", "type": "datetime"}],
|
||||
},
|
||||
]
|
||||
|
||||
schema = self.kusto.get_schema()
|
||||
print(schema)
|
||||
self.assertEqual(schema, expected_schema)
|
||||
@@ -28,4 +28,4 @@ class TestJsonDumps(BaseTestCase):
|
||||
}
|
||||
json_data = json_dumps(input_data)
|
||||
actual_output_data = json_loads(json_data)
|
||||
self.assertEquals(actual_output_data, expected_output_data)
|
||||
self.assertEqual(actual_output_data, expected_output_data)
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"less-loader": "^11.1.3",
|
||||
"less-plugin-autoprefix": "^2.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier": "3.3.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"style-loader": "^3.3.3",
|
||||
"ts-migrate": "^0.1.35",
|
||||
|
||||
@@ -5,6 +5,7 @@ import numeral from "numeral";
|
||||
import { isString, isArray, isUndefined, isFinite, isNil, toString } from "lodash";
|
||||
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
|
||||
|
||||
|
||||
numeral.options.scalePercentBy100 = false;
|
||||
|
||||
// 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;
|
||||
|
||||
function NullValueComponent() {
|
||||
return <span className="display-as-null">{visualizationsSettings.nullValue}</span>;
|
||||
}
|
||||
|
||||
export function createTextFormatter(highlightLinks: any) {
|
||||
if (highlightLinks) {
|
||||
return (value: any) => {
|
||||
if (value === null) {
|
||||
return <NullValueComponent/>
|
||||
}
|
||||
if (isString(value)) {
|
||||
const Link = visualizationsSettings.LinkComponent;
|
||||
value = value.replace(urlPattern, (unused, prefix, href) => {
|
||||
@@ -29,7 +37,7 @@ export function createTextFormatter(highlightLinks: any) {
|
||||
return toString(value);
|
||||
};
|
||||
}
|
||||
return (value: any) => toString(value);
|
||||
return (value: any) => value === null ? <NullValueComponent/> : toString(value);
|
||||
}
|
||||
|
||||
function toMoment(value: any) {
|
||||
@@ -46,11 +54,14 @@ function toMoment(value: any) {
|
||||
export function createDateTimeFormatter(format: any) {
|
||||
if (isString(format) && format !== "") {
|
||||
return (value: any) => {
|
||||
if (value === null) {
|
||||
return <NullValueComponent/>;
|
||||
}
|
||||
const wrapped = toMoment(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) {
|
||||
@@ -58,6 +69,9 @@ export function createBooleanFormatter(values: any) {
|
||||
if (values.length >= 2) {
|
||||
// Both `true` and `false` specified
|
||||
return (value: any) => {
|
||||
if (value === null) {
|
||||
return <NullValueComponent/>;
|
||||
}
|
||||
if (isNil(value)) {
|
||||
return "";
|
||||
}
|
||||
@@ -69,6 +83,9 @@ export function createBooleanFormatter(values: any) {
|
||||
}
|
||||
}
|
||||
return (value: any) => {
|
||||
if (value === null) {
|
||||
return <NullValueComponent/>;
|
||||
}
|
||||
if (isNil(value)) {
|
||||
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 !== "") {
|
||||
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) {
|
||||
|
||||
@@ -10,7 +10,7 @@ export default {
|
||||
Renderer,
|
||||
Editor,
|
||||
|
||||
defaultColumns: 3,
|
||||
defaultColumns: 6,
|
||||
defaultRows: 8,
|
||||
minColumns: 1,
|
||||
minRows: 5,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Plotly from "plotly.js";
|
||||
|
||||
import "./locales"
|
||||
import prepareData from "./prepareData";
|
||||
import prepareLayout from "./prepareLayout";
|
||||
import updateData from "./updateData";
|
||||
@@ -11,6 +12,7 @@ import { prepareCustomChartData, createCustomChartRenderer } from "./customChart
|
||||
Plotly.setPlotConfig({
|
||||
modeBarButtonsToRemove: ["sendDataToCloud"],
|
||||
modeBarButtonsToAdd: ["togglespikelines", "v1hovermode"],
|
||||
locale: window.navigator.language,
|
||||
});
|
||||
|
||||
export {
|
||||
|
||||
230
viz-lib/src/visualizations/chart/plotly/locales.ts
Normal file
230
viz-lib/src/visualizations/chart/plotly/locales.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import * as Plotly from "plotly.js";
|
||||
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeAf from "plotly.js/lib/locales/af";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeAm from "plotly.js/lib/locales/am";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeAr_dz from "plotly.js/lib/locales/ar-dz";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeAr_eg from "plotly.js/lib/locales/ar-eg";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeAr from "plotly.js/lib/locales/ar";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeAz from "plotly.js/lib/locales/az";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeBg from "plotly.js/lib/locales/bg";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeBs from "plotly.js/lib/locales/bs";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeCa from "plotly.js/lib/locales/ca";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeCs from "plotly.js/lib/locales/cs";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeCy from "plotly.js/lib/locales/cy";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeDa from "plotly.js/lib/locales/da";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeDe_ch from "plotly.js/lib/locales/de-ch";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeDe from "plotly.js/lib/locales/de";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeEl from "plotly.js/lib/locales/el";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeEo from "plotly.js/lib/locales/eo";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeEs_ar from "plotly.js/lib/locales/es-ar";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeEs_pe from "plotly.js/lib/locales/es-pe";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeEs from "plotly.js/lib/locales/es";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeEt from "plotly.js/lib/locales/et";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeEu from "plotly.js/lib/locales/eu";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeFa from "plotly.js/lib/locales/fa";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeFi from "plotly.js/lib/locales/fi";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeFo from "plotly.js/lib/locales/fo";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeFr_ch from "plotly.js/lib/locales/fr-ch";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeFr from "plotly.js/lib/locales/fr";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeGl from "plotly.js/lib/locales/gl";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeGu from "plotly.js/lib/locales/gu";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeHe from "plotly.js/lib/locales/he";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeHi_in from "plotly.js/lib/locales/hi-in";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeHr from "plotly.js/lib/locales/hr";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeHu from "plotly.js/lib/locales/hu";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeHy from "plotly.js/lib/locales/hy";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeId from "plotly.js/lib/locales/id";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeIs from "plotly.js/lib/locales/is";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeIt from "plotly.js/lib/locales/it";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeJa from "plotly.js/lib/locales/ja";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeKa from "plotly.js/lib/locales/ka";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeKm from "plotly.js/lib/locales/km";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeKo from "plotly.js/lib/locales/ko";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeLt from "plotly.js/lib/locales/lt";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeLv from "plotly.js/lib/locales/lv";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeMe_me from "plotly.js/lib/locales/me-me";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeMe from "plotly.js/lib/locales/me";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeMk from "plotly.js/lib/locales/mk";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeMl from "plotly.js/lib/locales/ml";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeMs from "plotly.js/lib/locales/ms";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeMt from "plotly.js/lib/locales/mt";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeNl_be from "plotly.js/lib/locales/nl-be";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeNl from "plotly.js/lib/locales/nl";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeNo from "plotly.js/lib/locales/no";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localePa from "plotly.js/lib/locales/pa";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localePl from "plotly.js/lib/locales/pl";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localePt_br from "plotly.js/lib/locales/pt-br";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localePt_pt from "plotly.js/lib/locales/pt-pt";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeRm from "plotly.js/lib/locales/rm";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeRo from "plotly.js/lib/locales/ro";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeRu from "plotly.js/lib/locales/ru";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeSk from "plotly.js/lib/locales/sk";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeSl from "plotly.js/lib/locales/sl";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeSq from "plotly.js/lib/locales/sq";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeSr_sr from "plotly.js/lib/locales/sr-sr";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeSr from "plotly.js/lib/locales/sr";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeSv from "plotly.js/lib/locales/sv";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeSw from "plotly.js/lib/locales/sw";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeTa from "plotly.js/lib/locales/ta";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeTh from "plotly.js/lib/locales/th";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeTr from "plotly.js/lib/locales/tr";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeTt from "plotly.js/lib/locales/tt";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeUk from "plotly.js/lib/locales/uk";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeUr from "plotly.js/lib/locales/ur";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeVi from "plotly.js/lib/locales/vi";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeZh_cn from "plotly.js/lib/locales/zh-cn";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeZh_hk from "plotly.js/lib/locales/zh-hk";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
|
||||
import localeZh_tw from "plotly.js/lib/locales/zh-tw";
|
||||
|
||||
(Plotly as any).register([
|
||||
localeAf,
|
||||
localeAm,
|
||||
localeAr_dz,
|
||||
localeAr_eg,
|
||||
localeAr,
|
||||
localeAz,
|
||||
localeBg,
|
||||
localeBs,
|
||||
localeCa,
|
||||
localeCs,
|
||||
localeCy,
|
||||
localeDa,
|
||||
localeDe_ch,
|
||||
localeDe,
|
||||
localeEl,
|
||||
localeEo,
|
||||
localeEs_ar,
|
||||
localeEs_pe,
|
||||
localeEs,
|
||||
localeEt,
|
||||
localeEu,
|
||||
localeFa,
|
||||
localeFi,
|
||||
localeFo,
|
||||
localeFr_ch,
|
||||
localeFr,
|
||||
localeGl,
|
||||
localeGu,
|
||||
localeHe,
|
||||
localeHi_in,
|
||||
localeHr,
|
||||
localeHu,
|
||||
localeHy,
|
||||
localeId,
|
||||
localeIs,
|
||||
localeIt,
|
||||
localeJa,
|
||||
localeKa,
|
||||
localeKm,
|
||||
localeKo,
|
||||
localeLt,
|
||||
localeLv,
|
||||
localeMe_me,
|
||||
localeMe,
|
||||
localeMk,
|
||||
localeMl,
|
||||
localeMs,
|
||||
localeMt,
|
||||
localeNl_be,
|
||||
localeNl,
|
||||
localeNo,
|
||||
localePa,
|
||||
localePl,
|
||||
localePt_br,
|
||||
localePt_pt,
|
||||
localeRm,
|
||||
localeRo,
|
||||
localeRu,
|
||||
localeSk,
|
||||
localeSl,
|
||||
localeSq,
|
||||
localeSr_sr,
|
||||
localeSr,
|
||||
localeSv,
|
||||
localeSw,
|
||||
localeTa,
|
||||
localeTh,
|
||||
localeTr,
|
||||
localeTt,
|
||||
localeUk,
|
||||
localeUr,
|
||||
localeVi,
|
||||
localeZh_cn,
|
||||
localeZh_hk,
|
||||
localeZh_tw,
|
||||
]);
|
||||
@@ -9,7 +9,7 @@ export default {
|
||||
Renderer,
|
||||
Editor,
|
||||
|
||||
defaultColumns: 3,
|
||||
defaultColumns: 6,
|
||||
defaultRows: 8,
|
||||
minColumns: 2,
|
||||
};
|
||||
|
||||
@@ -22,6 +22,6 @@ export default {
|
||||
Renderer,
|
||||
Editor,
|
||||
|
||||
defaultColumns: 2,
|
||||
defaultColumns: 4,
|
||||
defaultRows: 5,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,6 @@ export default {
|
||||
...options,
|
||||
}),
|
||||
Renderer: DetailsRenderer,
|
||||
defaultColumns: 2,
|
||||
defaultColumns: 4,
|
||||
defaultRows: 2,
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ export default {
|
||||
Renderer,
|
||||
Editor,
|
||||
|
||||
defaultColumns: 3,
|
||||
defaultColumns: 6,
|
||||
defaultRows: 8,
|
||||
minColumns: 2,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,6 @@ export default {
|
||||
Editor,
|
||||
|
||||
defaultRows: 10,
|
||||
defaultColumns: 3,
|
||||
defaultColumns: 6,
|
||||
minColumns: 2,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ Object {
|
||||
"linkTitleTemplate": "{{ @ }}",
|
||||
"linkUrlTemplate": "{{ @ }}",
|
||||
"name": "a",
|
||||
"nullValue": "null",
|
||||
"numberFormat": undefined,
|
||||
"order": 100000,
|
||||
"title": "a",
|
||||
@@ -56,6 +57,7 @@ Object {
|
||||
"linkTitleTemplate": "{{ @ }}",
|
||||
"linkUrlTemplate": "{{ @ }}",
|
||||
"name": "a",
|
||||
"nullValue": "null",
|
||||
"numberFormat": undefined,
|
||||
"order": 100000,
|
||||
"title": "a",
|
||||
@@ -89,6 +91,7 @@ Object {
|
||||
"linkTitleTemplate": "{{ @ }}",
|
||||
"linkUrlTemplate": "{{ @ }}",
|
||||
"name": "a",
|
||||
"nullValue": "null",
|
||||
"numberFormat": undefined,
|
||||
"order": 100000,
|
||||
"title": "test",
|
||||
@@ -122,6 +125,7 @@ Object {
|
||||
"linkTitleTemplate": "{{ @ }}",
|
||||
"linkUrlTemplate": "{{ @ }}",
|
||||
"name": "a",
|
||||
"nullValue": "null",
|
||||
"numberFormat": undefined,
|
||||
"order": 100000,
|
||||
"title": "a",
|
||||
@@ -155,6 +159,7 @@ Object {
|
||||
"linkTitleTemplate": "{{ @ }}",
|
||||
"linkUrlTemplate": "{{ @ }}",
|
||||
"name": "a",
|
||||
"nullValue": "null",
|
||||
"numberFormat": undefined,
|
||||
"order": 100000,
|
||||
"title": "a",
|
||||
|
||||
@@ -33,7 +33,7 @@ function Editor({ column, onChange }: Props) {
|
||||
}
|
||||
|
||||
export default function initNumberColumn(column: any) {
|
||||
const format = createNumberFormatter(column.numberFormat);
|
||||
const format = createNumberFormatter(column.numberFormat, true);
|
||||
|
||||
function prepareData(row: any) {
|
||||
return {
|
||||
|
||||
@@ -73,6 +73,7 @@ function getDefaultFormatOptions(column: any) {
|
||||
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
|
||||
numberFormat: numberFormat[column.type],
|
||||
nullValue: visualizationsSettings.nullValue,
|
||||
booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
|
||||
// `image` cell options
|
||||
imageUrlTemplate: "{{ @ }}",
|
||||
|
||||
@@ -11,6 +11,6 @@ export default {
|
||||
|
||||
autoHeight: true,
|
||||
defaultRows: 14,
|
||||
defaultColumns: 3,
|
||||
defaultColumns: 6,
|
||||
minColumns: 2,
|
||||
};
|
||||
|
||||
@@ -39,6 +39,11 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.display-as-null {
|
||||
font-style: italic;
|
||||
color: @text-muted;
|
||||
}
|
||||
|
||||
.table-visualization-spacer {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
@@ -42,6 +42,7 @@ export const visualizationsSettings = {
|
||||
dateTimeFormat: "DD/MM/YYYY HH:mm",
|
||||
integerFormat: "0,0",
|
||||
floatFormat: "0,0.00",
|
||||
nullValue: "null",
|
||||
booleanValues: ["false", "true"],
|
||||
tableCellMaxJSONSize: 50000,
|
||||
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"
|
||||
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
||||
|
||||
prettier@^1.19.1:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
prettier@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a"
|
||||
integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==
|
||||
|
||||
pretty-format@^24.9.0:
|
||||
version "24.9.0"
|
||||
|
||||
@@ -133,6 +133,11 @@ const config = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: "pre",
|
||||
use: ["source-map-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.(t|j)sx?$/,
|
||||
exclude: /node_modules/,
|
||||
|
||||
33
yarn.lock
33
yarn.lock
@@ -2624,7 +2624,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
||||
|
||||
abab@^2.0.0:
|
||||
abab@^2.0.0, abab@^2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
||||
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
|
||||
@@ -7745,6 +7745,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@^0.6.2:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
icss-utils@^5.0.0, icss-utils@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||
@@ -11149,10 +11156,10 @@ prelude-ls@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
|
||||
|
||||
prettier@^1.19.1:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
prettier@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a"
|
||||
integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==
|
||||
|
||||
pretty-bytes@^5.6.0:
|
||||
version "5.6.0"
|
||||
@@ -12650,7 +12657,7 @@ safe-regex@^1.1.0:
|
||||
dependencies:
|
||||
ret "~0.1.10"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -13069,6 +13076,18 @@ source-map-js@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
source-map-loader@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.1.3.tgz#7dbc2fe7ea09d3e43c51fd9fc478b7f016c1f820"
|
||||
integrity sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==
|
||||
dependencies:
|
||||
abab "^2.0.5"
|
||||
iconv-lite "^0.6.2"
|
||||
loader-utils "^2.0.0"
|
||||
schema-utils "^3.0.0"
|
||||
source-map "^0.6.1"
|
||||
whatwg-mimetype "^2.3.0"
|
||||
|
||||
source-map-resolve@^0.5.0:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
|
||||
@@ -14714,7 +14733,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
|
||||
dependencies:
|
||||
iconv-lite "0.4.24"
|
||||
|
||||
whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0:
|
||||
whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
|
||||
|
||||
Reference in New Issue
Block a user