good stuff (#12435)

This commit is contained in:
Vadim Ogievetsky 2022-04-14 00:23:06 -07:00 committed by GitHub
parent 5824ab9608
commit a72cc28959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 191 additions and 26 deletions

View File

@ -20,6 +20,7 @@
@import 'common/color-aliases'; @import 'common/color-aliases';
@import 'common/variables'; @import 'common/variables';
@import 'components/button/common'; @import 'components/button/common';
@import 'components/button/button-group';
@import 'components/forms/common'; @import 'components/forms/common';
@import 'components/navbar/navbar'; @import 'components/navbar/navbar';
@import 'components/card/card'; @import 'components/card/card';

View File

@ -0,0 +1,29 @@
/*
* 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.
*/
// Add body to make the selector more specific than what is in
// node_modules/@blueprintjs/core/src/components/button/_button-group.scss
body .#{$ns}-button-group {
&:not(.#{$ns}-minimal) {
> .#{$ns}-popover-wrapper:not(:last-child) .#{$ns}-button,
> .#{$ns}-button:not(:last-child) {
// Due to our flat styling this in needed to override the -1px that blueprint tries to set
margin-right: 1px;
}
}
}

View File

@ -168,6 +168,16 @@ exports[`HeaderBar matches snapshot 1`] = `
shouldDismissPopover={true} shouldDismissPopover={true}
text="Force Overlord mode" text="Force Overlord mode"
/> />
<Blueprint4.MenuItem
active={false}
disabled={false}
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
selected={false}
shouldDismissPopover={true}
text="Force no management proxy mode"
/>
</React.Fragment> </React.Fragment>
</Blueprint4.MenuItem> </Blueprint4.MenuItem>
</Blueprint4.Menu> </Blueprint4.Menu>

View File

@ -296,6 +296,12 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
onClick={() => setForcedMode(Capabilities.OVERLORD)} onClick={() => setForcedMode(Capabilities.OVERLORD)}
/> />
)} )}
{capabilitiesMode !== 'no-proxy' && (
<MenuItem
text="Force no management proxy mode"
onClick={() => setForcedMode(Capabilities.NO_PROXY)}
/>
)}
</> </>
)} )}
</MenuItem> </MenuItem>

View File

@ -545,8 +545,26 @@ describe('ingestion-spec', () => {
expect(guessInputFormat(['Obj1lol']).type).toEqual('regex'); expect(guessInputFormat(['Obj1lol']).type).toEqual('regex');
}); });
it('works for JSON', () => { it('works for JSON (strict)', () => {
expect(guessInputFormat(['{"a":1}']).type).toEqual('json'); expect(guessInputFormat(['{"a":1}'])).toEqual({ type: 'json' });
});
it('works for JSON (lax)', () => {
expect(guessInputFormat([`{hello:'world'}`])).toEqual({
type: 'json',
featureSpec: {
ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER: true,
ALLOW_COMMENTS: true,
ALLOW_MISSING_VALUES: true,
ALLOW_NON_NUMERIC_NUMBERS: true,
ALLOW_NUMERIC_LEADING_ZEROS: true,
ALLOW_SINGLE_QUOTES: true,
ALLOW_TRAILING_COMMA: true,
ALLOW_UNQUOTED_CONTROL_CHARS: true,
ALLOW_UNQUOTED_FIELD_NAMES: true,
ALLOW_YAML_COMMENTS: true,
},
});
}); });
it('works for CSV (with header)', () => { it('works for CSV (with header)', () => {

View File

@ -2192,7 +2192,27 @@ export function guessInputFormat(sampleData: string[]): InputFormat {
// If the string starts and ends with curly braces assume JSON // If the string starts and ends with curly braces assume JSON
if (sampleDatum.startsWith('{') && sampleDatum.endsWith('}')) { if (sampleDatum.startsWith('{') && sampleDatum.endsWith('}')) {
return inputFormatFromType({ type: 'json' }); try {
JSON.parse(sampleDatum);
return { type: 'json' };
} catch {
// If the standard JSON parse does not parse then try setting a very lax parsing style
return {
type: 'json',
featureSpec: {
ALLOW_COMMENTS: true,
ALLOW_YAML_COMMENTS: true,
ALLOW_UNQUOTED_FIELD_NAMES: true,
ALLOW_SINGLE_QUOTES: true,
ALLOW_UNQUOTED_CONTROL_CHARS: true,
ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER: true,
ALLOW_NUMERIC_LEADING_ZEROS: true,
ALLOW_NON_NUMERIC_NUMBERS: true,
ALLOW_MISSING_VALUES: true,
ALLOW_TRAILING_COMMA: true,
},
};
}
} }
// Contains more than 3 tabs assume TSV // Contains more than 3 tabs assume TSV

View File

@ -34,7 +34,8 @@ export interface InputFormat {
readonly listDelimiter?: string; readonly listDelimiter?: string;
readonly pattern?: string; readonly pattern?: string;
readonly function?: string; readonly function?: string;
readonly flattenSpec?: FlattenSpec; readonly flattenSpec?: FlattenSpec | null;
readonly featureSpec?: Record<string, boolean>;
readonly keepNullColumns?: boolean; readonly keepNullColumns?: boolean;
} }
@ -58,6 +59,35 @@ export const INPUT_FORMAT_FIELDS: Field<InputFormat>[] = [
</> </>
), ),
}, },
{
name: 'featureSpec',
label: 'JSON parser features',
type: 'json',
defined: typeIs('json'),
info: (
<>
<p>
<ExternalLink href="https://github.com/FasterXML/jackson-core/wiki/JsonParser-Features">
JSON parser features
</ExternalLink>{' '}
supported by Jackson library. Those features will be applied when parsing the input JSON
data.
</p>
<p>
Example:{' '}
<Code>{`{ "ALLOW_SINGLE_QUOTES": true, "ALLOW_UNQUOTED_FIELD_NAMES": true }`}</Code>
</p>
</>
),
},
{
name: 'delimiter',
type: 'string',
defaultValue: '\t',
suggestions: ['\t', ';', '|', '#'],
defined: typeIs('tsv'),
info: <>A custom delimiter for data values.</>,
},
{ {
name: 'pattern', name: 'pattern',
type: 'string', type: 'string',
@ -110,14 +140,6 @@ export const INPUT_FORMAT_FIELDS: Field<InputFormat>[] = [
</> </>
), ),
}, },
{
name: 'delimiter',
type: 'string',
defaultValue: '\t',
suggestions: ['\t', ';', '|', '#'],
defined: typeIs('tsv'),
info: <>A custom delimiter for data values.</>,
},
{ {
name: 'listDelimiter', name: 'listDelimiter',
type: 'string', type: 'string',

View File

@ -41,6 +41,10 @@ export interface InputSource {
// hdfs // hdfs
paths?: string; paths?: string;
// http
httpAuthenticationUsername?: any;
httpAuthenticationPassword?: any;
} }
export function issueWithInputSource(inputSource: InputSource | undefined): string | undefined { export function issueWithInputSource(inputSource: InputSource | undefined): string | undefined {

View File

@ -45,6 +45,7 @@ export class Capabilities {
static COORDINATOR_OVERLORD: Capabilities; static COORDINATOR_OVERLORD: Capabilities;
static COORDINATOR: Capabilities; static COORDINATOR: Capabilities;
static OVERLORD: Capabilities; static OVERLORD: Capabilities;
static NO_PROXY: Capabilities;
private readonly queryType: QueryType; private readonly queryType: QueryType;
private readonly coordinator: boolean; private readonly coordinator: boolean;
@ -96,7 +97,7 @@ export class Capabilities {
static async detectNode(node: 'coordinator' | 'overlord'): Promise<boolean | undefined> { static async detectNode(node: 'coordinator' | 'overlord'): Promise<boolean | undefined> {
try { try {
await Api.instance.get(`/druid/${node === 'overlord' ? 'indexer' : node}/v1/isLeader`, { await Api.instance.get(`/proxy/${node}/status`, {
timeout: Capabilities.STATUS_TIMEOUT, timeout: Capabilities.STATUS_TIMEOUT,
}); });
} catch (e) { } catch (e) {
@ -218,3 +219,8 @@ Capabilities.OVERLORD = new Capabilities({
coordinator: false, coordinator: false,
overlord: true, overlord: true,
}); });
Capabilities.NO_PROXY = new Capabilities({
queryType: 'nativeAndSql',
coordinator: false,
overlord: false,
});

View File

@ -310,6 +310,21 @@ export function formatDurationWithMs(ms: NumberLike): string {
); );
} }
export function formatDurationHybrid(ms: NumberLike): string {
const n = Number(ms);
if (n < 600000) {
// anything that looks like 1:23.45 (max 9:59.99)
const timeInMin = Math.floor(n / 60000);
const timeInSec = Math.floor(n / 1000) % 60;
const timeInMs = Math.floor(n) % 1000;
return `${timeInMin ? `${timeInMin}:` : ''}${timeInMin ? pad2(timeInSec) : timeInSec}.${pad3(
timeInMs,
).substring(0, 2)}s`;
} else {
return formatDuration(n);
}
}
export function pluralIfNeeded(n: NumberLike, singular: string, plural?: string): string { export function pluralIfNeeded(n: NumberLike, singular: string, plural?: string): string {
if (!plural) plural = singular + 's'; if (!plural) plural = singular + 's';
return `${formatInteger(n)} ${n === 1 ? singular : plural}`; return `${formatInteger(n)} ${n === 1 ? singular : plural}`;
@ -512,3 +527,7 @@ export function hashJoaat(str: string): number {
export function objectHash(obj: any): string { export function objectHash(obj: any): string {
return hashJoaat(JSONBig.stringify(obj)).toString(16).padStart(8); return hashJoaat(JSONBig.stringify(obj)).toString(16).padStart(8);
} }
export function hasPopoverOpen(): boolean {
return Boolean(document.querySelector('.bp4-portal .bp4-overlay .bp4-popover2'));
}

View File

@ -139,13 +139,15 @@ export interface HeaderFromSampleResponseOptions {
ignoreTimeColumn?: boolean; ignoreTimeColumn?: boolean;
columnOrder?: string[]; columnOrder?: string[];
suffixColumnOrder?: string[]; suffixColumnOrder?: string[];
useInput?: boolean;
} }
export function headerFromSampleResponse(options: HeaderFromSampleResponseOptions): string[] { export function headerFromSampleResponse(options: HeaderFromSampleResponseOptions): string[] {
const { sampleResponse, ignoreTimeColumn, columnOrder, suffixColumnOrder } = options; const { sampleResponse, ignoreTimeColumn, columnOrder, suffixColumnOrder, useInput } = options;
const key = useInput ? 'input' : 'parsed';
let columns = arrangeWithPrefixSuffix( let columns = arrangeWithPrefixSuffix(
dedupe(sampleResponse.data.flatMap(s => (s.parsed ? Object.keys(s.parsed) : []))), dedupe(sampleResponse.data.flatMap(s => (s[key] ? Object.keys(s[key]!) : []))),
columnOrder || [TIME_COLUMN], columnOrder || [TIME_COLUMN],
suffixColumnOrder || [], suffixColumnOrder || [],
); );

View File

@ -35,7 +35,7 @@ $druid-brand-background: #1c1c26;
background: $white; background: $white;
border-radius: $pt-border-radius; border-radius: $pt-border-radius;
.bp3-dark & { .#{$bp-ns}-dark & {
background: $dark-gray3; background: $dark-gray3;
} }
} }

View File

@ -57,6 +57,7 @@ import {
formatMillions, formatMillions,
formatPercent, formatPercent,
getDruidErrorMessage, getDruidErrorMessage,
hasPopoverOpen,
isNumberLikeNaN, isNumberLikeNaN,
LocalStorageBackedVisibility, LocalStorageBackedVisibility,
LocalStorageKeys, LocalStorageKeys,
@ -493,7 +494,8 @@ ORDER BY 1`;
}); });
} }
private readonly refresh = (auto: any): void => { private readonly refresh = (auto: boolean): void => {
if (auto && hasPopoverOpen()) return;
this.datasourceQueryManager.rerunLastQuery(auto); this.datasourceQueryManager.rerunLastQuery(auto);
this.tiersQueryManager.rerunLastQuery(auto); this.tiersQueryManager.rerunLastQuery(auto);
}; };

