mirror of https://github.com/apache/druid.git
Web console: Better data loader flow (#8763)
* filter table * go over the entire data loader flow
This commit is contained in:
parent
b65d2ac648
commit
ec8ce74f1c
|
@ -79,8 +79,6 @@ writeFile(
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import './${name}.scss';
|
||||
|
@ -88,22 +86,13 @@ import './${name}.scss';
|
|||
export interface ${camelName}Props {
|
||||
}
|
||||
|
||||
export interface ${camelName}State {
|
||||
}
|
||||
|
||||
export class ${camelName} extends React.PureComponent<${camelName}Props, ${camelName}State> {
|
||||
constructor(props: ${camelName}Props, context: any) {
|
||||
super(props, context);
|
||||
// this.state = {};
|
||||
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return <div className="${name}">
|
||||
export const ${camelName} = React.memo(function ${camelName}(props: ${camelName}Props) {
|
||||
return (
|
||||
<div className="${name}">
|
||||
Stuff...
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import { Intent, TextArea } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { compact } from '../../utils';
|
||||
|
||||
|
@ -31,40 +31,33 @@ export interface ArrayInputProps {
|
|||
intent?: Intent;
|
||||
}
|
||||
|
||||
export class ArrayInput extends React.PureComponent<ArrayInputProps, { stringValue: string }> {
|
||||
constructor(props: ArrayInputProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
stringValue: Array.isArray(props.values) ? props.values.join(', ') : '',
|
||||
};
|
||||
}
|
||||
export const ArrayInput = React.memo(function ArrayInput(props: ArrayInputProps) {
|
||||
const { className, placeholder, large, disabled, intent } = props;
|
||||
const [stringValue, setStringValue] = useState();
|
||||
|
||||
private handleChange = (e: any) => {
|
||||
const { onChange } = this.props;
|
||||
const handleChange = (e: any) => {
|
||||
const { onChange } = props;
|
||||
const stringValue = e.target.value;
|
||||
const newValues: string[] = stringValue.split(',').map((v: string) => v.trim());
|
||||
const newValues: string[] = stringValue.split(/[,\s]+/).map((v: string) => v.trim());
|
||||
const newValuesFiltered = compact(newValues);
|
||||
this.setState({
|
||||
stringValue:
|
||||
newValues.length === newValuesFiltered.length ? newValues.join(', ') : stringValue,
|
||||
});
|
||||
if (onChange) onChange(stringValue === '' ? undefined : newValuesFiltered);
|
||||
if (newValues.length === newValuesFiltered.length) {
|
||||
onChange(stringValue === '' ? undefined : newValuesFiltered);
|
||||
setStringValue(undefined);
|
||||
} else {
|
||||
setStringValue(stringValue);
|
||||
}
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const { className, placeholder, large, disabled, intent } = this.props;
|
||||
const { stringValue } = this.state;
|
||||
return (
|
||||
<TextArea
|
||||
className={className}
|
||||
value={stringValue}
|
||||
onChange={this.handleChange}
|
||||
placeholder={placeholder}
|
||||
large={large}
|
||||
disabled={disabled}
|
||||
intent={intent}
|
||||
fill
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<TextArea
|
||||
className={className}
|
||||
value={stringValue || props.values.join(', ')}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
large={large}
|
||||
disabled={disabled}
|
||||
intent={intent}
|
||||
fill
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -81,7 +81,7 @@ function ingestionTypeToIoAndTuningConfigType(ingestionType: IngestionType): str
|
|||
}
|
||||
}
|
||||
|
||||
export function getIngestionComboType(spec: IngestionSpec): IngestionComboType | null {
|
||||
export function getIngestionComboType(spec: IngestionSpec): IngestionComboType | undefined {
|
||||
const ioConfig = deepGet(spec, 'ioConfig') || EMPTY_OBJECT;
|
||||
|
||||
switch (ioConfig.type) {
|
||||
|
@ -103,7 +103,7 @@ export function getIngestionComboType(spec: IngestionSpec): IngestionComboType |
|
|||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
export function getIngestionTitle(ingestionType: IngestionComboTypeWithExtra): string {
|
||||
|
@ -152,6 +152,21 @@ export function getIngestionImage(ingestionType: IngestionComboTypeWithExtra): s
|
|||
return ingestionType;
|
||||
}
|
||||
|
||||
export function getIngestionDocLink(spec: IngestionSpec): string {
|
||||
const type = getSpecType(spec);
|
||||
|
||||
switch (type) {
|
||||
case 'kafka':
|
||||
return 'https://druid.apache.org/docs/latest/development/extensions-core/kafka-ingestion.html';
|
||||
|
||||
case 'kinesis':
|
||||
return 'https://druid.apache.org/docs/latest/development/extensions-core/kinesis-ingestion.html';
|
||||
|
||||
default:
|
||||
return 'https://druid.apache.org/docs/latest/ingestion/native-batch.html#firehoses';
|
||||
}
|
||||
}
|
||||
|
||||
export function getRequiredModule(ingestionType: IngestionComboTypeWithExtra): string | undefined {
|
||||
switch (ingestionType) {
|
||||
case 'index:static-s3':
|
||||
|
@ -326,6 +341,8 @@ const PARSE_SPEC_FORM_FIELDS: Field<ParseSpec>[] = [
|
|||
{
|
||||
name: 'columns',
|
||||
type: 'string-array',
|
||||
required: (p: ParseSpec) =>
|
||||
((p.format === 'csv' || p.format === 'tsv') && !p.hasHeaderRow) || p.format === 'regex',
|
||||
defined: (p: ParseSpec) =>
|
||||
((p.format === 'csv' || p.format === 'tsv') && !p.hasHeaderRow) || p.format === 'regex',
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { deepDelete, deepGet, deepSet, makePath, parsePath } from './object-change';
|
||||
import { deepDelete, deepExtend, deepGet, deepSet, makePath, parsePath } from './object-change';
|
||||
|
||||
describe('object-change', () => {
|
||||
describe('parsePath', () => {
|
||||
|
@ -149,4 +149,43 @@ describe('object-change', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deepExtend', () => {
|
||||
it('works', () => {
|
||||
const obj1 = {
|
||||
money: 1,
|
||||
bag: 2,
|
||||
nice: {
|
||||
a: 1,
|
||||
b: [],
|
||||
c: { an: 123, ice: 321, bag: 1 },
|
||||
},
|
||||
swag: {
|
||||
diamond: ['collar'],
|
||||
},
|
||||
pockets: { ice: 3 },
|
||||
f: ['bag'],
|
||||
};
|
||||
|
||||
const obj2 = {
|
||||
bag: 3,
|
||||
nice: null,
|
||||
pockets: { need: 1, an: 2 },
|
||||
swag: {
|
||||
diamond: ['collar', 'molar'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(deepExtend(obj1, obj2)).toEqual({
|
||||
money: 1,
|
||||
bag: 3,
|
||||
nice: null,
|
||||
swag: {
|
||||
diamond: ['collar', 'molar'],
|
||||
},
|
||||
pockets: { need: 1, an: 2, ice: 3 },
|
||||
f: ['bag'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,10 +20,14 @@ export function shallowCopy(v: any): any {
|
|||
return Array.isArray(v) ? v.slice() : Object.assign({}, v);
|
||||
}
|
||||
|
||||
function isEmpty(v: any): boolean {
|
||||
export function isEmpty(v: any): boolean {
|
||||
return !(Array.isArray(v) ? v.length : Object.keys(v).length);
|
||||
}
|
||||
|
||||
function isObjectOrArray(v: any): boolean {
|
||||
return Boolean(v && typeof v === 'object');
|
||||
}
|
||||
|
||||
export function parsePath(path: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let rest = path;
|
||||
|
@ -107,6 +111,28 @@ export function deepDelete<T extends Record<string, any>>(value: T, path: string
|
|||
return valueCopy;
|
||||
}
|
||||
|
||||
export function deepExtend<T extends Record<string, any>>(target: T, diff: Record<string, any>): T {
|
||||
if (typeof target !== 'object') throw new TypeError(`Invalid target`);
|
||||
if (typeof diff !== 'object') throw new TypeError(`Invalid diff`);
|
||||
|
||||
const newValue = shallowCopy(target);
|
||||
for (const key in diff) {
|
||||
const targetValue = target[key];
|
||||
const diffValue = diff[key];
|
||||
if (typeof diffValue === 'undefined') {
|
||||
delete newValue[key];
|
||||
} else {
|
||||
if (isObjectOrArray(targetValue) && isObjectOrArray(diffValue)) {
|
||||
newValue[key] = deepExtend(targetValue, diffValue);
|
||||
} else {
|
||||
newValue[key] = diffValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
export function whitelistKeys(obj: Record<string, any>, whitelist: string[]): Record<string, any> {
|
||||
const newObj: Record<string, any> = {};
|
||||
for (const w of whitelist) {
|
||||
|
|
|
@ -38,7 +38,7 @@ describe('filter table', () => {
|
|||
sampleData={sampleData}
|
||||
columnFilter=""
|
||||
dimensionFilters={[]}
|
||||
selectedFilterIndex={-1}
|
||||
selectedFilterName={undefined}
|
||||
onShowGlobalFilter={() => {}}
|
||||
onFilterSelect={() => {}}
|
||||
/>
|
||||
|
|
|
@ -27,11 +27,21 @@ import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
|
|||
|
||||
import './filter-table.scss';
|
||||
|
||||
export function filterTableSelectedColumnName(
|
||||
sampleData: HeaderAndRows,
|
||||
selectedFilter: DruidFilter | undefined,
|
||||
): string | undefined {
|
||||
if (!selectedFilter) return;
|
||||
const selectedFilterName = selectedFilter.dimension;
|
||||
if (!sampleData.header.includes(selectedFilterName)) return;
|
||||
return selectedFilterName;
|
||||
}
|
||||
|
||||
export interface FilterTableProps {
|
||||
sampleData: HeaderAndRows;
|
||||
columnFilter: string;
|
||||
dimensionFilters: DruidFilter[];
|
||||
selectedFilterIndex: number;
|
||||
selectedFilterName: string | undefined;
|
||||
onShowGlobalFilter: () => void;
|
||||
onFilterSelect: (filter: DruidFilter, index: number) => void;
|
||||
}
|
||||
|
@ -41,7 +51,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro
|
|||
sampleData,
|
||||
columnFilter,
|
||||
dimensionFilters,
|
||||
selectedFilterIndex,
|
||||
selectedFilterName,
|
||||
onShowGlobalFilter,
|
||||
onFilterSelect,
|
||||
} = props;
|
||||
|
@ -58,7 +68,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro
|
|||
|
||||
const columnClassName = classNames({
|
||||
filtered: filter,
|
||||
selected: filter && filterIndex === selectedFilterIndex,
|
||||
selected: columnName === selectedFilterName,
|
||||
});
|
||||
return {
|
||||
Header: (
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`learn more matches snapshot 1`] = `
|
||||
<p
|
||||
class="learn-more"
|
||||
>
|
||||
<a
|
||||
href="https://druid.apache.org/docs/latest/development/extensions-core/kinesis-ingestion.html"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
`;
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { LearnMore } from './learn-more';
|
||||
|
||||
describe('learn more', () => {
|
||||
it('matches snapshot', () => {
|
||||
const learnMore = (
|
||||
<LearnMore href="https://druid.apache.org/docs/latest/development/extensions-core/kinesis-ingestion.html" />
|
||||
);
|
||||
|
||||
const { container } = render(learnMore);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ExternalLink } from '../../../components';
|
||||
|
||||
export interface LearnMoreProps {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export const LearnMore = React.memo(function LearnMore(props: LearnMoreProps) {
|
||||
const { href } = props;
|
||||
|
||||
return (
|
||||
<p className="learn-more">
|
||||
<ExternalLink href={href}>Learn more</ExternalLink>
|
||||
</p>
|
||||
);
|
||||
});
|
|
@ -206,12 +206,20 @@
|
|||
&.timestamp {
|
||||
background: rgba(19, 129, 201, 0.5);
|
||||
}
|
||||
|
||||
&.used {
|
||||
background: rgba(24, 201, 201, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.rt-td {
|
||||
&.timestamp {
|
||||
background: rgba(19, 129, 201, 0.15);
|
||||
}
|
||||
|
||||
&.used {
|
||||
background: rgba(24, 201, 201, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -235,6 +243,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.apply-button-bar {
|
||||
.revert {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.next-bar {
|
||||
grid-area: next;
|
||||
text-align: right;
|
||||
|
@ -255,7 +269,7 @@
|
|||
border-radius: 2px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.controls-buttons {
|
||||
.control-buttons {
|
||||
position: relative;
|
||||
|
||||
.bp3-button {
|
||||
|
@ -265,6 +279,7 @@
|
|||
.cancel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ import {
|
|||
getFilterFormFields,
|
||||
getFlattenFieldFormFields,
|
||||
getIngestionComboType,
|
||||
getIngestionDocLink,
|
||||
getIngestionImage,
|
||||
getIngestionTitle,
|
||||
getIoConfigFormFields,
|
||||
|
@ -141,9 +142,13 @@ import {
|
|||
import { computeFlattenPathsForData } from '../../utils/spec-utils';
|
||||
|
||||
import { ExamplePicker } from './example-picker/example-picker';
|
||||
import { FilterTable } from './filter-table/filter-table';
|
||||
import { FilterTable, filterTableSelectedColumnName } from './filter-table/filter-table';
|
||||
import { LearnMore } from './learn-more/learn-more';
|
||||
import { ParseDataTable } from './parse-data-table/parse-data-table';
|
||||
import { ParseTimeTable } from './parse-time-table/parse-time-table';
|
||||
import {
|
||||
ParseTimeTable,
|
||||
parseTimeTableSelectedColumnName,
|
||||
} from './parse-time-table/parse-time-table';
|
||||
import { SchemaTable } from './schema-table/schema-table';
|
||||
import {
|
||||
TransformTable,
|
||||
|
@ -250,6 +255,7 @@ export interface LoadDataViewProps {
|
|||
export interface LoadDataViewState {
|
||||
step: Step;
|
||||
spec: IngestionSpec;
|
||||
specPreview: IngestionSpec;
|
||||
cacheKey?: string;
|
||||
// dialogs / modals
|
||||
continueToSpec: boolean;
|
||||
|
@ -318,6 +324,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
this.state = {
|
||||
step: 'welcome',
|
||||
spec,
|
||||
specPreview: spec,
|
||||
|
||||
// dialogs / modals
|
||||
showResetConfirm: false,
|
||||
|
@ -422,41 +429,70 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
}
|
||||
|
||||
private updateStep = (newStep: Step) => {
|
||||
this.doQueryForStep(newStep);
|
||||
this.setState({ step: newStep });
|
||||
this.setState(state => ({ step: newStep, specPreview: state.spec }));
|
||||
};
|
||||
|
||||
doQueryForStep(step: Step): any {
|
||||
private updateSpec = (newSpec: IngestionSpec) => {
|
||||
newSpec = normalizeSpec(newSpec);
|
||||
this.setState({ spec: newSpec, specPreview: newSpec });
|
||||
localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSON.stringify(newSpec));
|
||||
};
|
||||
|
||||
private updateSpecPreview = (newSpecPreview: IngestionSpec) => {
|
||||
this.setState({ specPreview: newSpecPreview });
|
||||
};
|
||||
|
||||
private applyPreviewSpec = () => {
|
||||
this.setState(state => {
|
||||
localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSON.stringify(state.specPreview));
|
||||
return { spec: state.specPreview };
|
||||
});
|
||||
};
|
||||
|
||||
private revertPreviewSpec = () => {
|
||||
this.setState(state => ({ specPreview: state.spec }));
|
||||
};
|
||||
|
||||
isPreviewSpecSame() {
|
||||
const { spec, specPreview } = this.state;
|
||||
return spec === specPreview;
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: LoadDataViewProps, prevState: LoadDataViewState) {
|
||||
const { spec, step } = this.state;
|
||||
const { spec: prevSpec, step: prevStep } = prevState;
|
||||
if (spec !== prevSpec || step !== prevStep) {
|
||||
this.doQueryForStep(step !== prevStep);
|
||||
}
|
||||
}
|
||||
|
||||
doQueryForStep(initRun: boolean): any {
|
||||
const { step } = this.state;
|
||||
|
||||
switch (step) {
|
||||
case 'welcome':
|
||||
return this.queryForWelcome();
|
||||
|
||||
case 'connect':
|
||||
return this.queryForConnect(true);
|
||||
return this.queryForConnect(initRun);
|
||||
|
||||
case 'parser':
|
||||
return this.queryForParser(true);
|
||||
return this.queryForParser(initRun);
|
||||
|
||||
case 'timestamp':
|
||||
return this.queryForTimestamp(true);
|
||||
return this.queryForTimestamp(initRun);
|
||||
|
||||
case 'transform':
|
||||
return this.queryForTransform(true);
|
||||
return this.queryForTransform(initRun);
|
||||
|
||||
case 'filter':
|
||||
return this.queryForFilter(true);
|
||||
return this.queryForFilter(initRun);
|
||||
|
||||
case 'schema':
|
||||
return this.queryForSchema(true);
|
||||
return this.queryForSchema(initRun);
|
||||
}
|
||||
}
|
||||
|
||||
private updateSpec = (newSpec: IngestionSpec) => {
|
||||
newSpec = normalizeSpec(newSpec);
|
||||
this.setState({ spec: newSpec });
|
||||
localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSON.stringify(newSpec));
|
||||
};
|
||||
|
||||
renderActionCard(icon: IconName, title: string, caption: string, onClick: () => void) {
|
||||
return (
|
||||
<Card className={'spec-card'} interactive onClick={onClick}>
|
||||
|
@ -515,6 +551,29 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
);
|
||||
}
|
||||
|
||||
renderApplyButtonBar() {
|
||||
const previewSpecSame = this.isPreviewSpecSame();
|
||||
|
||||
return (
|
||||
<FormGroup className="apply-button-bar">
|
||||
<Button
|
||||
text="Apply"
|
||||
disabled={previewSpecSame}
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={this.applyPreviewSpec}
|
||||
/>
|
||||
{!previewSpecSame && (
|
||||
<Button
|
||||
className="revert"
|
||||
icon={IconNames.UNDO}
|
||||
disabled={this.isPreviewSpecSame()}
|
||||
onClick={this.revertPreviewSpec}
|
||||
/>
|
||||
)}
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderStepNav() {
|
||||
const { step } = this.state;
|
||||
|
||||
|
@ -567,9 +626,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
if (disabled) return;
|
||||
if (onNextStep) onNextStep();
|
||||
|
||||
setTimeout(() => {
|
||||
this.updateStep(nextStep);
|
||||
}, 10);
|
||||
this.updateStep(nextStep);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -787,9 +844,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
intent={Intent.PRIMARY}
|
||||
onClick={() => {
|
||||
this.updateSpec(updateIngestionType(spec, selectedComboType as any));
|
||||
setTimeout(() => {
|
||||
this.updateStep('connect');
|
||||
}, 10);
|
||||
this.updateStep('connect');
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -814,9 +869,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
exampleManifests={exampleManifests}
|
||||
onSelectExample={exampleManifest => {
|
||||
this.updateSpec(exampleManifest.spec);
|
||||
setTimeout(() => {
|
||||
this.updateStep('connect');
|
||||
}, 10);
|
||||
this.updateStep('connect');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -949,10 +1002,9 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
}
|
||||
|
||||
renderConnectStep() {
|
||||
const { spec, inputQueryState, sampleStrategy } = this.state;
|
||||
const { specPreview: spec, inputQueryState, sampleStrategy } = this.state;
|
||||
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 = '';
|
||||
|
@ -964,7 +1016,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
value={deepGet(spec, 'ioConfig.firehose.data')}
|
||||
onChange={(e: any) => {
|
||||
const stringValue = e.target.value.substr(0, MAX_INLINE_DATA_LENGTH);
|
||||
this.updateSpec(deepSet(spec, 'ioConfig.firehose.data', stringValue));
|
||||
this.updateSpecPreview(deepSet(spec, 'ioConfig.firehose.data', stringValue));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -1006,36 +1058,31 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<p>
|
||||
Druid ingests raw data and converts it into a custom,{' '}
|
||||
<ExternalLink href="https://druid.apache.org/docs/latest/design/segments.html">
|
||||
indexed
|
||||
indexed format
|
||||
</ExternalLink>{' '}
|
||||
format that is optimized for analytic queries.
|
||||
that is optimized for analytic queries.
|
||||
</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>Click "Apply" 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>
|
||||
</>
|
||||
<p>To get started, please specify what data you want to ingest.</p>
|
||||
)}
|
||||
<LearnMore href={getIngestionDocLink(spec)} />
|
||||
</Callout>
|
||||
{ingestionComboType ? (
|
||||
<AutoForm
|
||||
fields={getIoConfigFormFields(ingestionComboType)}
|
||||
model={ioConfig}
|
||||
onChange={c => this.updateSpec(deepSet(spec, 'ioConfig', c))}
|
||||
onChange={c => this.updateSpecPreview(deepSet(spec, 'ioConfig', c))}
|
||||
/>
|
||||
) : (
|
||||
<FormGroup label="IO Config">
|
||||
<JsonInput
|
||||
value={ioConfig}
|
||||
onChange={c => this.updateSpec(deepSet(spec, 'ioConfig', c))}
|
||||
onChange={c => this.updateSpecPreview(deepSet(spec, 'ioConfig', c))}
|
||||
height="300px"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -1058,12 +1105,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
</HTMLSelect>
|
||||
</FormGroup>
|
||||
)}
|
||||
<Button
|
||||
text={inlineMode ? 'Register data' : 'Preview'}
|
||||
disabled={isBlank}
|
||||
intent={inputQueryState.data ? undefined : Intent.PRIMARY}
|
||||
onClick={() => this.queryForConnect()}
|
||||
/>
|
||||
{this.renderApplyButtonBar()}
|
||||
</div>
|
||||
{this.renderNextBar({
|
||||
disabled: !inputQueryState.data,
|
||||
|
@ -1176,7 +1218,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
|
||||
renderParserStep() {
|
||||
const {
|
||||
spec,
|
||||
specPreview: spec,
|
||||
columnFilter,
|
||||
specialColumnsOnly,
|
||||
parserQueryState,
|
||||
|
@ -1186,7 +1228,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
const flattenFields: FlattenField[] =
|
||||
deepGet(spec, 'dataSchema.parser.parseSpec.flattenSpec.fields') || EMPTY_ARRAY;
|
||||
|
||||
const isBlank = !parseSpec.format;
|
||||
const canFlatten = parseSpec.format === 'json';
|
||||
|
||||
let mainFill: JSX.Element | string = '';
|
||||
|
@ -1251,23 +1292,28 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
{canFlatten && (
|
||||
<p>
|
||||
If you have nested data, you can{' '}
|
||||
<ExternalLink href="https://druid.apache.org/docs/latest/ingestion/flatten-json.html">
|
||||
<ExternalLink href="https://druid.apache.org/docs/latest/ingestion/index.html#flattenspec">
|
||||
flatten
|
||||
</ExternalLink>{' '}
|
||||
it here. If the provided flattening capabilities are not sufficient, please
|
||||
pre-process your data before ingesting it into Druid.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
Click "Preview" to ensure that your data appears correctly in a row/column
|
||||
orientation.
|
||||
</p>
|
||||
<p>Ensure that your data appears correctly in a row/column orientation.</p>
|
||||
<LearnMore href="https://druid.apache.org/docs/latest/ingestion/data-formats.html" />
|
||||
</Callout>
|
||||
<AutoForm
|
||||
fields={getParseSpecFormFields()}
|
||||
model={parseSpec}
|
||||
onChange={p => this.updateSpec(deepSet(spec, 'dataSchema.parser.parseSpec', p))}
|
||||
/>
|
||||
{!selectedFlattenField && (
|
||||
<>
|
||||
<AutoForm
|
||||
fields={getParseSpecFormFields()}
|
||||
model={parseSpec}
|
||||
onChange={p =>
|
||||
this.updateSpecPreview(deepSet(spec, 'dataSchema.parser.parseSpec', p))
|
||||
}
|
||||
/>
|
||||
{this.renderApplyButtonBar()}
|
||||
</>
|
||||
)}
|
||||
{this.renderFlattenControls()}
|
||||
{Boolean(sugestedFlattenFields && sugestedFlattenFields.length) && (
|
||||
<FormGroup>
|
||||
|
@ -1282,16 +1328,10 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
sugestedFlattenFields,
|
||||
),
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.queryForParser();
|
||||
}, 10);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{!selectedFlattenField && (
|
||||
<Button text="Preview" disabled={isBlank} onClick={() => this.queryForParser()} />
|
||||
)}
|
||||
</div>
|
||||
{this.renderNextBar({
|
||||
disabled: !parserQueryState.data,
|
||||
|
@ -1340,13 +1380,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
});
|
||||
};
|
||||
|
||||
const closeAndQuery = () => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
this.queryForParser();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
if (selectedFlattenField) {
|
||||
return (
|
||||
<div className="edit-controls">
|
||||
|
@ -1355,7 +1388,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
model={selectedFlattenField}
|
||||
onChange={f => this.setState({ selectedFlattenField: f })}
|
||||
/>
|
||||
<div className="controls-buttons">
|
||||
<div className="control-buttons">
|
||||
<Button
|
||||
className="add-update"
|
||||
text={selectedFlattenFieldIndex === -1 ? 'Add' : 'Update'}
|
||||
|
@ -1368,7 +1401,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
selectedFlattenField,
|
||||
),
|
||||
);
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{selectedFlattenFieldIndex !== -1 && (
|
||||
|
@ -1382,7 +1415,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
`dataSchema.parser.parseSpec.flattenSpec.fields.${selectedFlattenFieldIndex}`,
|
||||
),
|
||||
);
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -1467,14 +1500,11 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
}
|
||||
|
||||
renderTimestampStep() {
|
||||
const { spec, columnFilter, specialColumnsOnly, timestampQueryState } = this.state;
|
||||
const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || EMPTY_OBJECT;
|
||||
const { specPreview: spec, columnFilter, specialColumnsOnly, timestampQueryState } = this.state;
|
||||
const timestampSpec: TimestampSpec =
|
||||
deepGet(spec, 'dataSchema.parser.parseSpec.timestampSpec') || EMPTY_OBJECT;
|
||||
const timestampSpecFromColumn = isColumnTimestampSpec(timestampSpec);
|
||||
|
||||
const isBlank = !parseSpec.format;
|
||||
|
||||
let mainFill: JSX.Element | string = '';
|
||||
if (timestampQueryState.isInit()) {
|
||||
mainFill = (
|
||||
|
@ -1506,6 +1536,10 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
sampleBundle={timestampQueryState.data}
|
||||
columnFilter={columnFilter}
|
||||
possibleTimestampColumnsOnly={specialColumnsOnly}
|
||||
selectedColumnName={parseTimeTableSelectedColumnName(
|
||||
timestampQueryState.data.headerAndRows,
|
||||
timestampSpec,
|
||||
)}
|
||||
onTimestampColumnSelect={this.onTimestampColumnSelect}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1520,10 +1554,10 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<p>
|
||||
Druid partitions data based on the primary time column of your data. This column is
|
||||
stored internally in Druid as <Code>__time</Code>. Please specify the primary time
|
||||
column. If you do not have any time columns, you can choose "Constant Value" to create
|
||||
column. If you do not have any time columns, you can choose "Constant value" to create
|
||||
a default one.
|
||||
</p>
|
||||
<p>Click "Preview" to check if Druid can properly parse your time values.</p>
|
||||
<LearnMore href="https://druid.apache.org/docs/latest/ingestion/index.html#timestampspec" />
|
||||
</Callout>
|
||||
<FormGroup label="Timestamp spec">
|
||||
<ButtonGroup>
|
||||
|
@ -1535,28 +1569,22 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
column: 'timestamp',
|
||||
format: 'auto',
|
||||
};
|
||||
this.updateSpec(
|
||||
this.updateSpecPreview(
|
||||
deepSet(spec, 'dataSchema.parser.parseSpec.timestampSpec', timestampSpec),
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.queryForTimestamp();
|
||||
}, 10);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
text="Constant value"
|
||||
active={!timestampSpecFromColumn}
|
||||
onClick={() => {
|
||||
this.updateSpec(
|
||||
this.updateSpecPreview(
|
||||
deepSet(
|
||||
spec,
|
||||
'dataSchema.parser.parseSpec.timestampSpec',
|
||||
getEmptyTimestampSpec(),
|
||||
),
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.queryForTimestamp();
|
||||
}, 10);
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
@ -1565,12 +1593,12 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
fields={getTimestampSpecFormFields(timestampSpec)}
|
||||
model={timestampSpec}
|
||||
onChange={timestampSpec => {
|
||||
this.updateSpec(
|
||||
this.updateSpecPreview(
|
||||
deepSet(spec, 'dataSchema.parser.parseSpec.timestampSpec', timestampSpec),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Button text="Preview" disabled={isBlank} onClick={() => this.queryForTimestamp()} />
|
||||
{this.renderApplyButtonBar()}
|
||||
</div>
|
||||
{this.renderNextBar({
|
||||
disabled: !timestampQueryState.data,
|
||||
|
@ -1580,8 +1608,10 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
}
|
||||
|
||||
private onTimestampColumnSelect = (newTimestampSpec: TimestampSpec) => {
|
||||
const { spec } = this.state;
|
||||
this.updateSpec(deepSet(spec, 'dataSchema.parser.parseSpec.timestampSpec', newTimestampSpec));
|
||||
const { specPreview } = this.state;
|
||||
this.updateSpecPreview(
|
||||
deepSet(specPreview, 'dataSchema.parser.parseSpec.timestampSpec', newTimestampSpec),
|
||||
);
|
||||
};
|
||||
|
||||
// ==================================================================
|
||||
|
@ -1689,13 +1719,13 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<Callout className="intro">
|
||||
<p className="optional">Optional</p>
|
||||
<p>
|
||||
Druid can perform simple{' '}
|
||||
Druid can perform per-row{' '}
|
||||
<ExternalLink href="https://druid.apache.org/docs/latest/ingestion/transform-spec.html#transforms">
|
||||
transforms
|
||||
</ExternalLink>{' '}
|
||||
of column values.
|
||||
of column values allowing you to create new derived columns or alter existing column.
|
||||
</p>
|
||||
<p>Click "Preview" to see the result of any specified transforms.</p>
|
||||
<LearnMore href="https://druid.apache.org/docs/latest/ingestion/index.html#transforms" />
|
||||
</Callout>
|
||||
{Boolean(transformQueryState.error && transforms.length) && (
|
||||
<FormGroup>
|
||||
|
@ -1713,7 +1743,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
</FormGroup>
|
||||
)}
|
||||
{this.renderTransformControls()}
|
||||
<Button text="Preview" onClick={() => this.queryForTransform()} />
|
||||
</div>
|
||||
{this.renderNextBar({
|
||||
disabled: !transformQueryState.data,
|
||||
|
@ -1747,13 +1776,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
});
|
||||
};
|
||||
|
||||
const closeAndQuery = () => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
this.queryForTransform();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
if (selectedTransform) {
|
||||
return (
|
||||
<div className="edit-controls">
|
||||
|
@ -1762,7 +1784,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
model={selectedTransform}
|
||||
onChange={selectedTransform => this.setState({ selectedTransform })}
|
||||
/>
|
||||
<div className="controls-buttons">
|
||||
<div className="control-buttons">
|
||||
<Button
|
||||
className="add-update"
|
||||
text={selectedTransformIndex === -1 ? 'Add' : 'Update'}
|
||||
|
@ -1775,7 +1797,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
selectedTransform,
|
||||
),
|
||||
);
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{selectedTransformIndex !== -1 && (
|
||||
|
@ -1789,7 +1811,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
`dataSchema.transformSpec.transforms.${selectedTransformIndex}`,
|
||||
),
|
||||
);
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -1898,19 +1920,9 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
});
|
||||
|
||||
renderFilterStep() {
|
||||
const {
|
||||
spec,
|
||||
columnFilter,
|
||||
filterQueryState,
|
||||
selectedFilter,
|
||||
selectedFilterIndex,
|
||||
showGlobalFilter,
|
||||
} = this.state;
|
||||
const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || EMPTY_OBJECT;
|
||||
const { spec, columnFilter, filterQueryState, selectedFilter, showGlobalFilter } = this.state;
|
||||
const dimensionFilters = this.getMemoizedDimensionFiltersFromSpec(spec);
|
||||
|
||||
const isBlank = !parseSpec.format;
|
||||
|
||||
let mainFill: JSX.Element | string = '';
|
||||
if (filterQueryState.isInit()) {
|
||||
mainFill = <CenterMessage>Please enter more details for the previous steps</CenterMessage>;
|
||||
|
@ -1932,7 +1944,10 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
sampleData={filterQueryState.data}
|
||||
columnFilter={columnFilter}
|
||||
dimensionFilters={dimensionFilters}
|
||||
selectedFilterIndex={selectedFilterIndex}
|
||||
selectedFilterName={filterTableSelectedColumnName(
|
||||
filterQueryState.data,
|
||||
selectedFilter,
|
||||
)}
|
||||
onShowGlobalFilter={this.onShowGlobalFilter}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
/>
|
||||
|
@ -1951,15 +1966,12 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<ExternalLink href="https://druid.apache.org/docs/latest/querying/filters.html">
|
||||
filter
|
||||
</ExternalLink>{' '}
|
||||
out unwanted data.
|
||||
out unwanted data by applying per-row filters.
|
||||
</p>
|
||||
<p>Click "Preview" to see the impact of any specified filters.</p>
|
||||
<LearnMore href="https://druid.apache.org/docs/latest/ingestion/index.html#filter" />
|
||||
</Callout>
|
||||
{!showGlobalFilter && this.renderColumnFilterControls()}
|
||||
{!selectedFilter && this.renderGlobalFilterControls()}
|
||||
{!selectedFilter && !showGlobalFilter && (
|
||||
<Button text="Preview" disabled={isBlank} onClick={() => this.queryForFilter()} />
|
||||
)}
|
||||
</div>
|
||||
{this.renderNextBar({})}
|
||||
</>
|
||||
|
@ -1987,13 +1999,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
});
|
||||
};
|
||||
|
||||
const closeAndQuery = () => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
this.queryForFilter();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
if (selectedFilter) {
|
||||
return (
|
||||
<div className="edit-controls">
|
||||
|
@ -2003,7 +2008,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
onChange={f => this.setState({ selectedFilter: f })}
|
||||
showCustom={f => !['selector', 'in', 'regex', 'like', 'not'].includes(f.type)}
|
||||
/>
|
||||
<div className="controls-buttons">
|
||||
<div className="control-buttons">
|
||||
<Button
|
||||
className="add-update"
|
||||
text={selectedFilterIndex === -1 ? 'Add' : 'Update'}
|
||||
|
@ -2014,7 +2019,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
deepSet(curFilter, `dimensionFilters.${selectedFilterIndex}`, selectedFilter),
|
||||
);
|
||||
this.updateSpec(deepSet(spec, 'dataSchema.transformSpec.filter', newFilter));
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{selectedFilterIndex !== -1 && (
|
||||
|
@ -2027,7 +2032,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
deepDelete(curFilter, `dimensionFilters.${selectedFilterIndex}`),
|
||||
);
|
||||
this.updateSpec(deepSet(spec, 'dataSchema.transformSpec.filter', newFilter));
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -2090,10 +2095,10 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
height="200px"
|
||||
/>
|
||||
</FormGroup>
|
||||
<div className="controls-buttons">
|
||||
<div className="control-buttons">
|
||||
<Button
|
||||
className="add-update"
|
||||
text="Preview"
|
||||
text="Apply"
|
||||
intent={Intent.PRIMARY}
|
||||
onClick={() => this.queryForFilter()}
|
||||
/>
|
||||
|
@ -2174,7 +2179,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
|
||||
renderSchemaStep() {
|
||||
const {
|
||||
spec,
|
||||
specPreview: spec,
|
||||
columnFilter,
|
||||
schemaQueryState,
|
||||
selectedDimensionSpec,
|
||||
|
@ -2221,16 +2226,14 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<Callout className="intro">
|
||||
<p>
|
||||
Each column in Druid must have an assigned type (string, long, float, complex, etc).
|
||||
Default primitive types have been automatically assigned to your columns. If you want
|
||||
to change the type, click on the column header.
|
||||
</p>
|
||||
<p>
|
||||
Select whether or not you want to{' '}
|
||||
<ExternalLink href="https://druid.apache.org/docs/latest/tutorials/tutorial-rollup.html">
|
||||
roll-up
|
||||
</ExternalLink>{' '}
|
||||
your data.
|
||||
</p>
|
||||
{dimensionMode === 'specific' && (
|
||||
<p>
|
||||
Default primitive types have been automatically assigned to your columns. If you
|
||||
want to change the type, click on the column header.
|
||||
</p>
|
||||
)}
|
||||
<LearnMore href="https://druid.apache.org/docs/latest/ingestion/schema-design.html" />
|
||||
</Callout>
|
||||
{!somethingSelected && (
|
||||
<>
|
||||
|
@ -2296,10 +2299,14 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
content={
|
||||
<div className="label-info-text">
|
||||
<p>
|
||||
If you enable roll-up, Druid will try to pre-aggregate data before indexing
|
||||
it to conserve storage. The primary timestamp will be truncated to the
|
||||
specified query granularity, and rows containing the same string field
|
||||
values will be aggregated together.
|
||||
If you enable{' '}
|
||||
<ExternalLink href="https://druid.apache.org/docs/latest/tutorials/tutorial-rollup.html">
|
||||
roll-up
|
||||
</ExternalLink>
|
||||
, Druid will try to pre-aggregate data before indexing it to conserve
|
||||
storage. The primary timestamp will be truncated to the specified query
|
||||
granularity, and rows containing the same string field values will be
|
||||
aggregated together.
|
||||
</p>
|
||||
<p>
|
||||
If you enable rollup, you must specify which columns are{' '}
|
||||
|
@ -2325,7 +2332,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
name: 'dataSchema.granularitySpec.queryGranularity',
|
||||
label: 'Query granularity',
|
||||
type: 'string',
|
||||
suggestions: ['NONE', 'MINUTE', 'HOUR', 'DAY'],
|
||||
suggestions: ['NONE', 'SECOND', 'MINUTE', 'HOUR', 'DAY'],
|
||||
info: (
|
||||
<>
|
||||
This granularity determines how timestamps will be truncated (not at all, to
|
||||
|
@ -2336,12 +2343,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
},
|
||||
]}
|
||||
model={spec}
|
||||
onChange={s => this.updateSpec(s)}
|
||||
onFinalize={() => {
|
||||
setTimeout(() => {
|
||||
this.queryForSchema();
|
||||
}, 10);
|
||||
}}
|
||||
onChange={s => this.updateSpecPreview(s)}
|
||||
onFinalize={this.applyPreviewSpec}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -2387,9 +2390,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
newRollup,
|
||||
),
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.queryForSchema();
|
||||
}, 10);
|
||||
}}
|
||||
confirmButtonText={`Yes - ${newRollup ? 'enable' : 'disable'} rollup`}
|
||||
successText={`Rollup was ${newRollup ? 'enabled' : 'disabled'}. Schema has been updated.`}
|
||||
|
@ -2420,9 +2420,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
getRollup(spec),
|
||||
),
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.queryForSchema();
|
||||
}, 10);
|
||||
}}
|
||||
confirmButtonText={`Yes - ${autoDetect ? 'auto detect' : 'explicitly set'} columns`}
|
||||
successText={`Dimension mode changes to ${
|
||||
|
@ -2452,13 +2449,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
});
|
||||
};
|
||||
|
||||
const closeAndQuery = () => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
this.queryForSchema();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
if (selectedDimensionSpec) {
|
||||
const curDimensions =
|
||||
deepGet(spec, `dataSchema.parser.parseSpec.dimensionsSpec.dimensions`) || EMPTY_ARRAY;
|
||||
|
@ -2470,7 +2460,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
model={selectedDimensionSpec}
|
||||
onChange={selectedDimensionSpec => this.setState({ selectedDimensionSpec })}
|
||||
/>
|
||||
<div className="controls-buttons">
|
||||
<div className="control-buttons">
|
||||
<Button
|
||||
className="add-update"
|
||||
text={selectedDimensionSpecIndex === -1 ? 'Add' : 'Update'}
|
||||
|
@ -2483,7 +2473,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
selectedDimensionSpec,
|
||||
),
|
||||
);
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{selectedDimensionSpecIndex !== -1 && (
|
||||
|
@ -2500,7 +2490,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
`dataSchema.parser.parseSpec.dimensionsSpec.dimensions.${selectedDimensionSpecIndex}`,
|
||||
),
|
||||
);
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -2539,13 +2529,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
});
|
||||
};
|
||||
|
||||
const closeAndQuery = () => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
this.queryForSchema();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
if (selectedMetricSpec) {
|
||||
return (
|
||||
<div className="edit-controls">
|
||||
|
@ -2554,7 +2537,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
model={selectedMetricSpec}
|
||||
onChange={selectedMetricSpec => this.setState({ selectedMetricSpec })}
|
||||
/>
|
||||
<div className="controls-buttons">
|
||||
<div className="control-buttons">
|
||||
<Button
|
||||
className="add-update"
|
||||
text={selectedMetricSpecIndex === -1 ? 'Add' : 'Update'}
|
||||
|
@ -2567,7 +2550,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
selectedMetricSpec,
|
||||
),
|
||||
);
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{selectedMetricSpecIndex !== -1 && (
|
||||
|
@ -2578,7 +2561,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
this.updateSpec(
|
||||
deepDelete(spec, `dataSchema.metricsSpec.${selectedMetricSpecIndex}`),
|
||||
);
|
||||
closeAndQuery();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -2678,6 +2661,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<Callout className="intro">
|
||||
<p className="optional">Optional</p>
|
||||
<p>Configure how Druid will partition data.</p>
|
||||
<LearnMore href="https://druid.apache.org/docs/latest/ingestion/index.html#partitioning" />
|
||||
</Callout>
|
||||
{this.renderParallelPickerIfNeeded()}
|
||||
</div>
|
||||
|
@ -2742,6 +2726,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
|||
<Callout className="intro">
|
||||
<p className="optional">Optional</p>
|
||||
<p>Fine tune how Druid will ingest data.</p>
|
||||
<LearnMore href="https://druid.apache.org/docs/latest/ingestion/index.html#tuningconfig" />
|
||||
</Callout>
|
||||
{this.renderParallelPickerIfNeeded()}
|
||||
</div>
|
||||
|
|
|
@ -43,6 +43,7 @@ describe('parse time table', () => {
|
|||
}}
|
||||
columnFilter=""
|
||||
possibleTimestampColumnsOnly={false}
|
||||
selectedColumnName={undefined}
|
||||
onTimestampColumnSelect={() => {}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -32,6 +32,16 @@ import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
|
|||
|
||||
import './parse-time-table.scss';
|
||||
|
||||
export function parseTimeTableSelectedColumnName(
|
||||
sampleData: HeaderAndRows,
|
||||
timestampSpec: TimestampSpec | undefined,
|
||||
): string | undefined {
|
||||
if (!timestampSpec) return;
|
||||
const timestampColumn = timestampSpec.column;
|
||||
if (!timestampColumn || !sampleData.header.includes(timestampColumn)) return;
|
||||
return timestampColumn;
|
||||
}
|
||||
|
||||
export interface ParseTimeTableProps {
|
||||
sampleBundle: {
|
||||
headerAndRows: HeaderAndRows;
|
||||
|
@ -39,6 +49,7 @@ export interface ParseTimeTableProps {
|
|||
};
|
||||
columnFilter: string;
|
||||
possibleTimestampColumnsOnly: boolean;
|
||||
selectedColumnName: string | undefined;
|
||||
onTimestampColumnSelect: (newTimestampSpec: TimestampSpec) => void;
|
||||
}
|
||||
|
||||
|
@ -47,6 +58,7 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim
|
|||
sampleBundle,
|
||||
columnFilter,
|
||||
possibleTimestampColumnsOnly,
|
||||
selectedColumnName,
|
||||
onTimestampColumnSelect,
|
||||
} = props;
|
||||
const { headerAndRows, timestampSpec } = sampleBundle;
|
||||
|
@ -62,7 +74,7 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim
|
|||
(columnName, i) => {
|
||||
const timestamp = columnName === '__time';
|
||||
if (!timestamp && !caseInsensitiveContains(columnName, columnFilter)) return;
|
||||
const selected = timestampSpec.column === columnName;
|
||||
const used = timestampSpec.column === columnName;
|
||||
const possibleFormat = timestamp
|
||||
? null
|
||||
: possibleDruidFormatForValues(
|
||||
|
@ -72,7 +84,8 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim
|
|||
|
||||
const columnClassName = classNames({
|
||||
timestamp,
|
||||
selected,
|
||||
used,
|
||||
selected: selectedColumnName === columnName,
|
||||
});
|
||||
return {
|
||||
Header: (
|
||||
|
|
Loading…
Reference in New Issue