Web console: Power up the data loader init step (#7947)
* Power up the data loader init step * update snapshot * normalize spec * allow deselect * added HDFS tile * update border style * text updates * goodies * new reset icon
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 16 KiB |
|
@ -229,9 +229,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@blueprintjs/core": {
|
"@blueprintjs/core": {
|
||||||
"version": "3.15.1",
|
"version": "3.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.16.2.tgz",
|
||||||
"integrity": "sha512-M8ltbqqlMZuZ6SEuqo/3Fr59ZcUfd8Er7ocbm7EACVfRW7dRhOCd/TKkf2kfICNtCDwznwXk0iAePLXZhUGtQg==",
|
"integrity": "sha512-u+mSITWaNDwbdaPrbKx9XyxGsF4725SCAidWjd367ysX7AxCo4PK4SsFQVfXNylXpVWHQhJZekuo7+hdksc9lA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@blueprintjs/icons": "^3.8.0",
|
"@blueprintjs/icons": "^3.8.0",
|
||||||
"@types/dom4": "^2.0.1",
|
"@types/dom4": "^2.0.1",
|
||||||
|
@ -246,9 +246,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@blueprintjs/icons": {
|
"@blueprintjs/icons": {
|
||||||
"version": "3.8.0",
|
"version": "3.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.9.0.tgz",
|
||||||
"integrity": "sha512-yHaRQ3vfV9Gf3foZ4ONtxddz+u5ufkHqHj8Ia5VhPbFgG4el+cPdmsGGIIM72rgKS1KQa5Ay+ggjpByUlXvrKg==",
|
"integrity": "sha512-kq1Bh6PtOF4PcuxcDme8NmnSlkfO0IV89FriZGo6zSA1+OOzSwzvoKqa6S7vJe8xCPPLO5r7lE9AjeOuGeH97g==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"classnames": "^2.2",
|
"classnames": "^2.2",
|
||||||
"tslib": "^1.9.0"
|
"tslib": "^1.9.0"
|
||||||
|
|
|
@ -50,7 +50,8 @@
|
||||||
"stylelint": "stylelint 'src/**/*.scss'"
|
"stylelint": "stylelint 'src/**/*.scss'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blueprintjs/core": "^3.15.1",
|
"@blueprintjs/core": "^3.16.2",
|
||||||
|
"@blueprintjs/icons": "^3.9.0",
|
||||||
"@types/memoize-one": "^4.1.1",
|
"@types/memoize-one": "^4.1.1",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
"brace": "^0.11.1",
|
"brace": "^0.11.1",
|
||||||
|
|
|
@ -25,3 +25,4 @@ cp -r coordinator-console "$1"
|
||||||
cp -r old-console "$1"
|
cp -r old-console "$1"
|
||||||
cp -r pages "$1"
|
cp -r pages "$1"
|
||||||
cp -r public "$1"
|
cp -r public "$1"
|
||||||
|
cp -r assets "$1"
|
||||||
|
|
|
@ -34,27 +34,31 @@ const readDoc = async () => {
|
||||||
if (functionMatch) {
|
if (functionMatch) {
|
||||||
functionDocs.push({
|
functionDocs.push({
|
||||||
syntax: functionMatch[1],
|
syntax: functionMatch[1],
|
||||||
description: functionMatch[2]
|
description: functionMatch[2],
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataTypeMatch = line.match(/^\|([A-Z]+)\|([A-Z]+)\|(.*)\|(.*)\|$/);
|
const dataTypeMatch = line.match(/^\|([A-Z]+)\|([A-Z]+)\|(.*)\|(.*)\|$/);
|
||||||
if (dataTypeMatch) {
|
if (dataTypeMatch) {
|
||||||
dataTypeDocs.push({
|
dataTypeDocs.push({
|
||||||
syntax: dataTypeMatch[1],
|
syntax: dataTypeMatch[1],
|
||||||
description: dataTypeMatch[4] || `Druid runtime type: ${dataTypeMatch[2]}`
|
description: dataTypeMatch[4] || `Druid runtime type: ${dataTypeMatch[2]}`,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there are at least 10 functions for sanity
|
// Make sure there are at least 10 functions for sanity
|
||||||
if (functionDocs.length < 10) {
|
if (functionDocs.length < 10) {
|
||||||
throw new Error(`Did not find enough function entries did the structure of '${readfile}' change? (found ${functionDocs.length})`);
|
throw new Error(
|
||||||
|
`Did not find enough function entries did the structure of '${readfile}' change? (found ${functionDocs.length})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure there are at least 5 data types for sanity
|
// Make sure there are at least 5 data types for sanity
|
||||||
if (dataTypeDocs.length < 10) {
|
if (dataTypeDocs.length < 10) {
|
||||||
throw new Error(`Did not find enough data type entries did the structure of '${readfile}' change? (found ${dataTypeDocs.length})`);
|
throw new Error(
|
||||||
|
`Did not find enough data type entries did the structure of '${readfile}' change? (found ${dataTypeDocs.length})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = `/*
|
const content = `/*
|
||||||
|
|
|
@ -37,6 +37,10 @@ export interface IngestionSpec {
|
||||||
tuningConfig?: TuningConfig;
|
tuningConfig?: TuningConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEmptyIngestionSpec(spec: IngestionSpec) {
|
||||||
|
return Object.keys(spec).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
export type IngestionType = 'kafka' | 'kinesis' | 'index_hadoop' | 'index' | 'index_parallel';
|
export type IngestionType = 'kafka' | 'kinesis' | 'index_hadoop' | 'index' | 'index_parallel';
|
||||||
|
|
||||||
// A combination of IngestionType and firehose
|
// A combination of IngestionType and firehose
|
||||||
|
@ -48,6 +52,9 @@ export type IngestionComboType =
|
||||||
| 'index:static-s3'
|
| 'index:static-s3'
|
||||||
| 'index:static-google-blobstore';
|
| 'index:static-google-blobstore';
|
||||||
|
|
||||||
|
// Some extra values that can be selected in the initial screen
|
||||||
|
export type IngestionComboTypeWithExtra = IngestionComboType | 'hadoop' | 'example' | 'other';
|
||||||
|
|
||||||
function ingestionTypeToIoAndTuningConfigType(ingestionType: IngestionType): string {
|
function ingestionTypeToIoAndTuningConfigType(ingestionType: IngestionType): string {
|
||||||
switch (ingestionType) {
|
switch (ingestionType) {
|
||||||
case 'kafka':
|
case 'kafka':
|
||||||
|
@ -87,6 +94,65 @@ export function getIngestionComboType(spec: IngestionSpec): IngestionComboType |
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getIngestionTitle(ingestionType: IngestionComboTypeWithExtra): string {
|
||||||
|
switch (ingestionType) {
|
||||||
|
case 'index:local':
|
||||||
|
return 'Local disk';
|
||||||
|
|
||||||
|
case 'index:http':
|
||||||
|
return 'HTTP(s)';
|
||||||
|
|
||||||
|
case 'index:static-s3':
|
||||||
|
return 'Amazon S3';
|
||||||
|
|
||||||
|
case 'index:static-google-blobstore':
|
||||||
|
return 'Google Cloud Storage';
|
||||||
|
|
||||||
|
case 'kafka':
|
||||||
|
return 'Apache Kafka';
|
||||||
|
|
||||||
|
case 'kinesis':
|
||||||
|
return 'Amazon Kinesis';
|
||||||
|
|
||||||
|
case 'hadoop':
|
||||||
|
return 'HDFS';
|
||||||
|
|
||||||
|
case 'example':
|
||||||
|
return 'Example data';
|
||||||
|
|
||||||
|
case 'other':
|
||||||
|
return 'Other';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'Unknown ingestion';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIngestionImage(ingestionType: IngestionComboTypeWithExtra): string {
|
||||||
|
const parts = ingestionType.split(':');
|
||||||
|
if (parts.length === 2) return parts[1];
|
||||||
|
return ingestionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRequiredModule(ingestionType: IngestionComboTypeWithExtra): string | null {
|
||||||
|
switch (ingestionType) {
|
||||||
|
case 'index:static-s3':
|
||||||
|
return 'druid-s3-extensions';
|
||||||
|
|
||||||
|
case 'index:static-google-blobstore':
|
||||||
|
return 'druid-google-extensions';
|
||||||
|
|
||||||
|
case 'kafka':
|
||||||
|
return 'druid-kafka-indexing-service';
|
||||||
|
|
||||||
|
case 'kinesis':
|
||||||
|
return 'druid-kinesis-indexing-service';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --------------
|
// --------------
|
||||||
|
|
||||||
export interface DataSchema {
|
export interface DataSchema {
|
||||||
|
@ -138,7 +204,7 @@ export function getRollup(spec: IngestionSpec): boolean {
|
||||||
return typeof specRollup === 'boolean' ? specRollup : true;
|
return typeof specRollup === 'boolean' ? specRollup : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSpecType(spec: IngestionSpec): IngestionType | undefined {
|
export function getSpecType(spec: Partial<IngestionSpec>): IngestionType | undefined {
|
||||||
return (
|
return (
|
||||||
deepGet(spec, 'type') || deepGet(spec, 'ioConfig.type') || deepGet(spec, 'tuningConfig.type')
|
deepGet(spec, 'type') || deepGet(spec, 'ioConfig.type') || deepGet(spec, 'tuningConfig.type')
|
||||||
);
|
);
|
||||||
|
@ -158,13 +224,21 @@ export function changeParallel(spec: IngestionSpec, parallel: boolean): Ingestio
|
||||||
* Make sure that the types are set in the root, ioConfig, and tuningConfig
|
* Make sure that the types are set in the root, ioConfig, and tuningConfig
|
||||||
* @param spec
|
* @param spec
|
||||||
*/
|
*/
|
||||||
export function normalizeSpecType(spec: IngestionSpec) {
|
export function normalizeSpec(spec: Partial<IngestionSpec>): IngestionSpec {
|
||||||
|
if (!spec || typeof spec !== 'object') {
|
||||||
|
// This does not match the type of IngestionSpec but this dialog is robust enough to deal with anything but spec must be an object
|
||||||
|
spec = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that if we actually get a task payload we extract the spec
|
||||||
|
if (typeof (spec as any).spec === 'object') spec = (spec as any).spec;
|
||||||
|
|
||||||
const specType = getSpecType(spec);
|
const specType = getSpecType(spec);
|
||||||
if (!specType) return spec;
|
if (!specType) return spec as IngestionSpec;
|
||||||
if (!deepGet(spec, 'type')) spec = deepSet(spec, 'type', specType);
|
if (!deepGet(spec, 'type')) spec = deepSet(spec, 'type', specType);
|
||||||
if (!deepGet(spec, 'ioConfig.type')) spec = deepSet(spec, 'ioConfig.type', specType);
|
if (!deepGet(spec, 'ioConfig.type')) spec = deepSet(spec, 'ioConfig.type', specType);
|
||||||
if (!deepGet(spec, 'tuningConfig.type')) spec = deepSet(spec, 'tuningConfig.type', specType);
|
if (!deepGet(spec, 'tuningConfig.type')) spec = deepSet(spec, 'tuningConfig.type', specType);
|
||||||
return spec;
|
return spec as IngestionSpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PARSE_SPEC_FORM_FIELDS: Field<ParseSpec>[] = [
|
const PARSE_SPEC_FORM_FIELDS: Field<ParseSpec>[] = [
|
||||||
|
@ -851,7 +925,7 @@ export function getIoConfigFormFields(ingestionComboType: IngestionComboType): F
|
||||||
],
|
],
|
||||||
info: (
|
info: (
|
||||||
<>
|
<>
|
||||||
The AWS Kinesis stream endpoint for a region. You can find a list of endpoints{' '}
|
The Amazon Kinesis stream endpoint for a region. You can find a list of endpoints{' '}
|
||||||
<ExternalLink href="http://docs.aws.amazon.com/general/latest/gr/rande.html#ak_region">
|
<ExternalLink href="http://docs.aws.amazon.com/general/latest/gr/rande.html#ak_region">
|
||||||
here
|
here
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
|
@ -1662,40 +1736,40 @@ export interface Bitmap {
|
||||||
|
|
||||||
// --------------
|
// --------------
|
||||||
|
|
||||||
export function getBlankSpec(comboType: IngestionComboType): IngestionSpec {
|
export function updateIngestionType(
|
||||||
|
spec: IngestionSpec,
|
||||||
|
comboType: IngestionComboType,
|
||||||
|
): IngestionSpec {
|
||||||
let [ingestionType, firehoseType] = comboType.split(':');
|
let [ingestionType, firehoseType] = comboType.split(':');
|
||||||
if (ingestionType === 'index') ingestionType = 'index_parallel';
|
if (ingestionType === 'index') ingestionType = 'index_parallel';
|
||||||
const ioAndTuningConfigType = ingestionTypeToIoAndTuningConfigType(
|
const ioAndTuningConfigType = ingestionTypeToIoAndTuningConfigType(
|
||||||
ingestionType as IngestionType,
|
ingestionType as IngestionType,
|
||||||
);
|
);
|
||||||
|
|
||||||
const granularitySpec: GranularitySpec = {
|
let newSpec = spec;
|
||||||
type: 'uniform',
|
newSpec = deepSet(newSpec, 'type', ingestionType);
|
||||||
segmentGranularity: ingestionType === 'index_parallel' ? 'DAY' : 'HOUR',
|
newSpec = deepSet(newSpec, 'ioConfig.type', ioAndTuningConfigType);
|
||||||
queryGranularity: 'HOUR',
|
newSpec = deepSet(newSpec, 'tuningConfig.type', ioAndTuningConfigType);
|
||||||
};
|
|
||||||
|
|
||||||
const spec: IngestionSpec = {
|
|
||||||
type: ingestionType,
|
|
||||||
dataSchema: {
|
|
||||||
dataSource: 'new-data-source',
|
|
||||||
granularitySpec,
|
|
||||||
},
|
|
||||||
ioConfig: {
|
|
||||||
type: ioAndTuningConfigType,
|
|
||||||
},
|
|
||||||
tuningConfig: {
|
|
||||||
type: ioAndTuningConfigType,
|
|
||||||
},
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
if (firehoseType) {
|
if (firehoseType) {
|
||||||
spec.ioConfig.firehose = {
|
newSpec = deepSet(newSpec, 'ioConfig.firehose', { type: firehoseType });
|
||||||
type: firehoseType,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return spec;
|
if (!deepGet(spec, 'dataSchema.dataSource')) {
|
||||||
|
newSpec = deepSet(newSpec, 'dataSchema.dataSource', 'new-data-source');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deepGet(spec, 'dataSchema.granularitySpec')) {
|
||||||
|
const granularitySpec: GranularitySpec = {
|
||||||
|
type: 'uniform',
|
||||||
|
segmentGranularity: ingestionType === 'index_parallel' ? 'DAY' : 'HOUR',
|
||||||
|
queryGranularity: 'HOUR',
|
||||||
|
};
|
||||||
|
|
||||||
|
newSpec = deepSet(newSpec, 'dataSchema.granularitySpec', granularitySpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fillParser(spec: IngestionSpec, sampleData: string[]): IngestionSpec {
|
export function fillParser(spec: IngestionSpec, sampleData: string[]): IngestionSpec {
|
||||||
|
|
|
@ -2,41 +2,169 @@
|
||||||
|
|
||||||
exports[`load data view matches snapshot 1`] = `
|
exports[`load data view matches snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
className="load-data-view app-view init"
|
className="load-data-view app-view welcome"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="intro"
|
className="bp3-tabs step-nav"
|
||||||
>
|
>
|
||||||
Please specify where your raw data is located
|
<div
|
||||||
|
className="step-section"
|
||||||
|
key="Connect and parse raw data"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="step-nav-l1"
|
||||||
|
>
|
||||||
|
Connect and parse raw data
|
||||||
|
</div>
|
||||||
|
<Blueprint3.ButtonGroup
|
||||||
|
className="step-nav-l2"
|
||||||
|
>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={true}
|
||||||
|
className="welcome"
|
||||||
|
icon={false}
|
||||||
|
key="welcome"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Start"
|
||||||
|
/>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="connect"
|
||||||
|
icon={false}
|
||||||
|
key="connect"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Connect"
|
||||||
|
/>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="parser"
|
||||||
|
icon={false}
|
||||||
|
key="parser"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Parse data"
|
||||||
|
/>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="timestamp"
|
||||||
|
icon={false}
|
||||||
|
key="timestamp"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Parse time"
|
||||||
|
/>
|
||||||
|
</Blueprint3.ButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="step-section"
|
||||||
|
key="Transform and configure schema"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="step-nav-l1"
|
||||||
|
>
|
||||||
|
Transform and configure schema
|
||||||
|
</div>
|
||||||
|
<Blueprint3.ButtonGroup
|
||||||
|
className="step-nav-l2"
|
||||||
|
>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="transform"
|
||||||
|
icon={false}
|
||||||
|
key="transform"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Transform"
|
||||||
|
/>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="filter"
|
||||||
|
icon={false}
|
||||||
|
key="filter"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Filter"
|
||||||
|
/>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="schema"
|
||||||
|
icon={false}
|
||||||
|
key="schema"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Configure schema"
|
||||||
|
/>
|
||||||
|
</Blueprint3.ButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="step-section"
|
||||||
|
key="Tune parameters"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="step-nav-l1"
|
||||||
|
>
|
||||||
|
Tune parameters
|
||||||
|
</div>
|
||||||
|
<Blueprint3.ButtonGroup
|
||||||
|
className="step-nav-l2"
|
||||||
|
>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="partition"
|
||||||
|
icon={false}
|
||||||
|
key="partition"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Partition"
|
||||||
|
/>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="tuning"
|
||||||
|
icon={false}
|
||||||
|
key="tuning"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Tune"
|
||||||
|
/>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="publish"
|
||||||
|
icon={false}
|
||||||
|
key="publish"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Publish"
|
||||||
|
/>
|
||||||
|
</Blueprint3.ButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="step-section"
|
||||||
|
key="Verify and submit"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="step-nav-l1"
|
||||||
|
>
|
||||||
|
Verify and submit
|
||||||
|
</div>
|
||||||
|
<Blueprint3.ButtonGroup
|
||||||
|
className="step-nav-l2"
|
||||||
|
>
|
||||||
|
<Blueprint3.Button
|
||||||
|
active={false}
|
||||||
|
className="spec"
|
||||||
|
icon="manually-entered-data"
|
||||||
|
key="spec"
|
||||||
|
onClick={[Function]}
|
||||||
|
text="Edit JSON spec"
|
||||||
|
/>
|
||||||
|
</Blueprint3.ButtonGroup>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="cards"
|
className="main"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="control"
|
||||||
>
|
>
|
||||||
<Blueprint3.Card
|
<Blueprint3.Callout
|
||||||
elevation={0}
|
className="intro"
|
||||||
interactive={true}
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
>
|
||||||
Other (streaming)
|
<p>
|
||||||
</Blueprint3.Card>
|
Please specify where your raw data is located
|
||||||
<Blueprint3.Card
|
</p>
|
||||||
elevation={0}
|
</Blueprint3.Callout>
|
||||||
interactive={true}
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
Other (batch)
|
|
||||||
</Blueprint3.Card>
|
|
||||||
</div>
|
</div>
|
||||||
<Blueprint3.Alert
|
|
||||||
canEscapeKeyCancel={false}
|
|
||||||
canOutsideClickCancel={false}
|
|
||||||
confirmButtonText="Close"
|
|
||||||
icon="warning-sign"
|
|
||||||
intent="warning"
|
|
||||||
isOpen={false}
|
|
||||||
onConfirm={[Function]}
|
|
||||||
>
|
|
||||||
<p />
|
|
||||||
</Blueprint3.Alert>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -27,32 +27,45 @@
|
||||||
'main ctrl'
|
'main ctrl'
|
||||||
'main next';
|
'main next';
|
||||||
|
|
||||||
&.init {
|
&.welcome {
|
||||||
display: block;
|
.main {
|
||||||
|
margin-left: -10px;
|
||||||
|
|
||||||
& > * {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cards {
|
|
||||||
.bp3-card {
|
.bp3-card {
|
||||||
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
margin-right: 15px;
|
margin-left: 15px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
font-size: 24px;
|
font-size: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 47px;
|
|
||||||
|
& > * {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
content: '';
|
||||||
|
border: 2px solid #48aff0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,7 +205,7 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
|
|
||||||
.prev {
|
.left {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
Card,
|
Card,
|
||||||
Classes,
|
Classes,
|
||||||
Code,
|
Code,
|
||||||
|
Elevation,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
H5,
|
H5,
|
||||||
HTMLSelect,
|
HTMLSelect,
|
||||||
|
@ -50,6 +51,7 @@ import {
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
import { AsyncActionDialog } from '../../dialogs';
|
import { AsyncActionDialog } from '../../dialogs';
|
||||||
import { AppToaster } from '../../singletons/toaster';
|
import { AppToaster } from '../../singletons/toaster';
|
||||||
|
import { UrlBaser } from '../../singletons/url-baser';
|
||||||
import {
|
import {
|
||||||
filterMap,
|
filterMap,
|
||||||
getDruidErrorMessage,
|
getDruidErrorMessage,
|
||||||
|
@ -72,18 +74,20 @@ import {
|
||||||
fillDataSourceName,
|
fillDataSourceName,
|
||||||
fillParser,
|
fillParser,
|
||||||
FlattenField,
|
FlattenField,
|
||||||
getBlankSpec,
|
|
||||||
getDimensionMode,
|
getDimensionMode,
|
||||||
getDimensionSpecFormFields,
|
getDimensionSpecFormFields,
|
||||||
getEmptyTimestampSpec,
|
getEmptyTimestampSpec,
|
||||||
getFilterFormFields,
|
getFilterFormFields,
|
||||||
getFlattenFieldFormFields,
|
getFlattenFieldFormFields,
|
||||||
getIngestionComboType,
|
getIngestionComboType,
|
||||||
|
getIngestionImage,
|
||||||
|
getIngestionTitle,
|
||||||
getIoConfigFormFields,
|
getIoConfigFormFields,
|
||||||
getIoConfigTuningFormFields,
|
getIoConfigTuningFormFields,
|
||||||
getMetricSpecFormFields,
|
getMetricSpecFormFields,
|
||||||
getParseSpecFormFields,
|
getParseSpecFormFields,
|
||||||
getPartitionRelatedTuningSpecFormFields,
|
getPartitionRelatedTuningSpecFormFields,
|
||||||
|
getRequiredModule,
|
||||||
getRollup,
|
getRollup,
|
||||||
getSpecType,
|
getSpecType,
|
||||||
getTimestampSpecFormFields,
|
getTimestampSpecFormFields,
|
||||||
|
@ -91,16 +95,17 @@ import {
|
||||||
getTuningSpecFormFields,
|
getTuningSpecFormFields,
|
||||||
GranularitySpec,
|
GranularitySpec,
|
||||||
hasParallelAbility,
|
hasParallelAbility,
|
||||||
IngestionComboType,
|
IngestionComboTypeWithExtra,
|
||||||
IngestionSpec,
|
IngestionSpec,
|
||||||
IoConfig,
|
IoConfig,
|
||||||
isColumnTimestampSpec,
|
isColumnTimestampSpec,
|
||||||
|
isEmptyIngestionSpec,
|
||||||
isParallel,
|
isParallel,
|
||||||
issueWithIoConfig,
|
issueWithIoConfig,
|
||||||
issueWithParser,
|
issueWithParser,
|
||||||
joinFilter,
|
joinFilter,
|
||||||
MetricSpec,
|
MetricSpec,
|
||||||
normalizeSpecType,
|
normalizeSpec,
|
||||||
Parser,
|
Parser,
|
||||||
ParseSpec,
|
ParseSpec,
|
||||||
parseSpecHasFlatten,
|
parseSpecHasFlatten,
|
||||||
|
@ -108,6 +113,7 @@ import {
|
||||||
TimestampSpec,
|
TimestampSpec,
|
||||||
Transform,
|
Transform,
|
||||||
TuningConfig,
|
TuningConfig,
|
||||||
|
updateIngestionType,
|
||||||
} from '../../utils/ingestion-spec';
|
} from '../../utils/ingestion-spec';
|
||||||
import { deepDelete, deepGet, deepSet } from '../../utils/object-change';
|
import { deepDelete, deepGet, deepSet } from '../../utils/object-change';
|
||||||
import {
|
import {
|
||||||
|
@ -163,6 +169,7 @@ function getTimestampSpec(headerAndRows: HeaderAndRows | null): TimestampSpec {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Step =
|
type Step =
|
||||||
|
| 'welcome'
|
||||||
| 'connect'
|
| 'connect'
|
||||||
| 'parser'
|
| 'parser'
|
||||||
| 'timestamp'
|
| 'timestamp'
|
||||||
|
@ -172,9 +179,11 @@ type Step =
|
||||||
| 'partition'
|
| 'partition'
|
||||||
| 'tuning'
|
| 'tuning'
|
||||||
| 'publish'
|
| 'publish'
|
||||||
| 'json-spec'
|
| 'spec'
|
||||||
| 'loading';
|
| 'loading';
|
||||||
|
|
||||||
const STEPS: Step[] = [
|
const STEPS: Step[] = [
|
||||||
|
'welcome',
|
||||||
'connect',
|
'connect',
|
||||||
'parser',
|
'parser',
|
||||||
'timestamp',
|
'timestamp',
|
||||||
|
@ -184,18 +193,19 @@ const STEPS: Step[] = [
|
||||||
'partition',
|
'partition',
|
||||||
'tuning',
|
'tuning',
|
||||||
'publish',
|
'publish',
|
||||||
'json-spec',
|
'spec',
|
||||||
'loading',
|
'loading',
|
||||||
];
|
];
|
||||||
|
|
||||||
const SECTIONS: { name: string; steps: Step[] }[] = [
|
const SECTIONS: { name: string; steps: Step[] }[] = [
|
||||||
{ name: 'Connect and parse raw data', steps: ['connect', 'parser', 'timestamp'] },
|
{ name: 'Connect and parse raw data', steps: ['welcome', 'connect', 'parser', 'timestamp'] },
|
||||||
{ name: 'Transform and configure schema', steps: ['transform', 'filter', 'schema'] },
|
{ name: 'Transform and configure schema', steps: ['transform', 'filter', 'schema'] },
|
||||||
{ name: 'Tune parameters', steps: ['partition', 'tuning', 'publish'] },
|
{ name: 'Tune parameters', steps: ['partition', 'tuning', 'publish'] },
|
||||||
{ name: 'Verify and submit', steps: ['json-spec'] },
|
{ name: 'Verify and submit', steps: ['spec'] },
|
||||||
];
|
];
|
||||||
|
|
||||||
const VIEW_TITLE: Record<Step, string> = {
|
const VIEW_TITLE: Record<Step, string> = {
|
||||||
|
welcome: 'Start',
|
||||||
connect: 'Connect',
|
connect: 'Connect',
|
||||||
parser: 'Parse data',
|
parser: 'Parse data',
|
||||||
timestamp: 'Parse time',
|
timestamp: 'Parse time',
|
||||||
|
@ -205,7 +215,7 @@ const VIEW_TITLE: Record<Step, string> = {
|
||||||
partition: 'Partition',
|
partition: 'Partition',
|
||||||
tuning: 'Tune',
|
tuning: 'Tune',
|
||||||
publish: 'Publish',
|
publish: 'Publish',
|
||||||
'json-spec': 'Edit JSON spec',
|
spec: 'Edit JSON spec',
|
||||||
loading: 'Loading',
|
loading: 'Loading',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -224,9 +234,11 @@ export interface LoadDataViewState {
|
||||||
newRollup: boolean | null;
|
newRollup: boolean | null;
|
||||||
newDimensionMode: DimensionMode | null;
|
newDimensionMode: DimensionMode | null;
|
||||||
|
|
||||||
// general
|
// welcome
|
||||||
overlordModules: string[] | null;
|
overlordModules: string[] | null;
|
||||||
overlordModuleNeededMessage: string | null;
|
selectedComboType: IngestionComboTypeWithExtra | null;
|
||||||
|
|
||||||
|
// general
|
||||||
sampleStrategy: SampleStrategy;
|
sampleStrategy: SampleStrategy;
|
||||||
columnFilter: string;
|
columnFilter: string;
|
||||||
specialColumnsOnly: boolean;
|
specialColumnsOnly: boolean;
|
||||||
|
@ -278,7 +290,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
let spec = parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
|
let spec = parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
|
||||||
if (!spec || typeof spec !== 'object') spec = {};
|
if (!spec || typeof spec !== 'object') spec = {};
|
||||||
this.state = {
|
this.state = {
|
||||||
step: 'connect',
|
step: 'welcome',
|
||||||
spec,
|
spec,
|
||||||
cacheKey: undefined,
|
cacheKey: undefined,
|
||||||
|
|
||||||
|
@ -287,9 +299,11 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
newRollup: null,
|
newRollup: null,
|
||||||
newDimensionMode: null,
|
newDimensionMode: null,
|
||||||
|
|
||||||
// general
|
// welcome
|
||||||
overlordModules: null,
|
overlordModules: null,
|
||||||
overlordModuleNeededMessage: null,
|
selectedComboType: null,
|
||||||
|
|
||||||
|
// general
|
||||||
sampleStrategy: 'start',
|
sampleStrategy: 'start',
|
||||||
columnFilter: '',
|
columnFilter: '',
|
||||||
specialColumnsOnly: false,
|
specialColumnsOnly: false,
|
||||||
|
@ -329,6 +343,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
const { spec } = this.state;
|
||||||
|
|
||||||
this.getOverlordModules();
|
this.getOverlordModules();
|
||||||
if (this.props.initTaskId) {
|
if (this.props.initTaskId) {
|
||||||
this.updateStep('loading');
|
this.updateStep('loading');
|
||||||
|
@ -336,6 +352,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
} else if (this.props.initSupervisorId) {
|
} else if (this.props.initSupervisorId) {
|
||||||
this.updateStep('loading');
|
this.updateStep('loading');
|
||||||
this.getSupervisorJson();
|
this.getSupervisorJson();
|
||||||
|
} else if (isEmptyIngestionSpec(spec)) {
|
||||||
|
this.updateStep('welcome');
|
||||||
} else {
|
} else {
|
||||||
this.updateStep('connect');
|
this.updateStep('connect');
|
||||||
}
|
}
|
||||||
|
@ -380,28 +398,18 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSpec = (newSpec: IngestionSpec) => {
|
private updateSpec = (newSpec: IngestionSpec) => {
|
||||||
if (!newSpec || typeof newSpec !== 'object') {
|
newSpec = normalizeSpec(newSpec);
|
||||||
// This does not match the type of IngestionSpec but this dialog is robust enough to deal with anything but spec must be an object
|
|
||||||
newSpec = {} as any;
|
|
||||||
}
|
|
||||||
this.setState({ spec: newSpec });
|
this.setState({ spec: newSpec });
|
||||||
localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSON.stringify(newSpec));
|
localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSON.stringify(newSpec));
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { step, spec } = this.state;
|
const { step } = this.state;
|
||||||
if (!Object.keys(spec).length && !this.props.initSupervisorId && !this.props.initTaskId) {
|
|
||||||
return (
|
|
||||||
<div className={classNames('load-data-view', 'app-view', 'init')}>
|
|
||||||
{this.renderInitStep()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('load-data-view', 'app-view', step)}>
|
<div className={classNames('load-data-view', 'app-view', step)}>
|
||||||
{this.renderStepNav()}
|
{this.renderStepNav()}
|
||||||
|
|
||||||
|
{step === 'welcome' && this.renderWelcomeStep()}
|
||||||
{step === 'connect' && this.renderConnectStep()}
|
{step === 'connect' && this.renderConnectStep()}
|
||||||
{step === 'parser' && this.renderParserStep()}
|
{step === 'parser' && this.renderParserStep()}
|
||||||
{step === 'timestamp' && this.renderTimestampStep()}
|
{step === 'timestamp' && this.renderTimestampStep()}
|
||||||
|
@ -414,7 +422,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
{step === 'tuning' && this.renderTuningStep()}
|
{step === 'tuning' && this.renderTuningStep()}
|
||||||
{step === 'publish' && this.renderPublishStep()}
|
{step === 'publish' && this.renderPublishStep()}
|
||||||
|
|
||||||
{step === 'json-spec' && this.renderJsonSpecStep()}
|
{step === 'spec' && this.renderSpecStep()}
|
||||||
{step === 'loading' && this.renderLoading()}
|
{step === 'loading' && this.renderLoading()}
|
||||||
|
|
||||||
{this.renderResetConfirm()}
|
{this.renderResetConfirm()}
|
||||||
|
@ -437,7 +445,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
key={s}
|
key={s}
|
||||||
active={s === step}
|
active={s === step}
|
||||||
onClick={() => this.updateStep(s)}
|
onClick={() => this.updateStep(s)}
|
||||||
icon={s === 'json-spec' && IconNames.MANUALLY_ENTERED_DATA}
|
icon={s === 'spec' && IconNames.MANUALLY_ENTERED_DATA}
|
||||||
text={VIEW_TITLE[s]}
|
text={VIEW_TITLE[s]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -484,79 +492,234 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
|
|
||||||
initWith(comboType: IngestionComboType) {
|
renderIngestionCard(comboType: IngestionComboTypeWithExtra) {
|
||||||
this.setState({
|
const { overlordModules, selectedComboType } = this.state;
|
||||||
spec: getBlankSpec(comboType),
|
|
||||||
});
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateStep('connect');
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIngestionCard(title: string, comboType: IngestionComboType, requiredModule?: string) {
|
|
||||||
const { overlordModules } = this.state;
|
|
||||||
if (!overlordModules) return null;
|
if (!overlordModules) return null;
|
||||||
|
const requiredModule = getRequiredModule(comboType);
|
||||||
const goodToGo = !requiredModule || overlordModules.includes(requiredModule);
|
const goodToGo = !requiredModule || overlordModules.includes(requiredModule);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={classNames({ disabled: !goodToGo })}
|
className={classNames({ disabled: !goodToGo, active: selectedComboType === comboType })}
|
||||||
interactive
|
interactive
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (goodToGo) {
|
this.setState({ selectedComboType: selectedComboType !== comboType ? comboType : null });
|
||||||
this.initWith(comboType);
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
overlordModuleNeededMessage: `${title} ingestion requires the '${requiredModule}' to be loaded.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
<img src={UrlBaser.base(`/assets/${getIngestionImage(comboType)}.png`)} />
|
||||||
|
<p>{getIngestionTitle(comboType)}</p>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInitStep() {
|
renderWelcomeStep() {
|
||||||
const { goToTask } = this.props;
|
const { spec } = this.state;
|
||||||
const { overlordModuleNeededMessage } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="intro">Please specify where your raw data is located</div>
|
<div className="main">
|
||||||
|
{this.renderIngestionCard('kafka')}
|
||||||
<div className="cards">
|
{this.renderIngestionCard('kinesis')}
|
||||||
{this.renderIngestionCard('Apache Kafka', 'kafka', 'druid-kafka-indexing-service')}
|
{this.renderIngestionCard('index:static-s3')}
|
||||||
{this.renderIngestionCard('AWS Kinesis', 'kinesis', 'druid-kinesis-indexing-service')}
|
{this.renderIngestionCard('index:static-google-blobstore')}
|
||||||
{this.renderIngestionCard('HTTP(s)', 'index:http')}
|
{this.renderIngestionCard('hadoop')}
|
||||||
{this.renderIngestionCard('AWS S3', 'index:static-s3', 'druid-s3-extensions')}
|
{this.renderIngestionCard('index:http')}
|
||||||
{this.renderIngestionCard(
|
{this.renderIngestionCard('index:local')}
|
||||||
'Google Cloud Storage',
|
{/* this.renderIngestionCard('example') */}
|
||||||
'index:static-google-blobstore',
|
{this.renderIngestionCard('other')}
|
||||||
'druid-google-extensions',
|
</div>
|
||||||
)}
|
<div className="control">
|
||||||
{this.renderIngestionCard('Local disk', 'index:local')}
|
<Callout className="intro">{this.renderWelcomeStepMessage()}</Callout>
|
||||||
<Card interactive onClick={() => goToTask(null, 'supervisor')}>
|
{this.renderWelcomeStepControls()}
|
||||||
Other (streaming)
|
{!isEmptyIngestionSpec(spec) && (
|
||||||
</Card>
|
<Button icon={IconNames.RESET} text="Reset spec" onClick={this.handleResetConfirm} />
|
||||||
<Card interactive onClick={() => goToTask(null, 'task')}>
|
)}
|
||||||
Other (batch)
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Alert
|
|
||||||
icon={IconNames.WARNING_SIGN}
|
|
||||||
intent={Intent.WARNING}
|
|
||||||
isOpen={Boolean(overlordModuleNeededMessage)}
|
|
||||||
confirmButtonText="Close"
|
|
||||||
onConfirm={() => this.setState({ overlordModuleNeededMessage: null })}
|
|
||||||
>
|
|
||||||
<p>{overlordModuleNeededMessage}</p>
|
|
||||||
</Alert>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderWelcomeStepMessage() {
|
||||||
|
const { selectedComboType } = this.state;
|
||||||
|
|
||||||
|
if (!selectedComboType) {
|
||||||
|
return <p>Please specify where your raw data is located</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issue = this.selectedIngestionTypeIssue();
|
||||||
|
if (issue) return issue;
|
||||||
|
|
||||||
|
switch (selectedComboType) {
|
||||||
|
case 'index:http':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Load data accessible through HTTP(s).</p>
|
||||||
|
<p>
|
||||||
|
Data must be in a text format and the HTTP(s) endpoint must be reachable by every
|
||||||
|
Druid process in the cluster.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'index:local':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<em>Recommended only in single server deployments.</em>
|
||||||
|
</p>
|
||||||
|
<p>Load data directly from a local file.</p>
|
||||||
|
<p>
|
||||||
|
Files must be in a text format and must be accessible to all the Druid processes in
|
||||||
|
the cluster.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'index:static-s3':
|
||||||
|
return <p>Load text based data from Amazon S3.</p>;
|
||||||
|
|
||||||
|
case 'index:static-google-blobstore':
|
||||||
|
return <p>Load text based data from the Google Blobstore.</p>;
|
||||||
|
|
||||||
|
case 'kafka':
|
||||||
|
return <p>Load streaming data in real-time from Apache Kafka.</p>;
|
||||||
|
|
||||||
|
case 'kinesis':
|
||||||
|
return <p>Load streaming data in real-time from Amazon Kinesis.</p>;
|
||||||
|
|
||||||
|
case 'hadoop':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<em>Data loader support coming soon!</em>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can not ingest data from HDFS via the data loader at this time, however you can
|
||||||
|
ingest it through a Druid task.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Please follow{' '}
|
||||||
|
<ExternalLink href="https://druid.apache.org/docs/latest/ingestion/hadoop.html">
|
||||||
|
the hadoop docs
|
||||||
|
</ExternalLink>{' '}
|
||||||
|
and submit a JSON spec to start the task.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'example':
|
||||||
|
return <p>Pick one of these examples to get you started.</p>;
|
||||||
|
|
||||||
|
case 'other':
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
If you do not see your source of raw data here, you can try to ingest it by submitting a{' '}
|
||||||
|
<ExternalLink href="https://druid.apache.org/docs/latest/ingestion/index.html">
|
||||||
|
JSON task or supervisor spec
|
||||||
|
</ExternalLink>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <p>Unknown ingestion type.</p>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWelcomeStepControls() {
|
||||||
|
const { goToTask } = this.props;
|
||||||
|
const { spec, selectedComboType } = this.state;
|
||||||
|
|
||||||
|
const issue = this.selectedIngestionTypeIssue();
|
||||||
|
if (issue) return null;
|
||||||
|
|
||||||
|
switch (selectedComboType) {
|
||||||
|
case 'index:http':
|
||||||
|
case 'index:local':
|
||||||
|
case 'index:static-s3':
|
||||||
|
case 'index:static-google-blobstore':
|
||||||
|
case 'kafka':
|
||||||
|
case 'kinesis':
|
||||||
|
return (
|
||||||
|
<FormGroup>
|
||||||
|
<Button
|
||||||
|
text="Connect data"
|
||||||
|
rightIcon={IconNames.ARROW_RIGHT}
|
||||||
|
onClick={() => {
|
||||||
|
this.setState({
|
||||||
|
spec: updateIngestionType(spec, selectedComboType as any),
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateStep('connect');
|
||||||
|
}, 10);
|
||||||
|
}}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'hadoop':
|
||||||
|
return (
|
||||||
|
<FormGroup>
|
||||||
|
<Button
|
||||||
|
text="Submit task"
|
||||||
|
rightIcon={IconNames.ARROW_RIGHT}
|
||||||
|
onClick={() => goToTask(null, 'task')}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'example':
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case 'other':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup>
|
||||||
|
<Button
|
||||||
|
text="Submit supervisor"
|
||||||
|
rightIcon={IconNames.ARROW_RIGHT}
|
||||||
|
onClick={() => goToTask(null, 'supervisor')}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<Button
|
||||||
|
text="Submit task"
|
||||||
|
rightIcon={IconNames.ARROW_RIGHT}
|
||||||
|
onClick={() => goToTask(null, 'task')}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIngestionTypeIssue(): JSX.Element | null {
|
||||||
|
const { selectedComboType, overlordModules } = this.state;
|
||||||
|
if (!selectedComboType || !overlordModules) return null;
|
||||||
|
|
||||||
|
const requiredModule = getRequiredModule(selectedComboType);
|
||||||
|
if (!requiredModule || overlordModules.includes(requiredModule)) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
{`${getIngestionTitle(selectedComboType)} ingestion requires the `}
|
||||||
|
<strong>{requiredModule}</strong>
|
||||||
|
{` extension to be loaded.`}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleResetConfirm = () => {
|
||||||
|
this.setState({ showResetConfirm: true });
|
||||||
|
};
|
||||||
|
|
||||||
renderResetConfirm() {
|
renderResetConfirm() {
|
||||||
const { showResetConfirm } = this.state;
|
const { showResetConfirm } = this.state;
|
||||||
if (!showResetConfirm) return null;
|
if (!showResetConfirm) return null;
|
||||||
|
@ -713,8 +876,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
if (!inputQueryState.data) return;
|
if (!inputQueryState.data) return;
|
||||||
this.updateSpec(fillDataSourceName(fillParser(spec, inputQueryState.data)));
|
this.updateSpec(fillDataSourceName(fillParser(spec, inputQueryState.data)));
|
||||||
},
|
},
|
||||||
prevLabel: 'Restart',
|
|
||||||
onPrevStep: () => this.setState({ showResetConfirm: true }),
|
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -2398,8 +2559,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await axios.get(`/druid/indexer/v1/supervisor/${initSupervisorId}`);
|
const resp = await axios.get(`/druid/indexer/v1/supervisor/${initSupervisorId}`);
|
||||||
this.updateSpec(normalizeSpecType(resp.data));
|
this.updateSpec(resp.data);
|
||||||
this.updateStep('json-spec');
|
this.updateStep('spec');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: `Failed to get supervisor spec: ${getDruidErrorMessage(e)}`,
|
message: `Failed to get supervisor spec: ${getDruidErrorMessage(e)}`,
|
||||||
|
@ -2413,8 +2574,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await axios.get(`/druid/indexer/v1/task/${initTaskId}`);
|
const resp = await axios.get(`/druid/indexer/v1/task/${initTaskId}`);
|
||||||
this.updateSpec(normalizeSpecType(resp.data.payload.spec));
|
this.updateSpec(resp.data.payload);
|
||||||
this.updateStep('json-spec');
|
this.updateStep('spec');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
message: `Failed to get task spec: ${getDruidErrorMessage(e)}`,
|
message: `Failed to get task spec: ${getDruidErrorMessage(e)}`,
|
||||||
|
@ -2427,7 +2588,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
return <Loader loading />;
|
return <Loader loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderJsonSpecStep() {
|
renderSpecStep() {
|
||||||
const { goToTask } = this.props;
|
const { goToTask } = this.props;
|
||||||
const { spec } = this.state;
|
const { spec } = this.state;
|
||||||
|
|
||||||
|
@ -2438,7 +2599,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
value={spec}
|
value={spec}
|
||||||
onChange={s => {
|
onChange={s => {
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
this.updateSpec(normalizeSpecType(s));
|
this.updateSpec(s);
|
||||||
}}
|
}}
|
||||||
height="100%"
|
height="100%"
|
||||||
/>
|
/>
|
||||||
|
@ -2455,6 +2616,14 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
</Callout>
|
</Callout>
|
||||||
</div>
|
</div>
|
||||||
<div className="next-bar">
|
<div className="next-bar">
|
||||||
|
{!isEmptyIngestionSpec(spec) && (
|
||||||
|
<Button
|
||||||
|
className="left"
|
||||||
|
icon={IconNames.RESET}
|
||||||
|
text="Reset spec"
|
||||||
|
onClick={this.handleResetConfirm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
text="Submit"
|
text="Submit"
|
||||||
intent={Intent.PRIMARY}
|
intent={Intent.PRIMARY}
|
||||||
|
|