View File

@ -47,6 +47,7 @@ import {
deepGet, deepGet,
formatDuration, formatDuration,
getDruidErrorMessage, getDruidErrorMessage,
hasPopoverOpen,
LocalStorageBackedVisibility, LocalStorageBackedVisibility,
localStorageGet, localStorageGet,
LocalStorageKeys, LocalStorageKeys,
@ -1089,7 +1090,10 @@ ORDER BY "rank" DESC, "created_time" DESC`;
<ViewControlBar label="Supervisors"> <ViewControlBar label="Supervisors">
<RefreshButton <RefreshButton
localStorageKey={LocalStorageKeys.SUPERVISORS_REFRESH_RATE} localStorageKey={LocalStorageKeys.SUPERVISORS_REFRESH_RATE}
onRefresh={auto => this.supervisorQueryManager.rerunLastQuery(auto)} onRefresh={auto => {
if (auto && hasPopoverOpen()) return;
this.supervisorQueryManager.rerunLastQuery(auto);
}}
/> />
{this.renderBulkSupervisorActions()} {this.renderBulkSupervisorActions()}
<TableColumnSelector <TableColumnSelector
@ -1141,7 +1145,10 @@ ORDER BY "rank" DESC, "created_time" DESC`;
</ButtonGroup> </ButtonGroup>
<RefreshButton <RefreshButton
localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE} localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE}
onRefresh={auto => this.taskQueryManager.rerunLastQuery(auto)} onRefresh={auto => {
if (auto && hasPopoverOpen()) return;
this.taskQueryManager.rerunLastQuery(auto);
}}
/> />
{this.renderBulkTasksActions()} {this.renderBulkTasksActions()}
<TableColumnSelector <TableColumnSelector

