mirror of https://github.com/apache/druid.git
Data loader (GUI component) (#7572)
* data loader init * fix timecolumn text * feedback changes * fixing typos and improving error reporting * added local firehose warning * update warning copy * refine copy * better copy * fix tests * remove console log * copy change * add banner message
This commit is contained in:
parent
afbcb9c07f
commit
baf54f373c
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,16 @@
|
|||
"type": "git",
|
||||
"url": "https://github.com/apache/druid/"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "jsdom",
|
||||
"moduleNameMapper": {
|
||||
"\\.scss$": "identity-obj-proxy"
|
||||
},
|
||||
"testMatch": [
|
||||
"**/?(*.)+(spec).ts?(x)"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "./script/watch",
|
||||
"compile": "./script/build",
|
||||
|
@ -16,7 +26,7 @@
|
|||
"test": "jest --silent 2>&1",
|
||||
"tslint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter 'src/**/*.ts?(x)'",
|
||||
"tslint-fix": "npm run tslint -- --fix",
|
||||
"tslint-changed-only": "git diff --diff-filter=ACMR --name-only | grep -E \\.tsx\\?$ | xargs ./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter",
|
||||
"tslint-changed-only": "git diff --diff-filter=ACMR --cached --name-only | grep -E \\.tsx\\?$ | xargs ./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter",
|
||||
"tslint-fix-changed-only": "npm run tslint-changed-only -- --fix",
|
||||
"generate-licenses-file": "license-checker --production --json --out licenses.json",
|
||||
"check-licenses": "license-checker --production --onlyAllow 'Apache-1.1;Apache-2.0;BSD-2-Clause;BSD-3-Clause;MIT;CC0-1.0' --summary",
|
||||
|
@ -49,7 +59,6 @@
|
|||
"@types/hjson": "^2.4.1",
|
||||
"@types/jest": "^24.0.11",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/node": "^11.13.4",
|
||||
"@types/numeral": "^0.0.25",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
|
@ -62,7 +71,6 @@
|
|||
"ignore-styles": "^5.0.1",
|
||||
"jest": "^24.7.1",
|
||||
"license-checker": "^25.0.1",
|
||||
"mocha": "^6.1.3",
|
||||
"node-sass": "^4.11.0",
|
||||
"node-sass-chokidar": "^1.3.4",
|
||||
"postcss-cli": "^6.1.2",
|
||||
|
@ -81,6 +89,7 @@
|
|||
"tslint-loader": "^3.5.4",
|
||||
"typescript": "^3.4.3",
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-cli": "^3.3.0",
|
||||
"webpack-dev-server": "^3.3.1"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
#!/usr/bin/env node
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
let fs = require('fs-extra');
|
||||
|
||||
if (!(process.argv.length === 3 || process.argv.length === 4)) {
|
||||
console.log('Usage: mkcomp <what?> <component-name>');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
let name;
|
||||
let what;
|
||||
if (process.argv.length === 4) {
|
||||
what = process.argv[2];
|
||||
name = process.argv[3];
|
||||
if (!(what === 'component' || what === 'dialog' || what === 'singleton')) {
|
||||
console.log(`Bad what, should be on of: component, dialog, singleton`);
|
||||
process.exit();
|
||||
}
|
||||
} else {
|
||||
what = 'component';
|
||||
name = process.argv[2];
|
||||
}
|
||||
|
||||
if (!/^([a-z-])+$/.test(name)) {
|
||||
console.log('must be a hyphen case name');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
let path = `./src/${what}s/`;
|
||||
fs.ensureDirSync(path);
|
||||
console.log('Making path:', path);
|
||||
|
||||
const camelName = name.replace(/(^|-)[a-z]/g, (s) => s.replace('-', '').toUpperCase());
|
||||
const year = (new Date()).getFullYear();
|
||||
|
||||
function writeFile(path, data) {
|
||||
try {
|
||||
return fs.writeFileSync(path, data, {
|
||||
flag: 'wx', // x = fail if file exists
|
||||
encoding: 'utf8'
|
||||
});
|
||||
} catch (error) {
|
||||
return console.log(`Skipping ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Make the TypeScript file
|
||||
writeFile(path + name + '.tsx',
|
||||
`/*
|
||||
* 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 { Button, InputGroup } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
import './${name}.scss';
|
||||
|
||||
export interface ${camelName}Props extends React.Props<any> {
|
||||
}
|
||||
|
||||
export interface ${camelName}State {
|
||||
}
|
||||
|
||||
export class ${camelName} extends React.Component<${camelName}Props, ${camelName}State> {
|
||||
constructor(props: ${camelName}Props, context: any) {
|
||||
super(props, context);
|
||||
// this.state = {};
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="${name}">
|
||||
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
// Make the SASS file
|
||||
writeFile(path + name + '.scss',
|
||||
`/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.${name} {
|
||||
|
||||
}
|
||||
`);
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 { InputGroup, ITagInputProps } from '@blueprintjs/core';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ArrayInputProps extends ITagInputProps {
|
||||
|
||||
}
|
||||
|
||||
export class ArrayInput extends React.Component<ArrayInputProps, { stringValue: string }> {
|
||||
constructor(props: ArrayInputProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
stringValue: Array.isArray(props.values) ? props.values.join(', ') : ''
|
||||
};
|
||||
}
|
||||
|
||||
private handleChange = (e: any) => {
|
||||
const { onChange } = this.props;
|
||||
const stringValue = e.target.value;
|
||||
const newValues = stringValue.split(',').map((v: string) => v.trim());
|
||||
const newValuesFiltered = newValues.filter(Boolean);
|
||||
this.setState({
|
||||
stringValue: newValues.length === newValuesFiltered.length ? newValues.join(', ') : stringValue
|
||||
});
|
||||
if (onChange) onChange(stringValue === '' ? undefined : newValuesFiltered);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, placeholder, large, disabled } = this.props;
|
||||
const { stringValue } = this.state;
|
||||
return <InputGroup
|
||||
className={className}
|
||||
value={stringValue}
|
||||
onChange={this.handleChange}
|
||||
placeholder={placeholder}
|
||||
large={large}
|
||||
disabled={disabled}
|
||||
/>;
|
||||
}
|
||||
}
|
|
@ -20,4 +20,22 @@
|
|||
.ace_scroller {
|
||||
background-color: #212c36;
|
||||
}
|
||||
|
||||
// Popover in info label
|
||||
label.bp3-label {
|
||||
position: relative;
|
||||
|
||||
.bp3-text-muted {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
.bp3-popover-wrapper {
|
||||
display: inline;
|
||||
|
||||
.bp3-popover-target {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,35 +16,62 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputGroup } from '@blueprintjs/core';
|
||||
import { FormGroup, HTMLSelect, NumericInput, TagInput } from '@blueprintjs/core';
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
HTMLSelect,
|
||||
Icon,
|
||||
InputGroup,
|
||||
Menu,
|
||||
MenuItem,
|
||||
NumericInput,
|
||||
Popover,
|
||||
Position
|
||||
} from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import * as React from 'react';
|
||||
|
||||
import { deepDelete, deepGet, deepSet } from '../utils/object-change';
|
||||
|
||||
import { ArrayInput } from './array-input';
|
||||
import { JSONInput } from './json-input';
|
||||
|
||||
import './auto-form.scss';
|
||||
|
||||
interface Field {
|
||||
export interface SuggestionGroup {
|
||||
group: string;
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
export interface Field<T> {
|
||||
name: string;
|
||||
label?: string;
|
||||
info?: React.ReactNode;
|
||||
type: 'number' | 'size-bytes' | 'string' | 'boolean' | 'string-array' | 'json';
|
||||
defaultValue?: any;
|
||||
isDefined?: (model: T) => boolean;
|
||||
disabled?: boolean;
|
||||
suggestions?: (string | SuggestionGroup)[];
|
||||
placeholder?: string;
|
||||
min?: number;
|
||||
}
|
||||
|
||||
export interface AutoFormProps<T> extends React.Props<any> {
|
||||
fields: Field[];
|
||||
fields: Field<T>[];
|
||||
model: T | null;
|
||||
onChange: (newValue: T) => void;
|
||||
onChange: (newModel: T) => void;
|
||||
showCustom?: (model: T) => boolean;
|
||||
updateJSONValidity?: (jsonValidity: boolean) => void;
|
||||
large?: boolean;
|
||||
}
|
||||
|
||||
export interface AutoFormState<T> {
|
||||
jsonInputsValidity: any;
|
||||
}
|
||||
|
||||
export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState<T>> {
|
||||
export class AutoForm<T extends Record<string, any>> extends React.Component<AutoFormProps<T>, AutoFormState<T>> {
|
||||
static makeLabelName(label: string): string {
|
||||
let newLabel = label.split(/(?=[A-Z])/).map(s => s.toLowerCase()).join(' ');
|
||||
let newLabel = label.split(/(?=[A-Z])/).join(' ').toLowerCase().replace(/\./g, ' ');
|
||||
newLabel = newLabel[0].toUpperCase() + newLabel.slice(1);
|
||||
return newLabel;
|
||||
}
|
||||
|
@ -56,57 +83,135 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState
|
|||
};
|
||||
}
|
||||
|
||||
private renderNumberInput(field: Field): JSX.Element {
|
||||
const { model, onChange } = this.props;
|
||||
private fieldChange = (field: Field<T>, newValue: any) => {
|
||||
const { model } = this.props;
|
||||
if (!model) return;
|
||||
const newModel = typeof newValue === 'undefined' ? deepDelete(model, field.name) : deepSet(model, field.name, newValue);
|
||||
this.modelChange(newModel);
|
||||
}
|
||||
|
||||
private modelChange = (newModel: T) => {
|
||||
const { fields, onChange } = this.props;
|
||||
|
||||
for (const someField of fields) {
|
||||
if (someField.isDefined && !someField.isDefined(newModel)) {
|
||||
newModel = deepDelete(newModel, someField.name);
|
||||
} else if (typeof someField.defaultValue !== 'undefined' && typeof deepGet(newModel, someField.name) === 'undefined') {
|
||||
newModel = deepSet(newModel, someField.name, someField.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(newModel);
|
||||
}
|
||||
|
||||
private renderNumberInput(field: Field<T>): JSX.Element {
|
||||
const { model, large } = this.props;
|
||||
return <NumericInput
|
||||
value={(model as any)[field.name]}
|
||||
onValueChange={(v: any) => {
|
||||
if (isNaN(v)) return;
|
||||
onChange(Object.assign({}, model, { [field.name]: v }));
|
||||
value={deepGet(model as any, field.name) || field.defaultValue}
|
||||
onValueChange={(valueAsNumber: number, valueAsString: string) => {
|
||||
if (valueAsString === '') {
|
||||
this.fieldChange(field, undefined);
|
||||
return;
|
||||
}
|
||||
if (isNaN(valueAsNumber)) return;
|
||||
this.fieldChange(field, valueAsNumber);
|
||||
}}
|
||||
min={field.min || 0}
|
||||
fill
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
placeholder={field.placeholder}
|
||||
/>;
|
||||
}
|
||||
|
||||
private renderSizeBytesInput(field: Field): JSX.Element {
|
||||
const { model, onChange } = this.props;
|
||||
private renderSizeBytesInput(field: Field<T>): JSX.Element {
|
||||
const { model, large } = this.props;
|
||||
return <NumericInput
|
||||
value={(model as any)[field.name]}
|
||||
value={deepGet(model as any, field.name) || field.defaultValue}
|
||||
onValueChange={(v: number) => {
|
||||
if (isNaN(v)) return;
|
||||
onChange(Object.assign({}, model, { [field.name]: v }));
|
||||
this.fieldChange(field, v);
|
||||
}}
|
||||
min={0}
|
||||
stepSize={1000}
|
||||
majorStepSize={1000000}
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
/>;
|
||||
}
|
||||
|
||||
private renderStringInput(field: Field): JSX.Element {
|
||||
const { model, onChange } = this.props;
|
||||
private renderStringInput(field: Field<T>): JSX.Element {
|
||||
const { model, large } = this.props;
|
||||
|
||||
const suggestionsMenu = field.suggestions ?
|
||||
<Menu>
|
||||
{
|
||||
field.suggestions.map(suggestion => {
|
||||
if (typeof suggestion === 'string') {
|
||||
return <MenuItem
|
||||
key={suggestion}
|
||||
text={suggestion}
|
||||
onClick={() => this.fieldChange(field, suggestion)}
|
||||
/>;
|
||||
} else {
|
||||
return <MenuItem
|
||||
key={suggestion.group}
|
||||
text={suggestion.group}
|
||||
>
|
||||
{
|
||||
suggestion.suggestions.map(suggestion => (
|
||||
<MenuItem
|
||||
key={suggestion}
|
||||
text={suggestion}
|
||||
onClick={() => this.fieldChange(field, suggestion)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</MenuItem>;
|
||||
}
|
||||
})
|
||||
}
|
||||
</Menu> :
|
||||
undefined;
|
||||
|
||||
return <InputGroup
|
||||
value={(model as any)[field.name]}
|
||||
onChange={(v: any) => {
|
||||
onChange(Object.assign({}, model, { [field.name]: v }));
|
||||
value={deepGet(model as any, field.name) || field.defaultValue || ''}
|
||||
onChange={(e: any) => {
|
||||
const v = e.target.value;
|
||||
this.fieldChange(field, v === '' ? undefined : v);
|
||||
}}
|
||||
placeholder={field.placeholder}
|
||||
rightElement={
|
||||
suggestionsMenu &&
|
||||
<Popover content={suggestionsMenu} position={Position.BOTTOM_RIGHT} autoFocus={false}>
|
||||
<Button icon={IconNames.CARET_DOWN} minimal />
|
||||
</Popover>
|
||||
}
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
/>;
|
||||
}
|
||||
|
||||
private renderBooleanInput(field: Field): JSX.Element {
|
||||
const { model, onChange } = this.props;
|
||||
private renderBooleanInput(field: Field<T>): JSX.Element {
|
||||
const { model, large } = this.props;
|
||||
let curValue = deepGet(model as any, field.name);
|
||||
if (curValue == null) curValue = field.defaultValue;
|
||||
return <HTMLSelect
|
||||
value={(model as any)[field.name] === true ? 'True' : 'False'}
|
||||
value={curValue === true ? 'True' : 'False'}
|
||||
onChange={(e: any) => {
|
||||
onChange(Object.assign({}, model, { [field.name]: e.currentTarget.value === 'True' }));
|
||||
const v = e.currentTarget.value === 'True';
|
||||
this.fieldChange(field, v);
|
||||
}}
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
>
|
||||
<option value="True">True</option>
|
||||
<option value="False">False</option>
|
||||
</HTMLSelect>;
|
||||
}
|
||||
|
||||
private renderJSONInput(field: Field): JSX.Element {
|
||||
const { model, onChange, updateJSONValidity } = this.props;
|
||||
private renderJSONInput(field: Field<T>): JSX.Element {
|
||||
const { model, updateJSONValidity } = this.props;
|
||||
const { jsonInputsValidity } = this.state;
|
||||
|
||||
const updateInputValidity = (e: any) => {
|
||||
|
@ -121,25 +226,28 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState
|
|||
};
|
||||
|
||||
return <JSONInput
|
||||
value={(model as any)[field.name]}
|
||||
onChange={(e: any) => onChange(Object.assign({}, model, { [field.name]: e}))}
|
||||
value={deepGet(model as any, field.name)}
|
||||
onChange={(v: any) => this.fieldChange(field, v)}
|
||||
updateInputValidity={updateInputValidity}
|
||||
/>;
|
||||
}
|
||||
|
||||
private renderStringArrayInput(field: Field): JSX.Element {
|
||||
const { model, onChange } = this.props;
|
||||
return <TagInput
|
||||
values={(model as any)[field.name] || []}
|
||||
private renderStringArrayInput(field: Field<T>): JSX.Element {
|
||||
const { model, large } = this.props;
|
||||
return <ArrayInput
|
||||
values={deepGet(model as any, field.name) || []}
|
||||
onChange={(v: any) => {
|
||||
onChange(Object.assign({}, model, { [field.name]: v }));
|
||||
this.fieldChange(field, v);
|
||||
}}
|
||||
placeholder={field.placeholder}
|
||||
addOnBlur
|
||||
fill
|
||||
large={large}
|
||||
disabled={field.disabled}
|
||||
/>;
|
||||
}
|
||||
|
||||
renderFieldInput(field: Field) {
|
||||
renderFieldInput(field: Field<T>) {
|
||||
switch (field.type) {
|
||||
case 'number': return this.renderNumberInput(field);
|
||||
case 'size-bytes': return this.renderSizeBytesInput(field);
|
||||
|
@ -151,17 +259,45 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState
|
|||
}
|
||||
}
|
||||
|
||||
renderField(field: Field) {
|
||||
private renderField = (field: Field<T>) => {
|
||||
const { model } = this.props;
|
||||
if (!model) return null;
|
||||
if (field.isDefined && !field.isDefined(model)) return null;
|
||||
|
||||
const label = field.label || AutoForm.makeLabelName(field.name);
|
||||
return <FormGroup label={label} key={field.name}>
|
||||
return <FormGroup
|
||||
key={field.name}
|
||||
label={label}
|
||||
labelInfo={
|
||||
field.info &&
|
||||
<Popover
|
||||
content={<div className="label-info-text">{field.info}</div>}
|
||||
position="left-bottom"
|
||||
>
|
||||
<Icon icon={IconNames.INFO_SIGN} iconSize={14}/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
{this.renderFieldInput(field)}
|
||||
</FormGroup>;
|
||||
}
|
||||
|
||||
renderCustom() {
|
||||
const { model } = this.props;
|
||||
|
||||
return <FormGroup label="Custom" key="custom">
|
||||
<JSONInput
|
||||
value={model}
|
||||
onChange={this.modelChange}
|
||||
/>
|
||||
</FormGroup>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fields, model } = this.props;
|
||||
const { fields, model, showCustom } = this.props;
|
||||
return <div className="auto-form">
|
||||
{model && fields.map(field => this.renderField(field))}
|
||||
{model && fields.map(this.renderField)}
|
||||
{model && showCustom && showCustom(model) && this.renderCustom()}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.center-message {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
overflow: hidden;
|
||||
|
||||
.center-message-inner {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
|
||||
import './center-message.scss';
|
||||
|
||||
export interface CenterMessageProps extends React.Props<any> {
|
||||
}
|
||||
|
||||
export class CenterMessage extends React.Component<CenterMessageProps, {}> {
|
||||
render() {
|
||||
return <div className="center-message bp3-input">
|
||||
<div className="center-message-inner">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { Button, InputGroup } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
||||
export interface ClearableInputProps extends React.Props<any> {
|
||||
className?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export class ClearableInput extends React.Component<ClearableInputProps, {}> {
|
||||
render() {
|
||||
const { className, value, onChange, placeholder } = this.props;
|
||||
|
||||
return <InputGroup
|
||||
className={classNames('clearable-input', className)}
|
||||
value={value}
|
||||
onChange={(e: any) => onChange(e.target.value)}
|
||||
rightElement={value ? <Button icon={IconNames.CROSS} minimal onClick={() => onChange('')} /> : undefined}
|
||||
placeholder={placeholder}
|
||||
/>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
|
||||
export interface ExternalLinkProps extends React.Props<any> {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export class ExternalLink extends React.Component<ExternalLinkProps, {}> {
|
||||
render() {
|
||||
const { href, children } = this.props;
|
||||
|
||||
return <a
|
||||
href={href}
|
||||
target="_blank"
|
||||
>
|
||||
{children}
|
||||
</a>;
|
||||
}
|
||||
}
|
|
@ -33,12 +33,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.config-popover .bp3-popover-content,
|
||||
.legacy-popover .bp3-popover-content {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.help-popover .bp3-popover-content {
|
||||
width: 180px;
|
||||
* {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,19 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Alignment, AnchorButton, Button, Classes, Menu, MenuItem, Navbar, NavbarDivider, NavbarGroup, Popover, Position } from '@blueprintjs/core';
|
||||
import {
|
||||
Alignment,
|
||||
AnchorButton,
|
||||
Button, Intent,
|
||||
Menu,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
Navbar,
|
||||
NavbarDivider,
|
||||
NavbarGroup,
|
||||
Popover,
|
||||
Position
|
||||
} from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
|
@ -24,6 +36,7 @@ import * as React from 'react';
|
|||
import { AboutDialog } from '../dialogs/about-dialog';
|
||||
import { CoordinatorDynamicConfigDialog } from '../dialogs/coordinator-dynamic-config';
|
||||
import { OverlordDynamicConfigDialog } from '../dialogs/overlord-dynamic-config';
|
||||
import { getWikipediaSpec } from '../utils/example-ingestion-spec';
|
||||
import {
|
||||
DRUID_DOCS,
|
||||
DRUID_GITHUB,
|
||||
|
@ -31,14 +44,16 @@ import {
|
|||
LEGACY_COORDINATOR_CONSOLE,
|
||||
LEGACY_OVERLORD_CONSOLE
|
||||
} from '../variables';
|
||||
import { LoadDataViewSeed } from '../views/load-data-view';
|
||||
|
||||
import './header-bar.scss';
|
||||
|
||||
export type HeaderActiveTab = null | 'datasources' | 'segments' | 'tasks' | 'servers' | 'sql' | 'lookups';
|
||||
export type HeaderActiveTab = null | 'load-data' | 'query' | 'datasources' | 'segments' | 'tasks' | 'servers' | 'lookups';
|
||||
|
||||
export interface HeaderBarProps extends React.Props<any> {
|
||||
active: HeaderActiveTab;
|
||||
hideLegacy: boolean;
|
||||
goToLoadDataView: (loadDataViewSeed: LoadDataViewSeed) => void;
|
||||
}
|
||||
|
||||
export interface HeaderBarState {
|
||||
|
@ -110,6 +125,8 @@ export class HeaderBar extends React.Component<HeaderBarProps, HeaderBarState> {
|
|||
const { active, hideLegacy } = this.props;
|
||||
const { aboutDialogOpen, coordinatorDynamicConfigDialogOpen, overlordDynamicConfigDialogOpen } = this.state;
|
||||
|
||||
const loadDataPrimary = false;
|
||||
|
||||
const legacyMenu = <Menu>
|
||||
<MenuItem icon={IconNames.GRAPH} text="Legacy coordinator console" href={LEGACY_COORDINATOR_CONSOLE} target="_blank" />
|
||||
<MenuItem icon={IconNames.MAP} text="Legacy overlord console" href={LEGACY_OVERLORD_CONSOLE} target="_blank" />
|
||||
|
@ -123,7 +140,7 @@ export class HeaderBar extends React.Component<HeaderBarProps, HeaderBarState> {
|
|||
</Menu>;
|
||||
|
||||
const configMenu = <Menu>
|
||||
<MenuItem icon={IconNames.COG} text="Coordinator dynamic config" onClick={() => this.setState({ coordinatorDynamicConfigDialogOpen: true })}/>
|
||||
<MenuItem icon={IconNames.SETTINGS} text="Coordinator dynamic config" onClick={() => this.setState({ coordinatorDynamicConfigDialogOpen: true })}/>
|
||||
<MenuItem icon={IconNames.WRENCH} text="Overlord dynamic config" onClick={() => this.setState({ overlordDynamicConfigDialogOpen: true })}/>
|
||||
<MenuItem icon={IconNames.PROPERTIES} active={active === 'lookups'} text="Lookups" href="#lookups"/>
|
||||
</Menu>;
|
||||
|
@ -133,26 +150,39 @@ export class HeaderBar extends React.Component<HeaderBarProps, HeaderBarState> {
|
|||
<a href="#">
|
||||
{this.renderLogo()}
|
||||
</a>
|
||||
<NavbarDivider />
|
||||
|
||||
<NavbarDivider/>
|
||||
<AnchorButton
|
||||
icon={IconNames.CLOUD_UPLOAD}
|
||||
text="Load data"
|
||||
active={active === 'load-data'}
|
||||
href="#load-data"
|
||||
minimal={!loadDataPrimary}
|
||||
intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE}
|
||||
/>
|
||||
<AnchorButton minimal active={active === 'query'} icon={IconNames.APPLICATION} text="Query" href="#query" />
|
||||
|
||||
<NavbarDivider/>
|
||||
<AnchorButton minimal active={active === 'datasources'} icon={IconNames.MULTI_SELECT} text="Datasources" href="#datasources" />
|
||||
<AnchorButton minimal active={active === 'segments'} icon={IconNames.STACKED_CHART} text="Segments" href="#segments" />
|
||||
<AnchorButton minimal active={active === 'tasks'} icon={IconNames.GANTT_CHART} text="Tasks" href="#tasks" />
|
||||
|
||||
<NavbarDivider/>
|
||||
<AnchorButton minimal active={active === 'servers'} icon={IconNames.DATABASE} text="Data servers" href="#servers" />
|
||||
<NavbarDivider />
|
||||
<AnchorButton minimal active={active === 'sql'} icon={IconNames.APPLICATION} text="SQL" href="#sql" />
|
||||
<Popover className="config-popover" content={configMenu} position={Position.BOTTOM_LEFT}>
|
||||
<Button className={Classes.MINIMAL} icon={IconNames.SETTINGS} text="Config"/>
|
||||
</Popover>
|
||||
|
||||
</NavbarGroup>
|
||||
<NavbarGroup align={Alignment.RIGHT}>
|
||||
{
|
||||
!hideLegacy &&
|
||||
<Popover className="legacy-popover" content={legacyMenu} position={Position.BOTTOM_RIGHT}>
|
||||
<Button className={Classes.MINIMAL} icon={IconNames.SHARE} text="Legacy"/>
|
||||
<Popover content={legacyMenu} position={Position.BOTTOM_RIGHT}>
|
||||
<Button minimal icon={IconNames.SHARE} text="Legacy"/>
|
||||
</Popover>
|
||||
}
|
||||
<Popover className="help-popover" content={helpMenu} position={Position.BOTTOM_RIGHT}>
|
||||
<Button className={Classes.MINIMAL} icon={IconNames.HELP} text="Help" />
|
||||
<Popover content={configMenu} position={Position.BOTTOM_RIGHT}>
|
||||
<Button minimal icon={IconNames.COG}/>
|
||||
</Popover>
|
||||
<Popover content={helpMenu} position={Position.BOTTOM_RIGHT}>
|
||||
<Button minimal icon={IconNames.HELP}/>
|
||||
</Popover>
|
||||
</NavbarGroup>
|
||||
{
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.null-table-cell {
|
||||
&.null {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.unparseable {
|
||||
color: #9E2B0E;
|
||||
}
|
||||
|
||||
&.timestamp {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
|
||||
import './null-table-cell.scss';
|
||||
|
||||
export interface NullTableCellProps extends React.Props<any> {
|
||||
value?: any;
|
||||
timestamp?: boolean;
|
||||
unparseable?: boolean;
|
||||
}
|
||||
|
||||
export class NullTableCell extends React.Component<NullTableCellProps, {}> {
|
||||
render() {
|
||||
const { value, timestamp, unparseable } = this.props;
|
||||
if (unparseable) {
|
||||
return <span className="null-table-cell unparseable">{`error`}</span>;
|
||||
} else if (value !== '' && value != null) {
|
||||
if (timestamp) {
|
||||
return <span className="null-table-cell timestamp" title={value}>{new Date(value).toISOString()}</span>;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
} else {
|
||||
return <span className="null-table-cell null">null</span>;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ import * as ReactDOMServer from 'react-dom/server';
|
|||
|
||||
import { SQLFunctionDoc } from '../../lib/sql-function-doc';
|
||||
import { AppToaster } from '../singletons/toaster';
|
||||
import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../variables';
|
||||
|
||||
import { MenuCheckbox } from './menu-checkbox';
|
||||
|
||||
|
@ -224,6 +225,12 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
|
|||
const { query, autoComplete, bypassCache, wrapQuery } = this.state;
|
||||
|
||||
return <Menu>
|
||||
<MenuItem
|
||||
icon={IconNames.HELP}
|
||||
text="Docs"
|
||||
href={isRune ? DRUID_DOCS_RUNE : DRUID_DOCS_SQL}
|
||||
target="_blank"
|
||||
/>
|
||||
{
|
||||
!isRune &&
|
||||
<>
|
||||
|
@ -232,16 +239,16 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
|
|||
text="Explain"
|
||||
onClick={() => onExplain(query)}
|
||||
/>
|
||||
<MenuCheckbox
|
||||
checked={autoComplete}
|
||||
label="Auto complete"
|
||||
onChange={() => this.setState({autoComplete: !autoComplete})}
|
||||
/>
|
||||
<MenuCheckbox
|
||||
checked={wrapQuery}
|
||||
label="Wrap query with limit"
|
||||
onChange={() => this.setState({wrapQuery: !wrapQuery})}
|
||||
/>
|
||||
<MenuCheckbox
|
||||
checked={autoComplete}
|
||||
label="Auto complete"
|
||||
onChange={() => this.setState({autoComplete: !autoComplete})}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
<MenuCheckbox
|
||||
|
|
|
@ -23,14 +23,16 @@ import * as classNames from 'classnames';
|
|||
import * as React from 'react';
|
||||
import { HashRouter, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { ExternalLink } from './components/external-link';
|
||||
import { HeaderActiveTab, HeaderBar } from './components/header-bar';
|
||||
import {Loader} from './components/loader';
|
||||
import { Loader } from './components/loader';
|
||||
import { AppToaster } from './singletons/toaster';
|
||||
import { UrlBaser } from './singletons/url-baser';
|
||||
import {QueryManager} from './utils';
|
||||
import {DRUID_DOCS_API, DRUID_DOCS_SQL, LEGACY_COORDINATOR_CONSOLE, LEGACY_OVERLORD_CONSOLE} from './variables';
|
||||
import { QueryManager } from './utils';
|
||||
import { DRUID_DOCS_API, DRUID_DOCS_SQL } from './variables';
|
||||
import { DatasourcesView } from './views/datasource-view';
|
||||
import { HomeView } from './views/home-view';
|
||||
import { LoadDataView, LoadDataViewSeed } from './views/load-data-view';
|
||||
import { LookupsView } from './views/lookups-view';
|
||||
import { SegmentsView } from './views/segments-view';
|
||||
import { ServersView } from './views/servers-view';
|
||||
|
@ -80,8 +82,8 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||
if (capabilities === 'working-without-sql') {
|
||||
message = <>
|
||||
It appears that the SQL endpoint is disabled. The console will fall back
|
||||
to <a href={DRUID_DOCS_API} target="_blank">native Druid APIs</a> and will be
|
||||
limited in functionality. Look at <a href={DRUID_DOCS_SQL} target="_blank">the SQL docs</a> to
|
||||
to <ExternalLink href={DRUID_DOCS_API}>native Druid APIs</ExternalLink> and will be
|
||||
limited in functionality. Look at <ExternalLink href={DRUID_DOCS_SQL}>the SQL docs</ExternalLink> to
|
||||
enable the SQL endpoint.
|
||||
</>;
|
||||
} else if (capabilities === 'broken') {
|
||||
|
@ -98,6 +100,7 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||
});
|
||||
}
|
||||
|
||||
private loadDataViewSeed: LoadDataViewSeed | null;
|
||||
private taskId: string | null;
|
||||
private datasource: string | null;
|
||||
private onlyUnavailable: boolean | null;
|
||||
|
@ -145,8 +148,9 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||
this.capabilitiesQueryManager.terminate();
|
||||
}
|
||||
|
||||
private resetInitialsDelay() {
|
||||
private resetInitialsWithDelay() {
|
||||
setTimeout(() => {
|
||||
this.loadDataViewSeed = null;
|
||||
this.taskId = null;
|
||||
this.datasource = null;
|
||||
this.onlyUnavailable = null;
|
||||
|
@ -155,41 +159,85 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||
}, 50);
|
||||
}
|
||||
|
||||
private goToTask = (taskId: string) => {
|
||||
private goToLoadDataView = (loadDataViewSeed?: LoadDataViewSeed) => {
|
||||
if (loadDataViewSeed) this.loadDataViewSeed = loadDataViewSeed;
|
||||
window.location.hash = 'load-data';
|
||||
this.resetInitialsWithDelay();
|
||||
}
|
||||
|
||||
private goToTask = (taskId: string | null) => {
|
||||
this.taskId = taskId;
|
||||
window.location.hash = 'tasks';
|
||||
this.resetInitialsDelay();
|
||||
this.resetInitialsWithDelay();
|
||||
}
|
||||
|
||||
private goToSegments = (datasource: string, onlyUnavailable = false) => {
|
||||
this.datasource = `"${datasource}"`;
|
||||
this.onlyUnavailable = onlyUnavailable;
|
||||
window.location.hash = 'segments';
|
||||
this.resetInitialsDelay();
|
||||
this.resetInitialsWithDelay();
|
||||
}
|
||||
|
||||
private goToMiddleManager = (middleManager: string) => {
|
||||
this.middleManager = middleManager;
|
||||
window.location.hash = 'servers';
|
||||
this.resetInitialsDelay();
|
||||
this.resetInitialsWithDelay();
|
||||
}
|
||||
|
||||
private goToSql = (initSql: string) => {
|
||||
this.initSql = initSql;
|
||||
window.location.hash = 'sql';
|
||||
this.resetInitialsDelay();
|
||||
window.location.hash = 'query';
|
||||
this.resetInitialsWithDelay();
|
||||
}
|
||||
|
||||
private wrapInViewContainer = (active: HeaderActiveTab, el: JSX.Element, scrollable = false) => {
|
||||
const { hideLegacy } = this.props;
|
||||
|
||||
return <>
|
||||
<HeaderBar active={active} hideLegacy={hideLegacy} goToLoadDataView={this.goToLoadDataView}/>
|
||||
<div className={classNames('view-container', { scrollable })}>{el}</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
private wrappedHomeView = () => {
|
||||
const { noSqlMode } = this.state;
|
||||
return this.wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode}/>);
|
||||
}
|
||||
|
||||
private wrappedLoadDataView = () => {
|
||||
return this.wrapInViewContainer('load-data', <LoadDataView seed={this.loadDataViewSeed} goToTask={this.goToTask}/>);
|
||||
}
|
||||
|
||||
private wrappedSqlView = () => {
|
||||
return this.wrapInViewContainer('query', <SqlView initSql={this.initSql}/>);
|
||||
}
|
||||
|
||||
private wrappedDatasourcesView = () => {
|
||||
const { noSqlMode } = this.state;
|
||||
return this.wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
|
||||
}
|
||||
|
||||
private wrappedSegmentsView = () => {
|
||||
const { noSqlMode } = this.state;
|
||||
return this.wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToSql={this.goToSql} noSqlMode={noSqlMode}/>);
|
||||
}
|
||||
|
||||
private wrappedTasksView = () => {
|
||||
const { noSqlMode } = this.state;
|
||||
return this.wrapInViewContainer('tasks', <TasksView taskId={this.taskId} goToSql={this.goToSql} goToMiddleManager={this.goToMiddleManager} goToLoadDataView={this.goToLoadDataView} noSqlMode={noSqlMode}/>, true);
|
||||
}
|
||||
|
||||
private wrappedServersView = () => {
|
||||
const { noSqlMode } = this.state;
|
||||
return this.wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToSql={this.goToSql} goToTask={this.goToTask} noSqlMode={noSqlMode}/>, true);
|
||||
}
|
||||
|
||||
private wrappedLookupsView = () => {
|
||||
return this.wrapInViewContainer('lookups', <LookupsView/>);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hideLegacy } = this.props;
|
||||
const { noSqlMode, capabilitiesLoading } = this.state;
|
||||
|
||||
const wrapInViewContainer = (active: HeaderActiveTab, el: JSX.Element, scrollable = false) => {
|
||||
return <>
|
||||
<HeaderBar active={active} hideLegacy={hideLegacy}/>
|
||||
<div className={classNames('view-container', { scrollable })}>{el}</div>
|
||||
</>;
|
||||
};
|
||||
const { capabilitiesLoading } = this.state;
|
||||
|
||||
if (capabilitiesLoading) {
|
||||
return <div className={'loading-capabilities'}>
|
||||
|
@ -203,47 +251,17 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||
return <HashRouter hashType="noslash">
|
||||
<div className="console-application">
|
||||
<Switch>
|
||||
<Route
|
||||
path="/datasources"
|
||||
component={() => {
|
||||
return wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path="/segments"
|
||||
component={() => {
|
||||
return wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToSql={this.goToSql} noSqlMode={noSqlMode}/>);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path="/tasks"
|
||||
component={() => {
|
||||
return wrapInViewContainer('tasks', <TasksView taskId={this.taskId} goToSql={this.goToSql} goToMiddleManager={this.goToMiddleManager} noSqlMode={noSqlMode}/>, true);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path="/servers"
|
||||
component={() => {
|
||||
return wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToSql={this.goToSql} goToTask={this.goToTask} noSqlMode={noSqlMode}/>, true);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path="/sql"
|
||||
component={() => {
|
||||
return wrapInViewContainer('sql', <SqlView initSql={this.initSql}/>);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path="/lookups"
|
||||
component={() => {
|
||||
return wrapInViewContainer('lookups', <LookupsView />);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
component={() => {
|
||||
return wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode}/>);
|
||||
}}
|
||||
/>
|
||||
<Route path="/load-data" component={this.wrappedLoadDataView}/>
|
||||
<Route path="/query" component={this.wrappedSqlView}/>
|
||||
<Route path="/sql" component={this.wrappedSqlView}/>
|
||||
|
||||
<Route path="/datasources" component={this.wrappedDatasourcesView}/>
|
||||
<Route path="/segments" component={this.wrappedSegmentsView}/>
|
||||
<Route path="/tasks" component={this.wrappedTasksView}/>
|
||||
<Route path="/servers" component={this.wrappedServersView}/>
|
||||
|
||||
<Route path="/lookups" component={this.wrappedLookupsView}/>
|
||||
<Route component={this.wrappedHomeView}/>
|
||||
</Switch>
|
||||
</div>
|
||||
</HashRouter>;
|
||||
|
|
|
@ -20,6 +20,7 @@ import { AnchorButton, Button, Classes, Dialog, Intent } from '@blueprintjs/core
|
|||
import { IconNames } from '@blueprintjs/icons';
|
||||
import * as React from 'react';
|
||||
|
||||
import { ExternalLink } from '../components/external-link';
|
||||
import { DRUID_COMMUNITY, DRUID_DEVELOPER_GROUP, DRUID_USER_GROUP, DRUID_WEBSITE } from '../variables';
|
||||
|
||||
export interface AboutDialogProps extends React.Props<any> {
|
||||
|
@ -53,14 +54,14 @@ export class AboutDialog extends React.Component<AboutDialogProps, AboutDialogSt
|
|||
</strong>
|
||||
</p>
|
||||
<p>
|
||||
For help and support with Druid, please refer to the <a
|
||||
href={DRUID_COMMUNITY} target="_blank">community page</a> and the <a
|
||||
href={DRUID_USER_GROUP} target="_blank">user groups</a>.
|
||||
For help and support with Druid, please refer to the <ExternalLink
|
||||
href={DRUID_COMMUNITY}>community page</ExternalLink> and the <ExternalLink
|
||||
href={DRUID_USER_GROUP}>user groups</ExternalLink>.
|
||||
</p>
|
||||
<p>
|
||||
Druid is made with ❤️ by a community of passionate developers.
|
||||
To contribute, join in the discussion on the <a
|
||||
href={DRUID_DEVELOPER_GROUP} target="_blank">developer group</a>.
|
||||
To contribute, join in the discussion on the <ExternalLink
|
||||
href={DRUID_DEVELOPER_GROUP}>developer group</ExternalLink>.
|
||||
</p>
|
||||
</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
|
|
|
@ -66,16 +66,16 @@ export class AsyncActionDialog extends React.Component<AsyncAlertDialogProps, As
|
|||
message: `${failText}: ${e.message}`,
|
||||
intent: Intent.DANGER
|
||||
});
|
||||
onClose(false);
|
||||
this.setState({ working: false });
|
||||
onClose(false);
|
||||
return;
|
||||
}
|
||||
AppToaster.show({
|
||||
message: successText,
|
||||
intent: Intent.SUCCESS
|
||||
});
|
||||
onClose(true);
|
||||
this.setState({ working: false });
|
||||
onClose(true);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -22,6 +22,7 @@ import axios from 'axios';
|
|||
import * as React from 'react';
|
||||
|
||||
import { AutoForm } from '../components/auto-form';
|
||||
import { ExternalLink } from '../components/external-link';
|
||||
import { AppToaster } from '../singletons/toaster';
|
||||
import { getDruidErrorMessage, QueryManager } from '../utils';
|
||||
|
||||
|
@ -124,7 +125,7 @@ export class CoordinatorDynamicConfigDialog extends React.Component<CoordinatorD
|
|||
>
|
||||
<p>
|
||||
Edit the coordinator dynamic configuration on the fly.
|
||||
For more information please refer to the <a href="http://druid.io/docs/latest/configuration/index.html#dynamic-configuration" target="_blank">documentation</a>.
|
||||
For more information please refer to the <ExternalLink href="http://druid.io/docs/latest/configuration/index.html#dynamic-configuration">documentation</ExternalLink>.
|
||||
</p>
|
||||
<AutoForm
|
||||
fields={[
|
||||
|
|
|
@ -22,6 +22,7 @@ import axios from 'axios';
|
|||
import * as React from 'react';
|
||||
|
||||
import { AutoForm } from '../components/auto-form';
|
||||
import { ExternalLink } from '../components/external-link';
|
||||
import { AppToaster } from '../singletons/toaster';
|
||||
import { getDruidErrorMessage, QueryManager } from '../utils';
|
||||
|
||||
|
@ -127,7 +128,7 @@ export class OverlordDynamicConfigDialog extends React.Component<OverlordDynamic
|
|||
>
|
||||
<p>
|
||||
Edit the overlord dynamic configuration on the fly.
|
||||
For more information please refer to the <a href="http://druid.io/docs/latest/configuration/index.html#overlord-dynamic-configuration" target="_blank">documentation</a>.
|
||||
For more information please refer to the <ExternalLink href="http://druid.io/docs/latest/configuration/index.html#overlord-dynamic-configuration">documentation</ExternalLink>.
|
||||
</p>
|
||||
<AutoForm
|
||||
fields={[
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
(React as any).PropTypes = require('prop-types'); // Trick blueprint 1.0.1 into accepting React 16 as React 15.
|
||||
|
||||
import { reorderArray } from './retention-dialog';
|
||||
|
|
@ -50,3 +50,24 @@ svg {
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.label-info-text {
|
||||
max-width: 400px;
|
||||
padding: 15px;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-form-group {
|
||||
.bp3-form-content {
|
||||
position: relative;
|
||||
|
||||
& > .bp3-popover-wrapper {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
const UNSAFE_CHAR = /[^a-z0-9 ,._\-;:(){}\[\]<>!@#$%^&*`~?]/ig;
|
||||
|
||||
function escape(str: string): string {
|
||||
return str.replace(UNSAFE_CHAR, (s) => {
|
||||
return '\\u' + ('000' + s.charCodeAt(0).toString(16)).substr(-4);
|
||||
});
|
||||
}
|
||||
|
||||
export function escapeColumnName(name: string): string {
|
||||
return `"${escape(name)}"`;
|
||||
}
|
|
@ -19,14 +19,35 @@
|
|||
import axios from 'axios';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
export function parseHtmlError(htmlStr: string): string | null {
|
||||
const startIndex = htmlStr.indexOf('</h3><pre>');
|
||||
const endIndex = htmlStr.indexOf('\n\tat');
|
||||
if (startIndex === -1 || endIndex === -1) return null;
|
||||
|
||||
return htmlStr
|
||||
.substring(startIndex + 10, endIndex)
|
||||
.replace(/"/g, '"')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
export function getDruidErrorMessage(e: any) {
|
||||
const data: any = ((e.response || {}).data || {});
|
||||
return [
|
||||
data.error,
|
||||
data.errorMessage,
|
||||
data.errorClass,
|
||||
data.host ? `on host ${data.host}` : null
|
||||
].filter(Boolean).join(' / ') || e.message;
|
||||
switch (typeof data) {
|
||||
case 'object':
|
||||
return [
|
||||
data.error,
|
||||
data.errorMessage,
|
||||
data.errorClass,
|
||||
data.host ? `on host ${data.host}` : null
|
||||
].filter(Boolean).join(' / ') || e.message;
|
||||
|
||||
case 'string':
|
||||
const htmlResp = parseHtmlError(data);
|
||||
return htmlResp ? `HTML Error: ${htmlResp}` : e.message;
|
||||
|
||||
default:
|
||||
return e.message;
|
||||
}
|
||||
}
|
||||
|
||||
export async function queryDruidRune(runeQuery: Record<string, any>): Promise<any> {
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type DruidTimestampFormat = 'iso' | 'millis' | 'posix' | 'auto' | 'd/M/yyyy' | 'dd-M-yyyy hh:mm:ss a' |
|
||||
'MM/dd/YYYY' | 'M/d/YY' | 'MM/dd/YYYY hh:mm:ss a' | 'YYYY-MM-dd HH:mm:ss' | 'YYYY-MM-dd HH:mm:ss.S';
|
||||
|
||||
export const TIMESTAMP_FORMAT_VALUES: DruidTimestampFormat[] = [
|
||||
'iso', 'millis', 'posix', 'MM/dd/YYYY hh:mm:ss a', 'MM/dd/YYYY', 'M/d/YY', 'd/M/yyyy',
|
||||
'YYYY-MM-dd HH:mm:ss', 'YYYY-MM-dd HH:mm:ss.S'
|
||||
];
|
||||
|
||||
const EXAMPLE_DATE_ISO = '2015-10-29T23:00:00.000Z';
|
||||
const EXAMPLE_DATE_VALUE = Date.parse(EXAMPLE_DATE_ISO);
|
||||
const MIN_MILLIS = 3.15576e11; // 3 years in millis, so Tue Jan 01 1980
|
||||
const MAX_MILLIS = EXAMPLE_DATE_VALUE * 10;
|
||||
const MIN_POSIX = MIN_MILLIS / 1000;
|
||||
const MAX_POSIX = MAX_MILLIS / 1000;
|
||||
|
||||
// copied from http://goo.gl/0ejHHW with small tweak to make dddd not pass on its own
|
||||
// tslint:disable-next-line:max-line-length
|
||||
export const ISO_MATCHER = new RegExp(/^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))(T((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)$/);
|
||||
export const JODA_TO_REGEXP_LOOKUP: Record<string, RegExp> = {
|
||||
'd/M/yyyy': /^[12]?\d\/1?\d\/\d\d\d\d$/,
|
||||
'MM/dd/YYYY': /^\d\d\/\d\d\/\d\d\d\d$/,
|
||||
'M/d/YY': /^1?\d\/[12]?\d\/\d\d$/,
|
||||
'd-M-yyyy hh:mm:ss a': /^[12]?\d-1?\d-\d\d\d\d \d\d:\d\d:\d\d [ap]m$/i,
|
||||
'MM/dd/YYYY hh:mm:ss a' : /^\d\d\/\d\d\/\d\d\d\d \d\d:\d\d:\d\d [ap]m$/i,
|
||||
'YYYY-MM-dd HH:mm:ss' : /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/,
|
||||
'YYYY-MM-dd HH:mm:ss.S': /^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d\d\d$/
|
||||
};
|
||||
|
||||
export function timeFormatMatches(format: DruidTimestampFormat, value: string | number): boolean {
|
||||
if (format === 'iso') {
|
||||
return ISO_MATCHER.test(String(value));
|
||||
}
|
||||
|
||||
if (format === 'millis') {
|
||||
const absValue = Math.abs(Number(value));
|
||||
return MIN_MILLIS < absValue && absValue < MAX_MILLIS;
|
||||
}
|
||||
|
||||
if (format === 'posix') {
|
||||
const absValue = Math.abs(Number(value));
|
||||
return MIN_POSIX < absValue && absValue < MAX_POSIX;
|
||||
}
|
||||
|
||||
const formatRegexp = JODA_TO_REGEXP_LOOKUP[format];
|
||||
if (!formatRegexp) throw new Error(`unknown Druid format ${format}`);
|
||||
|
||||
return formatRegexp.test(String(value));
|
||||
}
|
||||
|
||||
export function possibleDruidFormatForValues(values: any[]): DruidTimestampFormat | null {
|
||||
return TIMESTAMP_FORMAT_VALUES.filter(format => {
|
||||
return values.every(value => timeFormatMatches(format, value));
|
||||
})[0] || null;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { filterMap } from './general';
|
||||
import { DimensionMode, DimensionSpec, IngestionSpec, MetricSpec } from './ingestion-spec';
|
||||
import { deepDelete, deepSet } from './object-change';
|
||||
import { HeaderAndRows } from './sampler';
|
||||
|
||||
export function guessTypeFromSample(sample: any[]): string {
|
||||
const definedValues = sample.filter(v => v != null);
|
||||
if (definedValues.length && definedValues.every(v => !isNaN(v) && (typeof v === 'number' || typeof v === 'string'))) {
|
||||
if (definedValues.every(v => v % 1 === 0)) {
|
||||
return 'long';
|
||||
} else {
|
||||
return 'float';
|
||||
}
|
||||
} else {
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
export function getColumnTypeFromHeaderAndRows(headerAndRows: HeaderAndRows, column: string): string {
|
||||
return guessTypeFromSample(filterMap(headerAndRows.rows, (r: any) => r.parsed ? r.parsed[column] : null));
|
||||
}
|
||||
|
||||
export function getDimensionSpecs(headerAndRows: HeaderAndRows, hasRollup: boolean): (string | DimensionSpec)[] {
|
||||
return filterMap(headerAndRows.header, (h) => {
|
||||
if (h === '__time') return null;
|
||||
const guessedType = getColumnTypeFromHeaderAndRows(headerAndRows, h);
|
||||
if (guessedType === 'string') return h;
|
||||
if (hasRollup) return null;
|
||||
return {
|
||||
type: guessedType,
|
||||
name: h
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getMetricSecs(headerAndRows: HeaderAndRows): MetricSpec[] {
|
||||
return [{ name: 'count', type: 'count' }].concat(filterMap(headerAndRows.header, (h) => {
|
||||
if (h === '__time') return null;
|
||||
const guessedType = getColumnTypeFromHeaderAndRows(headerAndRows, h);
|
||||
switch (guessedType) {
|
||||
case 'double': return { name: `sum_${h}`, type: 'doubleSum', fieldName: h };
|
||||
case 'long': return { name: `sum_${h}`, type: 'longSum', fieldName: h };
|
||||
default: return null;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
export function updateSchemaWithSample(spec: IngestionSpec, headerAndRows: HeaderAndRows, dimensionMode: DimensionMode, rollup: boolean): IngestionSpec {
|
||||
let newSpec = spec;
|
||||
|
||||
if (dimensionMode === 'auto-detect') {
|
||||
newSpec = deepSet(newSpec, 'dataSchema.parser.parseSpec.dimensionsSpec.dimensions', []);
|
||||
|
||||
} else {
|
||||
newSpec = deepDelete(newSpec, 'dataSchema.parser.parseSpec.dimensionsSpec.dimensionExclusions');
|
||||
|
||||
const dimensions = getDimensionSpecs(headerAndRows, rollup);
|
||||
if (dimensions) {
|
||||
newSpec = deepSet(newSpec, 'dataSchema.parser.parseSpec.dimensionsSpec.dimensions', dimensions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (rollup) {
|
||||
newSpec = deepSet(newSpec, 'dataSchema.granularitySpec.queryGranularity', 'HOUR');
|
||||
|
||||
const metrics = getMetricSecs(headerAndRows);
|
||||
if (metrics) {
|
||||
newSpec = deepSet(newSpec, 'dataSchema.metricsSpec', metrics);
|
||||
}
|
||||
|
||||
} else {
|
||||
newSpec = deepSet(newSpec, 'dataSchema.granularitySpec.queryGranularity', 'NONE');
|
||||
newSpec = deepDelete(newSpec, 'dataSchema.metricsSpec');
|
||||
|
||||
}
|
||||
|
||||
newSpec = deepSet(newSpec, 'dataSchema.granularitySpec.rollup', rollup);
|
||||
return newSpec;
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 { IngestionSpec } from './ingestion-spec';
|
||||
|
||||
export function getWikipediaSpec(dataSourceSuffix: string): IngestionSpec {
|
||||
return {
|
||||
'type': 'index',
|
||||
'dataSchema': {
|
||||
'dataSource': 'wikipedia-' + dataSourceSuffix,
|
||||
'parser': {
|
||||
'type': 'string',
|
||||
'parseSpec': {
|
||||
'format': 'json',
|
||||
'dimensionsSpec': {
|
||||
'dimensions': [
|
||||
'isRobot',
|
||||
'channel',
|
||||
'flags',
|
||||
'isUnpatrolled',
|
||||
'page',
|
||||
'diffUrl',
|
||||
{
|
||||
'name': 'added',
|
||||
'type': 'long'
|
||||
},
|
||||
'comment',
|
||||
{
|
||||
'name': 'commentLength',
|
||||
'type': 'long'
|
||||
},
|
||||
'isNew',
|
||||
'isMinor',
|
||||
{
|
||||
'name': 'delta',
|
||||
'type': 'long'
|
||||
},
|
||||
'isAnonymous',
|
||||
'user',
|
||||
{
|
||||
'name': 'deltaBucket',
|
||||
'type': 'long'
|
||||
},
|
||||
{
|
||||
'name': 'deleted',
|
||||
'type': 'long'
|
||||
},
|
||||
'namespace'
|
||||
]
|
||||
},
|
||||
'timestampSpec': {
|
||||
'column': 'timestamp',
|
||||
'format': 'iso'
|
||||
}
|
||||
}
|
||||
},
|
||||
'granularitySpec': {
|
||||
'type': 'uniform',
|
||||
'segmentGranularity': 'DAY',
|
||||
'rollup': false,
|
||||
'queryGranularity': 'none'
|
||||
},
|
||||
'metricsSpec': []
|
||||
},
|
||||
'ioConfig': {
|
||||
'type': 'index',
|
||||
'firehose': {
|
||||
'fetchTimeout': 300000,
|
||||
'type': 'http',
|
||||
'uris': [
|
||||
'https://static.imply.io/data/wikipedia.json.gz'
|
||||
]
|
||||
}
|
||||
},
|
||||
'tuningConfig': {
|
||||
'type': 'index',
|
||||
'forceExtendableShardSpecs': true,
|
||||
'maxParseExceptions': 100,
|
||||
'maxSavedParseExceptions': 10
|
||||
}
|
||||
};
|
||||
}
|
|
@ -171,6 +171,14 @@ export function getHeadProp(results: Record<string, any>[], prop: string): any {
|
|||
|
||||
// ----------------------------
|
||||
|
||||
export function parseJson(json: string): any {
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function validJson(json: string): boolean {
|
||||
try {
|
||||
JSON.parse(json);
|
||||
|
@ -197,3 +205,14 @@ export function parseStringToJSON(s: string): JSON | null {
|
|||
return JSON.parse(s);
|
||||
}
|
||||
}
|
||||
|
||||
export function filterMap<T, Q>(xs: T[], f: (x: T, i?: number) => Q | null | undefined): Q[] {
|
||||
return (xs.map(f) as any).filter(Boolean);
|
||||
}
|
||||
|
||||
export function sortWithPrefixSuffix(things: string[], prefix: string[], suffix: string[]): string[] {
|
||||
const pre = things.filter((x) => prefix.includes(x)).sort();
|
||||
const mid = things.filter((x) => !prefix.includes(x) && !suffix.includes(x)).sort();
|
||||
const post = things.filter((x) => suffix.includes(x)).sort();
|
||||
return pre.concat(mid, post);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
export * from './general';
|
||||
export * from './druid-query';
|
||||
export * from './query-manager';
|
||||
export * from './query-state';
|
||||
export * from './rune-decoder';
|
||||
export * from './table-column-selection-handler';
|
||||
export * from './local-storage-keys';
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
export const LocalStorageKeys = {
|
||||
INGESTION_SPEC: 'ingestion-spec' as 'ingestion-spec',
|
||||
DATASOURCE_TABLE_COLUMN_SELECTION: 'datasource-table-column-selection' as 'datasource-table-column-selection',
|
||||
SEGMENT_TABLE_COLUMN_SELECTION: 'segment-table-column-selection' as 'segment-table-column-selection',
|
||||
SUPERVISOR_TABLE_COLUMN_SELECTION: 'supervisor-table-column-selection' as 'supervisor-table-column-selection',
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 { deepDelete, deepGet, deepSet, makePath, parsePath } from './object-change';
|
||||
|
||||
describe('object-change', () => {
|
||||
describe('parsePath', () => {
|
||||
it('works', () => {
|
||||
expect(parsePath('hello.wow.0')).toEqual(['hello', 'wow', '0']);
|
||||
expect(parsePath('hello.{wow.moon}.0')).toEqual(['hello', 'wow.moon', '0']);
|
||||
expect(parsePath('hello.#.0.[append]')).toEqual(['hello', '#', '0', '[append]']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('makePath', () => {
|
||||
it('works', () => {
|
||||
expect(makePath(['hello', 'wow', '0'])).toEqual('hello.wow.0');
|
||||
expect(makePath(['hello', 'wow.moon', '0'])).toEqual('hello.{wow.moon}.0');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('deepGet', () => {
|
||||
const thing = {
|
||||
hello: {
|
||||
'consumer.props': 'lol',
|
||||
wow: [
|
||||
'a',
|
||||
{ test: 'moon' }
|
||||
]
|
||||
},
|
||||
zetrix: null
|
||||
};
|
||||
|
||||
it('works', () => {
|
||||
expect(deepGet(thing, 'hello.wow.0')).toEqual('a');
|
||||
expect(deepGet(thing, 'hello.wow.4')).toEqual(undefined);
|
||||
expect(deepGet(thing, 'hello.{consumer.props}')).toEqual('lol');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('deepSet', () => {
|
||||
const thing = {
|
||||
hello: {
|
||||
wow: [
|
||||
'a',
|
||||
{ test: 'moon' }
|
||||
]
|
||||
},
|
||||
zetrix: null
|
||||
};
|
||||
|
||||
it('works to set an existing thing', () => {
|
||||
expect(deepSet(thing, 'hello.wow.0', 5)).toEqual({
|
||||
hello: {
|
||||
wow: [
|
||||
5,
|
||||
{
|
||||
test: 'moon'
|
||||
}
|
||||
]
|
||||
},
|
||||
zetrix: null
|
||||
});
|
||||
});
|
||||
|
||||
it('works to set a non-existing thing', () => {
|
||||
expect(deepSet(thing, 'lets.do.this.now', 5)).toEqual({
|
||||
hello: {
|
||||
wow: [
|
||||
'a',
|
||||
{
|
||||
test: 'moon'
|
||||
}
|
||||
]
|
||||
},
|
||||
lets: {
|
||||
do: {
|
||||
this: {
|
||||
now: 5
|
||||
}
|
||||
}
|
||||
},
|
||||
zetrix: null
|
||||
});
|
||||
});
|
||||
|
||||
it('works to set an existing array', () => {
|
||||
expect(deepSet(thing, 'hello.wow.[append]', 5)).toEqual({
|
||||
hello: {
|
||||
wow: [
|
||||
'a',
|
||||
{
|
||||
test: 'moon'
|
||||
},
|
||||
5
|
||||
]
|
||||
},
|
||||
zetrix: null
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('deepDelete', () => {
|
||||
const thing = {
|
||||
hello: {
|
||||
moon: 1,
|
||||
wow: [
|
||||
'a',
|
||||
{ test: 'moon' }
|
||||
]
|
||||
},
|
||||
zetrix: null
|
||||
};
|
||||
|
||||
it('works to delete an existing thing', () => {
|
||||
expect(deepDelete(thing, 'hello.wow')).toEqual({
|
||||
hello: { moon: 1 },
|
||||
zetrix: null
|
||||
});
|
||||
});
|
||||
|
||||
it('works is harmless to delete a non-existing thing', () => {
|
||||
expect(deepDelete(thing, 'hello.there.lol.why')).toEqual(thing);
|
||||
});
|
||||
|
||||
it('removes things completely', () => {
|
||||
expect(deepDelete(deepDelete(thing, 'hello.wow'), 'hello.moon')).toEqual({
|
||||
zetrix: null
|
||||
});
|
||||
});
|
||||
|
||||
it('works with arrays', () => {
|
||||
expect(JSON.parse(JSON.stringify(deepDelete(thing, 'hello.wow.0')))).toEqual({
|
||||
hello: {
|
||||
moon: 1,
|
||||
wow: [
|
||||
{
|
||||
test: 'moon'
|
||||
}
|
||||
]
|
||||
},
|
||||
zetrix: null
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export function shallowCopy(v: any): any {
|
||||
return Array.isArray(v) ? v.slice() : Object.assign({}, v);
|
||||
}
|
||||
|
||||
function isEmpty(v: any): boolean {
|
||||
return !(Array.isArray(v) ? v.length : Object.keys(v).length);
|
||||
}
|
||||
|
||||
export function parsePath(path: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let rest = path;
|
||||
while (rest) {
|
||||
const escapedMatch = rest.match(/^\{([^{}]*)\}(?:\.(.*))?$/);
|
||||
if (escapedMatch) {
|
||||
parts.push(escapedMatch[1]);
|
||||
rest = escapedMatch[2];
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalMatch = rest.match(/^([^.]*)(?:\.(.*))?$/);
|
||||
if (normalMatch) {
|
||||
parts.push(normalMatch[1]);
|
||||
rest = normalMatch[2];
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Could not parse path ${path}`);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function makePath(parts: string[]): string {
|
||||
return parts.map(p => p.includes('.') ? `{${p}}` : p).join('.');
|
||||
}
|
||||
|
||||
function isAppend(key: string): boolean {
|
||||
return key === '[append]' || key === '-1';
|
||||
}
|
||||
|
||||
export function deepGet<T extends Record<string, any>>(value: T, path: string): any {
|
||||
const parts = parsePath(path);
|
||||
for (const part of parts) {
|
||||
value = (value || {})[part];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function deepSet<T extends Record<string, any>>(value: T, path: string, x: any): T {
|
||||
const parts = parsePath(path);
|
||||
let myKey = parts.shift() as string; // Must be defined
|
||||
const valueCopy = shallowCopy(value);
|
||||
if (Array.isArray(valueCopy) && isAppend(myKey)) myKey = String(valueCopy.length);
|
||||
if (parts.length) {
|
||||
const nextKey = parts[0];
|
||||
const rest = makePath(parts);
|
||||
valueCopy[myKey] = deepSet(value[myKey] || (isAppend(nextKey) ? [] : {}), rest, x);
|
||||
} else {
|
||||
valueCopy[myKey] = x;
|
||||
}
|
||||
return valueCopy;
|
||||
}
|
||||
|
||||
export function deepDelete<T extends Record<string, any>>(value: T, path: string): T {
|
||||
const valueCopy = shallowCopy(value);
|
||||
const parts = parsePath(path);
|
||||
const firstKey = parts.shift() as string; // Must be defined
|
||||
if (parts.length) {
|
||||
const firstKeyValue = value[firstKey];
|
||||
if (firstKeyValue) {
|
||||
const restPath = makePath(parts);
|
||||
const prunedFirstKeyValue = deepDelete(value[firstKey], restPath);
|
||||
|
||||
if (isEmpty(prunedFirstKeyValue)) {
|
||||
delete valueCopy[firstKey];
|
||||
} else {
|
||||
valueCopy[firstKey] = prunedFirstKeyValue;
|
||||
}
|
||||
} else {
|
||||
delete valueCopy[firstKey];
|
||||
}
|
||||
|
||||
} else {
|
||||
if (Array.isArray(valueCopy) && !isNaN(Number(firstKey))) {
|
||||
valueCopy.splice(Number(firstKey), 1);
|
||||
} else {
|
||||
delete valueCopy[firstKey];
|
||||
}
|
||||
}
|
||||
return valueCopy;
|
||||
}
|
||||
|
||||
export function whitelistKeys(obj: Record<string, any>, whitelist: string[]): Record<string, any> {
|
||||
const newObj: Record<string, any> = {};
|
||||
for (const w of whitelist) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, w)) {
|
||||
newObj[w] = obj[w];
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
import debounce = require('lodash.debounce');
|
||||
|
||||
export interface QueryState<R> {
|
||||
export interface QueryStateInt<R> {
|
||||
result: R | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
|
@ -26,20 +26,20 @@ export interface QueryState<R> {
|
|||
|
||||
export interface QueryManagerOptions<Q, R> {
|
||||
processQuery: (query: Q) => Promise<R>;
|
||||
onStateChange?: (queryResolve: QueryState<R>) => void;
|
||||
onStateChange?: (queryResolve: QueryStateInt<R>) => void;
|
||||
debounceIdle?: number;
|
||||
debounceLoading?: number;
|
||||
}
|
||||
|
||||
export class QueryManager<Q, R> {
|
||||
private processQuery: (query: Q) => Promise<R>;
|
||||
private onStateChange?: (queryResolve: QueryState<R>) => void;
|
||||
private onStateChange?: (queryResolve: QueryStateInt<R>) => void;
|
||||
|
||||
private terminated = false;
|
||||
private nextQuery: Q;
|
||||
private lastQuery: Q;
|
||||
private actuallyLoading = false;
|
||||
private state: QueryState<R> = {
|
||||
private state: QueryStateInt<R> = {
|
||||
result: null,
|
||||
loading: false,
|
||||
error: null
|
||||
|
@ -64,7 +64,7 @@ export class QueryManager<Q, R> {
|
|||
}
|
||||
}
|
||||
|
||||
private setState(queryState: QueryState<R>) {
|
||||
private setState(queryState: QueryStateInt<R>) {
|
||||
this.state = queryState;
|
||||
if (this.onStateChange && !this.terminated) {
|
||||
this.onStateChange(queryState);
|
||||
|
@ -130,7 +130,7 @@ export class QueryManager<Q, R> {
|
|||
return this.lastQuery;
|
||||
}
|
||||
|
||||
public getState(): QueryState<R> {
|
||||
public getState(): QueryStateInt<R> {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type QueryStateState = 'init' | 'loading' | 'data' | 'error';
|
||||
|
||||
export class QueryState<T> {
|
||||
static INIT: QueryState<any> = new QueryState({});
|
||||
|
||||
public state: QueryStateState = 'init';
|
||||
public error?: string | null;
|
||||
public data?: T | null;
|
||||
|
||||
constructor(opts: { loading?: boolean, error?: string, data?: T }) {
|
||||
if (opts.error) {
|
||||
if (opts.data) {
|
||||
throw new Error('can not have both error and data');
|
||||
} else {
|
||||
this.state = 'error';
|
||||
this.error = opts.error;
|
||||
}
|
||||
} else {
|
||||
if (opts.data) {
|
||||
this.state = 'data';
|
||||
this.data = opts.data;
|
||||
} else {
|
||||
this.state = opts.loading ? 'loading' : 'init';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isInit(): boolean {
|
||||
return this.state === 'init';
|
||||
}
|
||||
|
||||
isLoading(): boolean {
|
||||
return this.state === 'loading';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,342 @@
|
|||
/*
|
||||
* 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 axios from 'axios';
|
||||
|
||||
import { getDruidErrorMessage } from './druid-query';
|
||||
import { filterMap, sortWithPrefixSuffix } from './general';
|
||||
import {
|
||||
DimensionsSpec,
|
||||
getEmptyTimestampSpec,
|
||||
IngestionSpec,
|
||||
IoConfig, MetricSpec,
|
||||
Parser,
|
||||
ParseSpec,
|
||||
Transform, TransformSpec
|
||||
} from './ingestion-spec';
|
||||
import { deepGet, deepSet, shallowCopy, whitelistKeys } from './object-change';
|
||||
import { QueryState } from './query-state';
|
||||
|
||||
const SAMPLER_URL = `/druid/indexer/v1/sampler`;
|
||||
const BASE_SAMPLER_CONFIG: SamplerConfig = {
|
||||
// skipCache: true,
|
||||
numRows: 500
|
||||
};
|
||||
|
||||
export interface SampleSpec {
|
||||
type: 'index';
|
||||
spec: IngestionSpec;
|
||||
samplerConfig: SamplerConfig;
|
||||
}
|
||||
|
||||
export interface SamplerConfig {
|
||||
numRows?: number;
|
||||
cacheKey?: string;
|
||||
skipCache?: boolean;
|
||||
}
|
||||
|
||||
export interface SampleResponse {
|
||||
cacheKey?: string;
|
||||
data: SampleEntry[];
|
||||
}
|
||||
|
||||
export interface SampleEntry {
|
||||
raw: string;
|
||||
parsed?: Record<string, any>;
|
||||
unparseable?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface HeaderAndRows {
|
||||
header: string[];
|
||||
rows: SampleEntry[];
|
||||
}
|
||||
|
||||
function dedupe(xs: string[]): string[] {
|
||||
const seen: Record<string, boolean> = {};
|
||||
return xs.filter((x) => {
|
||||
if (seen[x]) {
|
||||
return false;
|
||||
} else {
|
||||
seen[x] = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function headerFromSampleResponse(sampleResponse: SampleResponse, ignoreColumn?: string): string[] {
|
||||
let columns = sortWithPrefixSuffix(dedupe(
|
||||
[].concat(...(filterMap(sampleResponse.data, s => s.parsed ? Object.keys(s.parsed) : null) as any))
|
||||
).sort(), ['__time'], []);
|
||||
|
||||
if (ignoreColumn) {
|
||||
columns = columns.filter(c => c !== ignoreColumn);
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
export function headerAndRowsFromSampleResponse(sampleResponse: SampleResponse, ignoreColumn?: string, parsedOnly = false): HeaderAndRows {
|
||||
return {
|
||||
header: headerFromSampleResponse(sampleResponse, ignoreColumn),
|
||||
rows: parsedOnly ? sampleResponse.data.filter((d: any) => d.parsed) : sampleResponse.data
|
||||
};
|
||||
}
|
||||
|
||||
async function postToSampler(sampleSpec: SampleSpec, forStr: string): Promise<SampleResponse> {
|
||||
let sampleResp: any;
|
||||
try {
|
||||
sampleResp = await axios.post(`${SAMPLER_URL}?for=${forStr}`, sampleSpec);
|
||||
} catch (e) {
|
||||
throw new Error(getDruidErrorMessage(e));
|
||||
}
|
||||
|
||||
return sampleResp.data;
|
||||
}
|
||||
|
||||
export async function sampleForConnect(spec: IngestionSpec): Promise<SampleResponse> {
|
||||
const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
|
||||
|
||||
const sampleSpec: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
ioConfig: deepSet(ioConfig, 'type', 'index')
|
||||
// dataSchema: {
|
||||
// dataSource: 'sample',
|
||||
// parser: {
|
||||
// type: 'string',
|
||||
// parseSpec: {
|
||||
// format: 'json',
|
||||
// dimensionsSpec: {},
|
||||
// timestampSpec: getEmptyTimestampSpec()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
} as any,
|
||||
samplerConfig: BASE_SAMPLER_CONFIG
|
||||
};
|
||||
|
||||
return postToSampler(sampleSpec, 'connect');
|
||||
}
|
||||
|
||||
export async function sampleForParser(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
|
||||
const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
|
||||
const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
|
||||
|
||||
const sampleSpec: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
ioConfig: deepSet(ioConfig, 'type', 'index'),
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: {
|
||||
type: parser.type,
|
||||
parseSpec: (
|
||||
parser.parseSpec ?
|
||||
Object.assign({}, parser.parseSpec, {
|
||||
dimensionsSpec: {},
|
||||
timestampSpec: getEmptyTimestampSpec()
|
||||
}) :
|
||||
undefined
|
||||
) as any
|
||||
}
|
||||
}
|
||||
},
|
||||
samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
|
||||
cacheKey
|
||||
})
|
||||
};
|
||||
|
||||
return postToSampler(sampleSpec, 'parser');
|
||||
}
|
||||
|
||||
export async function sampleForTimestamp(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
|
||||
const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
|
||||
const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
|
||||
const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
|
||||
|
||||
const sampleSpec: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
ioConfig: deepSet(ioConfig, 'type', 'index'),
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: {
|
||||
type: parser.type,
|
||||
parseSpec: Object.assign({}, parseSpec, {
|
||||
dimensionsSpec: {}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
|
||||
cacheKey
|
||||
})
|
||||
};
|
||||
|
||||
return postToSampler(sampleSpec, 'timestamp');
|
||||
}
|
||||
|
||||
export async function sampleForTransform(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
|
||||
const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
|
||||
const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
|
||||
const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
|
||||
const transforms: Transform[] = deepGet(spec, 'dataSchema.transformSpec.transforms') || [];
|
||||
|
||||
// Extra step to simulate auto detecting dimension with transforms
|
||||
const specialDimensionSpec: DimensionsSpec = {};
|
||||
if (transforms && transforms.length) {
|
||||
|
||||
const sampleSpecHack: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
ioConfig: deepSet(ioConfig, 'type', 'index'),
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: {
|
||||
type: parser.type,
|
||||
parseSpec: Object.assign({}, parseSpec, {
|
||||
dimensionsSpec: {}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
|
||||
cacheKey
|
||||
})
|
||||
};
|
||||
|
||||
const sampleResponseHack = await postToSampler(sampleSpecHack, 'transform-pre');
|
||||
|
||||
specialDimensionSpec.dimensions = dedupe(headerFromSampleResponse(sampleResponseHack, '__time').concat(transforms.map(t => t.name)));
|
||||
}
|
||||
|
||||
const sampleSpec: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
ioConfig: deepSet(ioConfig, 'type', 'index'),
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: {
|
||||
type: parser.type,
|
||||
parseSpec: Object.assign({}, parseSpec, {
|
||||
dimensionsSpec: specialDimensionSpec // Hack Hack Hack
|
||||
})
|
||||
},
|
||||
transformSpec: {
|
||||
transforms
|
||||
}
|
||||
}
|
||||
},
|
||||
samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
|
||||
cacheKey
|
||||
})
|
||||
};
|
||||
|
||||
return postToSampler(sampleSpec, 'transform');
|
||||
}
|
||||
|
||||
export async function sampleForFilter(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
|
||||
const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
|
||||
const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
|
||||
const parseSpec: ParseSpec = deepGet(spec, 'dataSchema.parser.parseSpec') || {};
|
||||
const transforms: Transform[] = deepGet(spec, 'dataSchema.transformSpec.transforms') || [];
|
||||
const filter: any = deepGet(spec, 'dataSchema.transformSpec.filter');
|
||||
|
||||
// Extra step to simulate auto detecting dimension with transforms
|
||||
const specialDimensionSpec: DimensionsSpec = {};
|
||||
if (transforms && transforms.length) {
|
||||
|
||||
const sampleSpecHack: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
ioConfig: deepSet(ioConfig, 'type', 'index'),
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: {
|
||||
type: parser.type,
|
||||
parseSpec: Object.assign({}, parseSpec, {
|
||||
dimensionsSpec: {}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
|
||||
cacheKey
|
||||
})
|
||||
};
|
||||
|
||||
const sampleResponseHack = await postToSampler(sampleSpecHack, 'filter-pre');
|
||||
|
||||
specialDimensionSpec.dimensions = dedupe(headerFromSampleResponse(sampleResponseHack, '__time').concat(transforms.map(t => t.name)));
|
||||
}
|
||||
|
||||
const sampleSpec: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
ioConfig: deepSet(ioConfig, 'type', 'index'),
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: {
|
||||
type: parser.type,
|
||||
parseSpec: Object.assign({}, parseSpec, {
|
||||
dimensionsSpec: specialDimensionSpec // Hack Hack Hack
|
||||
})
|
||||
},
|
||||
transformSpec: {
|
||||
transforms,
|
||||
filter
|
||||
}
|
||||
}
|
||||
},
|
||||
samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
|
||||
cacheKey
|
||||
})
|
||||
};
|
||||
|
||||
return postToSampler(sampleSpec, 'filter');
|
||||
}
|
||||
|
||||
export async function sampleForSchema(spec: IngestionSpec, cacheKey: string | undefined): Promise<SampleResponse> {
|
||||
const ioConfig: IoConfig = deepGet(spec, 'ioConfig') || {};
|
||||
const parser: Parser = deepGet(spec, 'dataSchema.parser') || {};
|
||||
const transformSpec: TransformSpec = deepGet(spec, 'dataSchema.transformSpec') || ({} as TransformSpec);
|
||||
const metricsSpec: MetricSpec[] = deepGet(spec, 'dataSchema.metricsSpec') || [];
|
||||
const queryGranularity: string = deepGet(spec, 'dataSchema.granularitySpec.queryGranularity') || 'NONE';
|
||||
|
||||
const sampleSpec: SampleSpec = {
|
||||
type: 'index',
|
||||
spec: {
|
||||
ioConfig: deepSet(ioConfig, 'type', 'index'),
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: whitelistKeys(parser, ['type', 'parseSpec']) as Parser,
|
||||
transformSpec,
|
||||
metricsSpec,
|
||||
granularitySpec: {
|
||||
queryGranularity
|
||||
}
|
||||
}
|
||||
},
|
||||
samplerConfig: Object.assign({}, BASE_SAMPLER_CONFIG, {
|
||||
cacheKey
|
||||
})
|
||||
};
|
||||
|
||||
return postToSampler(sampleSpec, 'schema');
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 { computeFlattenExprsForData } from './spec-utils';
|
||||
|
||||
describe('spec-utils', () => {
|
||||
describe('computeFlattenExprsForData', () => {
|
||||
const data = [
|
||||
{
|
||||
context: {'host': 'clarity', 'topic': 'moon', 'bonus': {'foo': 'bar'}},
|
||||
tags: ['a', 'b', 'c'],
|
||||
messages: [
|
||||
{ metric: 'request/time', value: 122 },
|
||||
{ metric: 'request/time', value: 434 },
|
||||
{ metric: 'request/time', value: 565 }
|
||||
],
|
||||
'value': 5
|
||||
},
|
||||
{
|
||||
context: {'host': 'pivot', 'popic': 'sun'},
|
||||
tags: ['a', 'd'],
|
||||
messages: [
|
||||
{ metric: 'request/time', value: 44 },
|
||||
{ metric: 'request/time', value: 65 }
|
||||
],
|
||||
'value': 4
|
||||
},
|
||||
{
|
||||
context: {'host': 'imply', 'dopik': 'fun'},
|
||||
tags: ['x', 'y'],
|
||||
messages: [
|
||||
{ metric: 'request/time', value: 4 },
|
||||
{ metric: 'request/time', value: 5 }
|
||||
],
|
||||
'value': 2
|
||||
}
|
||||
];
|
||||
|
||||
it('works for path, ignore-arrays', () => {
|
||||
expect(computeFlattenExprsForData(data, 'path', 'ignore-arrays')).toEqual([
|
||||
'$.context.bonus.foo',
|
||||
'$.context.dopik',
|
||||
'$.context.host',
|
||||
'$.context.popic',
|
||||
'$.context.topic'
|
||||
]);
|
||||
});
|
||||
|
||||
it('works for jq, ignore-arrays', () => {
|
||||
expect(computeFlattenExprsForData(data, 'jq', 'ignore-arrays')).toEqual([
|
||||
'.context.bonus.foo',
|
||||
'.context.dopik',
|
||||
'.context.host',
|
||||
'.context.popic',
|
||||
'.context.topic'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { FlattenField } from './ingestion-spec';
|
||||
|
||||
export type ExprType = 'path' | 'jq';
|
||||
export type ArrayHandling = 'ignore-arrays' | 'include-arrays';
|
||||
|
||||
export function computeFlattenPathsForData(data: Record<string, any>[], exprType: ExprType, arrayHandling: ArrayHandling): FlattenField[] {
|
||||
return computeFlattenExprsForData(data, exprType, arrayHandling).map((expr, i) => {
|
||||
return {
|
||||
type: exprType,
|
||||
name: `expr_${i}`,
|
||||
expr
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function computeFlattenExprsForData(data: Record<string, any>[], exprType: ExprType, arrayHandling: ArrayHandling): string[] {
|
||||
const seenPaths: Record<string, boolean> = {};
|
||||
for (const datum of data) {
|
||||
const datumKeys = Object.keys(datum);
|
||||
for (const datumKey of datumKeys) {
|
||||
const datumValue = datum[datumKey];
|
||||
if (isNested(datumValue)) {
|
||||
addPath(seenPaths, (exprType === 'path' ? `$.${datumKey}` : `.${datumKey}`), datumValue, arrayHandling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(seenPaths).sort();
|
||||
}
|
||||
|
||||
function addPath(paths: Record<string, boolean>, path: string, value: any, arrayHandling: ArrayHandling) {
|
||||
if (isNested(value)) {
|
||||
if (!Array.isArray(value)) {
|
||||
const valueKeys = Object.keys(value);
|
||||
for (const valueKey of valueKeys) {
|
||||
addPath(paths, `${path}.${valueKey}`, value[valueKey], arrayHandling);
|
||||
}
|
||||
} else if (arrayHandling === 'include-arrays') {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
addPath(paths, `${path}[${i}]`, value[i], arrayHandling);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
paths[path] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Checks that the given value is nested as far as Druid is concerned
|
||||
function isNested(v: any): boolean {
|
||||
return Boolean(v) && typeof v === 'object' && (!Array.isArray(v) || v.some(isNested));
|
||||
}
|
|
@ -23,6 +23,7 @@ export const DRUID_WEBSITE = 'http://druid.io';
|
|||
export const DRUID_GITHUB = 'https://github.com/apache/druid';
|
||||
export const DRUID_DOCS = 'http://druid.io/docs/latest';
|
||||
export const DRUID_DOCS_SQL = 'http://druid.io/docs/latest/querying/sql.html';
|
||||
export const DRUID_DOCS_RUNE = 'http://druid.io/docs/latest/querying/querying.html';
|
||||
export const DRUID_COMMUNITY = 'http://druid.io/community/';
|
||||
export const DRUID_USER_GROUP = 'https://groups.google.com/forum/#!forum/druid-user';
|
||||
export const DRUID_DEVELOPER_GROUP = 'https://lists.apache.org/list.html?dev@druid.apache.org';
|
||||
|
|
|
@ -211,7 +211,7 @@ GROUP BY 1`);
|
|||
} : null
|
||||
}
|
||||
confirmButtonText="Drop data"
|
||||
successText="Data has been dropped"
|
||||
successText="Data drop request acknowledged, next time the coordinator runs data will be dropped"
|
||||
failText="Could not drop data"
|
||||
intent={Intent.DANGER}
|
||||
onClose={(success) => {
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.load-data-view {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-gap: 10px 15px;
|
||||
grid-template-columns: 1fr 280px;
|
||||
grid-template-rows: 55px 1fr 28px;
|
||||
grid-template-areas:
|
||||
"navi navi"
|
||||
"main ctrl"
|
||||
"main next";
|
||||
|
||||
&.init {
|
||||
display: block;
|
||||
|
||||
& > * {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cards {
|
||||
.bp3-card {
|
||||
display: inline-block;
|
||||
width: 250px;
|
||||
height: 140px;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
padding-top: 47px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.partition,
|
||||
&.tuning,
|
||||
&.publish {
|
||||
grid-gap: 20px 50px;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-areas:
|
||||
"navi navi navi"
|
||||
"main othr ctrl"
|
||||
"main othr next";
|
||||
|
||||
.main,
|
||||
.other {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-nav {
|
||||
grid-area: navi;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
|
||||
.stage-section {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.stage-nav-l1 {
|
||||
height: 25px;
|
||||
font-weight: bold;
|
||||
color: #eeeeee;
|
||||
}
|
||||
|
||||
.stage-nav-l2 {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
grid-area: main;
|
||||
position: relative;
|
||||
|
||||
.raw-lines {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.table-with-control {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.table-control {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: 30px;
|
||||
|
||||
& > * {
|
||||
display: inline-block;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.clearable-input {
|
||||
width: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
.ReactTable {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 45px;
|
||||
bottom: 0;
|
||||
|
||||
.rt-th {
|
||||
position: relative;
|
||||
|
||||
&.selected::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
content: "";
|
||||
border: 2px solid #ff5d10;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
&.flattened,
|
||||
&.transformed {
|
||||
background: rgba(201, 128, 22, 0.2);
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.timestamp {
|
||||
background: rgba(19, 129, 201, 0.5);
|
||||
}
|
||||
|
||||
&.dimension {
|
||||
background: rgba(38, 170, 201, 0.5);
|
||||
|
||||
&.long { background: rgba(19, 129, 201, 0.5); }
|
||||
&.float { background: rgba(25, 145, 201, 0.5); }
|
||||
}
|
||||
|
||||
&.metric {
|
||||
background: rgba(201, 191, 55, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.rt-td {
|
||||
&.flattened,
|
||||
&.transformed {
|
||||
background: rgba(201, 128, 22, 0.05);
|
||||
}
|
||||
|
||||
&.timestamp {
|
||||
background: rgba(19, 129, 201, 0.15);
|
||||
}
|
||||
|
||||
&.dimension {
|
||||
background: rgba(38, 170, 201, 0.1);
|
||||
|
||||
&.long { background: rgba(19, 129, 201, 0.1); }
|
||||
&.float { background: rgba(25, 145, 201, 0.1); }
|
||||
}
|
||||
|
||||
&.metric {
|
||||
background: rgba(201, 191, 55, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.parse-detail {
|
||||
padding: 10px;
|
||||
|
||||
.parse-error {
|
||||
color: #9E2B0E;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.other {
|
||||
grid-area: othr;
|
||||
}
|
||||
|
||||
.control {
|
||||
grid-area: ctrl;
|
||||
|
||||
.intro {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.optional {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.next-bar {
|
||||
grid-area: next;
|
||||
text-align: right;
|
||||
|
||||
.prev {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.column-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.edit-controls {
|
||||
background: #30404c;
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.controls-buttons {
|
||||
position: relative;
|
||||
|
||||
.add-update {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -36,10 +36,6 @@
|
|||
|
||||
.ReactTable {
|
||||
flex: 1;
|
||||
|
||||
.null-table-cell {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import * as Hjson from 'hjson';
|
|||
import * as React from 'react';
|
||||
import ReactTable from 'react-table';
|
||||
|
||||
import { NullTableCell } from '../components/null-table-cell';
|
||||
import { SqlControl } from '../components/sql-control';
|
||||
import { QueryPlanDialog } from '../dialogs/query-plan-dialog';
|
||||
import {
|
||||
|
@ -200,11 +201,7 @@ export class SqlView extends React.Component<SqlViewProps, SqlViewState> {
|
|||
return {
|
||||
Header: h,
|
||||
accessor: String(i),
|
||||
Cell: row => {
|
||||
const value = row.value;
|
||||
if (value === '' || value === null) return <span className="null-table-cell">null</span>;
|
||||
return value;
|
||||
}
|
||||
Cell: row => <NullTableCell value={row.value}/>
|
||||
};
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Alert, Button, ButtonGroup, Intent, Label } from '@blueprintjs/core';
|
||||
import { Alert, Button, ButtonGroup, Intent, Label, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import axios from 'axios';
|
||||
import * as React from 'react';
|
||||
|
@ -34,10 +34,13 @@ import {
|
|||
booleanCustomTableFilter,
|
||||
countBy,
|
||||
formatDuration,
|
||||
getDruidErrorMessage, LocalStorageKeys,
|
||||
getDruidErrorMessage, localStorageGet, LocalStorageKeys,
|
||||
queryDruidSql,
|
||||
QueryManager, TableColumnSelectionHandler
|
||||
} from '../utils';
|
||||
import { IngestionType } from '../utils/ingestion-spec';
|
||||
|
||||
import { LoadDataViewSeed } from './load-data-view';
|
||||
|
||||
import './tasks-view.scss';
|
||||
|
||||
|
@ -48,6 +51,7 @@ export interface TasksViewProps extends React.Props<any> {
|
|||
taskId: string | null;
|
||||
goToSql: (initSql: string) => void;
|
||||
goToMiddleManager: (middleManager: string) => void;
|
||||
goToLoadDataView: () => void;
|
||||
noSqlMode: boolean;
|
||||
}
|
||||
|
||||
|
@ -71,6 +75,7 @@ export interface TasksViewState {
|
|||
|
||||
supervisorSpecDialogOpen: boolean;
|
||||
taskSpecDialogOpen: boolean;
|
||||
initSpec: any;
|
||||
alertErrorMsg: string | null;
|
||||
}
|
||||
|
||||
|
@ -131,6 +136,7 @@ export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
|
|||
|
||||
supervisorSpecDialogOpen: false,
|
||||
taskSpecDialogOpen: false,
|
||||
initSpec: null,
|
||||
alertErrorMsg: null
|
||||
|
||||
};
|
||||
|
@ -224,6 +230,14 @@ ORDER BY "rank" DESC, "created_time" DESC`);
|
|||
this.taskQueryManager.terminate();
|
||||
}
|
||||
|
||||
private closeSpecDialogs = () => {
|
||||
this.setState({
|
||||
supervisorSpecDialogOpen: false,
|
||||
taskSpecDialogOpen: false,
|
||||
initSpec: null
|
||||
});
|
||||
}
|
||||
|
||||
private submitSupervisor = async (spec: JSON) => {
|
||||
try {
|
||||
await axios.post('/druid/indexer/v1/supervisor', spec);
|
||||
|
@ -447,7 +461,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
|
|||
show: supervisorTableColumnSelectionHandler.showColumn('Actions')
|
||||
}
|
||||
]}
|
||||
defaultPageSize={10}
|
||||
defaultPageSize={5}
|
||||
className="-striped -highlight"
|
||||
/>
|
||||
{this.renderResumeSupervisorAction()}
|
||||
|
@ -614,10 +628,21 @@ ORDER BY "rank" DESC, "created_time" DESC`);
|
|||
}
|
||||
|
||||
render() {
|
||||
const { goToSql, noSqlMode } = this.props;
|
||||
const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg } = this.state;
|
||||
const { goToSql, goToLoadDataView, noSqlMode } = this.props;
|
||||
const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, initSpec, alertErrorMsg } = this.state;
|
||||
const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
|
||||
|
||||
const submitTaskMenu = <Menu>
|
||||
<MenuItem
|
||||
text="Raw JSON task"
|
||||
onClick={() => this.setState({ taskSpecDialogOpen: true })}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Go to data loader"
|
||||
onClick={() => goToLoadDataView()}
|
||||
/>
|
||||
</Menu>;
|
||||
|
||||
return <div className="tasks-view app-view">
|
||||
<ViewControlBar label="Supervisors">
|
||||
<Button
|
||||
|
@ -661,11 +686,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
|
|||
onClick={() => goToSql(this.taskQueryManager.getLastQuery())}
|
||||
/>
|
||||
}
|
||||
<Button
|
||||
icon={IconNames.PLUS}
|
||||
text="Submit task"
|
||||
onClick={() => this.setState({ taskSpecDialogOpen: true })}
|
||||
/>
|
||||
<Popover content={submitTaskMenu} position={Position.BOTTOM_LEFT}>
|
||||
<Button icon={IconNames.PLUS} text="Submit task"/>
|
||||
</Popover>
|
||||
<TableColumnSelection
|
||||
columns={taskTableColumns}
|
||||
onChange={(column) => taskTableColumnSelectionHandler.changeTableColumnSelection(column)}
|
||||
|
@ -673,16 +696,24 @@ ORDER BY "rank" DESC, "created_time" DESC`);
|
|||
/>
|
||||
</ViewControlBar>
|
||||
{this.renderTaskTable()}
|
||||
{ supervisorSpecDialogOpen ? <SpecDialog
|
||||
onClose={() => this.setState({ supervisorSpecDialogOpen: false })}
|
||||
onSubmit={this.submitSupervisor}
|
||||
title="Submit supervisor"
|
||||
/> : null }
|
||||
{ taskSpecDialogOpen ? <SpecDialog
|
||||
onClose={() => this.setState({ taskSpecDialogOpen: false })}
|
||||
onSubmit={this.submitTask}
|
||||
title="Submit task"
|
||||
/> : null }
|
||||
{
|
||||
supervisorSpecDialogOpen &&
|
||||
<SpecDialog
|
||||
onClose={this.closeSpecDialogs}
|
||||
onSubmit={this.submitSupervisor}
|
||||
title="Submit supervisor"
|
||||
initSpec={initSpec}
|
||||
/>
|
||||
}
|
||||
{
|
||||
taskSpecDialogOpen &&
|
||||
<SpecDialog
|
||||
onClose={this.closeSpecDialogs}
|
||||
onSubmit={this.submitTask}
|
||||
title="Submit task"
|
||||
initSpec={initSpec}
|
||||
/>
|
||||
}
|
||||
<Alert
|
||||
icon={IconNames.ERROR}
|
||||
intent={Intent.PRIMARY}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"lib/sql-function-doc.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.test.ts"
|
||||
"**/*.spec.ts",
|
||||
"**/*.spec.tsx"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -19,11 +19,20 @@
|
|||
const process = require('process');
|
||||
const path = require('path');
|
||||
const postcssPresetEnv = require('postcss-preset-env');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
const { version } = require('./package.json');
|
||||
|
||||
module.exports = (env) => {
|
||||
const druidUrl = 'http://' + ((env || {}).druid_host || process.env.druid_host || 'localhost:8888');
|
||||
let druidUrl = ((env || {}).druid_host || process.env.druid_host || 'localhost');
|
||||
if (!druidUrl.startsWith('http')) druidUrl = 'http://' + druidUrl;
|
||||
if (!/:\d+$/.test(druidUrl)) druidUrl += ':8888';
|
||||
|
||||
const proxyTarget = {
|
||||
target: druidUrl,
|
||||
secure: false
|
||||
};
|
||||
|
||||
return {
|
||||
mode: process.env.NODE_ENV || 'development',
|
||||
entry: {
|
||||
|
@ -42,10 +51,11 @@ module.exports = (env) => {
|
|||
devServer: {
|
||||
publicPath: '/public',
|
||||
index: './index.html',
|
||||
openPage: 'unified-console.html',
|
||||
port: 18081,
|
||||
proxy: {
|
||||
'/status': druidUrl,
|
||||
'/druid': druidUrl
|
||||
'/status': proxyTarget,
|
||||
'/druid': proxyTarget
|
||||
}
|
||||
},
|
||||
module: {
|
||||
|
@ -89,6 +99,9 @@ module.exports = (env) => {
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
// new BundleAnalyzerPlugin()
|
||||
]
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue