diff --git a/licenses.yaml b/licenses.yaml index 608633a01a3..62f1991f765 100644 --- a/licenses.yaml +++ b/licenses.yaml @@ -5814,7 +5814,7 @@ license_category: binary module: web-console license_name: Apache License version 2.0 copyright: Imply Data -version: 0.18.3 +version: 0.18.12 --- diff --git a/web-console/package-lock.json b/web-console/package-lock.json index 119224bff1f..10ba50cf2f5 100644 --- a/web-console/package-lock.json +++ b/web-console/package-lock.json @@ -22,7 +22,7 @@ "d3-axis": "^2.1.0", "d3-scale": "^3.3.0", "d3-selection": "^2.0.0", - "druid-query-toolkit": "^0.18.3", + "druid-query-toolkit": "^0.18.12", "file-saver": "^2.0.2", "follow-redirects": "^1.14.7", "fontsource-open-sans": "^3.0.9", @@ -8211,9 +8211,9 @@ } }, "node_modules/druid-query-toolkit": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.18.3.tgz", - "integrity": "sha512-Za2U2NsFyun5HXeWnLCICnTFzZp4aC17aSOjgVbQgEWZNMPht51U4paE3SVhPDObkWDjDUYAqVv+mO+ZyMx9Og==", + "version": "0.18.12", + "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.18.12.tgz", + "integrity": "sha512-wDcZUW8vhiJXARC44EFFwUeZW6lawXWv++bxHIUKaxq3M5byBuWPKjEDTCdPEHprxmR2sxaTpsPw4A6KiRmBog==", "dependencies": { "tslib": "^2.3.1" }, @@ -32625,9 +32625,9 @@ } }, "druid-query-toolkit": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.18.3.tgz", - "integrity": "sha512-Za2U2NsFyun5HXeWnLCICnTFzZp4aC17aSOjgVbQgEWZNMPht51U4paE3SVhPDObkWDjDUYAqVv+mO+ZyMx9Og==", + "version": "0.18.12", + "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.18.12.tgz", + "integrity": "sha512-wDcZUW8vhiJXARC44EFFwUeZW6lawXWv++bxHIUKaxq3M5byBuWPKjEDTCdPEHprxmR2sxaTpsPw4A6KiRmBog==", "requires": { "tslib": "^2.3.1" } diff --git a/web-console/package.json b/web-console/package.json index 68b83bfe99c..9dba6ca8190 100644 --- a/web-console/package.json +++ b/web-console/package.json @@ -76,7 +76,7 @@ "d3-axis": "^2.1.0", "d3-scale": "^3.3.0", "d3-selection": "^2.0.0", - "druid-query-toolkit": "^0.18.3", + "druid-query-toolkit": "^0.18.12", "file-saver": "^2.0.2", "follow-redirects": "^1.14.7", "fontsource-open-sans": "^3.0.9", diff --git a/web-console/src/components/json-collapse/json-collapse.tsx b/web-console/src/components/json-collapse/json-collapse.tsx index 3a92db4db63..5a0869a5bd3 100644 --- a/web-console/src/components/json-collapse/json-collapse.tsx +++ b/web-console/src/components/json-collapse/json-collapse.tsx @@ -31,7 +31,7 @@ export const JsonCollapse = React.memo(function JsonCollapse(props: JsonCollapse const { stringValue, buttonText } = props; const [isOpen, setIsOpen] = useState(false); - const prettyValue = JSONBig.stringify(JSON.parse(stringValue), undefined, 2); + const prettyValue = JSONBig.stringify(JSONBig.parse(stringValue), undefined, 2); return (
diff --git a/web-console/src/dialogs/spec-dialog/spec-dialog.tsx b/web-console/src/dialogs/spec-dialog/spec-dialog.tsx index c4bc5a3fa9f..f38cd0d8df8 100644 --- a/web-console/src/dialogs/spec-dialog/spec-dialog.tsx +++ b/web-console/src/dialogs/spec-dialog/spec-dialog.tsx @@ -21,13 +21,14 @@ import * as JSONBig from 'json-bigint-native'; import React, { useState } from 'react'; import AceEditor from 'react-ace'; -import { validJson } from '../../utils'; +import { AppToaster } from '../../singletons'; +import { offsetToRowColumn } from '../../utils'; import './spec-dialog.scss'; export interface SpecDialogProps { - onSubmit: (spec: JSON) => void | Promise; - onClose: () => void; + onSubmit(spec: JSON): void | Promise; + onClose(): void; title: string; initSpec?: any; } @@ -38,9 +39,23 @@ export const SpecDialog = React.memo(function SpecDialog(props: SpecDialogProps) initSpec ? JSONBig.stringify(initSpec, undefined, 2) : '', ); - function postSpec(): void { - if (!validJson(spec)) return; - void onSubmit(JSON.parse(spec)); + function handleSubmit(): void { + let parsed: any; + 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(); } @@ -78,12 +93,7 @@ export const SpecDialog = React.memo(function SpecDialog(props: SpecDialogProps)
diff --git a/web-console/src/utils/general.spec.ts b/web-console/src/utils/general.spec.ts index fa11e1c8714..7b380ae7cf3 100644 --- a/web-console/src/utils/general.spec.ts +++ b/web-console/src/utils/general.spec.ts @@ -28,6 +28,7 @@ import { moveElement, moveToIndex, objectHash, + offsetToRowColumn, parseCsvLine, swapElements, } from './general'; @@ -171,4 +172,19 @@ describe('general', () => { 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, + }); + }); + }); }); diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 3b29dceafdc..f7c6823203c 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -465,3 +465,26 @@ export function tickIcon(checked: boolean): IconName { export function generate8HexId(): string { 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; +} diff --git a/web-console/src/utils/object-change.spec.ts b/web-console/src/utils/object-change.spec.ts index e7c28b2effa..bdfecca7ec1 100644 --- a/web-console/src/utils/object-change.spec.ts +++ b/web-console/src/utils/object-change.spec.ts @@ -138,7 +138,7 @@ describe('object-change', () => { }); 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: { moon: 1, wow: [ diff --git a/web-console/src/views/load-data-view/filter-table/filter-table.tsx b/web-console/src/views/load-data-view/filter-table/filter-table.tsx index 368072762f0..3f0323e1898 100644 --- a/web-console/src/views/load-data-view/filter-table/filter-table.tsx +++ b/web-console/src/views/load-data-view/filter-table/filter-table.tsx @@ -23,7 +23,7 @@ import ReactTable from 'react-table'; import { TableCell } from '../../../components'; import type { DruidFilter } from '../../../druid-models'; -import { getFilterDimension } from '../../../druid-models'; +import { getFilterDimension, TIME_COLUMN } from '../../../druid-models'; import { DEFAULT_TABLE_CLASS_NAME, 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} columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => { if (!caseInsensitiveContains(columnName, columnFilter)) return; - const timestamp = columnName === '__time'; + const isTimestamp = columnName === TIME_COLUMN; const filterIndex = dimensionFilters.findIndex(f => getFilterDimension(f) === columnName); const filter = dimensionFilters[filterIndex]; @@ -84,7 +84,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro onFilterSelect(filter, filterIndex); } else { onFilterSelect( - timestamp + isTimestamp ? { type: 'interval', dimension: columnName, intervals: [] } : { type: 'selector', dimension: columnName, value: '' }, -1, @@ -102,7 +102,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), width: 140, Cell: function FilterTableCell(row: RowRenderProps) { - return ; + return ; }, }; })} diff --git a/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx b/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx index 61cc4aeeccc..2d7a32bad12 100644 --- a/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx +++ b/web-console/src/views/load-data-view/parse-time-table/parse-time-table.tsx @@ -27,6 +27,7 @@ import { getTimestampDetailFromSpec, getTimestampSpecColumnFromSpec, possibleDruidFormatForValues, + TIME_COLUMN, } from '../../../druid-models'; import { DEFAULT_TABLE_CLASS_NAME, @@ -86,7 +87,7 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS} showPagination={sampleResponse.data.length > STANDARD_TABLE_PAGE_SIZE} columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => { - const isTimestamp = columnName === '__time'; + const isTimestamp = columnName === TIME_COLUMN; if (!isTimestamp && !caseInsensitiveContains(columnName, columnFilter)) return; const used = timestampSpecColumn === columnName; const possibleFormat = isTimestamp @@ -134,7 +135,7 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim if (row.original.unparseable) { return ; } - return ; + return ; }, width: isTimestamp ? 200 : 140, resizable: !isTimestamp, diff --git a/web-console/src/views/load-data-view/schema-table/schema-table.tsx b/web-console/src/views/load-data-view/schema-table/schema-table.tsx index b6ba0e58d32..b1245d6b39c 100644 --- a/web-console/src/views/load-data-view/schema-table/schema-table.tsx +++ b/web-console/src/views/load-data-view/schema-table/schema-table.tsx @@ -28,6 +28,7 @@ import { getDimensionSpecType, getMetricSpecName, inflateDimensionSpec, + TIME_COLUMN, } from '../../../druid-models'; import { DEFAULT_TABLE_CLASS_NAME, @@ -109,7 +110,7 @@ export const SchemaTable = React.memo(function SchemaTable(props: SchemaTablePro }, }; } else { - const isTimestamp = columnName === '__time'; + const isTimestamp = columnName === TIME_COLUMN; const dimensionSpecIndex = dimensions ? dimensions.findIndex(d => getDimensionSpecName(d) === columnName) : -1; @@ -151,7 +152,7 @@ export const SchemaTable = React.memo(function SchemaTable(props: SchemaTablePro width: isTimestamp ? 200 : 140, accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), Cell: function SchemaTableCell(row: RowRenderProps) { - return ; + return ; }, }; } diff --git a/web-console/src/views/load-data-view/transform-table/transform-table.tsx b/web-console/src/views/load-data-view/transform-table/transform-table.tsx index 55c4e091fb4..02d253d3a54 100644 --- a/web-console/src/views/load-data-view/transform-table/transform-table.tsx +++ b/web-console/src/views/load-data-view/transform-table/transform-table.tsx @@ -23,6 +23,7 @@ import ReactTable from 'react-table'; import { TableCell } from '../../../components'; import type { Transform } from '../../../druid-models'; +import { TIME_COLUMN } from '../../../druid-models'; import { DEFAULT_TABLE_CLASS_NAME, 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} columns={filterMap(getHeaderNamesFromSampleResponse(sampleResponse), (columnName, i) => { if (!caseInsensitiveContains(columnName, columnFilter)) return; - const timestamp = columnName === '__time'; + const isTimestamp = columnName === TIME_COLUMN; const transformIndex = transforms.findIndex(f => f.name === columnName); if (transformIndex === -1 && transformedColumnsOnly) return; 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), width: 140, Cell: function TransformTableCell(row: RowRenderProps) { - return ; + return ; }, }; })} diff --git a/web-console/src/views/workbench-view/execution-submit-dialog/execution-submit-dialog.tsx b/web-console/src/views/workbench-view/execution-submit-dialog/execution-submit-dialog.tsx index e6cc36ca646..e197e2d87cb 100644 --- a/web-console/src/views/workbench-view/execution-submit-dialog/execution-submit-dialog.tsx +++ b/web-console/src/views/workbench-view/execution-submit-dialog/execution-submit-dialog.tsx @@ -17,18 +17,19 @@ */ import { Button, Classes, Dialog, Intent } from '@blueprintjs/core'; +import * as JSONBig from 'json-bigint-native'; import React, { useState } from 'react'; import AceEditor from 'react-ace'; import { Execution } from '../../../druid-models'; import { AppToaster } from '../../../singletons'; -import { validJson } from '../../../utils'; +import { offsetToRowColumn } from '../../../utils'; import './execution-submit-dialog.scss'; export interface ExecutionSubmitDialogProps { - onSubmit: (execution: Execution) => void; - onClose: () => void; + onSubmit(execution: Execution): void; + onClose(): void; } export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog( @@ -37,15 +38,18 @@ export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog( const { onClose, onSubmit } = props; const [archive, setArchive] = useState(''); - function submitProfile(): void { - if (!validJson(archive)) return; + function handleSubmit(): void { let parsed: any; try { - parsed = JSON.parse(archive); + parsed = JSONBig.parse(archive); } catch (e) { + const rowColumn = typeof e.at === 'number' ? offsetToRowColumn(archive, e.at) : undefined; AppToaster.show({ 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; } @@ -129,8 +133,8 @@ export const ExecutionSubmitDialog = React.memo(function ExecutionSubmitDialog(