Web console: update DQT to latest version and fix bigint crash (#14318)

* update dqt

* don't crash on bigint values

* better submit experiance

* bump to an even version
This commit is contained in:
Vadim Ogievetsky 2023-05-24 17:40:45 -07:00 committed by GitHub
parent 88831b1dd0
commit 1873fca6c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 98 additions and 42 deletions

View File

@ -5814,7 +5814,7 @@ license_category: binary
module: web-console module: web-console
license_name: Apache License version 2.0 license_name: Apache License version 2.0
copyright: Imply Data copyright: Imply Data
version: 0.18.3 version: 0.18.12
--- ---

View File

@ -22,7 +22,7 @@
"d3-axis": "^2.1.0", "d3-axis": "^2.1.0",
"d3-scale": "^3.3.0", "d3-scale": "^3.3.0",
"d3-selection": "^2.0.0", "d3-selection": "^2.0.0",
"druid-query-toolkit": "^0.18.3", "druid-query-toolkit": "^0.18.12",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"follow-redirects": "^1.14.7", "follow-redirects": "^1.14.7",
"fontsource-open-sans": "^3.0.9", "fontsource-open-sans": "^3.0.9",
@ -8211,9 +8211,9 @@
} }
}, },
"node_modules/druid-query-toolkit": { "node_modules/druid-query-toolkit": {
"version": "0.18.3", "version": "0.18.12",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.18.3.tgz", "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.18.12.tgz",
"integrity": "sha512-Za2U2NsFyun5HXeWnLCICnTFzZp4aC17aSOjgVbQgEWZNMPht51U4paE3SVhPDObkWDjDUYAqVv+mO+ZyMx9Og==", "integrity": "sha512-wDcZUW8vhiJXARC44EFFwUeZW6lawXWv++bxHIUKaxq3M5byBuWPKjEDTCdPEHprxmR2sxaTpsPw4A6KiRmBog==",
"dependencies": { "dependencies": {
"tslib": "^2.3.1" "tslib": "^2.3.1"
}, },
@ -32625,9 +32625,9 @@
} }
}, },
"druid-query-toolkit": { "druid-query-toolkit": {
"version": "0.18.3", "version": "0.18.12",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.18.3.tgz", "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.18.12.tgz",
"integrity": "sha512-Za2U2NsFyun5HXeWnLCICnTFzZp4aC17aSOjgVbQgEWZNMPht51U4paE3SVhPDObkWDjDUYAqVv+mO+ZyMx9Og==", "integrity": "sha512-wDcZUW8vhiJXARC44EFFwUeZW6lawXWv++bxHIUKaxq3M5byBuWPKjEDTCdPEHprxmR2sxaTpsPw4A6KiRmBog==",
"requires": { "requires": {
"tslib": "^2.3.1" "tslib": "^2.3.1"
} }

View File

@ -76,7 +76,7 @@
"d3-axis": "^2.1.0", "d3-axis": "^2.1.0",
"d3-scale": "^3.3.0", "d3-scale": "^3.3.0",
"d3-selection": "^2.0.0", "d3-selection": "^2.0.0",
"druid-query-toolkit": "^0.18.3", "druid-query-toolkit": "^0.18.12",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"follow-redirects": "^1.14.7", "follow-redirects": "^1.14.7",
"fontsource-open-sans": "^3.0.9", "fontsource-open-sans": "^3.0.9",

View File

@ -31,7 +31,7 @@ export const JsonCollapse = React.memo(function JsonCollapse(props: JsonCollapse
const { stringValue, buttonText } = props; const { stringValue, buttonText } = props;
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const prettyValue = JSONBig.stringify(JSON.parse(stringValue), undefined, 2); const prettyValue = JSONBig.stringify(JSONBig.parse(stringValue), undefined, 2);
return ( return (
<div className="json-collapse"> <div className="json-collapse">
<div className="collapse-buttons"> <div className="collapse-buttons">

View File

@ -21,13 +21,14 @@ import * as JSONBig from 'json-bigint-native';
import React, { useState } from 'react'; import React, { useState } from 'react';
import AceEditor from 'react-ace'; import AceEditor from 'react-ace';
import { validJson } from '../../utils'; import { AppToaster } from '../../singletons';
import { offsetToRowColumn } from '../../utils';
import './spec-dialog.scss'; import './spec-dialog.scss';
export interface SpecDialogProps { export interface SpecDialogProps {
onSubmit: (spec: JSON) => void | Promise<void>; onSubmit(spec: JSON): void | Promise<void>;
onClose: () => void; onClose(): void;
title: string; title: string;
initSpec?: any; initSpec?: any;
} }
@ -38,9 +39,23 @@ export const SpecDialog = React.memo(function SpecDialog(props: SpecDialogProps)
initSpec ? JSONBig.stringify(initSpec, undefined, 2) : '', initSpec ? JSONBig.stringify(initSpec, undefined, 2) : '',
); );
function postSpec(): void { function handleSubmit(): void {
if (!validJson(spec)) return; let parsed: any;
void onSubmit(JSON.parse(spec)); try {
parsed = JSONBig.parse(spec);
} catch (e) {
const rowColumn = typeof e.at === 'number' ? offsetToRowColumn(spec, e.at) : undefined;
AppToaster.show({
intent: Intent.DANGER,
message: `Could not parse JSON: ${e.message}${
rowColumn ? ` (at line ${rowColumn.row + 1}, column ${rowColumn.column + 1})` : ''
}`,
timeout: 5000,
});
return;
}
void onSubmit(parsed);
onClose(); onClose();
} }
@ -78,12 +93,7 @@ export const SpecDialog = React.memo(function SpecDialog(props: SpecDialogProps)
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button text="Close" onClick={onClose} /> <Button text="Close" onClick={onClose} />
<Button <Button text="Submit" intent={Intent.PRIMARY} onClick={handleSubmit} disabled={!spec} />
text="Submit"
intent={Intent.PRIMARY}
onClick={postSpec}
disabled={!validJson(spec)}
/>
</div> </div>
</div> </div>
</Dialog> </Dialog>

View File

@ -28,6 +28,7 @@ import {
moveElement, moveElement,
moveToIndex, moveToIndex,
objectHash, objectHash,
offsetToRowColumn,
parseCsvLine, parseCsvLine,
swapElements, swapElements,
} from './general'; } from './general';
@ -171,4 +172,19 @@ describe('general', () => {
expect(objectHash({ hello: 'world1' })).toEqual('cc14ad13'); expect(objectHash({ hello: 'world1' })).toEqual('cc14ad13');
}); });
}); });
describe('offsetToRowColumn', () => {
it('works', () => {
expect(offsetToRowColumn('Hello\nThis is a test\nstring.', -6)).toBeUndefined();
expect(offsetToRowColumn('Hello\nThis is a test\nstring.', 666)).toBeUndefined();
expect(offsetToRowColumn('Hello\nThis is a test\nstring.', 3)).toEqual({
column: 3,
row: 0,
});
expect(offsetToRowColumn('Hello\nThis is a test\nstring.', 24)).toEqual({
column: 3,
row: 2,
});
});
});
}); });

