mirror of https://github.com/apache/druid.git
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:
parent
3ff5e02237
commit
ed6be81d12
|
@ -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>
|
||||
`;
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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
|
@ -21,10 +21,6 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
&.unparseable {
|
||||
color: #9e2b0e;
|
||||
}
|
||||
|
||||
&.timestamp {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
|
@ -23,49 +23,59 @@ import { TableCell } from './table-cell';
|
|||
|
||||
describe('table cell', () => {
|
||||
it('matches snapshot null', () => {
|
||||
const tableCell = <TableCell value={null} unparseable={false} timestamp={false} />;
|
||||
|
||||
const { container } = render(tableCell);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot null timestamp', () => {
|
||||
const tableCell = <TableCell value={null} unparseable={false} timestamp />;
|
||||
const tableCell = <TableCell value={null} />;
|
||||
|
||||
const { container } = render(tableCell);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot simple', () => {
|
||||
const tableCell = <TableCell value="Hello World" unparseable={false} timestamp={false} />;
|
||||
const tableCell = <TableCell value="Hello World" />;
|
||||
|
||||
const { container } = render(tableCell);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot array long', () => {
|
||||
const tableCell = (
|
||||
<TableCell
|
||||
value={Array.from(new Array(100)).map((_, i) => i)}
|
||||
unparseable={false}
|
||||
timestamp={false}
|
||||
/>
|
||||
);
|
||||
const tableCell = <TableCell value={Array.from(new Array(100)).map((_, i) => i)} />;
|
||||
|
||||
const { container } = render(tableCell);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot object', () => {
|
||||
const tableCell = <TableCell value={{ hello: 'world' }} />;
|
||||
|
||||
const { container } = render(tableCell);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('matches snapshot truncate', () => {
|
||||
const longString = new Array(100).join('test');
|
||||
const tableCell = <TableCell value={longString} unparseable={false} timestamp={false} />;
|
||||
const longString = new Array(100).join('test_');
|
||||
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);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
|
|
|
@ -25,6 +25,7 @@ import { ActionIcon } from '../action-icon/action-icon';
|
|||
import './table-cell.scss';
|
||||
|
||||
const MAX_CHARS_TO_SHOW = 50;
|
||||
const ABSOLUTE_MAX_CHARS_TO_SHOW = 5000;
|
||||
|
||||
interface ShortParts {
|
||||
prefix: string;
|
||||
|
@ -46,13 +47,12 @@ function shortenString(str: string): ShortParts {
|
|||
}
|
||||
|
||||
export interface TableCellProps {
|
||||
value?: any;
|
||||
timestamp?: boolean;
|
||||
unparseable?: boolean;
|
||||
value: any;
|
||||
unlimited?: boolean;
|
||||
}
|
||||
|
||||
export const TableCell = React.memo(function TableCell(props: TableCellProps) {
|
||||
const { value, timestamp, unparseable } = props;
|
||||
const { value, unlimited } = props;
|
||||
const [showValue, setShowValue] = useState();
|
||||
|
||||
function renderShowValueDialog(): JSX.Element | undefined {
|
||||
|
@ -62,7 +62,19 @@ export const TableCell = React.memo(function TableCell(props: TableCellProps) {
|
|||
}
|
||||
|
||||
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);
|
||||
return (
|
||||
|
@ -76,25 +88,21 @@ export const TableCell = React.memo(function TableCell(props: TableCellProps) {
|
|||
);
|
||||
}
|
||||
|
||||
if (unparseable) {
|
||||
return <span className="table-cell unparseable">error</span>;
|
||||
} else if (value !== '' && value != null) {
|
||||
if (timestamp) {
|
||||
if (value !== '' && value != null) {
|
||||
if (value instanceof Date) {
|
||||
return (
|
||||
<span className="table-cell timestamp" title={value}>
|
||||
{new Date(value).toISOString()}
|
||||
<span className="table-cell timestamp" title={String(value.valueOf())}>
|
||||
{value.toISOString()}
|
||||
</span>
|
||||
);
|
||||
} else if (Array.isArray(value)) {
|
||||
return renderTruncated(`[${value.join(', ')}]`);
|
||||
} else if (typeof value === 'object') {
|
||||
return renderTruncated(JSON.stringify(value));
|
||||
} else {
|
||||
return renderTruncated(String(value));
|
||||
}
|
||||
} else {
|
||||
if (timestamp) {
|
||||
return <span className="table-cell unparseable">unparseable timestamp</span>;
|
||||
} else {
|
||||
return <span className="table-cell null">null</span>;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -92,7 +92,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro
|
|||
className: columnClassName,
|
||||
id: String(i),
|
||||
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}
|
||||
|
|
|
@ -21,6 +21,7 @@ import React from 'react';
|
|||
import ReactTable from 'react-table';
|
||||
|
||||
import { TableCell } from '../../../components';
|
||||
import { TableCellUnparseable } from '../../../components/table-cell-unparseable/table-cell-unparseable';
|
||||
import { caseInsensitiveContains, filterMap, parseJson } from '../../../utils';
|
||||
import { FlattenField } from '../../../utils/ingestion-spec';
|
||||
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),
|
||||
Cell: row => {
|
||||
if (row.original.unparseable) {
|
||||
return <TableCell unparseable />;
|
||||
return <TableCellUnparseable />;
|
||||
}
|
||||
return <TableCell value={row.value} />;
|
||||
},
|
||||
|
|
|
@ -21,6 +21,7 @@ import React from 'react';
|
|||
import ReactTable from 'react-table';
|
||||
|
||||
import { TableCell } from '../../../components';
|
||||
import { TableCellUnparseable } from '../../../components/table-cell-unparseable/table-cell-unparseable';
|
||||
import { caseInsensitiveContains, filterMap } from '../../../utils';
|
||||
import { possibleDruidFormatForValues } from '../../../utils/druid-time';
|
||||
import {
|
||||
|
@ -122,9 +123,9 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim
|
|||
return <TableCell value={row.original.error} />;
|
||||
}
|
||||
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,
|
||||
resizable: !timestamp,
|
||||
|
|
|
@ -147,7 +147,7 @@ export const SchemaTable = React.memo(function SchemaTable(props: SchemaTablePro
|
|||
className: columnClassName,
|
||||
id: String(i),
|
||||
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} />,
|
||||
};
|
||||
}
|
||||
})}
|
||||
|
|
|
@ -101,7 +101,7 @@ export const TransformTable = React.memo(function TransformTable(props: Transfor
|
|||
className: columnClassName,
|
||||
id: String(i),
|
||||
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}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -49,40 +49,7 @@ ORDER BY "Count" DESC`);
|
|||
['JavaScript', 166, 1, 0],
|
||||
['Python', 62, 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}
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
import React, { useState } from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { TableCell } from '../../../components';
|
||||
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
|
||||
import { copyAndAlert } from '../../../utils';
|
||||
import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
|
||||
|
@ -288,11 +289,10 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
|
|||
accessor: String(i),
|
||||
Cell: row => {
|
||||
const value = row.value;
|
||||
if (!value) return value == null ? null : value;
|
||||
return (
|
||||
<div>
|
||||
<Popover content={getCellMenu(h, value)}>
|
||||
<div>{value}</div>
|
||||
<TableCell value={value} unlimited />
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue