Skip to content

Commit d8ae679

Browse files
Make NULL values visible (#7439)
* Make NULL value visible * Make the representation of NULL value configurable * use display-as-null css class for null-value styling
1 parent f3b0b60 commit d8ae679

File tree

9 files changed

+53
-6
lines changed

9 files changed

+53
-6
lines changed

client/app/components/visualizations/visualizationComponents.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function wrapComponentWithSettings(WrappedComponent) {
5959
"dateTimeFormat",
6060
"integerFormat",
6161
"floatFormat",
62+
"nullValue",
6263
"booleanValues",
6364
"tableCellMaxJSONSize",
6465
"allowCustomJSVisualizations",

redash/handlers/authentication.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ def number_format_config():
255255
}
256256

257257

258+
def null_value_config():
259+
return {
260+
"nullValue": current_org.get_setting("null_value"),
261+
}
262+
263+
258264
def client_config():
259265
if not current_user.is_api_user() and current_user.is_authenticated:
260266
client_config = {
@@ -289,6 +295,7 @@ def client_config():
289295
client_config.update({"basePath": base_href()})
290296
client_config.update(date_time_format_config())
291297
client_config.update(number_format_config())
298+
client_config.update(null_value_config())
292299

293300
return client_config
294301

redash/settings/organization.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
TIME_FORMAT = os.environ.get("REDASH_TIME_FORMAT", "HH:mm")
2828
INTEGER_FORMAT = os.environ.get("REDASH_INTEGER_FORMAT", "0,0")
2929
FLOAT_FORMAT = os.environ.get("REDASH_FLOAT_FORMAT", "0,0.00")
30+
NULL_VALUE = os.environ.get("REDASH_NULL_VALUE", "null")
3031
MULTI_BYTE_SEARCH_ENABLED = parse_boolean(os.environ.get("MULTI_BYTE_SEARCH_ENABLED", "false"))
3132

3233
JWT_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_JWT_LOGIN_ENABLED", "false"))
@@ -59,6 +60,7 @@
5960
"time_format": TIME_FORMAT,
6061
"integer_format": INTEGER_FORMAT,
6162
"float_format": FLOAT_FORMAT,
63+
"null_value": NULL_VALUE,
6264
"multi_byte_search_enabled": MULTI_BYTE_SEARCH_ENABLED,
6365
"auth_jwt_login_enabled": JWT_LOGIN_ENABLED,
6466
"auth_jwt_auth_issuer": JWT_AUTH_ISSUER,

viz-lib/src/lib/value-format.tsx

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,24 @@ import numeral from "numeral";
55
import { isString, isArray, isUndefined, isFinite, isNil, toString } from "lodash";
66
import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
77

8+
89
numeral.options.scalePercentBy100 = false;
910

1011
// eslint-disable-next-line
1112
const urlPattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
1213

1314
const hasOwnProperty = Object.prototype.hasOwnProperty;
1415

16+
function NullValueComponent() {
17+
return <span className="display-as-null">{visualizationsSettings.nullValue}</span>;
18+
}
19+
1520
export function createTextFormatter(highlightLinks: any) {
1621
if (highlightLinks) {
1722
return (value: any) => {
23+
if (value === null) {
24+
return <NullValueComponent/>
25+
}
1826
if (isString(value)) {
1927
const Link = visualizationsSettings.LinkComponent;
2028
value = value.replace(urlPattern, (unused, prefix, href) => {
@@ -29,7 +37,7 @@ export function createTextFormatter(highlightLinks: any) {
2937
return toString(value);
3038
};
3139
}
32-
return (value: any) => toString(value);
40+
return (value: any) => value === null ? <NullValueComponent/> : toString(value);
3341
}
3442

3543
function toMoment(value: any) {
@@ -46,18 +54,24 @@ function toMoment(value: any) {
4654
export function createDateTimeFormatter(format: any) {
4755
if (isString(format) && format !== "") {
4856
return (value: any) => {
57+
if (value === null) {
58+
return <NullValueComponent/>;
59+
}
4960
const wrapped = toMoment(value);
5061
return wrapped.isValid() ? wrapped.format(format) : toString(value);
5162
};
5263
}
53-
return (value: any) => toString(value);
64+
return (value: any) => value === null ? <NullValueComponent/> : toString(value);
5465
}
5566

5667
export function createBooleanFormatter(values: any) {
5768
if (isArray(values)) {
5869
if (values.length >= 2) {
5970
// Both `true` and `false` specified
6071
return (value: any) => {
72+
if (value === null) {
73+
return <NullValueComponent/>;
74+
}
6175
if (isNil(value)) {
6276
return "";
6377
}
@@ -69,19 +83,30 @@ export function createBooleanFormatter(values: any) {
6983
}
7084
}
7185
return (value: any) => {
86+
if (value === null) {
87+
return <NullValueComponent/>;
88+
}
7289
if (isNil(value)) {
7390
return "";
7491
}
7592
return value ? "true" : "false";
7693
};
7794
}
7895

79-
export function createNumberFormatter(format: any) {
96+
export function createNumberFormatter(format: any, canReturnHTMLElement: boolean = false) {
8097
if (isString(format) && format !== "") {
8198
const n = numeral(0); // cache `numeral` instance
82-
return (value: any) => (value === null || value === "" ? "" : n.set(value).format(format));
99+
return (value: any) => {
100+
if (canReturnHTMLElement && value === null) {
101+
return <NullValueComponent/>;
102+
}
103+
if (value === "" || value === null) {
104+
return "";
105+
}
106+
return n.set(value).format(format);
107+
}
83108
}
84-
return (value: any) => toString(value);
109+
return (value: any) => (canReturnHTMLElement && value === null) ? <NullValueComponent/> : toString(value);
85110
}
86111

87112
export function formatSimpleTemplate(str: any, data: any) {

viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Object {
2323
"linkTitleTemplate": "{{ @ }}",
2424
"linkUrlTemplate": "{{ @ }}",
2525
"name": "a",
26+
"nullValue": "null",
2627
"numberFormat": undefined,
2728
"order": 100000,
2829
"title": "a",
@@ -56,6 +57,7 @@ Object {
5657
"linkTitleTemplate": "{{ @ }}",
5758
"linkUrlTemplate": "{{ @ }}",
5859
"name": "a",
60+
"nullValue": "null",
5961
"numberFormat": undefined,
6062
"order": 100000,
6163
"title": "a",
@@ -89,6 +91,7 @@ Object {
8991
"linkTitleTemplate": "{{ @ }}",
9092
"linkUrlTemplate": "{{ @ }}",
9193
"name": "a",
94+
"nullValue": "null",
9295
"numberFormat": undefined,
9396
"order": 100000,
9497
"title": "test",
@@ -122,6 +125,7 @@ Object {
122125
"linkTitleTemplate": "{{ @ }}",
123126
"linkUrlTemplate": "{{ @ }}",
124127
"name": "a",
128+
"nullValue": "null",
125129
"numberFormat": undefined,
126130
"order": 100000,
127131
"title": "a",
@@ -155,6 +159,7 @@ Object {
155159
"linkTitleTemplate": "{{ @ }}",
156160
"linkUrlTemplate": "{{ @ }}",
157161
"name": "a",
162+
"nullValue": "null",
158163
"numberFormat": undefined,
159164
"order": 100000,
160165
"title": "a",

viz-lib/src/visualizations/table/columns/number.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function Editor({ column, onChange }: Props) {
3333
}
3434

3535
export default function initNumberColumn(column: any) {
36-
const format = createNumberFormatter(column.numberFormat);
36+
const format = createNumberFormatter(column.numberFormat, true);
3737

3838
function prepareData(row: any) {
3939
return {

viz-lib/src/visualizations/table/getOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function getDefaultFormatOptions(column: any) {
7373
dateTimeFormat: dateTimeFormat[column.type],
7474
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
7575
numberFormat: numberFormat[column.type],
76+
nullValue: visualizationsSettings.nullValue,
7677
booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
7778
// `image` cell options
7879
imageUrlTemplate: "{{ @ }}",

viz-lib/src/visualizations/table/renderer.less

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
white-space: nowrap;
4040
}
4141

42+
.display-as-null {
43+
font-style: italic;
44+
color: @text-muted;
45+
}
46+
4247
.table-visualization-spacer {
4348
padding-left: 0;
4449
padding-right: 0;

viz-lib/src/visualizations/visualizationsSettings.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const visualizationsSettings = {
4242
dateTimeFormat: "DD/MM/YYYY HH:mm",
4343
integerFormat: "0,0",
4444
floatFormat: "0,0.00",
45+
nullValue: "null",
4546
booleanValues: ["false", "true"],
4647
tableCellMaxJSONSize: 50000,
4748
allowCustomJSVisualizations: false,

0 commit comments

Comments
 (0)