View File

@ -465,3 +465,26 @@ export function tickIcon(checked: boolean): IconName {
export function generate8HexId(): string { export function generate8HexId(): string {
return (Math.random() * 1e10).toString(16).replace('.', '').slice(0, 8); return (Math.random() * 1e10).toString(16).replace('.', '').slice(0, 8);
} }
export function offsetToRowColumn(
str: string,
offset: number,
): { row: number; column: number } | undefined {
// Ensure offset is within the string length
if (offset < 0 || offset > str.length) return;
const lines = str.split('\n');
for (let row = 0; row < lines.length; row++) {
const line = lines[row];
if (offset < line.length) {
return {
row,
column: offset,
};
}
offset -= line.length + 1;
}
return;
}

View File

@ -138,7 +138,7 @@ describe('object-change', () => {
}); });
it('works with arrays', () => { it('works with arrays', () => {
expect(JSON.parse(JSONBig.stringify(deepDelete(thing, 'hello.wow.0')))).toEqual({ expect(JSONBig.parse(JSONBig.stringify(deepDelete(thing, 'hello.wow.0')))).toEqual({
hello: { hello: {
moon: 1, moon: 1,
wow: [ wow: [

View File

@ -23,7 +23,7 @@ import ReactTable from 'react-table';
import { TableCell } from '../../../components'; import { TableCell } from '../../../components';
import type { DruidFilter } from '../../../druid-models'; import type { DruidFilter } from '../../../druid-models';
import { getFilterDimension } from '../../../druid-models'; import { getFilterDimension, TIME_COLUMN } from '../../../druid-models';
import { import {
DEFAULT_TABLE_CLASS_NAME, DEFAULT_TABLE_CLASS_NAME,
STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE,
@ -67,7 +67,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro
showPagination={sampleResponse.data.length > STANDARD_TABLE_PAGE_SIZE} showPagination={sampleResponse.data.length > STANDARD_TABLE_PAGE_SIZE}
columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => { columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => {
if (!caseInsensitiveContains(columnName, columnFilter)) return; if (!caseInsensitiveContains(columnName, columnFilter)) return;
const timestamp = columnName === '__time'; const isTimestamp = columnName === TIME_COLUMN;
const filterIndex = dimensionFilters.findIndex(f => getFilterDimension(f) === columnName); const filterIndex = dimensionFilters.findIndex(f => getFilterDimension(f) === columnName);
const filter = dimensionFilters[filterIndex]; const filter = dimensionFilters[filterIndex];
@ -84,7 +84,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro
onFilterSelect(filter, filterIndex); onFilterSelect(filter, filterIndex);
} else { } else {
onFilterSelect( onFilterSelect(
timestamp isTimestamp
? { type: 'interval', dimension: columnName, intervals: [] } ? { type: 'interval', dimension: columnName, intervals: [] }
: { type: 'selector', dimension: columnName, value: '' }, : { type: 'selector', dimension: columnName, value: '' },
-1, -1,
@ -102,7 +102,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
width: 140, width: 140,
Cell: function FilterTableCell(row: RowRenderProps) { Cell: function FilterTableCell(row: RowRenderProps) {
return <TableCell value={timestamp ? new Date(row.value) : row.value} />; return <TableCell value={isTimestamp ? new Date(Number(row.value)) : row.value} />;
}, },
}; };
})} })}

View File

@ -27,6 +27,7 @@ import {
getTimestampDetailFromSpec, getTimestampDetailFromSpec,
getTimestampSpecColumnFromSpec, getTimestampSpecColumnFromSpec,
possibleDruidFormatForValues, possibleDruidFormatForValues,
TIME_COLUMN,
} from '../../../druid-models'; } from '../../../druid-models';
import { import {
DEFAULT_TABLE_CLASS_NAME, DEFAULT_TABLE_CLASS_NAME,
@ -86,7 +87,7 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim
pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS} pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
showPagination={sampleResponse.data.length > STANDARD_TABLE_PAGE_SIZE} showPagination={sampleResponse.data.length > STANDARD_TABLE_PAGE_SIZE}
columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => { columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => {
const isTimestamp = columnName === '__time'; const isTimestamp = columnName === TIME_COLUMN;
if (!isTimestamp && !caseInsensitiveContains(columnName, columnFilter)) return; if (!isTimestamp && !caseInsensitiveContains(columnName, columnFilter)) return;
const used = timestampSpecColumn === columnName; const used = timestampSpecColumn === columnName;
const possibleFormat = isTimestamp const possibleFormat = isTimestamp
@ -134,7 +135,7 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim
if (row.original.unparseable) { if (row.original.unparseable) {
return <TableCellUnparseable timestamp={isTimestamp} />; return <TableCellUnparseable timestamp={isTimestamp} />;
} }
return <TableCell value={isTimestamp ? new Date(row.value) : row.value} />; return <TableCell value={isTimestamp ? new Date(Number(row.value)) : row.value} />;
}, },
width: isTimestamp ? 200 : 140, width: isTimestamp ? 200 : 140,
resizable: !isTimestamp, resizable: !isTimestamp,

View File

@ -28,6 +28,7 @@ import {
getDimensionSpecType, getDimensionSpecType,
getMetricSpecName, getMetricSpecName,
inflateDimensionSpec, inflateDimensionSpec,
TIME_COLUMN,
} from '../../../druid-models'; } from '../../../druid-models';
import { import {
DEFAULT_TABLE_CLASS_NAME, DEFAULT_TABLE_CLASS_NAME,
@ -109,7 +110,7 @@ export const SchemaTable = React.memo(function SchemaTable(props: SchemaTablePro
}, },
}; };
} else { } else {
const isTimestamp = columnName === '__time'; const isTimestamp = columnName === TIME_COLUMN;
const dimensionSpecIndex = dimensions const dimensionSpecIndex = dimensions
? dimensions.findIndex(d => getDimensionSpecName(d) === columnName) ? dimensions.findIndex(d => getDimensionSpecName(d) === columnName)
: -1; : -1;
@ -151,7 +152,7 @@ export const SchemaTable = React.memo(function SchemaTable(props: SchemaTablePro
width: isTimestamp ? 200 : 140, width: isTimestamp ? 200 : 140,
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
Cell: function SchemaTableCell(row: RowRenderProps) { Cell: function SchemaTableCell(row: RowRenderProps) {
return <TableCell value={isTimestamp ? new Date(row.value) : row.value} />; return <TableCell value={isTimestamp ? new Date(Number(row.value)) : row.value} />;
}, },
}; };
} }

View File

@ -23,6 +23,7 @@ import ReactTable from 'react-table';
import { TableCell } from '../../../components'; import { TableCell } from '../../../components';
import type { Transform } from '../../../druid-models'; import type { Transform } from '../../../druid-models';
import { TIME_COLUMN } from '../../../druid-models';
import { import {
DEFAULT_TABLE_CLASS_NAME, DEFAULT_TABLE_CLASS_NAME,
STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE,
@ -79,7 +80,7 @@ export const TransformTable = React.memo(function TransformTable(props: Transfor
showPagination={sampleResponse.data.length > STANDARD_TABLE_PAGE_SIZE} showPagination={sampleResponse.data.length > STANDARD_TABLE_PAGE_SIZE}
columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => { columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => {
if (!caseInsensitiveContains(columnName, columnFilter)) return; if (!caseInsensitiveContains(columnName, columnFilter)) return;
const timestamp = columnName === '__time'; const isTimestamp = columnName === TIME_COLUMN;
const transformIndex = transforms.findIndex(f => f.name === columnName); const transformIndex = transforms.findIndex(f => f.name === columnName);
if (transformIndex === -1 && transformedColumnsOnly) return; if (transformIndex === -1 && transformedColumnsOnly) return;
const transform = transforms[transformIndex]; const transform = transforms[transformIndex];
@ -119,7 +120,7 @@ export const TransformTable = React.memo(function TransformTable(props: Transfor
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
width: 140, width: 140,
Cell: function TransformTableCell(row: RowRenderProps) { Cell: function TransformTableCell(row: RowRenderProps) {
return <TableCell value={timestamp ? new Date(row.value) : row.value} />; return <TableCell value={isTimestamp ? new Date(Number(row.value)) : row.value} />;
}, },
}; };
})} })}

View File

@ -17,18 +17,19 @@
*/ */
import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; import { Button, Classes, Dialog, Intent } from '@blueprintjs/core';
import * as JSONBig from 'json-bigint-native';
import React, { useState } from 'react'; import React, { useState } from 'react';
import AceEditor from 'react-ace'; import AceEditor from 'react-ace';
import { Execution } from '../../../druid-models'; import { Execution } from '../../../druid-models';
import { AppToaster } from '../../../singletons'; import { AppToaster } from '../../../singletons';
import { validJson } from '../../../utils'; import { offsetToRowColumn } from '../../../utils';
import './execution-submit-dialog.scss'; import './execution-submit-dialog.scss';
export interface ExecutionSubmitDialogProps { export interface ExecutionSubmitDialogProps {
onSubmit: (execution: Execution) => void; onSubmit(execution: Execution): void;
onClose: () => void; onClose(): void;
} }
export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog( export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog(
@ -37,15 +38,18 @@ export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog(
const { onClose, onSubmit } = props; const { onClose, onSubmit } = props;
const [archive, setArchive] = useState(''); const [archive, setArchive] = useState('');
function submitProfile(): void { function handleSubmit(): void {
if (!validJson(archive)) return;
let parsed: any; let parsed: any;
try { try {
parsed = JSON.parse(archive); parsed = JSONBig.parse(archive);
} catch (e) { } catch (e) {
const rowColumn = typeof e.at === 'number' ? offsetToRowColumn(archive, e.at) : undefined;
AppToaster.show({ AppToaster.show({
intent: Intent.DANGER, intent: Intent.DANGER,
message: `Could not parse JSON: ${e.message}`, message: `Could not parse JSON: ${e.message}${
rowColumn ? ` (at line ${rowColumn.row + 1}, column ${rowColumn.column + 1})` : ''
}`,
timeout: 5000,
}); });
return; return;
} }
@ -129,8 +133,8 @@ export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog(
<Button <Button
text="Submit" text="Submit"
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
onClick={submitProfile} onClick={handleSubmit}
disabled={!validJson(archive)} disabled={!archive}
/> />
</div> </div>
</div> </div>