Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ function wrapComponentWithSettings(WrappedComponent) {
"dateTimeFormat",
"integerFormat",
"floatFormat",
"thousandsSeparator",
"decimalSeparator",
"nullValue",
"booleanValues",
"tableCellMaxJSONSize",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { SettingsEditorPropTypes, SettingsEditorDefaultProps } from "../prop-types";
import Form from "antd/lib/form";
import Input from "antd/lib/input";
import Select from "antd/lib/select";
import Skeleton from "antd/lib/skeleton";
import DynamicComponent from "@/components/DynamicComponent";
Expand Down Expand Up @@ -41,6 +42,34 @@ export default function FormatSettings(props) {
</Select>
)}
</Form.Item>
<Form.Item
label="Thousands Separator"
help="Character inserted at thousands positions in numbers (e.g. a space for 1 234 567).">
{loading ? (
<Skeleton.Input style={{ width: 300 }} active />
) : (
<Input
style={{ width: 300 }}
value={values.thousands_separator}
onChange={e => onChange({ thousands_separator: e.target.value })}
data-test="ThousandsSeparatorInput"
/>
Comment on lines +51 to +57
)}
</Form.Item>
<Form.Item
label="Decimal Separator"
help="Character used for the decimal point in numbers (e.g. a comma for 1234,56).">
{loading ? (
<Skeleton.Input style={{ width: 300 }} active />
) : (
<Input
style={{ width: 300 }}
value={values.decimal_separator}
onChange={e => onChange({ decimal_separator: e.target.value })}
data-test="DecimalSeparatorInput"
/>
)}
</Form.Item>
</DynamicComponent>
);
}
Expand Down
22 changes: 22 additions & 0 deletions client/cypress/integration/settings/organization_settings_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,26 @@ describe("Settings", () => {
.should("exist");
});
});

it("can set a custom thousands separator", () => {
cy.getByTestId("ThousandsSeparatorInput").clear().type(" ");
// capture the new separator fields for the PR description
cy.getByTestId("OrganizationSettings").screenshot("format-settings-separators");
cy.getByTestId("OrganizationSettingsSaveButton").click();

// round-trips through the backend
cy.reload();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
cy.getByTestId("ThousandsSeparatorInput").should("have.value", " ");
Comment on lines +49 to +54

cy.createQuery({
name: "test thousands separator",
query: "SELECT 1234567 AS n",
}).then(({ id: queryId }) => {
cy.visit(`/queries/${queryId}`);
cy.findByText("Refresh Now").click();

// the integer is grouped with the configured space separator
cy.getByTestId("TableVisualization").should("contain", "1 234 567");
});
});
});
2 changes: 2 additions & 0 deletions redash/handlers/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ def number_format_config():
return {
"integerFormat": current_org.get_setting("integer_format"),
"floatFormat": current_org.get_setting("float_format"),
"thousandsSeparator": current_org.get_setting("thousands_separator"),
"decimalSeparator": current_org.get_setting("decimal_separator"),
}


Expand Down
7 changes: 7 additions & 0 deletions redash/settings/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
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")
# Thousands/decimal separators applied to numeral.js at frontend init. The `,` and `.`
# in numeral format strings are locale markers, not literals, so these control the actual
# characters rendered. Defaults preserve the previous en-locale behavior.
THOUSANDS_SEPARATOR = os.environ.get("REDASH_THOUSANDS_SEPARATOR", ",")
DECIMAL_SEPARATOR = os.environ.get("REDASH_DECIMAL_SEPARATOR", ".")
NULL_VALUE = os.environ.get("REDASH_NULL_VALUE", "null")
MULTI_BYTE_SEARCH_ENABLED = parse_boolean(os.environ.get("MULTI_BYTE_SEARCH_ENABLED", "false"))