View File

@ -1463,7 +1463,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
)} )}
</> </>
)} )}
{this.renderFlattenControls()} {canFlatten && this.renderFlattenControls()}
{suggestedFlattenFields && suggestedFlattenFields.length ? ( {suggestedFlattenFields && suggestedFlattenFields.length ? (
<FormGroup> <FormGroup>
<Button <Button

View File

@ -41,6 +41,7 @@ export interface ParseDataTableProps {
flattenedColumnsOnly: boolean; flattenedColumnsOnly: boolean;
flattenFields: FlattenField[]; flattenFields: FlattenField[];
onFlattenFieldSelect: (field: FlattenField, index: number) => void; onFlattenFieldSelect: (field: FlattenField, index: number) => void;
useInput?: boolean;
} }
export const ParseDataTable = React.memo(function ParseDataTable(props: ParseDataTableProps) { export const ParseDataTable = React.memo(function ParseDataTable(props: ParseDataTableProps) {
@ -51,8 +52,10 @@ export const ParseDataTable = React.memo(function ParseDataTable(props: ParseDat
flattenedColumnsOnly, flattenedColumnsOnly,
flattenFields, flattenFields,
onFlattenFieldSelect, onFlattenFieldSelect,
useInput,
} = props; } = props;
const key = useInput ? 'input' : 'parsed';
return ( return (
<ReactTable <ReactTable
className="parse-data-table -striped -highlight" className="parse-data-table -striped -highlight"
@ -82,7 +85,7 @@ export const ParseDataTable = React.memo(function ParseDataTable(props: ParseDat
</div> </div>
), ),
id: String(i), id: String(i),
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row[key] ? row[key]![columnName] : null),
Cell: function ParseDataTableCell(row) { Cell: function ParseDataTableCell(row) {
if (row.original.unparseable) { if (row.original.unparseable) {
return <TableCellUnparseable />; return <TableCellUnparseable />;

View File

@ -37,6 +37,7 @@ import { Api, AppToaster } from '../../singletons';
import { import {
deepGet, deepGet,
getDruidErrorMessage, getDruidErrorMessage,
hasPopoverOpen,
isLookupsUninitialized, isLookupsUninitialized,
LocalStorageBackedVisibility, LocalStorageBackedVisibility,
LocalStorageKeys, LocalStorageKeys,
@ -455,7 +456,10 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
<div className="lookups-view app-view"> <div className="lookups-view app-view">
<ViewControlBar label="Lookups"> <ViewControlBar label="Lookups">
<RefreshButton <RefreshButton
onRefresh={auto => this.lookupsQueryManager.rerunLastQuery(auto)} onRefresh={auto => {
if (auto && hasPopoverOpen()) return;
this.lookupsQueryManager.rerunLastQuery(auto);
}}
localStorageKey={LocalStorageKeys.LOOKUPS_REFRESH_RATE} localStorageKey={LocalStorageKeys.LOOKUPS_REFRESH_RATE}
/> />
{!lookupEntriesAndTiersState.isError() && ( {!lookupEntriesAndTiersState.isError() && (

View File

@ -21,6 +21,7 @@ import { IconNames } from '@blueprintjs/icons';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useInterval } from '../../../hooks'; import { useInterval } from '../../../hooks';
import { formatDurationHybrid } from '../../../utils';
import './query-timer.scss'; import './query-timer.scss';
@ -37,9 +38,10 @@ export const QueryTimer = React.memo(function QueryTimer() {
const elapsed = currentTime - startTime; const elapsed = currentTime - startTime;
if (elapsed <= 0) return null; if (elapsed <= 0) return null;
return ( return (
<div className="query-timer"> <div className="query-timer">
{`${(elapsed / 1000).toFixed(2)}s`} {formatDurationHybrid(elapsed)}
<Button icon={IconNames.STOPWATCH} minimal /> <Button icon={IconNames.STOPWATCH} minimal />
</div> </div>
); );

View File

@ -201,7 +201,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
}, },
}); });
const queryRunner = new QueryRunner(); const queryRunner = new QueryRunner({
inflateDateStrategy: 'none',
});
this.queryManager = new QueryManager({ this.queryManager = new QueryManager({
processQuery: async ( processQuery: async (

View File

@ -51,6 +51,7 @@ import {
formatBytes, formatBytes,
formatInteger, formatInteger,
getNeedleAndMode, getNeedleAndMode,
hasPopoverOpen,
isNumberLikeNaN, isNumberLikeNaN,
LocalStorageBackedVisibility, LocalStorageBackedVisibility,
LocalStorageKeys, LocalStorageKeys,
@ -893,7 +894,10 @@ END AS "time_span"`,
> >
<ViewControlBar label="Segments"> <ViewControlBar label="Segments">
<RefreshButton <RefreshButton
onRefresh={auto => this.segmentsQueryManager.rerunLastQuery(auto)} onRefresh={auto => {
if (auto && hasPopoverOpen()) return;
this.segmentsQueryManager.rerunLastQuery(auto);
}}
localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE} localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE}
/> />
<Label>Group by</Label> <Label>Group by</Label>

View File

@ -41,6 +41,7 @@ import {
deepGet, deepGet,
formatBytes, formatBytes,
formatBytesCompact, formatBytesCompact,
hasPopoverOpen,
LocalStorageBackedVisibility, LocalStorageBackedVisibility,
LocalStorageKeys, LocalStorageKeys,
lookupBy, lookupBy,
@ -718,7 +719,10 @@ ORDER BY
</Button> </Button>
</ButtonGroup> </ButtonGroup>
<RefreshButton <RefreshButton
onRefresh={auto => this.serviceQueryManager.rerunLastQuery(auto)} onRefresh={auto => {
if (auto && hasPopoverOpen()) return;
this.serviceQueryManager.rerunLastQuery(auto);
}}
localStorageKey={LocalStorageKeys.SERVICES_REFRESH_RATE} localStorageKey={LocalStorageKeys.SERVICES_REFRESH_RATE}
/> />
{this.renderBulkServicesActions()} {this.renderBulkServicesActions()}