diff --git a/web-console/src/druid-models/ingestion-spec.spec.ts b/web-console/src/druid-models/ingestion-spec.spec.ts
index 36d3f028715..52efaed3a82 100644
--- a/web-console/src/druid-models/ingestion-spec.spec.ts
+++ b/web-console/src/druid-models/ingestion-spec.spec.ts
@@ -549,28 +549,72 @@ describe('ingestion-spec', () => {
expect(guessInputFormat(['{"a":1}']).type).toEqual('json');
});
- it('works for TSV', () => {
- expect(guessInputFormat(['A\tB\tX\tY']).type).toEqual('tsv');
+ it('works for CSV (with header)', () => {
+ expect(guessInputFormat(['A,B,"X,1",Y'])).toEqual({
+ type: 'csv',
+ findColumnsFromHeader: true,
+ });
});
- it('works for CSV', () => {
- expect(guessInputFormat(['A,B,X,Y']).type).toEqual('csv');
+ it('works for CSV (no header)', () => {
+ expect(guessInputFormat(['"A,1","B,2",1,2'])).toEqual({
+ type: 'csv',
+ findColumnsFromHeader: false,
+ columns: ['column1', 'column2', 'column3', 'column4'],
+ });
+ });
+
+ it('works for TSV (with header)', () => {
+ expect(guessInputFormat(['A\tB\tX\tY'])).toEqual({
+ type: 'tsv',
+ findColumnsFromHeader: true,
+ });
+ });
+
+ it('works for TSV (no header)', () => {
+ expect(guessInputFormat(['A\tB\t1\t2\t3\t4\t5\t6\t7\t8\t9'])).toEqual({
+ type: 'tsv',
+ findColumnsFromHeader: false,
+ columns: [
+ 'column01',
+ 'column02',
+ 'column03',
+ 'column04',
+ 'column05',
+ 'column06',
+ 'column07',
+ 'column08',
+ 'column09',
+ 'column10',
+ 'column11',
+ ],
+ });
});
it('works for TSV with ;', () => {
const inputFormat = guessInputFormat(['A;B;X;Y']);
- expect(inputFormat.type).toEqual('tsv');
- expect(inputFormat.delimiter).toEqual(';');
+ expect(inputFormat).toEqual({
+ type: 'tsv',
+ delimiter: ';',
+ findColumnsFromHeader: true,
+ });
});
it('works for TSV with |', () => {
const inputFormat = guessInputFormat(['A|B|X|Y']);
- expect(inputFormat.type).toEqual('tsv');
- expect(inputFormat.delimiter).toEqual('|');
+ expect(inputFormat).toEqual({
+ type: 'tsv',
+ delimiter: '|',
+ findColumnsFromHeader: true,
+ });
});
it('works for regex', () => {
- expect(guessInputFormat(['A/B/X/Y']).type).toEqual('regex');
+ expect(guessInputFormat(['A/B/X/Y'])).toEqual({
+ type: 'regex',
+ pattern: '([\\s\\S]*)',
+ columns: ['line'],
+ });
});
});
});
diff --git a/web-console/src/druid-models/ingestion-spec.tsx b/web-console/src/druid-models/ingestion-spec.tsx
index 2de22e5aaf8..e58aeef3207 100644
--- a/web-console/src/druid-models/ingestion-spec.tsx
+++ b/web-console/src/druid-models/ingestion-spec.tsx
@@ -17,6 +17,7 @@
*/
import { Code } from '@blueprintjs/core';
+import { range } from 'd3-array';
import React from 'react';
import { AutoForm, ExternalLink, Field } from '../components';
@@ -32,6 +33,7 @@ import {
EMPTY_OBJECT,
filterMap,
oneOf,
+ parseCsvLine,
typeIs,
} from '../utils';
import { SampleHeaderAndRows } from '../utils/sampler';
@@ -2162,6 +2164,10 @@ export function fillInputFormatIfNeeded(
return deepSet(spec, 'spec.ioConfig.inputFormat', guessInputFormat(sampleData));
}
+function noNumbers(xs: string[]): boolean {
+ return xs.every(x => isNaN(Number(x)));
+}
+
export function guessInputFormat(sampleData: string[]): InputFormat {
let sampleDatum = sampleData[0];
if (sampleDatum) {
@@ -2171,62 +2177,102 @@ export function guessInputFormat(sampleData: string[]): InputFormat {
// Parquet 4 byte magic header: https://github.com/apache/parquet-format#file-format
if (sampleDatum.startsWith('PAR1')) {
- return inputFormatFromType('parquet');
+ return inputFormatFromType({ type: 'parquet' });
}
// ORC 3 byte magic header: https://orc.apache.org/specification/ORCv1/
if (sampleDatum.startsWith('ORC')) {
- return inputFormatFromType('orc');
+ return inputFormatFromType({ type: 'orc' });
}
// Avro OCF 4 byte magic header: https://avro.apache.org/docs/current/spec.html#Object+Container+Files
- if (sampleDatum.startsWith('Obj') && sampleDatum.charCodeAt(3) === 1) {
- return inputFormatFromType('avro_ocf');
+ if (sampleDatum.startsWith('Obj\x01')) {
+ return inputFormatFromType({ type: 'avro_ocf' });
}
// After checking for magic byte sequences perform heuristics to deduce string formats
// If the string starts and ends with curly braces assume JSON
if (sampleDatum.startsWith('{') && sampleDatum.endsWith('}')) {
- return inputFormatFromType('json');
+ return inputFormatFromType({ type: 'json' });
}
+
// Contains more than 3 tabs assume TSV
- if (sampleDatum.split('\t').length > 3) {
- return inputFormatFromType('tsv', !/\t\d+\t/.test(sampleDatum));
+ const lineAsTsv = sampleDatum.split('\t');
+ if (lineAsTsv.length > 3) {
+ return inputFormatFromType({
+ type: 'tsv',
+ findColumnsFromHeader: noNumbers(lineAsTsv),
+ numColumns: lineAsTsv.length,
+ });
}
- // Contains more than 3 commas assume CSV
- if (sampleDatum.split(',').length > 3) {
- return inputFormatFromType('csv', !/,\d+,/.test(sampleDatum));
+
+ // Contains more than fields if parsed as CSV line
+ const lineAsCsv = parseCsvLine(sampleDatum);
+ if (lineAsCsv.length > 3) {
+ return inputFormatFromType({
+ type: 'csv',
+ findColumnsFromHeader: noNumbers(lineAsCsv),
+ numColumns: lineAsCsv.length,
+ });
}
+
// Contains more than 3 semicolons assume semicolon separated
- if (sampleDatum.split(';').length > 3) {
- return inputFormatFromType('tsv', !/;\d+;/.test(sampleDatum), ';');
+ const lineAsTsvSemicolon = sampleDatum.split(';');
+ if (lineAsTsvSemicolon.length > 3) {
+ return inputFormatFromType({
+ type: 'tsv',
+ delimiter: ';',
+ findColumnsFromHeader: noNumbers(lineAsTsvSemicolon),
+ numColumns: lineAsTsvSemicolon.length,
+ });
}
+
// Contains more than 3 pipes assume pipe separated
- if (sampleDatum.split('|').length > 3) {
- return inputFormatFromType('tsv', !/\|\d+\|/.test(sampleDatum), '|');
+ const lineAsTsvPipe = sampleDatum.split('|');
+ if (lineAsTsvPipe.length > 3) {
+ return inputFormatFromType({
+ type: 'tsv',
+ delimiter: '|',
+ findColumnsFromHeader: noNumbers(lineAsTsvPipe),
+ numColumns: lineAsTsvPipe.length,
+ });
}
}
- return inputFormatFromType('regex');
+ return inputFormatFromType({ type: 'regex' });
}
-function inputFormatFromType(
- type: string,
- findColumnsFromHeader?: boolean,
- delimiter?: string,
-): InputFormat {
+interface InputFormatFromTypeOptions {
+ type: string;
+ delimiter?: string;
+ findColumnsFromHeader?: boolean;
+ numColumns?: number;
+}
+
+function inputFormatFromType(options: InputFormatFromTypeOptions): InputFormat {
+ const { type, delimiter, findColumnsFromHeader, numColumns } = options;
+
let inputFormat: InputFormat = { type };
if (type === 'regex') {
- inputFormat = deepSet(inputFormat, 'pattern', '(.*)');
- inputFormat = deepSet(inputFormat, 'columns', ['column1']);
- }
+ inputFormat = deepSet(inputFormat, 'pattern', '([\\s\\S]*)');
+ inputFormat = deepSet(inputFormat, 'columns', ['line']);
+ } else {
+ if (typeof findColumnsFromHeader === 'boolean') {
+ inputFormat = deepSet(inputFormat, 'findColumnsFromHeader', findColumnsFromHeader);
- if (typeof findColumnsFromHeader === 'boolean') {
- inputFormat = deepSet(inputFormat, 'findColumnsFromHeader', findColumnsFromHeader);
- }
+ if (!findColumnsFromHeader && numColumns) {
+ const padLength = String(numColumns).length;
+ inputFormat = deepSet(
+ inputFormat,
+ 'columns',
+ range(0, numColumns).map(c => `column${String(c + 1).padStart(padLength, '0')}`),
+ );
+ }
+ }
- if (delimiter) {
- inputFormat = deepSet(inputFormat, 'delimiter', delimiter);
+ if (delimiter) {
+ inputFormat = deepSet(inputFormat, 'delimiter', delimiter);
+ }
}
return inputFormat;
@@ -2234,6 +2280,13 @@ function inputFormatFromType(
// ------------------------
+export function guessIsArrayFromHeaderAndRows(
+ headerAndRows: SampleHeaderAndRows,
+ column: string,
+): boolean {
+ return headerAndRows.rows.some(r => Array.isArray(r.input?.[column]));
+}
+
export function guessColumnTypeFromInput(
sampleValues: any[],
guessNumericStringsAsNumbers: boolean,
diff --git a/web-console/src/druid-models/metric-spec.tsx b/web-console/src/druid-models/metric-spec.tsx
index 54c7fc7f393..3aa9c7b19c7 100644
--- a/web-console/src/druid-models/metric-spec.tsx
+++ b/web-console/src/druid-models/metric-spec.tsx
@@ -339,7 +339,7 @@ export function getMetricSpecSingleFieldName(metricSpec: MetricSpec): string | u
export function getMetricSpecOutputType(metricSpec: MetricSpec): string | undefined {
if (metricSpec.aggregator) return getMetricSpecOutputType(metricSpec.aggregator);
- const m = /^(long|float|double)/.exec(String(metricSpec.type));
+ const m = /^(long|float|double|string)/.exec(String(metricSpec.type));
if (!m) return;
return m[1];
}
diff --git a/web-console/src/utils/general.spec.ts b/web-console/src/utils/general.spec.ts
index 6d7596153f8..fcb6e8b3380 100644
--- a/web-console/src/utils/general.spec.ts
+++ b/web-console/src/utils/general.spec.ts
@@ -24,8 +24,11 @@ import {
formatMegabytes,
formatMillions,
formatPercent,
+ hashJoaat,
moveElement,
moveToIndex,
+ objectHash,
+ parseCsvLine,
sqlQueryCustomTableFilter,
swapElements,
} from './general';
@@ -154,4 +157,43 @@ describe('general', () => {
expect(formatMillions(345.2)).toEqual('345');
});
});
+
+ describe('parseCsvLine', () => {
+ it('works in general', () => {
+ expect(parseCsvLine(`Hello,,"",world,123,Hi "you","Quote, ""escapes"", work"\r\n`)).toEqual([
+ `Hello`,
+ ``,
+ ``,
+ `world`,
+ `123`,
+ `Hi "you"`,
+ `Quote, "escapes", work`,
+ ]);
+ });
+
+ it('works in empty case', () => {
+ expect(parseCsvLine(``)).toEqual([``]);
+ });
+
+ it('works in trivial case', () => {
+ expect(parseCsvLine(`Hello`)).toEqual([`Hello`]);
+ });
+
+ it('only parses first line', () => {
+ expect(parseCsvLine(`Hi,there\na,b\nx,y\n`)).toEqual([`Hi`, `there`]);
+ });
+ });
+
+ describe('hashJoaat', () => {
+ it('works', () => {
+ expect(hashJoaat('a')).toEqual(0xca2e9442);
+ expect(hashJoaat('The quick brown fox jumps over the lazy dog')).toEqual(0x7647f758);
+ });
+ });
+
+ describe('objectHash', () => {
+ it('works', () => {
+ expect(objectHash({ hello: 'world1' })).toEqual('cc14ad13');
+ });
+ });
});
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 22d0c53e6c2..fb651e18296 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -476,3 +476,39 @@ export function twoLines(line1: string, line2: string) {
>
);
}
+
+export function parseCsvLine(line: string): string[] {
+ line = ',' + line.replace(/\r?\n?$/, ''); // remove trailing new lines
+ const parts: string[] = [];
+ let m: RegExpExecArray | null;
+ while ((m = /^,(?:"([^"]*(?:""[^"]*)*)"|([^,\r\n]*))/m.exec(line))) {
+ parts.push(typeof m[1] === 'string' ? m[1].replace(/""/g, '"') : m[2]);
+ line = line.substr(m[0].length);
+ }
+ return parts;
+}
+
+// From: https://en.wikipedia.org/wiki/Jenkins_hash_function
+export function hashJoaat(str: string): number {
+ let hash = 0;
+ const n = str.length;
+ for (let i = 0; i < n; i++) {
+ hash += str.charCodeAt(i);
+ // eslint-disable-next-line no-bitwise
+ hash += hash << 10;
+ // eslint-disable-next-line no-bitwise
+ hash ^= hash >> 6;
+ }
+ // eslint-disable-next-line no-bitwise
+ hash += hash << 3;
+ // eslint-disable-next-line no-bitwise
+ hash ^= hash >> 11;
+ // eslint-disable-next-line no-bitwise
+ hash += hash << 15;
+ // eslint-disable-next-line no-bitwise
+ return (hash & 4294967295) >>> 0;
+}
+
+export function objectHash(obj: any): string {
+ return hashJoaat(JSONBig.stringify(obj)).toString(16).padStart(8);
+}
diff --git a/web-console/src/utils/object-change.ts b/web-console/src/utils/object-change.ts
index 40efacfa5c3..1e06fc01fb5 100644
--- a/web-console/src/utils/object-change.ts
+++ b/web-console/src/utils/object-change.ts
@@ -159,12 +159,20 @@ export function deepExtend
>(target: T, diff: Recor
return newValue;
}
-export function allowKeys(obj: Record, whitelist: string[]): Record {
+export function allowKeys(obj: Record, keys: string[]): Record {
const newObj: Record = {};
- for (const w of whitelist) {
- if (Object.prototype.hasOwnProperty.call(obj, w)) {
- newObj[w] = obj[w];
+ for (const key of keys) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ newObj[key] = obj[key];
}
}
return newObj;
}
+
+export function deleteKeys(obj: Record, keys: string[]): Record {
+ const newObj: Record = { ...obj };
+ for (const key of keys) {
+ delete newObj[key];
+ }
+ return newObj;
+}
diff --git a/web-console/src/variables.scss b/web-console/src/variables.scss
index e4105e3a11b..d14c0b79a3d 100644
--- a/web-console/src/variables.scss
+++ b/web-console/src/variables.scss
@@ -31,6 +31,15 @@ $druid-brand: #2ceefb;
$druid-brand2: #00b6bf;
$druid-brand-background: #1c1c26;
+@mixin card-background {
+ background: $white;
+ border-radius: $pt-border-radius;
+
+ .bp3-dark & {
+ background: $dark-gray3;
+ }
+}
+
@mixin card-like {
background: $white;
border-radius: $pt-border-radius;
diff --git a/web-console/src/views/query-view/column-tree/column-tree.tsx b/web-console/src/views/query-view/column-tree/column-tree.tsx
index 9865066eb10..a97a72e0310 100644
--- a/web-console/src/views/query-view/column-tree/column-tree.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.tsx
@@ -41,10 +41,6 @@ import './column-tree.scss';
const COUNT_STAR = SqlFunction.COUNT_STAR.as('Count');
-function caseInsensitiveCompare(a: any, b: any): number {
- return String(a).toLowerCase().localeCompare(String(b).toLowerCase());
-}
-
function getCountExpression(columnNames: string[]): SqlExpression {
for (const columnName of columnNames) {
if (columnName === 'count' || columnName === '__count') {
@@ -234,7 +230,6 @@ export class ColumnTree extends React.PureComponent child.COLUMN_NAME)
- .sort(caseInsensitiveCompare)
.map(columnName => SqlRef.column(columnName)),
)
.changeWhereExpression(getWhere()),
@@ -376,48 +371,38 @@ export class ColumnTree extends React.PureComponent
),
- childNodes: metadata
- .map(
- (columnData): TreeNodeInfo => ({
- id: columnData.COLUMN_NAME,
- icon: dataTypeToIcon(columnData.DATA_TYPE),
- label: (
- {
- const parsedQuery = props.getParsedQuery();
- return (
-
+ );
+ }}
+ />
+ }
+ >
+ {columnData.COLUMN_NAME}
+
+ ),
+ }),
+ ),
}),
),
}),
diff --git a/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx b/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx
index d1b4301c644..dbd64bdd8fd 100644
--- a/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx
+++ b/web-console/src/views/query-view/explain-dialog/explain-dialog.tsx
@@ -52,7 +52,7 @@ function isExplainQuery(query: string): boolean {
function wrapInExplainIfNeeded(query: string): string {
query = trimSemicolon(query);
if (isExplainQuery(query)) return query;
- return `EXPLAIN PLAN FOR (${query}\n)`;
+ return `EXPLAIN PLAN FOR ${query}`;
}
export interface ExplainDialogProps {
diff --git a/web-console/src/views/query-view/query-utils.ts b/web-console/src/views/query-view/query-utils.ts
index b4d2f2b2943..770a68c7f5a 100644
--- a/web-console/src/views/query-view/query-utils.ts
+++ b/web-console/src/views/query-view/query-utils.ts
@@ -21,22 +21,35 @@ import { IconNames } from '@blueprintjs/icons';
export function dataTypeToIcon(dataType: string): IconName {
const typeUpper = dataType.toUpperCase();
- if (typeUpper.startsWith('COMPLEX')) {
- return IconNames.ASTERISK;
- }
switch (typeUpper) {
case 'TIMESTAMP':
return IconNames.TIME;
+
case 'VARCHAR':
case 'STRING':
return IconNames.FONT;
+
case 'BIGINT':
case 'LONG':
case 'FLOAT':
case 'DOUBLE':
return IconNames.NUMERICAL;
+
+ case 'ARRAY':
+ return IconNames.ARRAY_STRING;
+
+ case 'ARRAY':
+ case 'ARRAY':
+ case 'ARRAY':
+ return IconNames.ARRAY_NUMERIC;
+
+ case 'COMPLEX':
+ return IconNames.DIAGRAM_TREE;
+
default:
+ if (typeUpper.startsWith('ARRAY')) return IconNames.ARRAY;
+ if (typeUpper.startsWith('COMPLEX')) return IconNames.ASTERISK;
return IconNames.HELP;
}
}
diff --git a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index c51534d36e5..424f2ed6d46 100755
--- a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++ b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -57,8 +57,8 @@ exports[`SegmentsView matches snapshot 1`] = `
"End",
"Version",
"Time span",
- "Partitioning",
- "Shard detail",
+ "Shard type",
+ "Shard spec",
"Partition",
"Size",
"Num rows",
@@ -76,8 +76,6 @@ exports[`SegmentsView matches snapshot 1`] = `
tableColumnsHidden={
Array [
"Time span",
- "Partitioning",
- "Shard detail",
]
}
/>
@@ -203,19 +201,20 @@ exports[`SegmentsView matches snapshot 1`] = `
},
Object {
"Cell": [Function],
- "Header": "Partitioning",
- "accessor": "partitioning",
- "filterable": true,
- "show": false,
- "sortable": true,
+ "Header": "Shard type",
+ "accessor": [Function],
+ "id": "shard_type",
+ "show": true,
+ "sortable": false,
"width": 100,
},
Object {
"Cell": [Function],
- "Header": "Shard detail",
+ "Header": "Shard spec",
"accessor": "shard_spec",
"filterable": false,
- "show": false,
+ "id": "shard_spec",
+ "show": true,
"sortable": false,
"width": 400,
},
diff --git a/web-console/src/views/segments-view/segments-view.scss b/web-console/src/views/segments-view/segments-view.scss
index 2d47ca0cf50..5f9cb4fc6ca 100644
--- a/web-console/src/views/segments-view/segments-view.scss
+++ b/web-console/src/views/segments-view/segments-view.scss
@@ -42,6 +42,23 @@
margin-right: 5px;
}
}
+
+ .spec-detail {
+ position: relative;
+ cursor: pointer;
+
+ .full-shard-spec-icon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ color: #f5f8fa;
+ display: none;
+ }
+
+ &:hover .full-shard-spec-icon {
+ display: block;
+ }
+ }
}
&.show-segment-timeline {
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 43ddbe50665..21ed68f2e06 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -16,10 +16,11 @@
* limitations under the License.
*/
-import { Button, ButtonGroup, Intent, Label, MenuItem, Switch } from '@blueprintjs/core';
+import { Button, ButtonGroup, Icon, Intent, Label, MenuItem, Switch } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
-import { SqlExpression, SqlLiteral, SqlRef } from 'druid-query-toolkit';
+import { SqlComparison, SqlExpression, SqlLiteral, SqlRef } from 'druid-query-toolkit';
+import * as JSONBig from 'json-bigint-native';
import React from 'react';
import ReactTable, { Filter } from 'react-table';
@@ -37,6 +38,7 @@ import {
} from '../../components';
import { AsyncActionDialog } from '../../dialogs';
import { SegmentTableActionDialog } from '../../dialogs/segments-table-action-dialog/segment-table-action-dialog';
+import { ShowValueDialog } from '../../dialogs/show-value-dialog/show-value-dialog';
import { Api } from '../../singletons';
import {
addFilter,
@@ -74,8 +76,8 @@ const tableColumns: Record = {
'End',
'Version',
'Time span',
- 'Partitioning',
- 'Shard detail',
+ 'Shard type',
+ 'Shard spec',
'Partition',
'Size',
'Num rows',
@@ -103,8 +105,8 @@ const tableColumns: Record = {
'Start',
'End',
'Version',
- 'Partitioning',
- 'Shard detail',
+ 'Shard type',
+ 'Shard spec',
'Partition',
'Size',
'Num rows',
@@ -154,7 +156,6 @@ interface SegmentQueryResultRow {
segment_id: string;
version: string;
time_span: string;
- partitioning: string;
shard_spec: string;
partition_num: number;
size: number;
@@ -178,6 +179,7 @@ export interface SegmentsViewState {
visibleColumns: LocalStorageBackedVisibility;
groupByInterval: boolean;
showSegmentTimeline: boolean;
+ showFullShardSpec?: string;
}
export class SegmentsView extends React.PureComponent {
@@ -198,18 +200,7 @@ export class SegmentsView extends React.PureComponent {
- if (f.id.startsWith('is_')) {
+ if (f.id === 'shard_type') {
+ // Special handling for shard_type that needs to be search in the shard_spec
+ // Creates filters like `shard_spec LIKE '%"type":"numbered"%'`
+ const needleAndMode = getNeedleAndMode(f);
+ const closingQuote = needleAndMode.mode === 'exact' ? '"' : '';
+ return SqlComparison.like(
+ SqlRef.column('shard_spec'),
+ `%"type":"${needleAndMode.needle}${closingQuote}%`,
+ );
+ } else if (f.id.startsWith('is_')) {
if (f.value === 'all') return;
return SqlRef.columnWithQuotes(f.id).equal(f.value === 'true' ? 1 : 0);
} else {
@@ -394,7 +394,6 @@ END AS "partitioning"`,
interval: segment.interval,
version: segment.version,
time_span: SegmentsView.computeTimeSpan(start, end),
- partitioning: deepGet(segment, 'shardSpec.type') || '-',
shard_spec: deepGet(segment, 'shardSpec'),
partition_num: deepGet(segment, 'shardSpec.partitionNum') || 0,
size: segment.size,
@@ -590,17 +589,26 @@ END AS "partitioning"`,
Cell: renderFilterableCell('time_span'),
},
{
- Header: 'Partitioning',
- show: visibleColumns.shown('Partitioning'),
- accessor: 'partitioning',
+ Header: 'Shard type',
+ show: visibleColumns.shown('Shard type'),
+ id: 'shard_type',
width: 100,
- sortable: hasSql,
- filterable: allowGeneralFilter,
- Cell: renderFilterableCell('partitioning'),
+ sortable: false,
+ accessor: d => {
+ let v: any;
+ try {
+ v = JSONBig.parse(d.shard_spec);
+ } catch {}
+
+ if (typeof v?.type !== 'string') return '-';
+ return v?.type;
+ },
+ Cell: renderFilterableCell('shard_type'),
},
{
- Header: 'Shard detail',
- show: visibleColumns.shown('Shard detail'),
+ Header: 'Shard spec',
+ show: visibleColumns.shown('Shard spec'),
+ id: 'shard_spec',
accessor: 'shard_spec',
width: 400,
sortable: false,
@@ -608,10 +616,19 @@ END AS "partitioning"`,
Cell: ({ value }) => {
let v: any;
try {
- v = JSON.parse(value);
- } catch {
- return '-';
- }
+ v = JSONBig.parse(value);
+ } catch {}
+
+ const onShowFullShardSpec = () => {
+ this.setState({
+ showFullShardSpec:
+ v && typeof v === 'object' ? JSONBig.stringify(v, undefined, 2) : String(value),
+ });
+ };
+
+ const fullShardIcon = (
+
+ );
switch (v?.type) {
case 'range': {
@@ -620,24 +637,26 @@ END AS "partitioning"`,
values.map((x, i) => formatRangeDimensionValue(dimensions[i], x)).join('; ');
return (
-
+
Start:
{Array.isArray(v.start) ? formatEdge(v.start) : '-∞'}
End:
{Array.isArray(v.end) ? formatEdge(v.end) : '∞'}
+ {fullShardIcon}
);
}
case 'single': {
return (
-
+
Start:
{v.start != null ? formatRangeDimensionValue(v.dimension, v.start) : '-∞'}
End:
{v.end != null ? formatRangeDimensionValue(v.dimension, v.end) : '∞'}
+ {fullShardIcon}
);
}
@@ -645,17 +664,34 @@ END AS "partitioning"`,
case 'hashed': {
const { partitionDimensions } = v;
if (!Array.isArray(partitionDimensions)) return value;
- return `Partition dimensions: ${
- partitionDimensions.length ? partitionDimensions.join('; ') : 'all'
- }`;
+ return (
+
+ {`hash(${
+ partitionDimensions.length
+ ? partitionDimensions.join(', ')
+ : '
'
+ })`}
+ {fullShardIcon}
+
+ );
}
case 'numbered':
case 'none':
- return '-';
+ case 'tombstone':
+ return (
+
+ No detail{fullShardIcon}
+
+ );
default:
- return typeof value === 'string' ? value : '-';
+ return (
+
+ {String(value)}
+ {fullShardIcon}
+
+ );
}
},
},
@@ -843,6 +879,7 @@ END AS "partitioning"`,
actions,
visibleColumns,
showSegmentTimeline,
+ showFullShardSpec,
} = this.state;
const { capabilities } = this.props;
const { groupByInterval } = this.state;
@@ -913,6 +950,13 @@ END AS "partitioning"`,
onClose={() => this.setState({ segmentTableActionDialogId: undefined })}
/>
)}
+ {showFullShardSpec && (
+
this.setState({ showFullShardSpec: undefined })}
+ />
+ )}
>
);
}