Expand Down Expand Up @@ -60,6 +65,8 @@
"time_format": TIME_FORMAT,
"integer_format": INTEGER_FORMAT,
"float_format": FLOAT_FORMAT,
"thousands_separator": THOUSANDS_SEPARATOR,
"decimal_separator": DECIMAL_SEPARATOR,
"null_value": NULL_VALUE,
"multi_byte_search_enabled": MULTI_BYTE_SEARCH_ENABLED,
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
Expand Down
21 changes: 21 additions & 0 deletions tests/handlers/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,27 @@ def test_post(self):
self.assertEqual(rv.json["settings"]["auth_password_login_enabled"], True)
self.assertEqual(updated_org.settings["settings"]["auth_password_login_enabled"], True)

def test_updates_number_separators(self):
admin = self.factory.create_admin()
rv = self.make_request(
"post",
"/api/settings/organization",
data={"thousands_separator": " ", "decimal_separator": ","},
user=admin,
)
self.assertEqual(rv.json["settings"]["thousands_separator"], " ")
self.assertEqual(rv.json["settings"]["decimal_separator"], ",")

updated_org = Organization.get_by_slug(self.factory.org.slug)
self.assertEqual(updated_org.get_setting("thousands_separator"), " ")
self.assertEqual(updated_org.get_setting("decimal_separator"), ",")

def test_get_returns_default_number_separators(self):
admin = self.factory.create_admin()
rv = self.make_request("get", "/api/settings/organization", user=admin)
self.assertEqual(rv.json["settings"]["thousands_separator"], ",")
self.assertEqual(rv.json["settings"]["decimal_separator"], ".")

def test_updates_google_apps_domains(self):
admin = self.factory.create_admin()
domains = ["example.com"]
Expand Down
30 changes: 30 additions & 0 deletions viz-lib/src/lib/value-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createNumberFormatter } from "./value-format";
import { updateVisualizationsSettings } from "@/visualizations/visualizationsSettings";

// numeral keeps a single global locale, so restore the defaults after each test
// to avoid leaking separator overrides into unrelated specs.
afterEach(() => {
updateVisualizationsSettings({ thousandsSeparator: ",", decimalSeparator: "." });
});

describe("createNumberFormatter", () => {
test("uses the default en delimiters", () => {
expect(createNumberFormatter("0,0")(1234567)).toBe("1,234,567");
expect(createNumberFormatter("0,0.00")(1234567.89)).toBe("1,234,567.89");
});

test("applies a custom thousands separator (the reported space case)", () => {
updateVisualizationsSettings({ thousandsSeparator: " " });
expect(createNumberFormatter("0,0")(1234567)).toBe("1 234 567");
});

test("applies continental-European separators together", () => {
updateVisualizationsSettings({ thousandsSeparator: " ", decimalSeparator: "," });
expect(createNumberFormatter("0,0.00")(1234567.89)).toBe("1 234 567,89");
});

test("ignores non-string separator overrides", () => {
updateVisualizationsSettings({ thousandsSeparator: undefined, decimalSeparator: undefined });
expect(createNumberFormatter("0,0.00")(1234.5)).toBe("1,234.50");
});
});
16 changes: 15 additions & 1 deletion viz-lib/src/visualizations/visualizationsSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { extend } from "lodash";
import { extend, isString } from "lodash";
import numeral from "numeral";
import Tooltip from "antd/lib/tooltip";

type HelpTriggerProps = {
Expand Down Expand Up @@ -42,6 +43,8 @@ export const visualizationsSettings = {
dateTimeFormat: "DD/MM/YYYY HH:mm",
integerFormat: "0,0",
floatFormat: "0,0.00",
thousandsSeparator: ",",
decimalSeparator: ".",
nullValue: "null",
booleanValues: ["false", "true"],
tableCellMaxJSONSize: 50000,
Expand All @@ -52,4 +55,15 @@ export const visualizationsSettings = {

export function updateVisualizationsSettings(options: any) {
extend(visualizationsSettings, options);

// `,` and `.` in numeral format strings are locale markers, not literal characters —
// the rendered separators come from the active locale's delimiters. Override them so
// settings like a space thousands separator (e.g. "1 234 567") are reachable.
const { thousandsSeparator, decimalSeparator } = visualizationsSettings;
if (isString(thousandsSeparator) && isString(decimalSeparator)) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
extend(numeral.localeData().delimiters, {
thousands: thousandsSeparator,
decimal: decimalSeparator,
});
}
}
Comment on lines 59 to 72