Web console: misc bug fixes and tidy up (#8654)

* fix updates

* fix status dialog

* fix scan query deserialization

* extract error message

* update snapshot
This commit is contained in:
Vadim Ogievetsky 2019-10-10 10:52:46 -07:00 committed by Fangjin Yang
parent 0c387c1d47
commit 4c215b417e
11 changed files with 236 additions and 44 deletions

View File

@ -4456,9 +4456,9 @@
"integrity": "sha512-0sYnfUHHMoajaud/i5BHKA12bUxiWEHJ9rxGqVEppFxsEcxef0TZQ5J59lU+UniEBcz/sG5fTESRyS7cOm3tSQ=="
},
"druid-query-toolkit": {
"version": "0.3.28",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.28.tgz",
"integrity": "sha512-AtwTGlofLEzV1cnXwZrNlynHA3Htw6Fgt7r4ZEXx7SSzLUfMkS7lcZ0WY6haHzoZkC8GyUivEuoSivygRgEotg==",
"version": "0.3.29",
"resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.29.tgz",
"integrity": "sha512-WKpsmmqgZd5vgOGCyWZ+2h0aNpTbd82h0svC5GBbhqmXB++vkJchYPGjPmmHkNMV2JI2f40ztxel3hpZv5zSQg==",
"requires": {
"tslib": "^1.10.0"
}

View File

@ -63,7 +63,7 @@
"d3": "^5.10.1",
"d3-array": "^2.3.1",
"druid-console": "0.0.2",
"druid-query-toolkit": "^0.3.28",
"druid-query-toolkit": "^0.3.29",
"file-saver": "^2.0.2",
"has-own-prop": "^2.0.0",
"hjson": "^3.1.2",

View File

@ -53,6 +53,7 @@ export interface AutoFormProps<T> {
fields: Field<T>[];
model: T | undefined;
onChange: (newModel: T) => void;
onFinalize?: () => void;
showCustom?: (model: T) => boolean;
updateJsonValidity?: (jsonValidity: boolean) => void;
large?: boolean;
@ -128,7 +129,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
};
private renderNumberInput(field: Field<T>): JSX.Element {
const { model, large } = this.props;
const { model, large, onFinalize } = this.props;
const modelValue = deepGet(model as any, field.name) || field.defaultValue;
return (
@ -142,6 +143,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
if (e.target.value === '') {
this.fieldChange(field, undefined);
}
if (onFinalize) onFinalize();
}}
min={field.min || 0}
fill
@ -158,7 +160,8 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
}
private renderSizeBytesInput(field: Field<T>): JSX.Element {
const { model, large } = this.props;
const { model, large, onFinalize } = this.props;
return (
<NumericInput
value={deepGet(model as any, field.name) || field.defaultValue}
@ -166,6 +169,9 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
if (isNaN(v)) return;
this.fieldChange(field, v);
}}
onBlur={() => {
if (onFinalize) onFinalize();
}}
min={0}
stepSize={1000}
majorStepSize={1000000}
@ -177,7 +183,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
}
private renderStringInput(field: Field<T>, sanitize?: (str: string) => string): JSX.Element {
const { model, large } = this.props;
const { model, large, onFinalize } = this.props;
const modelValue = deepGet(model as any, field.name);
return (
@ -190,6 +196,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
onBlur={() => {
if (modelValue === '') this.fieldChange(field, undefined);
}}
onFinalize={onFinalize}
placeholder={field.placeholder}
suggestions={field.suggestions}
large={large}
@ -204,7 +211,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
}
private renderBooleanInput(field: Field<T>): JSX.Element {
const { model, large } = this.props;
const { model, large, onFinalize } = this.props;
const modelValue = deepGet(model as any, field.name);
const shownValue = modelValue == null ? field.defaultValue : modelValue;
const disabled = AutoForm.evaluateFunctor(field.disabled, model);
@ -219,7 +226,10 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
intent={intent}
disabled={disabled}
active={shownValue === false}
onClick={() => this.fieldChange(field, false)}
onClick={() => {
this.fieldChange(field, false);
if (onFinalize) onFinalize();
}}
>
False
</Button>
@ -227,7 +237,10 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
intent={intent}
disabled={disabled}
active={shownValue === true}
onClick={() => this.fieldChange(field, true)}
onClick={() => {
this.fieldChange(field, true);
if (onFinalize) onFinalize();
}}
>
True
</Button>
@ -235,7 +248,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
);
}
private renderJSONInput(field: Field<T>): JSX.Element {
private renderJsonInput(field: Field<T>): JSX.Element {
const { model, updateJsonValidity } = this.props;
const { jsonInputsValidity } = this.state;
@ -300,7 +313,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
case 'string-array':
return this.renderStringArrayInput(field);
case 'json':
return this.renderJSONInput(field);
return this.renderJsonInput(field);
default:
throw new Error(`unknown field type '${field.type}'`);
}

View File

@ -37,19 +37,28 @@ export interface SuggestionGroup {
export interface SuggestibleInputProps extends HTMLInputProps {
onValueChange: (newValue: string) => void;
onFinalize?: () => void;
suggestions?: (string | SuggestionGroup)[];
large?: boolean;
intent?: Intent;
}
export class SuggestibleInput extends React.PureComponent<SuggestibleInputProps> {
private lastFocusValue?: string;
constructor(props: SuggestibleInputProps, context: any) {
super(props, context);
// this.state = {};
}
public handleSuggestionSelect(suggestion: string) {
const { onValueChange, onFinalize } = this.props;
onValueChange(suggestion);
if (onFinalize) onFinalize();
}
renderSuggestionsMenu() {
const { suggestions, onValueChange } = this.props;
const { suggestions } = this.props;
if (!suggestions) return undefined;
return (
@ -60,7 +69,7 @@ export class SuggestibleInput extends React.PureComponent<SuggestibleInputProps>
<MenuItem
key={suggestion}
text={suggestion}
onClick={() => onValueChange(suggestion)}
onClick={() => this.handleSuggestionSelect(suggestion)}
/>
);
} else {
@ -70,7 +79,7 @@ export class SuggestibleInput extends React.PureComponent<SuggestibleInputProps>
<MenuItem
key={suggestion}
text={suggestion}
onClick={() => onValueChange(suggestion)}
onClick={() => this.handleSuggestionSelect(suggestion)}
/>
))}
</MenuItem>
@ -82,9 +91,17 @@ export class SuggestibleInput extends React.PureComponent<SuggestibleInputProps>
}
render(): JSX.Element {
const { className, value, defaultValue, onValueChange, ...rest } = this.props;
const suggestionsMenu = this.renderSuggestionsMenu();
const {
className,
value,
defaultValue,
onValueChange,
onFinalize,
onBlur,
...rest
} = this.props;
const suggestionsMenu = this.renderSuggestionsMenu();
return (
<InputGroup
className={classNames('suggestible-input', className)}
@ -93,6 +110,14 @@ export class SuggestibleInput extends React.PureComponent<SuggestibleInputProps>
onChange={(e: any) => {
onValueChange(e.target.value);
}}
onFocus={(e: any) => {
this.lastFocusValue = e.target.value;
}}
onBlur={(e: any) => {
if (onBlur) onBlur(e);
if (this.lastFocusValue === e.target.value) return;
if (onFinalize) onFinalize();
}}
rightElement={
suggestionsMenu && (
<Popover content={suggestionsMenu} position={Position.BOTTOM_RIGHT} autoFocus={false}>

View File

@ -1,3 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`status dialog matches snapshot 1`] = `<div />`;
exports[`status dialog matches snapshot 1`] = `
<div
class="bp3-portal"
>
<div
class="bp3-overlay bp3-overlay-open bp3-overlay-scroll-container"
>
<div
class="bp3-overlay-backdrop bp3-overlay-appear bp3-overlay-appear-active"
tabindex="0"
/>
<div
class="bp3-dialog-container bp3-overlay-content bp3-overlay-appear bp3-overlay-appear-active"
tabindex="0"
>
<div
class="bp3-dialog status-dialog"
>
<div
class="bp3-dialog-header"
>
<h4
class="bp3-heading"
>
Status
</h4>
<button
aria-label="Close"
class="bp3-button bp3-minimal bp3-dialog-close-button"
type="button"
>
<span
class="bp3-icon bp3-icon-small-cross"
icon="small-cross"
>
<svg
data-icon="small-cross"
height="20"
viewBox="0 0 20 20"
width="20"
>
<desc>
small-cross
</desc>
<path
d="M11.41 10l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L11.41 10z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</div>
<div
class="status-dialog-main-area"
>
<div
class="loader"
>
<div
class="loader-logo"
>
<svg
viewBox="0 0 100 100"
>
<path
class="one"
d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
/>
<path
class="two"
d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
C63.5,58,59.9,59.5,55.7,59.5z"
/>
<path
class="three"
d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
/>
<path
class="four"
d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
C46.4,69.2,45.8,69.8,45.1,69.8z"
/>
</svg>
</div>
</div>
</div>
<div
class="bp3-dialog-footer"
>
<div
class="viewRawButton"
>
<button
class="bp3-button bp3-minimal"
type="button"
>
<span
class="bp3-button-text"
>
View raw
</span>
</button>
</div>
<div
class="closeButton"
>
<button
class="bp3-button bp3-intent-primary"
type="button"
>
<span
class="bp3-button-text"
>
Close
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -18,6 +18,11 @@
$side-bar-width: 120px;
.status-dialog {
.loader {
position: relative;
height: 500px;
}
&.bp3-dialog {
margin-top: 5vh;
top: 5%;

View File

@ -27,13 +27,19 @@ import { QueryManager } from '../../utils';
import './status-dialog.scss';
interface StatusResponse {
version: string;
modules: any[];
}
interface StatusDialogProps {
onClose: () => void;
}
interface StatusDialogState {
response: any;
response?: StatusResponse;
loading: boolean;
error?: string;
}
export class StatusDialog extends React.PureComponent<StatusDialogProps, StatusDialogState> {
@ -42,22 +48,23 @@ export class StatusDialog extends React.PureComponent<StatusDialogProps, StatusD
}
private showStatusQueryManager: QueryManager<null, any>;
constructor(props: StatusDialogProps, context: any) {
super(props, context);
this.state = {
response: [],
loading: false,
};
this.showStatusQueryManager = new QueryManager({
processQuery: async () => {
const endpoint = UrlBaser.base(`/status`);
const resp = await axios.get(endpoint);
const resp = await axios.get(`/status`);
return resp.data;
},
onStateChange: ({ result, loading }) => {
onStateChange: ({ result, loading, error }) => {
this.setState({
loading,
response: result,
error,
});
},
});
@ -67,13 +74,16 @@ export class StatusDialog extends React.PureComponent<StatusDialogProps, StatusD
this.showStatusQueryManager.runQuery(null);
}
render(): JSX.Element {
const { onClose } = this.props;
const { response, loading } = this.state;
if (loading) return <Loader />;
return (
<Dialog className={'status-dialog'} onClose={onClose} isOpen title="Status">
<div className={'status-dialog-main-area'}>
renderContent(): JSX.Element | undefined {
const { response, loading, error } = this.state;
if (loading) return <Loader loading />;
if (error) return <span>{`Error while loading status: ${error}`}</span>;
if (response) {
return (
<>
<FormGroup label="Version" labelFor="version" inline>
<InputGroup id="version" defaultValue={response.version} readOnly />
</FormGroup>
@ -103,12 +113,23 @@ export class StatusDialog extends React.PureComponent<StatusDialogProps, StatusD
filterable
defaultFilterMethod={StatusDialog.anywhereMatcher}
/>
</div>
</>
);
}
return;
}
render(): JSX.Element {
const { onClose } = this.props;
return (
<Dialog className={'status-dialog'} onClose={onClose} isOpen title="Status">
<div className={'status-dialog-main-area'}>{this.renderContent()}</div>
<div className={Classes.DIALOG_FOOTER}>
<div className="viewRawButton">
<Button
text="View raw"
disabled={!response}
minimal
onClick={() => window.open(UrlBaser.base(`/status`), '_blank')}
/>

View File

@ -533,7 +533,7 @@ GROUP BY 1`;
this.datasourceQueryManager.rerunLastQuery();
} catch (e) {
AppToaster.show({
message: e,
message: getDruidErrorMessage(e),
intent: Intent.DANGER,
});
}
@ -556,7 +556,7 @@ GROUP BY 1`;
);
} catch (e) {
AppToaster.show({
message: e,
message: getDruidErrorMessage(e),
intent: Intent.DANGER,
});
}

View File

@ -258,7 +258,7 @@
.controls-buttons {
position: relative;
.add-update {
.bp3-button {
margin-right: 15px;
}

View File

@ -2325,6 +2325,11 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
]}
model={spec}
onChange={s => this.updateSpec(s)}
onFinalize={() => {
setTimeout(() => {
this.queryForSchema();
}, 10);
}}
/>
</>
)}
@ -2443,6 +2448,9 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
};
if (selectedDimensionSpec) {
const curDimensions =
deepGet(spec, `dataSchema.parser.parseSpec.dimensionsSpec.dimensions`) || EMPTY_ARRAY;
return (
<div className="edit-controls">
<AutoForm
@ -2470,11 +2478,9 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
<Button
icon={IconNames.TRASH}
intent={Intent.DANGER}
disabled={curDimensions.length <= 1}
onClick={() => {
const curDimensions =
deepGet(spec, `dataSchema.parser.parseSpec.dimensionsSpec.dimensions`) ||
EMPTY_ARRAY;
if (curDimensions.length <= 1) return; // Guard against removing the last dimension, ToDo: some better feedback here would be good
if (curDimensions.length <= 1) return; // Guard against removing the last dimension
this.updateSpec(
deepDelete(

View File

@ -73,17 +73,14 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
accessor: String(i),
Cell: row => {
const value = row.value;
const popover = (
if (!value) return value == null ? null : value;
return (
<div>
<Popover content={this.getRowActions(value, h)}>
<div>{value}</div>
</Popover>
</div>
);
if (value) {
return popover;
}
return value;
},
className:
aggregateColumns && aggregateColumns.includes(h) ? 'aggregate-column' : undefined,