This is an automated email from the ASF dual-hosted git repository.
fjy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git
The following commit(s) were added to refs/heads/master by this push:
new d677c83 Web console: Power up the data loader init step (#7947)
d677c83 is described below
commit d677c83ce43d4a31e1c08d68c2aa28ff13bdc753
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Wed Jun 26 15:50:48 2019 -0700
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
---
web-console/assets/druid.png | Bin 0 -> 15699 bytes
web-console/assets/example.png | Bin 0 -> 8873 bytes
web-console/assets/hadoop.png | Bin 0 -> 47082 bytes
web-console/assets/http.png | Bin 0 -> 10407 bytes
web-console/assets/kafka.png | Bin 0 -> 18022 bytes
web-console/assets/kinesis.png | Bin 0 -> 15631 bytes
web-console/assets/local.png | Bin 0 -> 9211 bytes
web-console/assets/other.png | Bin 0 -> 9549 bytes
web-console/assets/static-google-blobstore.png | Bin 0 -> 19344 bytes
web-console/assets/static-s3.png | Bin 0 -> 16541 bytes
web-console/package-lock.json | 12 +-
web-console/package.json | 3 +-
web-console/script/cp-to | 1 +
web-console/script/create-sql-function-doc.js | 16 +-
web-console/src/utils/ingestion-spec.tsx | 130 ++++++--
.../__snapshots__/load-data-view.spec.tsx.snap | 184 +++++++++--
.../src/views/load-data-view/load-data-view.scss | 43 ++-
.../src/views/load-data-view/load-data-view.tsx | 343 +++++++++++++++------
18 files changed, 561 insertions(+), 171 deletions(-)
diff --git a/web-console/assets/druid.png b/web-console/assets/druid.png
new file mode 100644
index 0000000..b494638
Binary files /dev/null and b/web-console/assets/druid.png differ
diff --git a/web-console/assets/example.png b/web-console/assets/example.png
new file mode 100644
index 0000000..faf9678
Binary files /dev/null and b/web-console/assets/example.png differ
diff --git a/web-console/assets/hadoop.png b/web-console/assets/hadoop.png
new file mode 100644
index 0000000..9b69d38
Binary files /dev/null and b/web-console/assets/hadoop.png differ
diff --git a/web-console/assets/http.png b/web-console/assets/http.png
new file mode 100644
index 0000000..ee041ba
Binary files /dev/null and b/web-console/assets/http.png differ
diff --git a/web-console/assets/kafka.png b/web-console/assets/kafka.png
new file mode 100644
index 0000000..0f0eb7c
Binary files /dev/null and b/web-console/assets/kafka.png differ
diff --git a/web-console/assets/kinesis.png b/web-console/assets/kinesis.png
new file mode 100644
index 0000000..d87a3cb
Binary files /dev/null and b/web-console/assets/kinesis.png differ
diff --git a/web-console/assets/local.png b/web-console/assets/local.png
new file mode 100644
index 0000000..25c523b
Binary files /dev/null and b/web-console/assets/local.png differ
diff --git a/web-console/assets/other.png b/web-console/assets/other.png
new file mode 100644
index 0000000..b2b977d
Binary files /dev/null and b/web-console/assets/other.png differ
diff --git a/web-console/assets/static-google-blobstore.png
b/web-console/assets/static-google-blobstore.png
new file mode 100644
index 0000000..960cbcf
Binary files /dev/null and b/web-console/assets/static-google-blobstore.png
differ
diff --git a/web-console/assets/static-s3.png b/web-console/assets/static-s3.png
new file mode 100644
index 0000000..e73112e
Binary files /dev/null and b/web-console/assets/static-s3.png differ
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index fde7305..7f7c804 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -229,9 +229,9 @@
}
},
"@blueprintjs/core": {
- "version": "3.15.1",
- "resolved":
"https://registry.npmjs.org/@blueprintjs/core/-/core-3.15.1.tgz",
- "integrity":
"sha512-M8ltbqqlMZuZ6SEuqo/3Fr59ZcUfd8Er7ocbm7EACVfRW7dRhOCd/TKkf2kfICNtCDwznwXk0iAePLXZhUGtQg==",
+ "version": "3.16.2",
+ "resolved":
"https://registry.npmjs.org/@blueprintjs/core/-/core-3.16.2.tgz",
+ "integrity":
"sha512-u+mSITWaNDwbdaPrbKx9XyxGsF4725SCAidWjd367ysX7AxCo4PK4SsFQVfXNylXpVWHQhJZekuo7+hdksc9lA==",
"requires": {
"@blueprintjs/icons": "^3.8.0",
"@types/dom4": "^2.0.1",
@@ -246,9 +246,9 @@
}
},
"@blueprintjs/icons": {
- "version": "3.8.0",
- "resolved":
"https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.8.0.tgz",
- "integrity":
"sha512-yHaRQ3vfV9Gf3foZ4ONtxddz+u5ufkHqHj8Ia5VhPbFgG4el+cPdmsGGIIM72rgKS1KQa5Ay+ggjpByUlXvrKg==",
+ "version": "3.9.0",
+ "resolved":
"https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.9.0.tgz",
+ "integrity":
"sha512-kq1Bh6PtOF4PcuxcDme8NmnSlkfO0IV89FriZGo6zSA1+OOzSwzvoKqa6S7vJe8xCPPLO5r7lE9AjeOuGeH97g==",
"requires": {
"classnames": "^2.2",
"tslib": "^1.9.0"
diff --git a/web-console/package.json b/web-console/package.json
index 3e07b65..a4369d6 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -50,7 +50,8 @@
"stylelint": "stylelint 'src/**/*.scss'"
},
"dependencies": {
- "@blueprintjs/core": "^3.15.1",
+ "@blueprintjs/core": "^3.16.2",
+ "@blueprintjs/icons": "^3.9.0",
"@types/memoize-one": "^4.1.1",
"axios": "^0.19.0",
"brace": "^0.11.1",
diff --git a/web-console/script/cp-to b/web-console/script/cp-to
index b8ad2eb..3943a10 100755
--- a/web-console/script/cp-to
+++ b/web-console/script/cp-to
@@ -25,3 +25,4 @@ cp -r coordinator-console "$1"
cp -r old-console "$1"
cp -r pages "$1"
cp -r public "$1"
+cp -r assets "$1"
diff --git a/web-console/script/create-sql-function-doc.js
b/web-console/script/create-sql-function-doc.js
index 51cac06..f8dd638 100755
--- a/web-console/script/create-sql-function-doc.js
+++ b/web-console/script/create-sql-function-doc.js
@@ -34,27 +34,31 @@ const readDoc = async () => {
if (functionMatch) {
functionDocs.push({
syntax: functionMatch[1],
- description: functionMatch[2]
- })
+ description: functionMatch[2],
+ });
}
const dataTypeMatch = line.match(/^\|([A-Z]+)\|([A-Z]+)\|(.*)\|(.*)\|$/);
if (dataTypeMatch) {
dataTypeDocs.push({
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
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
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 = `/*
diff --git a/web-console/src/utils/ingestion-spec.tsx
b/web-console/src/utils/ingestion-spec.tsx
index 84fa63c..dc115cf 100644
--- a/web-console/src/utils/ingestion-spec.tsx
+++ b/web-console/src/utils/ingestion-spec.tsx
@@ -37,6 +37,10 @@ export interface IngestionSpec {
tuningConfig?: TuningConfig;
}
+export function isEmptyIngestionSpec(spec: IngestionSpec) {
+ return Object.keys(spec).length === 0;
+}
+
export type IngestionType = 'kafka' | 'kinesis' | 'index_hadoop' | 'index' |
'index_parallel';
// A combination of IngestionType and firehose
@@ -48,6 +52,9 @@ export type IngestionComboType =
| 'index:static-s3'
| '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 {
switch (ingestionType) {
case 'kafka':
@@ -87,6 +94,65 @@ export function getIngestionComboType(spec: IngestionSpec):
IngestionComboType |
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 {
@@ -138,7 +204,7 @@ export function getRollup(spec: IngestionSpec): boolean {
return typeof specRollup === 'boolean' ? specRollup : true;
}
-export function getSpecType(spec: IngestionSpec): IngestionType | undefined {
+export function getSpecType(spec: Partial<IngestionSpec>): IngestionType |
undefined {
return (
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
* @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);
- if (!specType) return spec;
+ if (!specType) return spec as IngestionSpec;
if (!deepGet(spec, 'type')) spec = deepSet(spec, 'type', specType);
if (!deepGet(spec, 'ioConfig.type')) spec = deepSet(spec, 'ioConfig.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>[] = [
@@ -851,7 +925,7 @@ export function getIoConfigFormFields(ingestionComboType:
IngestionComboType): F
],
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">
here
</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(':');
if (ingestionType === 'index') ingestionType = 'index_parallel';
const ioAndTuningConfigType = ingestionTypeToIoAndTuningConfigType(
ingestionType as IngestionType,
);
- const granularitySpec: GranularitySpec = {
- type: 'uniform',
- segmentGranularity: ingestionType === 'index_parallel' ? 'DAY' : 'HOUR',
- queryGranularity: 'HOUR',
- };
-
- const spec: IngestionSpec = {
- type: ingestionType,
- dataSchema: {
- dataSource: 'new-data-source',
- granularitySpec,
- },
- ioConfig: {
- type: ioAndTuningConfigType,
- },
- tuningConfig: {
- type: ioAndTuningConfigType,
- },
- } as any;
+ let newSpec = spec;
+ newSpec = deepSet(newSpec, 'type', ingestionType);
+ newSpec = deepSet(newSpec, 'ioConfig.type', ioAndTuningConfigType);
+ newSpec = deepSet(newSpec, 'tuningConfig.type', ioAndTuningConfigType);
if (firehoseType) {
- spec.ioConfig.firehose = {
- type: firehoseType,
+ newSpec = deepSet(newSpec, 'ioConfig.firehose', { type: firehoseType });
+ }
+
+ 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 spec;
+ return newSpec;
}
export function fillParser(spec: IngestionSpec, sampleData: string[]):
IngestionSpec {
diff --git
a/web-console/src/views/load-data-view/__snapshots__/load-data-view.spec.tsx.snap
b/web-console/src/views/load-data-view/__snapshots__/load-data-view.spec.tsx.snap
index 829003f..9718f80 100644
---
a/web-console/src/views/load-data-view/__snapshots__/load-data-view.spec.tsx.snap
+++
b/web-console/src/views/load-data-view/__snapshots__/load-data-view.spec.tsx.snap
@@ -2,41 +2,169 @@
exports[`load data view matches snapshot 1`] = `
<div
- className="load-data-view app-view init"
+ className="load-data-view app-view welcome"
>
<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
- className="cards"
+ className="main"
+ />
+ <div
+ className="control"
>
- <Blueprint3.Card
- elevation={0}
- interactive={true}
- onClick={[Function]}
- >
- Other (streaming)
- </Blueprint3.Card>
- <Blueprint3.Card
- elevation={0}
- interactive={true}
- onClick={[Function]}
+ <Blueprint3.Callout
+ className="intro"
>
- Other (batch)
- </Blueprint3.Card>
+ <p>
+ Please specify where your raw data is located
+ </p>
+ </Blueprint3.Callout>
</div>
- <Blueprint3.Alert
- canEscapeKeyCancel={false}
- canOutsideClickCancel={false}
- confirmButtonText="Close"
- icon="warning-sign"
- intent="warning"
- isOpen={false}
- onConfirm={[Function]}
- >
- <p />
- </Blueprint3.Alert>
</div>
`;
diff --git a/web-console/src/views/load-data-view/load-data-view.scss
b/web-console/src/views/load-data-view/load-data-view.scss
index acd7ae0..847bea4 100644
--- a/web-console/src/views/load-data-view/load-data-view.scss
+++ b/web-console/src/views/load-data-view/load-data-view.scss
@@ -27,32 +27,45 @@
'main ctrl'
'main next';
- &.init {
- display: block;
+ &.welcome {
+ .main {
+ margin-left: -10px;
- & > * {
- margin-bottom: 15px;
- }
-
- .intro {
- font-size: 20px;
- }
-
- .cards {
.bp3-card {
+ position: relative;
display: inline-block;
vertical-align: top;
width: 250px;
height: 140px;
- margin-right: 15px;
+ margin-left: 15px;
margin-bottom: 15px;
- font-size: 24px;
+ font-size: 16px;
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 {
opacity: 0.4;
}
+
+ img {
+ width: 100px;
+ display: inline-block;
+ }
}
}
}
@@ -192,7 +205,7 @@
text-align: right;
padding: 0 5px;
- .prev {
+ .left {
float: left;
}
}
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx
b/web-console/src/views/load-data-view/load-data-view.tsx
index 0c3e426..efc853d 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -25,6 +25,7 @@ import {
Card,
Classes,
Code,
+ Elevation,
FormGroup,
H5,
HTMLSelect,
@@ -50,6 +51,7 @@ import {
} from '../../components';
import { AsyncActionDialog } from '../../dialogs';
import { AppToaster } from '../../singletons/toaster';
+import { UrlBaser } from '../../singletons/url-baser';
import {
filterMap,
getDruidErrorMessage,
@@ -72,18 +74,20 @@ import {
fillDataSourceName,
fillParser,
FlattenField,
- getBlankSpec,
getDimensionMode,
getDimensionSpecFormFields,
getEmptyTimestampSpec,
getFilterFormFields,
getFlattenFieldFormFields,
getIngestionComboType,
+ getIngestionImage,
+ getIngestionTitle,
getIoConfigFormFields,
getIoConfigTuningFormFields,
getMetricSpecFormFields,
getParseSpecFormFields,
getPartitionRelatedTuningSpecFormFields,
+ getRequiredModule,
getRollup,
getSpecType,
getTimestampSpecFormFields,
@@ -91,16 +95,17 @@ import {
getTuningSpecFormFields,
GranularitySpec,
hasParallelAbility,
- IngestionComboType,
+ IngestionComboTypeWithExtra,
IngestionSpec,
IoConfig,
isColumnTimestampSpec,
+ isEmptyIngestionSpec,
isParallel,
issueWithIoConfig,
issueWithParser,
joinFilter,
MetricSpec,
- normalizeSpecType,
+ normalizeSpec,
Parser,
ParseSpec,
parseSpecHasFlatten,
@@ -108,6 +113,7 @@ import {
TimestampSpec,
Transform,
TuningConfig,
+ updateIngestionType,
} from '../../utils/ingestion-spec';
import { deepDelete, deepGet, deepSet } from '../../utils/object-change';
import {
@@ -163,6 +169,7 @@ function getTimestampSpec(headerAndRows: HeaderAndRows |
null): TimestampSpec {
}
type Step =
+ | 'welcome'
| 'connect'
| 'parser'
| 'timestamp'
@@ -172,9 +179,11 @@ type Step =
| 'partition'
| 'tuning'
| 'publish'
- | 'json-spec'
+ | 'spec'
| 'loading';
+
const STEPS: Step[] = [
+ 'welcome',
'connect',
'parser',
'timestamp',
@@ -184,18 +193,19 @@ const STEPS: Step[] = [
'partition',
'tuning',
'publish',
- 'json-spec',
+ 'spec',
'loading',
];
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: '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> = {
+ welcome: 'Start',
connect: 'Connect',
parser: 'Parse data',
timestamp: 'Parse time',
@@ -205,7 +215,7 @@ const VIEW_TITLE: Record<Step, string> = {
partition: 'Partition',
tuning: 'Tune',
publish: 'Publish',
- 'json-spec': 'Edit JSON spec',
+ spec: 'Edit JSON spec',
loading: 'Loading',
};
@@ -224,9 +234,11 @@ export interface LoadDataViewState {
newRollup: boolean | null;
newDimensionMode: DimensionMode | null;
- // general
+ // welcome
overlordModules: string[] | null;
- overlordModuleNeededMessage: string | null;
+ selectedComboType: IngestionComboTypeWithExtra | null;
+
+ // general
sampleStrategy: SampleStrategy;
columnFilter: string;
specialColumnsOnly: boolean;
@@ -278,7 +290,7 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
let spec =
parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
if (!spec || typeof spec !== 'object') spec = {};
this.state = {
- step: 'connect',
+ step: 'welcome',
spec,
cacheKey: undefined,
@@ -287,9 +299,11 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
newRollup: null,
newDimensionMode: null,
- // general
+ // welcome
overlordModules: null,
- overlordModuleNeededMessage: null,
+ selectedComboType: null,
+
+ // general
sampleStrategy: 'start',
columnFilter: '',
specialColumnsOnly: false,
@@ -329,6 +343,8 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
}
componentDidMount(): void {
+ const { spec } = this.state;
+
this.getOverlordModules();
if (this.props.initTaskId) {
this.updateStep('loading');
@@ -336,6 +352,8 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
} else if (this.props.initSupervisorId) {
this.updateStep('loading');
this.getSupervisorJson();
+ } else if (isEmptyIngestionSpec(spec)) {
+ this.updateStep('welcome');
} else {
this.updateStep('connect');
}
@@ -380,28 +398,18 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
}
private updateSpec = (newSpec: IngestionSpec) => {
- if (!newSpec || typeof newSpec !== '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
- newSpec = {} as any;
- }
+ newSpec = normalizeSpec(newSpec);
this.setState({ spec: newSpec });
localStorageSet(LocalStorageKeys.INGESTION_SPEC, JSON.stringify(newSpec));
};
render() {
- const { step, spec } = 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>
- );
- }
-
+ const { step } = this.state;
return (
<div className={classNames('load-data-view', 'app-view', step)}>
{this.renderStepNav()}
+ {step === 'welcome' && this.renderWelcomeStep()}
{step === 'connect' && this.renderConnectStep()}
{step === 'parser' && this.renderParserStep()}
{step === 'timestamp' && this.renderTimestampStep()}
@@ -414,7 +422,7 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
{step === 'tuning' && this.renderTuningStep()}
{step === 'publish' && this.renderPublishStep()}
- {step === 'json-spec' && this.renderJsonSpecStep()}
+ {step === 'spec' && this.renderSpecStep()}
{step === 'loading' && this.renderLoading()}
{this.renderResetConfirm()}
@@ -437,7 +445,7 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
key={s}
active={s === step}
onClick={() => this.updateStep(s)}
- icon={s === 'json-spec' && IconNames.MANUALLY_ENTERED_DATA}
+ icon={s === 'spec' && IconNames.MANUALLY_ENTERED_DATA}
text={VIEW_TITLE[s]}
/>
))}
@@ -484,79 +492,234 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
// ==================================================================
- initWith(comboType: IngestionComboType) {
- this.setState({
- spec: getBlankSpec(comboType),
- });
- setTimeout(() => {
- this.updateStep('connect');
- }, 10);
- }
-
- renderIngestionCard(title: string, comboType: IngestionComboType,
requiredModule?: string) {
- const { overlordModules } = this.state;
+ renderIngestionCard(comboType: IngestionComboTypeWithExtra) {
+ const { overlordModules, selectedComboType } = this.state;
if (!overlordModules) return null;
+ const requiredModule = getRequiredModule(comboType);
const goodToGo = !requiredModule ||
overlordModules.includes(requiredModule);
return (
<Card
- className={classNames({ disabled: !goodToGo })}
+ className={classNames({ disabled: !goodToGo, active: selectedComboType
=== comboType })}
interactive
onClick={() => {
- if (goodToGo) {
- this.initWith(comboType);
- } else {
- this.setState({
- overlordModuleNeededMessage: `${title} ingestion requires the
'${requiredModule}' to be loaded.`,
- });
- }
+ this.setState({ selectedComboType: selectedComboType !== comboType ?
comboType : null });
}}
>
- {title}
+ <img
src={UrlBaser.base(`/assets/${getIngestionImage(comboType)}.png`)} />
+ <p>{getIngestionTitle(comboType)}</p>
</Card>
);
}
- renderInitStep() {
- const { goToTask } = this.props;
- const { overlordModuleNeededMessage } = this.state;
+ renderWelcomeStep() {
+ const { spec } = this.state;
return (
<>
- <div className="intro">Please specify where your raw data is
located</div>
-
- <div className="cards">
- {this.renderIngestionCard('Apache Kafka', 'kafka',
'druid-kafka-indexing-service')}
- {this.renderIngestionCard('AWS Kinesis', 'kinesis',
'druid-kinesis-indexing-service')}
- {this.renderIngestionCard('HTTP(s)', 'index:http')}
- {this.renderIngestionCard('AWS S3', 'index:static-s3',
'druid-s3-extensions')}
- {this.renderIngestionCard(
- 'Google Cloud Storage',
- 'index:static-google-blobstore',
- 'druid-google-extensions',
+ <div className="main">
+ {this.renderIngestionCard('kafka')}
+ {this.renderIngestionCard('kinesis')}
+ {this.renderIngestionCard('index:static-s3')}
+ {this.renderIngestionCard('index:static-google-blobstore')}
+ {this.renderIngestionCard('hadoop')}
+ {this.renderIngestionCard('index:http')}
+ {this.renderIngestionCard('index:local')}
+ {/* this.renderIngestionCard('example') */}
+ {this.renderIngestionCard('other')}
+ </div>
+ <div className="control">
+ <Callout
className="intro">{this.renderWelcomeStepMessage()}</Callout>
+ {this.renderWelcomeStepControls()}
+ {!isEmptyIngestionSpec(spec) && (
+ <Button icon={IconNames.RESET} text="Reset spec"
onClick={this.handleResetConfirm} />
)}
- {this.renderIngestionCard('Local disk', 'index:local')}
- <Card interactive onClick={() => goToTask(null, 'supervisor')}>
- Other (streaming)
- </Card>
- <Card interactive onClick={() => goToTask(null, 'task')}>
- Other (batch)
- </Card>
</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() {
const { showResetConfirm } = this.state;
if (!showResetConfirm) return null;
@@ -713,8 +876,6 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
if (!inputQueryState.data) return;
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 {
const resp = await
axios.get(`/druid/indexer/v1/supervisor/${initSupervisorId}`);
- this.updateSpec(normalizeSpecType(resp.data));
- this.updateStep('json-spec');
+ this.updateSpec(resp.data);
+ this.updateStep('spec');
} catch (e) {
AppToaster.show({
message: `Failed to get supervisor spec: ${getDruidErrorMessage(e)}`,
@@ -2413,8 +2574,8 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
try {
const resp = await axios.get(`/druid/indexer/v1/task/${initTaskId}`);
- this.updateSpec(normalizeSpecType(resp.data.payload.spec));
- this.updateStep('json-spec');
+ this.updateSpec(resp.data.payload);
+ this.updateStep('spec');
} catch (e) {
AppToaster.show({
message: `Failed to get task spec: ${getDruidErrorMessage(e)}`,
@@ -2427,7 +2588,7 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
return <Loader loading />;
}
- renderJsonSpecStep() {
+ renderSpecStep() {
const { goToTask } = this.props;
const { spec } = this.state;
@@ -2438,7 +2599,7 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
value={spec}
onChange={s => {
if (!s) return;
- this.updateSpec(normalizeSpecType(s));
+ this.updateSpec(s);
}}
height="100%"
/>
@@ -2455,6 +2616,14 @@ export class LoadDataView extends
React.PureComponent<LoadDataViewProps, LoadDat
</Callout>
</div>
<div className="next-bar">
+ {!isEmptyIngestionSpec(spec) && (
+ <Button
+ className="left"
+ icon={IconNames.RESET}
+ text="Reset spec"
+ onClick={this.handleResetConfirm}
+ />
+ )}
<Button
text="Submit"
intent={Intent.PRIMARY}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]