mirror of
https://github.com/apache/druid.git
synced 2025-02-22 18:30:13 +00:00
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:
parent
0c387c1d47
commit
4c215b417e
6
web-console/package-lock.json
generated
6
web-console/package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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}'`);
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -18,6 +18,11 @@
|
||||
$side-bar-width: 120px;
|
||||
|
||||
.status-dialog {
|
||||
.loader {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
&.bp3-dialog {
|
||||
margin-top: 5vh;
|
||||
top: 5%;
|
||||
|
@ -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')}
|
||||
/>
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -258,7 +258,7 @@
|
||||
.controls-buttons {
|
||||
position: relative;
|
||||
|
||||
.add-update {
|
||||
.bp3-button {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user