Web console: fix error when querying with grand totals (#8795)

* fix error when querying with grand totals

* also support object

* improve tests
This commit is contained in:
Vadim Ogievetsky 2019-10-30 19:37:53 -07:00 committed by Fangjin Yang
parent 3ff5e02237
commit ed6be81d12
16 changed files with 531 additions and 844 deletions

View File

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`table cell unparseable matches snapshot not timestamp 1`] = `
<div
class="table-cell-unparseable"
>
unparseable
</div>
`;
exports[`table cell unparseable matches snapshot timestamp 1`] = `
<div
class="table-cell-unparseable"
>
unparseable timestamp
</div>
`;

View File

@ -0,0 +1,21 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.table-cell-unparseable {
color: #9e2b0e;
}

View File

@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { TableCellUnparseable } from './table-cell-unparseable';
describe('table cell unparseable', () => {
it('matches snapshot not timestamp', () => {
const tableCellUnparseable = <TableCellUnparseable />;
const { container } = render(tableCellUnparseable);
expect(container.firstChild).toMatchSnapshot();
});
it('matches snapshot timestamp', () => {
const tableCellUnparseable = <TableCellUnparseable timestamp />;
const { container } = render(tableCellUnparseable);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import './table-cell-unparseable.scss';
export interface TableCellUnparseableProps {
timestamp?: boolean;
}
export const TableCellUnparseable = React.memo(function TableCellUnparseable(
props: TableCellUnparseableProps,
) {
const { timestamp } = props;
return (
<div className="table-cell-unparseable">
{timestamp ? 'unparseable timestamp' : 'unparseable'}
</div>
);
});

File diff suppressed because one or more lines are too long

View File

@ -21,10 +21,6 @@
font-style: italic; font-style: italic;
} }
&.unparseable {
color: #9e2b0e;
}
&.timestamp { &.timestamp {
font-weight: bold; font-weight: bold;
} }

View File

@ -23,49 +23,59 @@ import { TableCell } from './table-cell';
describe('table cell', () => { describe('table cell', () => {
it('matches snapshot null', () => { it('matches snapshot null', () => {
const tableCell = <TableCell value={null} unparseable={false} timestamp={false} />; const tableCell = <TableCell value={null} />;
const { container } = render(tableCell);
expect(container.firstChild).toMatchSnapshot();
});
it('matches snapshot null timestamp', () => {
const tableCell = <TableCell value={null} unparseable={false} timestamp />;
const { container } = render(tableCell); const { container } = render(tableCell);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
it('matches snapshot simple', () => { it('matches snapshot simple', () => {
const tableCell = <TableCell value="Hello World" unparseable={false} timestamp={false} />; const tableCell = <TableCell value="Hello World" />;
const { container } = render(tableCell); const { container } = render(tableCell);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
it('matches snapshot array short', () => { it('matches snapshot array short', () => {
const tableCell = <TableCell value={['a', 'b', 'c']} unparseable={false} timestamp={false} />; const tableCell = <TableCell value={['a', 'b', 'c']} />;
const { container } = render(tableCell); const { container } = render(tableCell);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
it('matches snapshot array long', () => { it('matches snapshot array long', () => {
const tableCell = ( const tableCell = <TableCell value={Array.from(new Array(100)).map((_, i) => i)} />;
<TableCell
value={Array.from(new Array(100)).map((_, i) => i)} const { container } = render(tableCell);
unparseable={false} expect(container.firstChild).toMatchSnapshot();
timestamp={false} });
/>
); it('matches snapshot object', () => {
const tableCell = <TableCell value={{ hello: 'world' }} />;
const { container } = render(tableCell); const { container } = render(tableCell);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
it('matches snapshot truncate', () => { it('matches snapshot truncate', () => {
const longString = new Array(100).join('test'); const longString = new Array(100).join('test_');
const tableCell = <TableCell value={longString} unparseable={false} timestamp={false} />; const tableCell = <TableCell value={longString} />;
const { container } = render(tableCell);
expect(container.firstChild).toMatchSnapshot();
});
it('matches snapshot unlimited', () => {
const longString = new Array(100).join('test_');
const tableCell = <TableCell value={longString} unlimited />;
const { container } = render(tableCell);
expect(container.firstChild).toMatchSnapshot();
});
it('matches snapshot unlimited (absolute max)', () => {
const longString = new Array(5000).join('test_');
const tableCell = <TableCell value={longString} unlimited />;
const { container } = render(tableCell); const { container } = render(tableCell);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();

View File

@ -25,6 +25,7 @@ import { ActionIcon } from '../action-icon/action-icon';
import './table-cell.scss'; import './table-cell.scss';
const MAX_CHARS_TO_SHOW = 50; const MAX_CHARS_TO_SHOW = 50;
const ABSOLUTE_MAX_CHARS_TO_SHOW = 5000;
interface ShortParts { interface ShortParts {
prefix: string; prefix: string;
@ -46,13 +47,12 @@ function shortenString(str: string): ShortParts {
} }
export interface TableCellProps { export interface TableCellProps {
value?: any; value: any;
timestamp?: boolean; unlimited?: boolean;
unparseable?: boolean;
} }
export const TableCell = React.memo(function TableCell(props: TableCellProps) { export const TableCell = React.memo(function TableCell(props: TableCellProps) {
const { value, timestamp, unparseable } = props; const { value, unlimited } = props;
const [showValue, setShowValue] = useState(); const [showValue, setShowValue] = useState();
function renderShowValueDialog(): JSX.Element | undefined { function renderShowValueDialog(): JSX.Element | undefined {
@ -62,7 +62,19 @@ export const TableCell = React.memo(function TableCell(props: TableCellProps) {
} }
function renderTruncated(str: string): JSX.Element { function renderTruncated(str: string): JSX.Element {
if (str.length <= MAX_CHARS_TO_SHOW) return <span className="table-cell plain">{str}</span>; if (str.length <= MAX_CHARS_TO_SHOW) {
return <span className="table-cell plain">{str}</span>;
}
if (unlimited) {
return (
<span className="table-cell plain">
{str.length < ABSOLUTE_MAX_CHARS_TO_SHOW
? str
: `${str.substr(0, ABSOLUTE_MAX_CHARS_TO_SHOW)}...`}
</span>
);
}
const { prefix, omitted, suffix } = shortenString(str); const { prefix, omitted, suffix } = shortenString(str);
return ( return (
@ -76,25 +88,21 @@ export const TableCell = React.memo(function TableCell(props: TableCellProps) {
); );
} }
if (unparseable) { if (value !== '' && value != null) {
return <span className="table-cell unparseable">error</span>; if (value instanceof Date) {
} else if (value !== '' && value != null) {
if (timestamp) {
return ( return (
<span className="table-cell timestamp" title={value}> <span className="table-cell timestamp" title={String(value.valueOf())}>
{new Date(value).toISOString()} {value.toISOString()}
</span> </span>
); );
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
return renderTruncated(`[${value.join(', ')}]`); return renderTruncated(`[${value.join(', ')}]`);
} else if (typeof value === 'object') {
return renderTruncated(JSON.stringify(value));
} else { } else {
return renderTruncated(String(value)); return renderTruncated(String(value));
} }
} else { } else {
if (timestamp) { return <span className="table-cell null">null</span>;
return <span className="table-cell unparseable">unparseable timestamp</span>;
} else {
return <span className="table-cell null">null</span>;
}
} }
}); });

View File

@ -92,7 +92,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro
className: columnClassName, className: columnClassName,
id: String(i), id: String(i),
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
Cell: row => <TableCell value={row.value} timestamp={timestamp} />, Cell: row => <TableCell value={timestamp ? new Date(row.value) : row.value} />,
}; };
})} })}
defaultPageSize={50} defaultPageSize={50}

View File

@ -21,6 +21,7 @@ import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { TableCell } from '../../../components'; import { TableCell } from '../../../components';
import { TableCellUnparseable } from '../../../components/table-cell-unparseable/table-cell-unparseable';
import { caseInsensitiveContains, filterMap, parseJson } from '../../../utils'; import { caseInsensitiveContains, filterMap, parseJson } from '../../../utils';
import { FlattenField } from '../../../utils/ingestion-spec'; import { FlattenField } from '../../../utils/ingestion-spec';
import { HeaderAndRows, SampleEntry } from '../../../utils/sampler'; import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
@ -74,7 +75,7 @@ export const ParseDataTable = React.memo(function ParseDataTable(props: ParseDat
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
Cell: row => { Cell: row => {
if (row.original.unparseable) { if (row.original.unparseable) {
return <TableCell unparseable />; return <TableCellUnparseable />;
} }
return <TableCell value={row.value} />; return <TableCell value={row.value} />;
}, },

View File

@ -21,6 +21,7 @@ import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { TableCell } from '../../../components'; import { TableCell } from '../../../components';
import { TableCellUnparseable } from '../../../components/table-cell-unparseable/table-cell-unparseable';
import { caseInsensitiveContains, filterMap } from '../../../utils'; import { caseInsensitiveContains, filterMap } from '../../../utils';
import { possibleDruidFormatForValues } from '../../../utils/druid-time'; import { possibleDruidFormatForValues } from '../../../utils/druid-time';
import { import {
@ -122,9 +123,9 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim
return <TableCell value={row.original.error} />; return <TableCell value={row.original.error} />;
} }
if (row.original.unparseable) { if (row.original.unparseable) {
return <TableCell unparseable />; return <TableCellUnparseable timestamp={timestamp} />;
} }
return <TableCell value={row.value} timestamp={timestamp} />; return <TableCell value={timestamp ? new Date(row.value) : row.value} />;
}, },
minWidth: timestamp ? 200 : 100, minWidth: timestamp ? 200 : 100,
resizable: !timestamp, resizable: !timestamp,

View File

@ -147,7 +147,7 @@ export const SchemaTable = React.memo(function SchemaTable(props: SchemaTablePro
className: columnClassName, className: columnClassName,
id: String(i), id: String(i),
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
Cell: row => <TableCell value={row.value} timestamp={timestamp} />, Cell: row => <TableCell value={timestamp ? new Date(row.value) : row.value} />,
}; };
} }
})} })}

View File

@ -101,7 +101,7 @@ export const TransformTable = React.memo(function TransformTable(props: Transfor
className: columnClassName, className: columnClassName,
id: String(i), id: String(i),
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
Cell: row => <TableCell value={row.value} timestamp={timestamp} />, Cell: row => <TableCell value={timestamp ? new Date(row.value) : row.value} />,
}; };
})} })}
defaultPageSize={50} defaultPageSize={50}

View File

@ -49,40 +49,7 @@ ORDER BY "Count" DESC`);
['JavaScript', 166, 1, 0], ['JavaScript', 166, 1, 0],
['Python', 62, 1, 0], ['Python', 62, 1, 0],
['HTML', 46, 1, 0], ['HTML', 46, 1, 0],
['Java', 42, 1, 0], [],
['C++', 28, 1, 0],
['Go', 24, 1, 0],
['Ruby', 20, 1, 0],
['C#', 14, 1, 0],
['C', 13, 1, 0],
['CSS', 13, 1, 0],
['Shell', 12, 1, 0],
['Makefile', 10, 1, 0],
['PHP', 9, 1, 0],
['Scala', 8, 1, 0],
['HCL', 6, 1, 0],
['Jupyter Notebook', 6, 1, 0],
['Smarty', 4, 1, 0],
['Elm', 4, 1, 0],
['Roff', 3, 1, 0],
['Dockerfile', 3, 1, 0],
['Rust', 3, 1, 0],
['Dart', 2, 1, 0],
['LLVM', 2, 1, 0],
['Objective-C', 2, 1, 0],
['Julia', 2, 1, 0],
['PowerShell', 2, 1, 0],
['Swift', 2, 1, 0],
['Nim', 2, 1, 0],
['XSLT', 1, 1, 0],
['Lua', 1, 1, 0],
['Vim script', 1, 1, 0],
['Vue', 1, 1, 0],
['Lasso', 1, 1, 0],
['Clojure', 1, 1, 0],
['OCaml', 1, 1, 0],
['Chapel', 1, 1, 0],
['Kotlin', 1, 1, 0],
], ],
}} }}
parsedQuery={parsedQuery} parsedQuery={parsedQuery}

View File

@ -26,6 +26,7 @@ import {
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { TableCell } from '../../../components';
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog'; import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
import { copyAndAlert } from '../../../utils'; import { copyAndAlert } from '../../../utils';
import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action'; import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
@ -288,11 +289,10 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
accessor: String(i), accessor: String(i),
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
if (!value) return value == null ? null : value;
return ( return (
<div> <div>
<Popover content={getCellMenu(h, value)}> <Popover content={getCellMenu(h, value)}>
<div>{value}</div> <TableCell value={value} unlimited />
</Popover> </Popover>
</div> </div>
); );