mirror of https://github.com/apache/druid.git
Web console: data loader native batch shuffle UI (#8229)
* data loader shuffle UI * better support for inline firehose * feedback changes * make numShards mandatory sometimes * edit context dialog fixes * make auto form less agro * download full log * improve auto column detection * add sample for example
This commit is contained in:
parent
566dc8c719
commit
56e440383f
|
@ -16,7 +16,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FormGroup, HTMLSelect, Icon, NumericInput, Popover } from '@blueprintjs/core';
|
||||
import { FormGroup, HTMLSelect, Icon, Intent, NumericInput, Popover } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import React from 'react';
|
||||
|
||||
|
@ -33,11 +33,12 @@ export interface Field<T> {
|
|||
info?: React.ReactNode;
|
||||
type: 'number' | 'size-bytes' | 'string' | 'duration' | 'boolean' | 'string-array' | 'json';
|
||||
defaultValue?: any;
|
||||
isDefined?: (model: T) => boolean;
|
||||
disabled?: boolean;
|
||||
suggestions?: (string | SuggestionGroup)[];
|
||||
placeholder?: string;
|
||||
min?: number;
|
||||
disabled?: boolean | ((model: T) => boolean);
|
||||
defined?: boolean | ((model: T) => boolean);
|
||||
required?: boolean | ((model: T) => boolean);
|
||||
}
|
||||
|
||||
export interface AutoFormProps<T> {
|
||||
|
@ -67,6 +68,24 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
return newLabel;
|
||||
}
|
||||
|
||||
static evaluateFunctor<T>(
|
||||
functor: undefined | boolean | ((model: T) => boolean),
|
||||
model: T | undefined,
|
||||
defaultValue = false,
|
||||
): boolean {
|
||||
if (!model || functor == null) return defaultValue;
|
||||
switch (typeof functor) {
|
||||
case 'boolean':
|
||||
return functor;
|
||||
|
||||
case 'function':
|
||||
return functor(model);
|
||||
|
||||
default:
|
||||
throw new TypeError(`invalid functor`);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props: AutoFormProps<T>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -77,10 +96,12 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
private fieldChange = (field: Field<T>, newValue: any) => {
|
||||
const { model } = this.props;
|
||||
if (!model) return;
|
||||
|
||||
const newModel =
|
||||
typeof newValue === 'undefined'
|
||||
? deepDelete(model, field.name)
|
||||
: deepSet(model, field.name, newValue);
|
||||
|
||||
this.modelChange(newModel);
|
||||
};
|
||||
|
||||
|
@ -88,13 +109,8 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
const { fields, onChange } = this.props;
|
||||
|
||||
for (const someField of fields) {
|
||||
if (someField.isDefined && !someField.isDefined(newModel)) {
|
||||
if (!AutoForm.evaluateFunctor(someField.defined, newModel, true)) {
|
||||
newModel = deepDelete(newModel, someField.name);
|
||||
} else if (
|
||||
typeof someField.defaultValue !== 'undefined' &&
|
||||
typeof deepGet(newModel, someField.name) === 'undefined'
|
||||
) {
|
||||
newModel = deepSet(newModel, someField.name, someField.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,9 +119,11 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
|
||||
private renderNumberInput(field: Field<T>): JSX.Element {
|
||||
const { model, large } = this.props;
|
||||
|
||||
const modelValue = deepGet(model as any, field.name) || field.defaultValue;
|
||||
return (
|
||||
<NumericInput
|
||||
value={deepGet(model as any, field.name) || field.defaultValue}
|
||||
value={modelValue}
|
||||
onValueChange={(valueAsNumber: number, valueAsString: string) => {
|
||||
if (valueAsString === '') {
|
||||
this.fieldChange(field, undefined);
|
||||
|
@ -117,8 +135,13 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
min={field.min || 0}
|
||||
fill
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
disabled={AutoForm.evaluateFunctor(field.disabled, model)}
|
||||
placeholder={field.placeholder}
|
||||
intent={
|
||||
AutoForm.evaluateFunctor(field.required, model) && modelValue == null
|
||||
? Intent.PRIMARY
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -136,7 +159,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
stepSize={1000}
|
||||
majorStepSize={1000000}
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
disabled={AutoForm.evaluateFunctor(field.disabled, model)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -158,7 +181,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
placeholder={field.placeholder}
|
||||
suggestions={field.suggestions}
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
disabled={AutoForm.evaluateFunctor(field.disabled, model)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -175,7 +198,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
this.fieldChange(field, v);
|
||||
}}
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
disabled={AutoForm.evaluateFunctor(field.disabled, model)}
|
||||
>
|
||||
<option value="True">True</option>
|
||||
<option value="False">False</option>
|
||||
|
@ -220,7 +243,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
}}
|
||||
placeholder={field.placeholder}
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
disabled={AutoForm.evaluateFunctor(field.disabled, model)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -250,8 +273,8 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
|
|||
|
||||
private renderField = (field: Field<T>) => {
|
||||
const { model } = this.props;
|
||||
if (!model) return null;
|
||||
if (field.isDefined && !field.isDefined(model)) return null;
|
||||
if (!model) return;
|
||||
if (!AutoForm.evaluateFunctor(field.defined, model, true)) return;
|
||||
|
||||
const label = field.label || AutoForm.makeLabelName(field.name);
|
||||
return (
|
||||
|
|
|
@ -17,9 +17,6 @@
|
|||
*/
|
||||
|
||||
.center-message {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
overflow: hidden;
|
||||
|
||||
.center-message-inner {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`rule editor matches snapshot 1`] = `null`;
|
||||
exports[`show history matches snapshot 1`] = `null`;
|
||||
|
|
|
@ -21,7 +21,7 @@ import React from 'react';
|
|||
|
||||
import { ShowHistory } from './show-history';
|
||||
|
||||
describe('rule editor', () => {
|
||||
describe('show history', () => {
|
||||
it('matches snapshot', () => {
|
||||
const showJson = <ShowHistory endpoint={'test'} downloadFilename={'test'} />;
|
||||
const { container } = render(showJson);
|
||||
|
|
|
@ -22,16 +22,19 @@ exports[`show log describe show log 1`] = `
|
|||
<div
|
||||
class="bp3-button-group right-buttons"
|
||||
>
|
||||
<button
|
||||
<a
|
||||
class="bp3-button bp3-minimal"
|
||||
type="button"
|
||||
download="test"
|
||||
href="test"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="bp3-button-text"
|
||||
>
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
<button
|
||||
class="bp3-button bp3-minimal"
|
||||
type="button"
|
||||
|
|
|
@ -16,14 +16,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, ButtonGroup, Checkbox, Intent } from '@blueprintjs/core';
|
||||
import { AnchorButton, Button, ButtonGroup, Checkbox, Intent } from '@blueprintjs/core';
|
||||
import axios from 'axios';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import React from 'react';
|
||||
|
||||
import { AppToaster } from '../../singletons/toaster';
|
||||
import { UrlBaser } from '../../singletons/url-baser';
|
||||
import { downloadFile, QueryManager } from '../../utils';
|
||||
import { QueryManager } from '../../utils';
|
||||
|
||||
import './show-log.scss';
|
||||
|
||||
|
@ -123,11 +123,7 @@ export class ShowLog extends React.PureComponent<ShowLogProps, ShowLogState> {
|
|||
)}
|
||||
<ButtonGroup className="right-buttons">
|
||||
{downloadFilename && (
|
||||
<Button
|
||||
text="Save"
|
||||
minimal
|
||||
onClick={() => downloadFile(logValue ? logValue : '', 'plain', downloadFilename)}
|
||||
/>
|
||||
<AnchorButton text="Save" minimal download={downloadFilename} href={endpoint} />
|
||||
)}
|
||||
<Button
|
||||
text="Copy"
|
||||
|
|
|
@ -44,6 +44,7 @@ type Capabilities = 'working-with-sql' | 'working-without-sql' | 'broken';
|
|||
|
||||
export interface ConsoleApplicationProps {
|
||||
hideLegacy: boolean;
|
||||
exampleManifestsUrl?: string;
|
||||
}
|
||||
|
||||
export interface ConsoleApplicationState {
|
||||
|
@ -232,11 +233,14 @@ export class ConsoleApplication extends React.PureComponent<
|
|||
};
|
||||
|
||||
private wrappedLoadDataView = () => {
|
||||
const { exampleManifestsUrl } = this.props;
|
||||
|
||||
return this.wrapInViewContainer(
|
||||
'load-data',
|
||||
<LoadDataView
|
||||
initSupervisorId={this.supervisorId}
|
||||
initTaskId={this.taskId}
|
||||
exampleManifestsUrl={exampleManifestsUrl}
|
||||
goToTask={this.goToTaskWithTaskId}
|
||||
/>,
|
||||
'narrow-pad',
|
||||
|
|
|
@ -40,6 +40,8 @@ export class CompactionDialog extends React.PureComponent<
|
|||
CompactionDialogProps,
|
||||
CompactionDialogState
|
||||
> {
|
||||
static DEFAULT_TARGET_COMPACTION_SIZE_BYTES = 419430400;
|
||||
|
||||
constructor(props: CompactionDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -54,7 +56,7 @@ export class CompactionDialog extends React.PureComponent<
|
|||
inputSegmentSizeBytes: 419430400,
|
||||
maxNumSegmentsToCompact: 150,
|
||||
skipOffsetFromLatest: 'P1D',
|
||||
targetCompactionSizeBytes: 419430400,
|
||||
targetCompactionSizeBytes: CompactionDialog.DEFAULT_TARGET_COMPACTION_SIZE_BYTES,
|
||||
taskContext: null,
|
||||
taskPriority: 25,
|
||||
tuningConfig: null,
|
||||
|
|
|
@ -62,9 +62,8 @@ exports[`clipboard dialog matches snapshot 1`] = `
|
|||
<div
|
||||
class="bp3-dialog-footer-actions"
|
||||
>
|
||||
|
||||
<div
|
||||
class="bp3-button-group edit-context-dialog-buttons"
|
||||
class="edit-context-dialog-buttons"
|
||||
>
|
||||
<button
|
||||
class="bp3-button"
|
||||
|
@ -83,7 +82,7 @@ exports[`clipboard dialog matches snapshot 1`] = `
|
|||
<span
|
||||
class="bp3-button-text"
|
||||
>
|
||||
Submit
|
||||
Save
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -24,7 +24,7 @@ import { EditContextDialog } from './edit-context-dialog';
|
|||
describe('clipboard dialog', () => {
|
||||
it('matches snapshot', () => {
|
||||
const compactionDialog = (
|
||||
<EditContextDialog queryContext={{}} onSubmit={() => null} onClose={() => null} />
|
||||
<EditContextDialog queryContext={{}} onQueryContextChange={() => null} onClose={() => null} />
|
||||
);
|
||||
render(compactionDialog);
|
||||
expect(document.body.lastChild).toMatchSnapshot();
|
||||
|
|
|
@ -15,22 +15,23 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Button, ButtonGroup, Callout, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
|
||||
import { Button, Callout, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
|
||||
import Hjson from 'hjson';
|
||||
import React from 'react';
|
||||
|
||||
import { validJson } from '../../utils';
|
||||
import { QueryContext } from '../../utils/query-context';
|
||||
|
||||
import './edit-context-dialog.scss';
|
||||
|
||||
export interface EditContextDialogProps {
|
||||
queryContext: QueryContext;
|
||||
onSubmit: (queryContext: {}) => void;
|
||||
onQueryContextChange: (queryContext: QueryContext) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface EditContextDialogState {
|
||||
queryContextString: string;
|
||||
queryContext?: QueryContext;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
|
@ -44,75 +45,61 @@ export class EditContextDialog extends React.PureComponent<
|
|||
queryContextString: Object.keys(props.queryContext).length
|
||||
? JSON.stringify(props.queryContext, undefined, 2)
|
||||
: '{\n\n}',
|
||||
error: '',
|
||||
};
|
||||
}
|
||||
|
||||
private onTextChange = (e: any) => {
|
||||
let { error } = this.state;
|
||||
const queryContextText = (e.target as HTMLInputElement).value;
|
||||
error = undefined;
|
||||
let queryContextObject;
|
||||
private handleTextChange = (e: any) => {
|
||||
const queryContextString = (e.target as HTMLInputElement).value;
|
||||
|
||||
let error: string | undefined;
|
||||
let queryContext: QueryContext | undefined;
|
||||
try {
|
||||
queryContextObject = JSON.parse(queryContextText);
|
||||
queryContext = Hjson.parse(queryContextString);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
|
||||
if (!(typeof queryContextObject === 'object')) {
|
||||
if (!error && (!queryContext || typeof queryContext !== 'object')) {
|
||||
error = 'Input is not a valid object';
|
||||
queryContext = undefined;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
queryContextString: queryContextText,
|
||||
queryContextString,
|
||||
queryContext,
|
||||
error,
|
||||
});
|
||||
};
|
||||
|
||||
private handleSave = () => {
|
||||
const { onQueryContextChange } = this.props;
|
||||
const { queryContext } = this.state;
|
||||
if (!queryContext) return;
|
||||
onQueryContextChange(queryContext);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { onClose, onSubmit } = this.props;
|
||||
const { queryContextString } = this.state;
|
||||
let { error } = this.state;
|
||||
|
||||
let queryContextObject: {} | undefined;
|
||||
try {
|
||||
queryContextObject = JSON.parse(queryContextString);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
|
||||
if (!(typeof queryContextObject === 'object') && !error) {
|
||||
error = 'Input is not a valid object';
|
||||
}
|
||||
const { onClose } = this.props;
|
||||
const { queryContextString, error } = this.state;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className="edit-context-dialog"
|
||||
isOpen
|
||||
onClose={() => onClose()}
|
||||
title={'Edit query context'}
|
||||
>
|
||||
<TextArea value={queryContextString} onChange={this.onTextChange} autoFocus />
|
||||
<Dialog className="edit-context-dialog" isOpen onClose={onClose} title={'Edit query context'}>
|
||||
<TextArea value={queryContextString} onChange={this.handleTextChange} autoFocus />
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
{error && (
|
||||
<Callout intent={Intent.DANGER} className="edit-context-dialog-error">
|
||||
{error}
|
||||
</Callout>
|
||||
)}
|
||||
<ButtonGroup className={'edit-context-dialog-buttons'}>
|
||||
<div className={'edit-context-dialog-buttons'}>
|
||||
<Button text={'Close'} onClick={onClose} />
|
||||
<Button
|
||||
text={'Close'}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!validJson(queryContextString) || typeof queryContextObject !== 'object'}
|
||||
text={'Submit'}
|
||||
disabled={Boolean(error)}
|
||||
text={'Save'}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => (queryContextObject ? onSubmit(queryContextObject) : null)}
|
||||
onClick={this.handleSave}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
|
|
@ -56,23 +56,12 @@ exports[`query plan dialog matches snapshot 1`] = `
|
|||
class="bp3-dialog-body"
|
||||
>
|
||||
<div
|
||||
class="bp3-tabs bp3-vertical tab-area"
|
||||
class="center-message bp3-input"
|
||||
>
|
||||
<div
|
||||
class="bp3-tab-list"
|
||||
role="tablist"
|
||||
class="center-message-inner"
|
||||
>
|
||||
<div
|
||||
class="bp3-tab-indicator-wrapper"
|
||||
style="display: none;"
|
||||
>
|
||||
<div
|
||||
class="bp3-tab-indicator"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="bp3-flex-expander"
|
||||
/>
|
||||
The query history is empty.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -92,16 +81,6 @@ exports[`query plan dialog matches snapshot 1`] = `
|
|||
Close
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="bp3-button bp3-intent-primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bp3-button-text"
|
||||
>
|
||||
Open
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -30,4 +30,8 @@
|
|||
height: 500px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.center-message {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
import { Button, Classes, Dialog, Intent, Tab, Tabs, TextArea } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
|
||||
import { CenterMessage } from '../../components';
|
||||
|
||||
import './query-history-dialog.scss';
|
||||
|
||||
export interface QueryRecord {
|
||||
|
@ -28,7 +30,7 @@ export interface QueryRecord {
|
|||
export interface QueryHistoryDialogProps {
|
||||
setQueryString: (queryString: string) => void;
|
||||
onClose: () => void;
|
||||
queryRecords: QueryRecord[];
|
||||
queryRecords: readonly QueryRecord[];
|
||||
}
|
||||
|
||||
export interface QueryHistoryDialogState {
|
||||
|
@ -39,6 +41,32 @@ export class QueryHistoryDialog extends React.PureComponent<
|
|||
QueryHistoryDialogProps,
|
||||
QueryHistoryDialogState
|
||||
> {
|
||||
static getHistoryVersion(): string {
|
||||
return new Date()
|
||||
.toISOString()
|
||||
.split('.')[0]
|
||||
.replace('T', ' ');
|
||||
}
|
||||
|
||||
static addQueryToHistory(
|
||||
queryHistory: readonly QueryRecord[],
|
||||
queryString: string,
|
||||
): readonly QueryRecord[] {
|
||||
// Do not add to history if already the same as the last element
|
||||
if (queryHistory.length && queryHistory[0].queryString === queryString) {
|
||||
return queryHistory;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
version: QueryHistoryDialog.getHistoryVersion(),
|
||||
queryString,
|
||||
},
|
||||
]
|
||||
.concat(queryHistory)
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
constructor(props: QueryHistoryDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -46,10 +74,25 @@ export class QueryHistoryDialog extends React.PureComponent<
|
|||
};
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { onClose, queryRecords, setQueryString } = this.props;
|
||||
private handleSelect = () => {
|
||||
const { queryRecords, setQueryString } = this.props;
|
||||
const { activeTab } = this.state;
|
||||
|
||||
setQueryString(queryRecords[activeTab].queryString);
|
||||
};
|
||||
|
||||
private handleTabChange = (tab: number) => {
|
||||
this.setState({ activeTab: tab });
|
||||
};
|
||||
|
||||
renderContent(): JSX.Element {
|
||||
const { queryRecords } = this.props;
|
||||
const { activeTab } = this.state;
|
||||
|
||||
if (!queryRecords.length) {
|
||||
return <CenterMessage>The query history is empty.</CenterMessage>;
|
||||
}
|
||||
|
||||
const versions = queryRecords.map((record, index) => (
|
||||
<Tab
|
||||
id={index}
|
||||
|
@ -60,29 +103,33 @@ export class QueryHistoryDialog extends React.PureComponent<
|
|||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
animate
|
||||
renderActiveTabPanelOnly
|
||||
vertical
|
||||
className={'tab-area'}
|
||||
selectedTabId={activeTab}
|
||||
onChange={this.handleTabChange}
|
||||
>
|
||||
{versions}
|
||||
<Tabs.Expander />
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { onClose, queryRecords } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog className="query-history-dialog" isOpen onClose={onClose} title="Query history">
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<Tabs
|
||||
animate
|
||||
renderActiveTabPanelOnly
|
||||
vertical
|
||||
className={'tab-area'}
|
||||
selectedTabId={activeTab}
|
||||
onChange={(tab: number) => this.setState({ activeTab: tab })}
|
||||
>
|
||||
{versions}
|
||||
<Tabs.Expander />
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_BODY}>{this.renderContent()}</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button text="Close" onClick={onClose} />
|
||||
<Button
|
||||
text="Open"
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => setQueryString(queryRecords[activeTab].queryString)}
|
||||
/>
|
||||
{Boolean(queryRecords.length) && (
|
||||
<Button text="Open" intent={Intent.PRIMARY} onClick={this.handleSelect} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
|
|
@ -110,7 +110,7 @@ export class TaskTableActionDialog extends React.PureComponent<
|
|||
<ShowLog
|
||||
status={status}
|
||||
endpoint={`/druid/indexer/v1/task/${taskId}/log`}
|
||||
downloadFilename={`task-log-${taskId}.json`}
|
||||
downloadFilename={`task-log-${taskId}.log`}
|
||||
tailOffset={16000}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -43,6 +43,7 @@ interface ConsoleConfig {
|
|||
customHeaderName?: string;
|
||||
customHeaderValue?: string;
|
||||
customHeaders?: Record<string, string>;
|
||||
exampleManifestsUrl?: string;
|
||||
}
|
||||
|
||||
const consoleConfig: ConsoleConfig = (window as any).consoleConfig;
|
||||
|
@ -64,6 +65,7 @@ if (consoleConfig.customHeaders) {
|
|||
ReactDOM.render(
|
||||
React.createElement(ConsoleApplication, {
|
||||
hideLegacy: Boolean(consoleConfig.hideLegacy),
|
||||
exampleManifestsUrl: consoleConfig.exampleManifestsUrl,
|
||||
}) as any,
|
||||
container,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { timeFormatMatches } from './druid-time';
|
||||
|
||||
describe('timeFormatMatches', () => {
|
||||
it('works for auto', () => {
|
||||
expect(timeFormatMatches('auto', '2019-05-22 22:42:51+0000')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('works for iso', () => {
|
||||
expect(timeFormatMatches('iso', '2019-05-22T22:42:51+0000')).toBeTruthy();
|
||||
expect(timeFormatMatches('iso', '2019-05-22 22:42:51+0000')).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -19,7 +19,7 @@
|
|||
import { jodaFormatToRegExp } from './joda-to-regexp';
|
||||
|
||||
export const NUMERIC_TIME_FORMATS: string[] = ['posix', 'millis', 'micro', 'nano'];
|
||||
export const BASIC_TIME_FORMATS: string[] = ['iso'].concat(NUMERIC_TIME_FORMATS);
|
||||
export const BASIC_TIME_FORMATS: string[] = ['auto', 'iso'].concat(NUMERIC_TIME_FORMATS);
|
||||
|
||||
export const DATE_ONLY_TIME_FORMATS: string[] = [
|
||||
'dd/MM/yyyy',
|
||||
|
@ -52,13 +52,20 @@ const MIN_MICRO = MIN_MILLIS * 1000;
|
|||
const MIN_NANO = MIN_MICRO * 1000;
|
||||
const MAX_NANO = MIN_NANO * 1000;
|
||||
|
||||
// copied from http://goo.gl/0ejHHW with small tweak to make dddd not pass on its own
|
||||
// tslint:disable-next-line:max-line-length
|
||||
export const AUTO_MATCHER = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))( ((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)$/;
|
||||
|
||||
// tslint:disable-next-line:max-line-length
|
||||
export const ISO_MATCHER = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))(T((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)$/;
|
||||
|
||||
// Note: AUTO and ISO are basically the same except ISO has a space as a separator instead of the T
|
||||
|
||||
export function timeFormatMatches(format: string, value: string | number): boolean {
|
||||
const absValue = Math.abs(Number(value));
|
||||
switch (format) {
|
||||
case 'auto':
|
||||
return AUTO_MATCHER.test(String(value));
|
||||
|
||||
case 'iso':
|
||||
return ISO_MATCHER.test(String(value));
|
||||
|
||||
|
|
|
@ -179,9 +179,9 @@ export function mapRecord<T, Q>(
|
|||
}
|
||||
|
||||
export function groupBy<T, Q>(
|
||||
array: T[],
|
||||
array: readonly T[],
|
||||
keyFn: (x: T, index: number) => string,
|
||||
aggregateFn: (xs: T[], key: string) => Q,
|
||||
aggregateFn: (xs: readonly T[], key: string) => Q,
|
||||
): Q[] {
|
||||
const buckets: Record<string, T[]> = {};
|
||||
const n = array.length;
|
||||
|
@ -194,7 +194,7 @@ export function groupBy<T, Q>(
|
|||
return Object.keys(buckets).map(key => aggregateFn(buckets[key], key));
|
||||
}
|
||||
|
||||
export function uniq(array: string[]): string[] {
|
||||
export function uniq(array: readonly string[]): string[] {
|
||||
const seen: Record<string, boolean> = {};
|
||||
return array.filter(s => {
|
||||
if (hasOwnProp(seen, s)) {
|
||||
|
|
|
@ -280,24 +280,24 @@ const PARSE_SPEC_FORM_FIELDS: Field<ParseSpec>[] = [
|
|||
{
|
||||
name: 'pattern',
|
||||
type: 'string',
|
||||
isDefined: (p: ParseSpec) => p.format === 'regex',
|
||||
defined: (p: ParseSpec) => p.format === 'regex',
|
||||
},
|
||||
{
|
||||
name: 'function',
|
||||
type: 'string',
|
||||
isDefined: (p: ParseSpec) => p.format === 'javascript',
|
||||
defined: (p: ParseSpec) => p.format === 'javascript',
|
||||
},
|
||||
{
|
||||
name: 'hasHeaderRow',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
isDefined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv',
|
||||
defined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv',
|
||||
},
|
||||
{
|
||||
name: 'skipHeaderRows',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
isDefined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv',
|
||||
defined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv',
|
||||
min: 0,
|
||||
info: (
|
||||
<>
|
||||
|
@ -310,14 +310,14 @@ const PARSE_SPEC_FORM_FIELDS: Field<ParseSpec>[] = [
|
|||
{
|
||||
name: 'columns',
|
||||
type: 'string-array',
|
||||
isDefined: (p: ParseSpec) =>
|
||||
defined: (p: ParseSpec) =>
|
||||
((p.format === 'csv' || p.format === 'tsv') && !p.hasHeaderRow) || p.format === 'regex',
|
||||
},
|
||||
{
|
||||
name: 'listDelimiter',
|
||||
type: 'string',
|
||||
defaultValue: '|',
|
||||
isDefined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv',
|
||||
defined: (p: ParseSpec) => p.format === 'csv' || p.format === 'tsv',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -385,7 +385,6 @@ const TIMESTAMP_SPEC_FORM_FIELDS: Field<TimestampSpec>[] = [
|
|||
type: 'string',
|
||||
defaultValue: 'auto',
|
||||
suggestions: [
|
||||
'auto',
|
||||
...BASIC_TIME_FORMATS,
|
||||
{
|
||||
group: 'Date and time formats',
|
||||
|
@ -400,7 +399,7 @@ const TIMESTAMP_SPEC_FORM_FIELDS: Field<TimestampSpec>[] = [
|
|||
suggestions: OTHER_TIME_FORMATS,
|
||||
},
|
||||
],
|
||||
isDefined: (timestampSpec: TimestampSpec) => isColumnTimestampSpec(timestampSpec),
|
||||
defined: (timestampSpec: TimestampSpec) => isColumnTimestampSpec(timestampSpec),
|
||||
info: (
|
||||
<p>
|
||||
Please specify your timestamp format by using the suggestions menu or typing in a{' '}
|
||||
|
@ -470,7 +469,7 @@ const DIMENSION_SPEC_FORM_FIELDS: Field<DimensionSpec>[] = [
|
|||
name: 'createBitmapIndex',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
isDefined: (dimensionSpec: DimensionSpec) => dimensionSpec.type === 'string',
|
||||
defined: (dimensionSpec: DimensionSpec) => dimensionSpec.type === 'string',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -518,7 +517,7 @@ const FLATTEN_FIELD_FORM_FIELDS: Field<FlattenField>[] = [
|
|||
name: 'expr',
|
||||
type: 'string',
|
||||
placeholder: '$.thing',
|
||||
isDefined: (flattenField: FlattenField) =>
|
||||
defined: (flattenField: FlattenField) =>
|
||||
flattenField.type === 'path' || flattenField.type === 'jq',
|
||||
info: (
|
||||
<>
|
||||
|
@ -642,7 +641,7 @@ const METRIC_SPEC_FORM_FIELDS: Field<MetricSpec>[] = [
|
|||
{
|
||||
name: 'fieldName',
|
||||
type: 'string',
|
||||
isDefined: m => {
|
||||
defined: m => {
|
||||
return [
|
||||
'longSum',
|
||||
'doubleSum',
|
||||
|
@ -670,7 +669,7 @@ const METRIC_SPEC_FORM_FIELDS: Field<MetricSpec>[] = [
|
|||
name: 'maxStringBytes',
|
||||
type: 'number',
|
||||
defaultValue: 1024,
|
||||
isDefined: m => {
|
||||
defined: m => {
|
||||
return ['stringFirst', 'stringLast'].includes(m.type);
|
||||
},
|
||||
},
|
||||
|
@ -678,21 +677,21 @@ const METRIC_SPEC_FORM_FIELDS: Field<MetricSpec>[] = [
|
|||
name: 'filterNullValues',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
isDefined: m => {
|
||||
defined: m => {
|
||||
return ['stringFirst', 'stringLast'].includes(m.type);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'filter',
|
||||
type: 'json',
|
||||
isDefined: m => {
|
||||
defined: m => {
|
||||
return m.type === 'filtered';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'aggregator',
|
||||
type: 'json',
|
||||
isDefined: m => {
|
||||
defined: m => {
|
||||
return m.type === 'filtered';
|
||||
},
|
||||
},
|
||||
|
@ -753,7 +752,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
|
|||
name: 'firehose.type',
|
||||
label: 'Firehose type',
|
||||
type: 'string',
|
||||
suggestions: ['local', 'http', 'static-s3', 'static-google-blobstore'],
|
||||
suggestions: ['local', 'http', 'inline', 'static-s3', 'static-google-blobstore'],
|
||||
info: (
|
||||
<p>
|
||||
Druid connects to raw data through{' '}
|
||||
|
@ -895,12 +894,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
|
|||
case 'index:inline':
|
||||
return [
|
||||
firehoseType,
|
||||
{
|
||||
name: 'firehose.data',
|
||||
label: 'Data',
|
||||
type: 'string',
|
||||
info: <p>The data to ingest.</p>,
|
||||
},
|
||||
// do not add 'data' here as it has special handling in the load-data view
|
||||
];
|
||||
|
||||
case 'index:static-s3':
|
||||
|
@ -911,7 +905,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
|
|||
label: 'S3 URIs',
|
||||
type: 'string-array',
|
||||
placeholder: 's3://your-bucket/some-file1.ext, s3://your-bucket/some-file2.ext',
|
||||
isDefined: ioConfig => !deepGet(ioConfig, 'firehose.prefixes'),
|
||||
defined: ioConfig => !deepGet(ioConfig, 'firehose.prefixes'),
|
||||
info: (
|
||||
<>
|
||||
<p>
|
||||
|
@ -927,7 +921,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
|
|||
label: 'S3 prefixes',
|
||||
type: 'string-array',
|
||||
placeholder: 's3://your-bucket/some-path1, s3://your-bucket/some-path2',
|
||||
isDefined: ioConfig => !deepGet(ioConfig, 'firehose.uris'),
|
||||
defined: ioConfig => !deepGet(ioConfig, 'firehose.uris'),
|
||||
info: (
|
||||
<>
|
||||
<p>A list of paths (with bucket) where your files are stored.</p>
|
||||
|
@ -979,7 +973,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
|
|||
{
|
||||
name: 'topic',
|
||||
type: 'string',
|
||||
isDefined: (i: IoConfig) => i.type === 'kafka',
|
||||
defined: (i: IoConfig) => i.type === 'kafka',
|
||||
},
|
||||
{
|
||||
name: 'consumerProperties',
|
||||
|
@ -1228,7 +1222,7 @@ export function getIoConfigTuningFormFields(
|
|||
name: 'useEarliestOffset',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
isDefined: (i: IoConfig) => i.type === 'kafka',
|
||||
defined: (i: IoConfig) => i.type === 'kafka',
|
||||
info: (
|
||||
<>
|
||||
<p>
|
||||
|
@ -1244,7 +1238,7 @@ export function getIoConfigTuningFormFields(
|
|||
name: 'skipOffsetGaps',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
isDefined: (i: IoConfig) => i.type === 'kafka',
|
||||
defined: (i: IoConfig) => i.type === 'kafka',
|
||||
info: (
|
||||
<>
|
||||
<p>
|
||||
|
@ -1260,7 +1254,7 @@ export function getIoConfigTuningFormFields(
|
|||
name: 'pollTimeout',
|
||||
type: 'number',
|
||||
defaultValue: 100,
|
||||
isDefined: (i: IoConfig) => i.type === 'kafka',
|
||||
defined: (i: IoConfig) => i.type === 'kafka',
|
||||
info: (
|
||||
<>
|
||||
<p>
|
||||
|
@ -1274,7 +1268,7 @@ export function getIoConfigTuningFormFields(
|
|||
name: 'useEarliestSequenceNumber',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
isDefined: (i: IoConfig) => i.type === 'kinesis',
|
||||
defined: (i: IoConfig) => i.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
If a supervisor is managing a dataSource for the first time, it will obtain a set of
|
||||
|
@ -1289,20 +1283,20 @@ export function getIoConfigTuningFormFields(
|
|||
name: 'recordsPerFetch',
|
||||
type: 'number',
|
||||
defaultValue: 2000,
|
||||
isDefined: (i: IoConfig) => i.type === 'kinesis',
|
||||
defined: (i: IoConfig) => i.type === 'kinesis',
|
||||
info: <>The number of records to request per GetRecords call to Kinesis.</>,
|
||||
},
|
||||
{
|
||||
name: 'fetchDelayMillis',
|
||||
type: 'number',
|
||||
defaultValue: 1000,
|
||||
isDefined: (i: IoConfig) => i.type === 'kinesis',
|
||||
defined: (i: IoConfig) => i.type === 'kinesis',
|
||||
info: <>Time in milliseconds to wait between subsequent GetRecords calls to Kinesis.</>,
|
||||
},
|
||||
{
|
||||
name: 'deaggregate',
|
||||
type: 'boolean',
|
||||
isDefined: (i: IoConfig) => i.type === 'kinesis',
|
||||
defined: (i: IoConfig) => i.type === 'kinesis',
|
||||
info: <>Whether to use the de-aggregate function of the KCL.</>,
|
||||
},
|
||||
|
||||
|
@ -1499,7 +1493,6 @@ export function guessDataSourceName(ioConfig: IoConfig): string | undefined {
|
|||
|
||||
export interface TuningConfig {
|
||||
type: string;
|
||||
targetPartitionSize?: number;
|
||||
maxRowsInMemory?: number;
|
||||
maxBytesInMemory?: number;
|
||||
maxTotalRows?: number;
|
||||
|
@ -1529,32 +1522,24 @@ export interface TuningConfig {
|
|||
fetchThreads?: number;
|
||||
}
|
||||
|
||||
export function invalidTuningConfig(tuningConfig: TuningConfig): boolean {
|
||||
return Boolean(
|
||||
tuningConfig.type === 'index_parallel' &&
|
||||
tuningConfig.forceGuaranteedRollup &&
|
||||
!tuningConfig.numShards,
|
||||
);
|
||||
}
|
||||
|
||||
export function getPartitionRelatedTuningSpecFormFields(
|
||||
specType: IngestionType,
|
||||
): Field<TuningConfig>[] {
|
||||
switch (specType) {
|
||||
case 'index':
|
||||
case 'index_parallel':
|
||||
const myIsParallel = specType === 'index_parallel';
|
||||
return [
|
||||
{
|
||||
name: 'partitionDimensions',
|
||||
type: 'string-array',
|
||||
disabled: myIsParallel,
|
||||
info: (
|
||||
<>
|
||||
<p>Does not currently work with parallel ingestion</p>
|
||||
<p>
|
||||
The dimensions to partition on. Leave blank to select all dimensions. Only used with
|
||||
forceGuaranteedRollup = true, will be ignored otherwise.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'forceGuaranteedRollup',
|
||||
type: 'boolean',
|
||||
disabled: myIsParallel,
|
||||
info: (
|
||||
<>
|
||||
<p>Does not currently work with parallel ingestion</p>
|
||||
|
@ -1569,18 +1554,25 @@ export function getPartitionRelatedTuningSpecFormFields(
|
|||
),
|
||||
},
|
||||
{
|
||||
name: 'targetPartitionSize',
|
||||
type: 'number',
|
||||
name: 'partitionDimensions',
|
||||
type: 'string-array',
|
||||
defined: (t: TuningConfig) => Boolean(t.forceGuaranteedRollup),
|
||||
info: (
|
||||
<>
|
||||
Target number of rows to include in a partition, should be a number that targets
|
||||
segments of 500MB~1GB.
|
||||
<p>Does not currently work with parallel ingestion</p>
|
||||
<p>
|
||||
The dimensions to partition on. Leave blank to select all dimensions. Only used with
|
||||
forceGuaranteedRollup = true, will be ignored otherwise.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'numShards',
|
||||
name: 'numShards', // This is mandatory if index_parallel and forceGuaranteedRollup
|
||||
type: 'number',
|
||||
defined: (t: TuningConfig) => Boolean(t.forceGuaranteedRollup),
|
||||
required: (t: TuningConfig) =>
|
||||
Boolean(t.type === 'index_parallel' && t.forceGuaranteedRollup),
|
||||
info: (
|
||||
<>
|
||||
Directly specify the number of shards to create. If this is specified and 'intervals'
|
||||
|
@ -1594,6 +1586,7 @@ export function getPartitionRelatedTuningSpecFormFields(
|
|||
name: 'maxRowsPerSegment',
|
||||
type: 'number',
|
||||
defaultValue: 5000000,
|
||||
defined: (t: TuningConfig) => t.numShards == null, // Can not be set if numShards is specified
|
||||
info: <>Determines how many rows are in each segment.</>,
|
||||
},
|
||||
{
|
||||
|
@ -1626,6 +1619,35 @@ export function getPartitionRelatedTuningSpecFormFields(
|
|||
}
|
||||
|
||||
const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
||||
{
|
||||
name: 'maxNumConcurrentSubTasks',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
defined: (t: TuningConfig) => t.type === 'index_parallel',
|
||||
info: (
|
||||
<>
|
||||
Maximum number of tasks which can be run at the same time. The supervisor task would spawn
|
||||
worker tasks up to maxNumConcurrentSubTasks regardless of the available task slots. If this
|
||||
value is set to 1, the supervisor task processes data ingestion on its own instead of
|
||||
spawning worker tasks. If this value is set to too large, too many worker tasks can be
|
||||
created which might block other ingestion.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'maxRetry',
|
||||
type: 'number',
|
||||
defaultValue: 3,
|
||||
defined: (t: TuningConfig) => t.type === 'index_parallel',
|
||||
info: <>Maximum number of retries on task failures.</>,
|
||||
},
|
||||
{
|
||||
name: 'taskStatusCheckPeriodMs',
|
||||
type: 'number',
|
||||
defaultValue: 1000,
|
||||
defined: (t: TuningConfig) => t.type === 'index_parallel',
|
||||
info: <>Polling period in milliseconds to check running task statuses.</>,
|
||||
},
|
||||
{
|
||||
name: 'maxRowsInMemory',
|
||||
type: 'number',
|
||||
|
@ -1638,6 +1660,24 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
placeholder: 'Default: 1/6 of max JVM memory',
|
||||
info: <>Used in determining when intermediate persists to disk should occur.</>,
|
||||
},
|
||||
{
|
||||
name: 'maxNumMergeTasks',
|
||||
type: 'number',
|
||||
defaultValue: 10,
|
||||
defined: (t: TuningConfig) => Boolean(t.type === 'index_parallel' && t.forceGuaranteedRollup),
|
||||
info: <>Number of tasks to merge partial segments after shuffle.</>,
|
||||
},
|
||||
{
|
||||
name: 'maxNumSegmentsToMerge',
|
||||
type: 'number',
|
||||
defaultValue: 100,
|
||||
defined: (t: TuningConfig) => Boolean(t.type === 'index_parallel' && t.forceGuaranteedRollup),
|
||||
info: (
|
||||
<>
|
||||
Max limit for the number of segments a single task can merge at the same time after shuffle.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'indexSpec.bitmap.type',
|
||||
label: 'Index bitmap type',
|
||||
|
@ -1681,14 +1721,14 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'intermediatePersistPeriod',
|
||||
type: 'duration',
|
||||
defaultValue: 'PT10M',
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: <>The period that determines the rate at which intermediate persists occur.</>,
|
||||
},
|
||||
{
|
||||
name: 'intermediateHandoffPeriod',
|
||||
type: 'duration',
|
||||
defaultValue: 'P2147483647D',
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
How often the tasks should hand off segments. Handoff will happen either if
|
||||
|
@ -1718,56 +1758,32 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'maxNumConcurrentSubTasks',
|
||||
type: 'number',
|
||||
defaultValue: 1,
|
||||
info: (
|
||||
<>
|
||||
Maximum number of tasks which can be run at the same time. The supervisor task would spawn
|
||||
worker tasks up to maxNumConcurrentSubTasks regardless of the available task slots. If this
|
||||
value is set to 1, the supervisor task processes data ingestion on its own instead of
|
||||
spawning worker tasks. If this value is set to too large, too many worker tasks can be
|
||||
created which might block other ingestion.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'maxRetry',
|
||||
type: 'number',
|
||||
defaultValue: 3,
|
||||
info: <>Maximum number of retries on task failures.</>,
|
||||
},
|
||||
{
|
||||
name: 'taskStatusCheckPeriodMs',
|
||||
type: 'number',
|
||||
defaultValue: 1000,
|
||||
info: <>Polling period in milliseconds to check running task statuses.</>,
|
||||
},
|
||||
{
|
||||
name: 'chatHandlerTimeout',
|
||||
type: 'duration',
|
||||
defaultValue: 'PT10S',
|
||||
defined: (t: TuningConfig) => t.type === 'index_parallel',
|
||||
info: <>Timeout for reporting the pushed segments in worker tasks.</>,
|
||||
},
|
||||
{
|
||||
name: 'chatHandlerNumRetries',
|
||||
type: 'number',
|
||||
defaultValue: 5,
|
||||
defined: (t: TuningConfig) => t.type === 'index_parallel',
|
||||
info: <>Retries for reporting the pushed segments in worker tasks.</>,
|
||||
},
|
||||
{
|
||||
name: 'handoffConditionTimeout',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: <>Milliseconds to wait for segment handoff. 0 means to wait forever.</>,
|
||||
},
|
||||
{
|
||||
name: 'resetOffsetAutomatically',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
Whether to reset the consumer offset if the next offset that it is trying to fetch is less
|
||||
|
@ -1779,7 +1795,7 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'workerThreads',
|
||||
type: 'number',
|
||||
placeholder: 'min(10, taskCount)',
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: (
|
||||
<>The number of threads that will be used by the supervisor for asynchronous operations.</>
|
||||
),
|
||||
|
@ -1788,14 +1804,14 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'chatThreads',
|
||||
type: 'number',
|
||||
placeholder: 'min(10, taskCount * replicas)',
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: <>The number of threads that will be used for communicating with indexing tasks.</>,
|
||||
},
|
||||
{
|
||||
name: 'chatRetries',
|
||||
type: 'number',
|
||||
defaultValue: 8,
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
The number of times HTTP requests to indexing tasks will be retried before considering tasks
|
||||
|
@ -1807,14 +1823,14 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'httpTimeout',
|
||||
type: 'duration',
|
||||
defaultValue: 'PT10S',
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: <>How long to wait for a HTTP response from an indexing task.</>,
|
||||
},
|
||||
{
|
||||
name: 'shutdownTimeout',
|
||||
type: 'duration',
|
||||
defaultValue: 'PT80S',
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka' || t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
How long to wait for the supervisor to attempt a graceful shutdown of tasks before exiting.
|
||||
|
@ -1825,7 +1841,7 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'offsetFetchPeriod',
|
||||
type: 'duration',
|
||||
defaultValue: 'PT30S',
|
||||
isDefined: (t: TuningConfig) => t.type === 'kafka',
|
||||
defined: (t: TuningConfig) => t.type === 'kafka',
|
||||
info: (
|
||||
<>
|
||||
How often the supervisor queries Kafka and the indexing tasks to fetch current offsets and
|
||||
|
@ -1837,7 +1853,7 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'recordBufferSize',
|
||||
type: 'number',
|
||||
defaultValue: 10000,
|
||||
isDefined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
Size of the buffer (number of events) used between the Kinesis fetch threads and the main
|
||||
|
@ -1849,7 +1865,7 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'recordBufferOfferTimeout',
|
||||
type: 'number',
|
||||
defaultValue: 5000,
|
||||
isDefined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
Length of time in milliseconds to wait for space to become available in the buffer before
|
||||
|
@ -1861,7 +1877,7 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'recordBufferFullWait',
|
||||
type: 'number',
|
||||
defaultValue: 5000,
|
||||
isDefined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
Length of time in milliseconds to wait for the buffer to drain before attempting to fetch
|
||||
|
@ -1873,7 +1889,7 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'fetchSequenceNumberTimeout',
|
||||
type: 'number',
|
||||
defaultValue: 60000,
|
||||
isDefined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
Length of time in milliseconds to wait for Kinesis to return the earliest or latest sequence
|
||||
|
@ -1887,7 +1903,7 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'fetchThreads',
|
||||
type: 'number',
|
||||
placeholder: 'max(1, {numProcessors} - 1)',
|
||||
isDefined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
Size of the pool of threads fetching data from Kinesis. There is no benefit in having more
|
||||
|
@ -1899,7 +1915,7 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
|
|||
name: 'maxRecordsPerPoll',
|
||||
type: 'number',
|
||||
defaultValue: 100,
|
||||
isDefined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
defined: (t: TuningConfig) => t.type === 'kinesis',
|
||||
info: (
|
||||
<>
|
||||
The maximum number of records/events to be fetched from buffer per poll. The actual maximum
|
||||
|
@ -2002,13 +2018,18 @@ function guessParseSpec(sampleData: string[]): ParseSpec | undefined {
|
|||
return parseSpecFromFormat('regex');
|
||||
}
|
||||
|
||||
function parseSpecFromFormat(format: string, hasHeaderRow: boolean | null = null): ParseSpec {
|
||||
function parseSpecFromFormat(format: string, hasHeaderRow?: boolean): ParseSpec {
|
||||
const parseSpec: ParseSpec = {
|
||||
format,
|
||||
timestampSpec: {},
|
||||
dimensionsSpec: {},
|
||||
};
|
||||
|
||||
if (format === 'regex') {
|
||||
parseSpec.pattern = '(.*)';
|
||||
parseSpec.columns = ['column1'];
|
||||
}
|
||||
|
||||
if (typeof hasHeaderRow === 'boolean') {
|
||||
parseSpec.hasHeaderRow = hasHeaderRow;
|
||||
}
|
||||
|
@ -2069,12 +2090,12 @@ const FILTER_FORM_FIELDS: Field<DruidFilter>[] = [
|
|||
{
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
isDefined: (druidFilter: DruidFilter) => druidFilter.type === 'selector',
|
||||
defined: (druidFilter: DruidFilter) => druidFilter.type === 'selector',
|
||||
},
|
||||
{
|
||||
name: 'values',
|
||||
type: 'string-array',
|
||||
isDefined: (druidFilter: DruidFilter) => druidFilter.type === 'in',
|
||||
defined: (druidFilter: DruidFilter) => druidFilter.type === 'in',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import axios from 'axios';
|
||||
|
||||
import { getDruidErrorMessage } from './druid-query';
|
||||
import { alphanumericCompare, sortWithPrefixSuffix } from './general';
|
||||
import { alphanumericCompare, filterMap, sortWithPrefixSuffix } from './general';
|
||||
import {
|
||||
DimensionsSpec,
|
||||
getEmptyTimestampSpec,
|
||||
|
@ -72,6 +72,12 @@ export interface HeaderAndRows {
|
|||
rows: SampleEntry[];
|
||||
}
|
||||
|
||||
export interface ExampleManifest {
|
||||
name: string;
|
||||
description: string;
|
||||
spec: any;
|
||||
}
|
||||
|
||||
function dedupe(xs: string[]): string[] {
|
||||
const seen: Record<string, boolean> = {};
|
||||
return xs.filter(x => {
|
||||
|
@ -521,3 +527,62 @@ export async function sampleForSchema(
|
|||
|
||||
return postToSampler(sampleSpec, 'schema');
|
||||
}
|
||||
|
||||
export async function sampleForExampleManifests(
|
||||
exampleManifestUrl: string,
|
||||
): Promise<ExampleManifest[]> {
|
||||
const sampleSpec: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
type: 'index',
|
||||
ioConfig: {
|
||||
type: 'index',
|
||||
firehose: { type: 'http', uris: [exampleManifestUrl] },
|
||||
},
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: {
|
||||
type: 'string',
|
||||
parseSpec: {
|
||||
format: 'tsv',
|
||||
timestampSpec: {
|
||||
column: 'timestamp',
|
||||
missingValue: '2010-01-01T00:00:00Z',
|
||||
},
|
||||
dimensionsSpec: {},
|
||||
hasHeaderRow: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
samplerConfig: { numRows: 50, timeoutMs: 10000 },
|
||||
};
|
||||
|
||||
const exampleData = await postToSampler(sampleSpec, 'example-manifest');
|
||||
|
||||
return filterMap(exampleData.data, datum => {
|
||||
const parsed = datum.parsed;
|
||||
if (!parsed) return;
|
||||
let { name, description, spec } = parsed;
|
||||
try {
|
||||
spec = JSON.parse(spec);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof name === 'string' &&
|
||||
typeof description === 'string' &&
|
||||
spec &&
|
||||
typeof spec === 'object'
|
||||
) {
|
||||
return {
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
spec,
|
||||
};
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -859,7 +859,13 @@ GROUP BY 1`;
|
|||
const { compaction } = row.original;
|
||||
let text: string;
|
||||
if (compaction) {
|
||||
text = `Target: ${formatBytes(compaction.targetCompactionSizeBytes)}`;
|
||||
if (compaction.targetCompactionSizeBytes == null) {
|
||||
text = `Target: Default (${formatBytes(
|
||||
CompactionDialog.DEFAULT_TARGET_COMPACTION_SIZE_BYTES,
|
||||
)})`;
|
||||
} else {
|
||||
text = `Target: ${formatBytes(compaction.targetCompactionSizeBytes)}`;
|
||||
}
|
||||
} else {
|
||||
text = 'None';
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={true}
|
||||
className="welcome"
|
||||
disabled={false}
|
||||
icon={false}
|
||||
key="welcome"
|
||||
onClick={[Function]}
|
||||
|
@ -30,6 +31,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="connect"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="connect"
|
||||
onClick={[Function]}
|
||||
|
@ -38,6 +40,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="parser"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="parser"
|
||||
onClick={[Function]}
|
||||
|
@ -46,6 +49,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="timestamp"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="timestamp"
|
||||
onClick={[Function]}
|
||||
|
@ -68,6 +72,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="transform"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="transform"
|
||||
onClick={[Function]}
|
||||
|
@ -76,6 +81,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="filter"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="filter"
|
||||
onClick={[Function]}
|
||||
|
@ -84,6 +90,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="schema"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="schema"
|
||||
onClick={[Function]}
|
||||
|
@ -106,6 +113,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="partition"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="partition"
|
||||
onClick={[Function]}
|
||||
|
@ -114,6 +122,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="tuning"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="tuning"
|
||||
onClick={[Function]}
|
||||
|
@ -122,6 +131,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="publish"
|
||||
disabled={true}
|
||||
icon={false}
|
||||
key="publish"
|
||||
onClick={[Function]}
|
||||
|
@ -144,6 +154,7 @@ exports[`load data view matches snapshot 1`] = `
|
|||
<Blueprint3.Button
|
||||
active={false}
|
||||
className="spec"
|
||||
disabled={false}
|
||||
icon="manually-entered-data"
|
||||
key="spec"
|
||||
onClick={[Function]}
|
||||
|
|
|
@ -150,6 +150,7 @@
|
|||
position: relative;
|
||||
margin: 0 5px;
|
||||
|
||||
.inline-data,
|
||||
.raw-lines {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
@ -267,4 +268,10 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.center-message {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@ import {
|
|||
hasParallelAbility,
|
||||
IngestionComboTypeWithExtra,
|
||||
IngestionSpec,
|
||||
invalidTuningConfig,
|
||||
IoConfig,
|
||||
isColumnTimestampSpec,
|
||||
isEmptyIngestionSpec,
|
||||
|
@ -117,11 +118,13 @@ import {
|
|||
} from '../../utils/ingestion-spec';
|
||||
import { deepDelete, deepGet, deepSet } from '../../utils/object-change';
|
||||
import {
|
||||
ExampleManifest,
|
||||
getOverlordModules,
|
||||
HeaderAndRows,
|
||||
headerAndRowsFromSampleResponse,
|
||||
SampleEntry,
|
||||
sampleForConnect,
|
||||
sampleForExampleManifests,
|
||||
sampleForFilter,
|
||||
sampleForParser,
|
||||
sampleForSchema,
|
||||
|
@ -231,6 +234,7 @@ const VIEW_TITLE: Record<Step, string> = {
|
|||
export interface LoadDataViewProps {
|
||||
initSupervisorId?: string;
|
||||
initTaskId?: string;
|
||||
exampleManifestsUrl?: string;
|
||||
goToTask: (taskId: string | undefined, supervisor?: string) => void;
|
||||
}
|
||||
|
||||
|
@ -248,6 +252,7 @@ export interface LoadDataViewState {
|
|||
// welcome
|
||||
overlordModules?: string[];
|
||||
selectedComboType?: IngestionComboTypeWithExtra;
|
||||
exampleManifests?: ExampleManifest[];
|
||||
|
||||
// general
|
||||
sampleStrategy: SampleStrategy;
|
||||
|
@ -385,6 +390,30 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
this.setState({ overlordModules });
|
||||
}
|
||||
|
||||
isStepEnabled(step: Step): boolean {
|
||||
const { spec } = this.state;
|
||||
const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || EMPTY_OBJECT;
|
||||
const parser: Parser = deepGet(spec, 'dataSchema.parser') || EMPTY_OBJECT;
|
||||
|
||||
switch (step) {
|
||||
case 'connect':
|
||||
return Boolean(spec.type);
|
||||
|
||||
case 'parser':
|
||||
case 'timestamp':
|
||||
case 'transform':
|
||||
case 'filter':
|
||||
case 'schema':
|
||||
case 'partition':
|
||||
case 'tuning':
|
||||
case 'publish':
|
||||
return Boolean(spec.type && !issueWithIoConfig(ioConfig) && !issueWithParser(parser));
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private updateStep = (newStep: Step) => {
|
||||
this.doQueryForStep(newStep);
|
||||
this.setState({ step: newStep });
|
||||
|
@ -392,16 +421,24 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
|
||||
doQueryForStep(step: Step): any {
|
||||
switch (step) {
|
||||
case 'welcome':
|
||||
return this.queryForWelcome();
|
||||
|
||||
case 'connect':
|
||||
return this.queryForConnect(true);
|
||||
|
||||
case 'parser':
|
||||
return this.queryForParser(true);
|
||||
|
||||
case 'timestamp':
|
||||
return this.queryForTimestamp(true);
|
||||
|
||||
case 'transform':
|
||||
return this.queryForTransform(true);
|
||||
|
||||
case 'filter':
|
||||
return this.queryForFilter(true);
|
||||
|
||||
case 'schema':
|
||||
return this.queryForSchema(true);
|
||||
}
|
||||
|
@ -485,6 +522,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
onClick={() => this.updateStep(s)}
|
||||
icon={s === 'spec' && IconNames.MANUALLY_ENTERED_DATA}
|
||||
text={VIEW_TITLE[s]}
|
||||
disabled={!this.isStepEnabled(s)}
|
||||
/>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
@ -530,11 +568,30 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
|
||||
// ==================================================================
|
||||
|
||||
renderIngestionCard(comboType: IngestionComboTypeWithExtra) {
|
||||
async queryForWelcome() {
|
||||
const { exampleManifestsUrl } = this.props;
|
||||
if (!exampleManifestsUrl) return;
|
||||
|
||||
let exampleManifests: ExampleManifest[] | undefined;
|
||||
try {
|
||||
exampleManifests = await sampleForExampleManifests(exampleManifestsUrl);
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
exampleManifests: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
exampleManifests,
|
||||
});
|
||||
}
|
||||
|
||||
renderIngestionCard(comboType: IngestionComboTypeWithExtra, disabled?: boolean) {
|
||||
const { overlordModules, selectedComboType } = this.state;
|
||||
if (!overlordModules) return null;
|
||||
const requiredModule = getRequiredModule(comboType);
|
||||
const goodToGo = !requiredModule || overlordModules.includes(requiredModule);
|
||||
const goodToGo = !disabled && (!requiredModule || overlordModules.includes(requiredModule));
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
@ -553,7 +610,9 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
}
|
||||
|
||||
renderWelcomeStep() {
|
||||
const { spec } = this.state;
|
||||
const { exampleManifestsUrl } = this.props;
|
||||
const { spec, exampleManifests } = this.state;
|
||||
const noExamples = Boolean(!exampleManifests || !exampleManifests.length);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -567,7 +626,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
{this.renderIngestionCard('index:http')}
|
||||
{this.renderIngestionCard('index:local')}
|
||||
{this.renderIngestionCard('index:inline')}
|
||||
{/* this.renderIngestionCard('example') */}
|
||||
{exampleManifestsUrl && this.renderIngestionCard('example', noExamples)}
|
||||
{this.renderIngestionCard('other')}
|
||||
</div>
|
||||
<div className="control">
|
||||
|
@ -590,7 +649,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
}
|
||||
|
||||
renderWelcomeStepMessage() {
|
||||
const { selectedComboType } = this.state;
|
||||
const { selectedComboType, exampleManifests } = this.state;
|
||||
|
||||
if (!selectedComboType) {
|
||||
return <p>Please specify where your raw data is located</p>;
|
||||
|
@ -676,7 +735,11 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
);
|
||||
|
||||
case 'example':
|
||||
return <p>Pick one of these examples to get you started.</p>;
|
||||
if (exampleManifests && exampleManifests.length) {
|
||||
return <p>Pick one of these examples to get you started.</p>;
|
||||
} else {
|
||||
return <p>Could not load example.</p>;
|
||||
}
|
||||
|
||||
case 'other':
|
||||
return (
|
||||
|
@ -696,7 +759,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
|
||||
renderWelcomeStepControls() {
|
||||
const { goToTask } = this.props;
|
||||
const { spec, selectedComboType } = this.state;
|
||||
const { spec, selectedComboType, exampleManifests } = this.state;
|
||||
|
||||
const issue = this.selectedIngestionTypeIssue();
|
||||
if (issue) return null;
|
||||
|
@ -715,13 +778,13 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<Button
|
||||
text="Connect data"
|
||||
rightIcon={IconNames.ARROW_RIGHT}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => {
|
||||
this.updateSpec(updateIngestionType(spec, selectedComboType as any));
|
||||
setTimeout(() => {
|
||||
this.updateStep('connect');
|
||||
}, 10);
|
||||
}}
|
||||
intent={Intent.PRIMARY}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
@ -732,14 +795,35 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<Button
|
||||
text="Submit task"
|
||||
rightIcon={IconNames.ARROW_RIGHT}
|
||||
onClick={() => goToTask(undefined, 'task')}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => goToTask(undefined, 'task')}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
case 'example':
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
{exampleManifests &&
|
||||
exampleManifests.map((exampleManifest, i) => (
|
||||
<FormGroup key={i}>
|
||||
<Button
|
||||
text={exampleManifest.name}
|
||||
rightIcon={IconNames.ARROW_RIGHT}
|
||||
intent={Intent.PRIMARY}
|
||||
title={exampleManifest.description}
|
||||
fill
|
||||
onClick={() => {
|
||||
this.updateSpec(exampleManifest.spec);
|
||||
setTimeout(() => {
|
||||
this.updateStep('connect');
|
||||
}, 10);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
case 'other':
|
||||
return (
|
||||
|
@ -748,16 +832,16 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<Button
|
||||
text="Submit supervisor"
|
||||
rightIcon={IconNames.ARROW_RIGHT}
|
||||
onClick={() => goToTask(undefined, 'supervisor')}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => goToTask(undefined, 'supervisor')}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Button
|
||||
text="Submit task"
|
||||
rightIcon={IconNames.ARROW_RIGHT}
|
||||
onClick={() => goToTask(undefined, 'task')}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => goToTask(undefined, 'task')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
|
@ -855,9 +939,22 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
const specType = getSpecType(spec);
|
||||
const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || EMPTY_OBJECT;
|
||||
const isBlank = !ioConfig.type;
|
||||
const inlineMode = deepGet(spec, 'ioConfig.firehose.type') === 'inline';
|
||||
|
||||
let mainFill: JSX.Element | string = '';
|
||||
if (inputQueryState.isInit()) {
|
||||
if (inlineMode) {
|
||||
mainFill = (
|
||||
<TextArea
|
||||
className="inline-data"
|
||||
placeholder="Paste your data here"
|
||||
value={deepGet(spec, 'ioConfig.firehose.data')}
|
||||
onChange={(e: any) => {
|
||||
const stringValue = e.target.value.substr(0, 65536);
|
||||
this.updateSpec(deepSet(spec, 'ioConfig.firehose.data', stringValue));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (inputQueryState.isInit()) {
|
||||
mainFill = (
|
||||
<CenterMessage>
|
||||
Please fill out the fields on the right sidebar to get started{' '}
|
||||
|
@ -899,11 +996,20 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
</ExternalLink>{' '}
|
||||
format that is optimized for analytic queries.
|
||||
</p>
|
||||
<p>
|
||||
To get started, please specify where your raw data is stored and what data you want to
|
||||
ingest.
|
||||
</p>
|
||||
<p>Click "Preview" to look at the sampled raw data.</p>
|
||||
{inlineMode ? (
|
||||
<>
|
||||
<p>To get started, please paste some data in the box to the left.</p>
|
||||
<p>Click "Register" to verify your data with Druid.</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
To get started, please specify where your raw data is stored and what data you
|
||||
want to ingest.
|
||||
</p>
|
||||
<p>Click "Preview" to look at the sampled raw data.</p>
|
||||
</>
|
||||
)}
|
||||
</Callout>
|
||||
{ingestionComboType ? (
|
||||
<AutoForm
|
||||
|
@ -938,7 +1044,11 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
</HTMLSelect>
|
||||
</FormGroup>
|
||||
)}
|
||||
<Button text="Preview" disabled={isBlank} onClick={() => this.queryForConnect()} />
|
||||
<Button
|
||||
text={inlineMode ? 'Register' : 'Preview'}
|
||||
disabled={isBlank}
|
||||
onClick={() => this.queryForConnect()}
|
||||
/>
|
||||
</div>
|
||||
{this.renderNextBar({
|
||||
disabled: !inputQueryState.data,
|
||||
|
@ -2433,7 +2543,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
name: 'segmentGranularity',
|
||||
type: 'string',
|
||||
suggestions: ['HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'],
|
||||
isDefined: (g: GranularitySpec) => g.type === 'uniform',
|
||||
defined: (g: GranularitySpec) => g.type === 'uniform',
|
||||
info: (
|
||||
<>
|
||||
The granularity to create time chunks at. Multiple segments can be created per
|
||||
|
@ -2463,7 +2573,9 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
</Callout>
|
||||
{this.renderParallelPickerIfNeeded()}
|
||||
</div>
|
||||
{this.renderNextBar({})}
|
||||
{this.renderNextBar({
|
||||
disabled: invalidTuningConfig(tuningConfig),
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2565,6 +2677,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
|
||||
renderPublishStep() {
|
||||
const { spec } = this.state;
|
||||
const parallel = isParallel(spec);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -2602,6 +2715,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
name: 'tuningConfig.logParseExceptions',
|
||||
label: 'Log parse exceptions',
|
||||
type: 'boolean',
|
||||
disabled: parallel,
|
||||
defaultValue: false,
|
||||
info: (
|
||||
<>
|
||||
|
@ -2614,6 +2728,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
name: 'tuningConfig.maxParseExceptions',
|
||||
label: 'Max parse exceptions',
|
||||
type: 'number',
|
||||
disabled: parallel,
|
||||
placeholder: '(unlimited)',
|
||||
info: (
|
||||
<>
|
||||
|
@ -2626,6 +2741,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
name: 'tuningConfig.maxSavedParseExceptions',
|
||||
label: 'Max saved parse exceptions',
|
||||
type: 'number',
|
||||
disabled: parallel,
|
||||
defaultValue: 0,
|
||||
info: (
|
||||
<>
|
||||
|
@ -2649,6 +2765,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<Callout className="intro">
|
||||
<p>Configure behavior of indexed data once it reaches Druid.</p>
|
||||
</Callout>
|
||||
{this.renderParallelPickerIfNeeded()}
|
||||
</div>
|
||||
{this.renderNextBar({})}
|
||||
</>
|
||||
|
|
|
@ -1,3 +1,64 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`number menu matches snapshot 1`] = `null`;
|
||||
exports[`number menu matches snapshot 1`] = `
|
||||
<li
|
||||
class="bp3-submenu"
|
||||
>
|
||||
<span
|
||||
class="bp3-popover-wrapper"
|
||||
>
|
||||
<span
|
||||
class="bp3-popover-target"
|
||||
>
|
||||
<a
|
||||
class="bp3-menu-item"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-filter"
|
||||
icon="filter"
|
||||
>
|
||||
<svg
|
||||
data-icon="filter"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<desc>
|
||||
filter
|
||||
</desc>
|
||||
<path
|
||||
d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 1.003 0 00-.71-1.71z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="bp3-text-overflow-ellipsis bp3-fill"
|
||||
>
|
||||
Filter
|
||||
</div>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-caret-right"
|
||||
icon="caret-right"
|
||||
>
|
||||
<svg
|
||||
data-icon="caret-right"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<desc>
|
||||
caret-right
|
||||
</desc>
|
||||
<path
|
||||
d="M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 006 4.5v7a.495.495 0 00.83.37l4-3.5c.1-.09.17-.22.17-.37z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
|
|
|
@ -17,11 +17,14 @@
|
|||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { sqlParserFactory } from 'druid-query-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import { NumberMenuItems } from './number-menu-items';
|
||||
|
||||
describe('number menu', () => {
|
||||
const parser = sqlParserFactory(['COUNT']);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
const numberMenu = (
|
||||
<NumberMenuItems
|
||||
|
@ -29,7 +32,8 @@ describe('number menu', () => {
|
|||
addToGroupBy={() => null}
|
||||
addAggregateColumn={() => null}
|
||||
filterByRow={() => null}
|
||||
columnName={'text'}
|
||||
columnName={'added'}
|
||||
queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,3 +1,64 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`string menu matches snapshot 1`] = `null`;
|
||||
exports[`string menu matches snapshot 1`] = `
|
||||
<li
|
||||
class="bp3-submenu"
|
||||
>
|
||||
<span
|
||||
class="bp3-popover-wrapper"
|
||||
>
|
||||
<span
|
||||
class="bp3-popover-target"
|
||||
>
|
||||
<a
|
||||
class="bp3-menu-item"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-filter"
|
||||
icon="filter"
|
||||
>
|
||||
<svg
|
||||
data-icon="filter"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<desc>
|
||||
filter
|
||||
</desc>
|
||||
<path
|
||||
d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 1.003 0 00-.71-1.71z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="bp3-text-overflow-ellipsis bp3-fill"
|
||||
>
|
||||
Filter
|
||||
</div>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-caret-right"
|
||||
icon="caret-right"
|
||||
>
|
||||
<svg
|
||||
data-icon="caret-right"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<desc>
|
||||
caret-right
|
||||
</desc>
|
||||
<path
|
||||
d="M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 006 4.5v7a.495.495 0 00.83.37l4-3.5c.1-.09.17-.22.17-.37z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
|
|
|
@ -17,11 +17,14 @@
|
|||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { sqlParserFactory } from 'druid-query-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import { StringMenuItems } from './string-menu-items';
|
||||
|
||||
describe('string menu', () => {
|
||||
const parser = sqlParserFactory(['COUNT']);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
const stringMenu = (
|
||||
<StringMenuItems
|
||||
|
@ -29,7 +32,8 @@ describe('string menu', () => {
|
|||
addToGroupBy={() => null}
|
||||
addAggregateColumn={() => null}
|
||||
filterByRow={() => null}
|
||||
columnName={'text'}
|
||||
columnName={'channel'}
|
||||
queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,3 +1,64 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`time menu matches snapshot 1`] = `null`;
|
||||
exports[`time menu matches snapshot 1`] = `
|
||||
<li
|
||||
class="bp3-submenu"
|
||||
>
|
||||
<span
|
||||
class="bp3-popover-wrapper"
|
||||
>
|
||||
<span
|
||||
class="bp3-popover-target"
|
||||
>
|
||||
<a
|
||||
class="bp3-menu-item"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-filter"
|
||||
icon="filter"
|
||||
>
|
||||
<svg
|
||||
data-icon="filter"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<desc>
|
||||
filter
|
||||
</desc>
|
||||
<path
|
||||
d="M13.99.99h-12a1.003 1.003 0 00-.71 1.71l4.71 4.71V14a1.003 1.003 0 001.71.71l2-2c.18-.18.29-.43.29-.71V7.41L14.7 2.7a1.003 1.003 0 00-.71-1.71z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="bp3-text-overflow-ellipsis bp3-fill"
|
||||
>
|
||||
Filter
|
||||
</div>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-caret-right"
|
||||
icon="caret-right"
|
||||
>
|
||||
<svg
|
||||
data-icon="caret-right"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
width="16"
|
||||
>
|
||||
<desc>
|
||||
caret-right
|
||||
</desc>
|
||||
<path
|
||||
d="M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 006 4.5v7a.495.495 0 00.83.37l4-3.5c.1-.09.17-.22.17-.37z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
|
|
|
@ -17,11 +17,14 @@
|
|||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { sqlParserFactory } from 'druid-query-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import { TimeMenuItems } from './time-menu-items';
|
||||
|
||||
describe('time menu', () => {
|
||||
const parser = sqlParserFactory(['COUNT']);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
const timeMenu = (
|
||||
<TimeMenuItems
|
||||
|
@ -30,7 +33,8 @@ describe('time menu', () => {
|
|||
addToGroupBy={() => null}
|
||||
addAggregateColumn={() => null}
|
||||
filterByRow={() => null}
|
||||
columnName={'text'}
|
||||
columnName={'__time'}
|
||||
queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ ORDER BY "Count" DESC`,
|
|||
|
||||
export interface ColumnTreeProps {
|
||||
columnMetadataLoading: boolean;
|
||||
columnMetadata?: ColumnMetadata[];
|
||||
columnMetadata?: readonly ColumnMetadata[];
|
||||
onQueryStringChange: (queryString: string, run: boolean) => void;
|
||||
defaultSchema?: string;
|
||||
defaultTable?: string;
|
||||
|
@ -141,7 +141,7 @@ export interface ColumnTreeProps {
|
|||
}
|
||||
|
||||
export interface ColumnTreeState {
|
||||
prevColumnMetadata?: ColumnMetadata[];
|
||||
prevColumnMetadata?: readonly ColumnMetadata[];
|
||||
prevGroupByStatus?: boolean;
|
||||
columnTree?: ITreeNode[];
|
||||
selectedTreeIndex: number;
|
||||
|
|
|
@ -36,7 +36,7 @@ export interface QueryInputProps {
|
|||
queryString: string;
|
||||
onQueryStringChange: (newQueryString: string) => void;
|
||||
runeMode: boolean;
|
||||
columnMetadata?: ColumnMetadata[];
|
||||
columnMetadata?: readonly ColumnMetadata[];
|
||||
currentSchema?: string;
|
||||
currentTable?: string;
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export interface QueryInputState {
|
|||
// Since this component will grown and shrink dynamically we will measure its height and then set it.
|
||||
editorHeight: number;
|
||||
completions: any[];
|
||||
prevColumnMetadata?: ColumnMetadata[];
|
||||
prevColumnMetadata?: readonly ColumnMetadata[];
|
||||
prevCurrentTable?: string;
|
||||
prevCurrentSchema?: string;
|
||||
}
|
||||
|
|
|
@ -58,7 +58,9 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
|
|||
data={queryResult ? queryResult.rows : []}
|
||||
loading={loading}
|
||||
noDataText={
|
||||
!loading && queryResult && !queryResult.rows.length ? 'No queryResults' : error || ''
|
||||
!loading && queryResult && !queryResult.rows.length
|
||||
? 'Query returned no data'
|
||||
: error || ''
|
||||
}
|
||||
sortable={false}
|
||||
columns={(queryResult ? queryResult.header : []).map((h: any, i) => {
|
||||
|
|
|
@ -70,8 +70,8 @@ import { RunButton } from './run-button/run-button';
|
|||
import './query-view.scss';
|
||||
|
||||
const parserRaw = sqlParserFactory(
|
||||
SQL_FUNCTIONS.map((sql_function: SyntaxDescription) => {
|
||||
return sql_function.syntax.substr(0, sql_function.syntax.indexOf('('));
|
||||
SQL_FUNCTIONS.map((sqlFunction: SyntaxDescription) => {
|
||||
return sqlFunction.syntax.substr(0, sqlFunction.syntax.indexOf('('));
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -107,7 +107,7 @@ export interface QueryViewState {
|
|||
autoRun: boolean;
|
||||
|
||||
columnMetadataLoading: boolean;
|
||||
columnMetadata?: ColumnMetadata[];
|
||||
columnMetadata?: readonly ColumnMetadata[];
|
||||
columnMetadataError?: string;
|
||||
|
||||
loading: boolean;
|
||||
|
@ -124,7 +124,7 @@ export interface QueryViewState {
|
|||
|
||||
editContextDialogOpen: boolean;
|
||||
historyDialogOpen: boolean;
|
||||
queryHistory: QueryRecord[];
|
||||
queryHistory: readonly QueryRecord[];
|
||||
}
|
||||
|
||||
interface QueryResult {
|
||||
|
@ -139,6 +139,10 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
return query.replace(/;+((?:\s*--[^\n]*)?\s*)$/, '$1');
|
||||
}
|
||||
|
||||
static isEmptyQuery(query: string): boolean {
|
||||
return query.trim() === '';
|
||||
}
|
||||
|
||||
static isExplainQuery(query: string): boolean {
|
||||
return /EXPLAIN\sPLAN\sFOR/i.test(query);
|
||||
}
|
||||
|
@ -456,7 +460,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
|
||||
return (
|
||||
<EditContextDialog
|
||||
onSubmit={(queryContext: QueryContext) =>
|
||||
onQueryContextChange={(queryContext: QueryContext) =>
|
||||
this.setState({ queryContext, editContextDialogOpen: false })
|
||||
}
|
||||
onClose={() => this.setState({ editContextDialogOpen: false })}
|
||||
|
@ -476,6 +480,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
autoRun,
|
||||
wrapQuery,
|
||||
} = this.state;
|
||||
const emptyQuery = QueryView.isEmptyQuery(queryString);
|
||||
|
||||
let currentSchema;
|
||||
let currentTable;
|
||||
|
@ -523,8 +528,8 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
runeMode={runeMode}
|
||||
queryContext={queryContext}
|
||||
onQueryContextChange={this.handleQueryContextChange}
|
||||
onRun={this.handleRun}
|
||||
onExplain={this.handleExplain}
|
||||
onRun={emptyQuery ? undefined : this.handleRun}
|
||||
onExplain={emptyQuery ? undefined : this.handleExplain}
|
||||
onHistory={() => this.setState({ historyDialogOpen: true })}
|
||||
/>
|
||||
{result && (
|
||||
|
@ -651,25 +656,14 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
|
|||
|
||||
private handleRun = () => {
|
||||
const { queryString, queryContext, wrapQuery, queryHistory } = this.state;
|
||||
|
||||
while (queryHistory.length > 9) {
|
||||
queryHistory.pop();
|
||||
}
|
||||
queryHistory.unshift({
|
||||
version: `${new Date().toISOString()}`,
|
||||
queryString,
|
||||
});
|
||||
let queryHistoryString;
|
||||
try {
|
||||
queryHistoryString = JSON.stringify(queryHistory);
|
||||
} catch {}
|
||||
if (queryHistoryString) {
|
||||
localStorageSet(LocalStorageKeys.QUERY_HISTORY, queryHistoryString);
|
||||
}
|
||||
|
||||
if (QueryView.isJsonLike(queryString) && !QueryView.validRune(queryString)) return;
|
||||
|
||||
const newQueryHistory = QueryHistoryDialog.addQueryToHistory(queryHistory, queryString);
|
||||
|
||||
localStorageSet(LocalStorageKeys.QUERY_HISTORY, JSON.stringify(newQueryHistory));
|
||||
localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
|
||||
|
||||
this.setState({ queryHistory: newQueryHistory });
|
||||
this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQuery });
|
||||
};
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@ export interface RunButtonProps {
|
|||
setWrapQuery: (wrapQuery: boolean) => void;
|
||||
queryContext: QueryContext;
|
||||
onQueryContextChange: (newQueryContext: QueryContext) => void;
|
||||
onRun: () => void;
|
||||
onExplain: () => void;
|
||||
onRun: (() => void) | undefined;
|
||||
onExplain: (() => void) | undefined;
|
||||
onEditContext: () => void;
|
||||
onHistory: () => void;
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
|
|||
/>
|
||||
{!runeMode && (
|
||||
<>
|
||||
<MenuItem icon={IconNames.CLEAN} text="Explain" onClick={onExplain} />
|
||||
{onExplain && <MenuItem icon={IconNames.CLEAN} text="Explain" onClick={onExplain} />}
|
||||
<MenuItem icon={IconNames.HISTORY} text="History" onClick={onHistory} />
|
||||
<MenuCheckbox
|
||||
checked={wrapQuery}
|
||||
|
@ -160,17 +160,17 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
|
|||
|
||||
render(): JSX.Element {
|
||||
const { runeMode, onRun, wrapQuery } = this.props;
|
||||
const runButtonText = runeMode ? 'Rune' : wrapQuery ? 'Run with limit' : 'Run as is';
|
||||
|
||||
return (
|
||||
<ButtonGroup className="run-button">
|
||||
<Tooltip content="Control + Enter" hoverOpenDelay={900}>
|
||||
<Button
|
||||
icon={IconNames.CARET_RIGHT}
|
||||
onClick={this.handleRun}
|
||||
text={runeMode ? 'Rune' : wrapQuery ? 'Run with limit' : 'Run as is'}
|
||||
disabled={!onRun}
|
||||
/>
|
||||
</Tooltip>
|
||||
{onRun ? (
|
||||
<Tooltip content="Control + Enter" hoverOpenDelay={900}>
|
||||
<Button icon={IconNames.CARET_RIGHT} onClick={this.handleRun} text={runButtonText} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button icon={IconNames.CARET_RIGHT} text={runButtonText} disabled />
|
||||
)}
|
||||
<Popover position={Position.BOTTOM_LEFT} content={this.renderExtraMenu()}>
|
||||
<Button icon={IconNames.MORE} />
|
||||
</Popover>
|
||||
|
|
Loading…
Reference in New Issue