This is an automated email from the ASF dual-hosted git repository. cwylie pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-druid.git
The following commit(s) were added to refs/heads/master by this push: new 1f4ad51 Add compaction dialog in druid console which allows users to add/edit data source compaction configuration (#7242) 1f4ad51 is described below commit 1f4ad518d8d1798e11f836a71efac02be38eeae6 Author: Qi Shu <shuqi...@gmail.com> AuthorDate: Sun Mar 17 19:21:23 2019 -0700 Add compaction dialog in druid console which allows users to add/edit data source compaction configuration (#7242) * Add compaction dialog in druid console which allows users to add/edit data source compaction configuration * Addressed naming issues; changed json input validating process --- .../auto-form.scss} | 32 +---- web-console/src/components/auto-form.tsx | 34 ++++- web-console/src/components/filler.tsx | 71 +++++++++++ ...etention-dialog.scss => compaction-dialog.scss} | 33 ++--- web-console/src/dialogs/compaction-dialog.tsx | 140 +++++++++++++++++++++ web-console/src/dialogs/retention-dialog.scss | 9 +- web-console/src/utils/general.tsx | 18 +++ web-console/src/views/datasource-view.tsx | 75 ++++++++++- 8 files changed, 344 insertions(+), 68 deletions(-) diff --git a/web-console/src/dialogs/retention-dialog.scss b/web-console/src/components/auto-form.scss similarity index 66% copy from web-console/src/dialogs/retention-dialog.scss copy to web-console/src/components/auto-form.scss index 7c9b51d..c27406c 100644 --- a/web-console/src/dialogs/retention-dialog.scss +++ b/web-console/src/components/auto-form.scss @@ -16,32 +16,8 @@ * limitations under the License. */ -.retention-dialog { - width: 750px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) !important; - - .dialog-body { - overflow: scroll; - max-height: 70vh; - - .form-group { - margin: 0 0 5px; - } - - .small { - width: 0px; - } - - .comment { - margin-top: 10px; - - textarea { - max-width: 200px; - padding: 0 15px; - } - } +.auto-form { + .ace_scroller { + background-color: #212c36; } -} +} \ No newline at end of file diff --git a/web-console/src/components/auto-form.tsx b/web-console/src/components/auto-form.tsx index 6f86a41..686a267 100644 --- a/web-console/src/components/auto-form.tsx +++ b/web-console/src/components/auto-form.tsx @@ -19,12 +19,14 @@ import { InputGroup } from "@blueprintjs/core"; import * as React from 'react'; -import { FormGroup, HTMLSelect, NumericInput, TagInput } from "../components/filler"; +import { FormGroup, HTMLSelect, JSONInput, NumericInput, TagInput } from "../components/filler"; + +import "./auto-form.scss"; interface Field { name: string; label?: string; - type: 'number' | 'size-bytes' | 'string' | 'boolean' | 'string-array'; + type: 'number' | 'size-bytes' | 'string' | 'boolean' | 'string-array' | 'json'; min?: number; } @@ -32,9 +34,11 @@ export interface AutoFormProps<T> extends React.Props<any> { fields: Field[]; model: T | null; onChange: (newValue: T) => void; + updateJSONValidity?: (jsonValidity: boolean) => void; } export interface AutoFormState<T> { + jsonInputsValidity: any; } export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState<T>> { @@ -47,6 +51,7 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState constructor(props: AutoFormProps<T>) { super(props); this.state = { + jsonInputsValidity: {} }; } @@ -99,9 +104,30 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState </HTMLSelect>; } + private renderJSONInput(field: Field): JSX.Element { + const { model, onChange, updateJSONValidity } = this.props; + const { jsonInputsValidity } = this.state; + + const updateInputValidity = (e: any) => { + if (updateJSONValidity) { + const newJSONInputValidity = Object.assign({}, jsonInputsValidity, { [field.name]: e}); + this.setState({ + jsonInputsValidity: newJSONInputValidity + }); + const allJSONValid: boolean = Object.keys(newJSONInputValidity).every(property => newJSONInputValidity[property] === true); + updateJSONValidity(allJSONValid); + } + }; + + return <JSONInput + value={(model as any)[field.name]} + onChange={(e: any) => onChange(Object.assign({}, model, { [field.name]: e}))} + updateInputValidity={updateInputValidity} + />; + } + private renderStringArrayInput(field: Field): JSX.Element { const { model, onChange } = this.props; - const label = field.label || AutoForm.makeLabelName(field.name); return <TagInput values={(model as any)[field.name] || []} onChange={(v: any) => { @@ -118,6 +144,7 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState case 'string': return this.renderStringInput(field); case 'boolean': return this.renderBooleanInput(field); case 'string-array': return this.renderStringArrayInput(field); + case 'json': return this.renderJSONInput(field); default: throw new Error(`unknown field type '${field.type}'`); } } @@ -131,7 +158,6 @@ export class AutoForm<T> extends React.Component<AutoFormProps<T>, AutoFormState render() { const { fields, model } = this.props; - return <div className="auto-form"> {model && fields.map(field => this.renderField(field))} </div>; diff --git a/web-console/src/components/filler.tsx b/web-console/src/components/filler.tsx index e20cd77..11dcf93 100644 --- a/web-console/src/components/filler.tsx +++ b/web-console/src/components/filler.tsx @@ -19,6 +19,9 @@ import { Button } from '@blueprintjs/core'; import classNames from 'classnames'; import * as React from 'react'; +import AceEditor from "react-ace"; + +import { parseStringToJSON, stringifyJSON, validJson } from "../utils"; import './filler.scss'; @@ -258,3 +261,71 @@ export class TagInput extends React.Component<TagInputProps, { stringValue: stri />; } } + +interface JSONInputProps extends React.Props<any> { + onChange: (newJSONValue: any) => void; + value: any; + updateInputValidity: (valueValid: boolean) => void; +} + +interface JSONInputState { + stringValue: string; +} + +export class JSONInput extends React.Component<JSONInputProps, JSONInputState> { + constructor(props: JSONInputProps) { + super(props); + this.state = { + stringValue: "" + }; + } + + componentDidMount(): void { + const { value } = this.props; + const stringValue = stringifyJSON(value); + this.setState({ + stringValue + }); + } + + componentWillReceiveProps(nextProps: JSONInputProps): void { + if (JSON.stringify(nextProps.value) !== JSON.stringify(this.props.value)) { + this.setState({ + stringValue: stringifyJSON(nextProps.value) + }); + } + } + + render() { + const { onChange, updateInputValidity } = this.props; + const { stringValue } = this.state; + return <AceEditor + className={"bp3-fill"} + key={"hjson"} + mode={"hjson"} + theme="solarized_dark" + name="ace-editor" + onChange={(e: string) => { + this.setState({stringValue: e}); + if (validJson(e) || e === "") onChange(parseStringToJSON(e)); + updateInputValidity(validJson(e) || e === ''); + }} + focus + fontSize={12} + width={'100%'} + height={"8vh"} + showPrintMargin={false} + showGutter={false} + value={stringValue} + editorProps={{ + $blockScrolling: Infinity + }} + setOptions={{ + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + showLineNumbers: false, + tabSize: 2 + }} + />; + } +} diff --git a/web-console/src/dialogs/retention-dialog.scss b/web-console/src/dialogs/compaction-dialog.scss similarity index 70% copy from web-console/src/dialogs/retention-dialog.scss copy to web-console/src/dialogs/compaction-dialog.scss index 7c9b51d..ee1cf67 100644 --- a/web-console/src/dialogs/retention-dialog.scss +++ b/web-console/src/dialogs/compaction-dialog.scss @@ -16,32 +16,15 @@ * limitations under the License. */ -.retention-dialog { - width: 750px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) !important; +.compaction-dialog { + &.pt-dialog { + top: 5%; + } - .dialog-body { - overflow: scroll; + .auto-form { + margin: 10px 15px; + padding: 0 5px 0 5px; max-height: 70vh; - - .form-group { - margin: 0 0 5px; - } - - .small { - width: 0px; - } - - .comment { - margin-top: 10px; - - textarea { - max-width: 200px; - padding: 0 15px; - } - } + overflow: scroll; } } diff --git a/web-console/src/dialogs/compaction-dialog.tsx b/web-console/src/dialogs/compaction-dialog.tsx new file mode 100644 index 0000000..d5006cd --- /dev/null +++ b/web-console/src/dialogs/compaction-dialog.tsx @@ -0,0 +1,140 @@ +/* + * 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, Classes, Dialog, Intent } from "@blueprintjs/core"; +import * as React from 'react'; + +import { AutoForm } from '../components/auto-form'; + +import './compaction-dialog.scss'; + +export interface CompactionDialogProps extends React.Props<any> { + onClose: () => void; + onSave: (config: any) => void; + onDelete: () => void; + datasource: string; + configData: any; +} + +export interface CompactionDialogState { + currentConfig: Record<string, any> | null; + allJSONValid: boolean; +} + +export class CompactionDialog extends React.Component<CompactionDialogProps, CompactionDialogState> { + constructor(props: CompactionDialogProps) { + super(props); + this.state = { + currentConfig: null, + allJSONValid: true + }; + } + + componentDidMount(): void { + const { datasource, configData } = this.props; + let config: Record<string, any> = { + dataSource: datasource, + inputSegmentSizeBytes: 419430400, + keepSegmentGranularity: true, + maxNumSegmentsToCompact: 150, + skipOffsetFromLatest: "P1D", + targetCompactionSizeBytes: 419430400, + taskContext: null, + taskPriority: 25, + tuningConfig: null + }; + if (configData !== undefined) { + config = configData; + } + this.setState({ + currentConfig: config + }); + } + + render() { + const { onClose, onSave, onDelete, datasource, configData } = this.props; + const { currentConfig, allJSONValid } = this.state; + return <Dialog + className="compaction-dialog" + isOpen + onClose={onClose} + canOutsideClickClose={false} + title={`Compaction config: ${datasource}`} + > + <AutoForm + fields={[ + { + name: "inputSegmentSizeBytes", + type: "number" + }, + { + name: "keepSegmentGranularity", + type: "boolean" + }, + { + name: "maxNumSegmentsToCompact", + type: "number" + }, + { + name: "skipOffsetFromLatest", + type: "string" + }, + { + name: "targetCompactionSizeBytes", + type: "number" + }, + { + name: "taskContext", + type: "json" + }, + { + name: "taskPriority", + type: "number" + }, + { + name: "tuningConfig", + type: "json" + } + ]} + model={currentConfig} + onChange={m => this.setState({currentConfig: m})} + updateJSONValidity={e => this.setState({allJSONValid: e})} + /> + <div className={Classes.DIALOG_FOOTER}> + <div className={Classes.DIALOG_FOOTER_ACTIONS}> + <Button + text="Delete" + intent={Intent.DANGER} + onClick={onDelete} + disabled={configData === undefined} + /> + <Button + text="Close" + onClick={onClose} + /> + <Button + text="Submit" + intent={Intent.PRIMARY} + onClick={() => onSave(currentConfig)} + disabled={currentConfig === null || !allJSONValid} + /> + </div> + </div> + </Dialog>; + } +} diff --git a/web-console/src/dialogs/retention-dialog.scss b/web-console/src/dialogs/retention-dialog.scss index 7c9b51d..eab743b 100644 --- a/web-console/src/dialogs/retention-dialog.scss +++ b/web-console/src/dialogs/retention-dialog.scss @@ -17,11 +17,10 @@ */ .retention-dialog { - width: 750px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) !important; + &.pt-dialog { + top: 5%; + width: 750px; + } .dialog-body { overflow: scroll; diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 7fce279..6867faf 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -148,3 +148,21 @@ export function validJson(json: string): boolean { return false; } } + +// stringify JSON to string; if JSON is null, parse empty string "" +export function stringifyJSON(item: any): string { + if (item != null) { + return JSON.stringify(item, null, 2); + } else { + return ""; + } +} + +// parse string to JSON object; if string is empty, return null +export function parseStringToJSON(s: string): JSON | null { + if (s === "") { + return null; + } else { + return JSON.parse(s); + } +} diff --git a/web-console/src/views/datasource-view.tsx b/web-console/src/views/datasource-view.tsx index 1deae85..3ee04a2 100644 --- a/web-console/src/views/datasource-view.tsx +++ b/web-console/src/views/datasource-view.tsx @@ -18,14 +18,13 @@ import { Button, Intent, Switch } from "@blueprintjs/core"; import axios from 'axios'; -import * as classNames from 'classnames'; import * as React from 'react'; -import ReactTable from "react-table"; -import { Filter } from "react-table"; +import ReactTable, { Filter } from "react-table"; import { IconNames } from "../components/filler"; import { RuleEditor } from '../components/rule-editor'; import { AsyncActionDialog } from '../dialogs/async-action-dialog'; +import { CompactionDialog } from "../dialogs/compaction-dialog"; import { RetentionDialog } from '../dialogs/retention-dialog'; import { AppToaster } from '../singletons/toaster'; import { @@ -61,6 +60,7 @@ export interface DatasourcesViewState { showDisabled: boolean; retentionDialogOpenOn: { datasource: string, rules: any[] } | null; + compactionDialogOpenOn: {datasource: string, configData: any} | null; dropDataDatasource: string | null; enableDatasource: string | null; killDatasource: string | null; @@ -95,6 +95,7 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas showDisabled: false, retentionDialogOpenOn: null, + compactionDialogOpenOn: null, dropDataDatasource: null, enableDatasource: null, killDatasource: null @@ -272,6 +273,44 @@ GROUP BY 1`); }, 50); } + private saveCompaction = async (compactionConfig: any) => { + if (compactionConfig === null) return; + try { + await axios.post(`/druid/coordinator/v1/config/compaction`, compactionConfig); + this.setState({compactionDialogOpenOn: null}); + this.datasourceQueryManager.rerunLastQuery(); + } catch (e) { + AppToaster.show({ + message: e, + intent: Intent.DANGER + }); + } + } + + private deleteCompaction = async () => { + const {compactionDialogOpenOn} = this.state; + if (compactionDialogOpenOn === null) return; + const datasource = compactionDialogOpenOn.datasource; + AppToaster.show({ + message: `Are you sure you want to delete ${datasource}'s compaction?`, + intent: Intent.DANGER, + action: { + text: "Confirm", + onClick: async () => { + try { + await axios.delete(`/druid/coordinator/v1/config/compaction/${datasource}`); + this.setState({compactionDialogOpenOn: null}, () => this.datasourceQueryManager.rerunLastQuery()); + } catch (e) { + AppToaster.show({ + message: e, + intent: Intent.DANGER + }); + } + } + } + }); + } + renderRetentionDialog() { const { retentionDialogOpenOn, tiers } = this.state; if (!retentionDialogOpenOn) return null; @@ -286,6 +325,20 @@ GROUP BY 1`); />; } + renderCompactionDialog() { + const { datasources, compactionDialogOpenOn } = this.state; + + if (!compactionDialogOpenOn || !datasources) return; + + return <CompactionDialog + datasource={compactionDialogOpenOn.datasource} + configData={compactionDialogOpenOn.configData} + onClose={() => this.setState({compactionDialogOpenOn: null})} + onSave={this.saveCompaction} + onDelete={this.deleteCompaction} + />; + } + renderDatasourceTable() { const { goToSegments } = this.props; const { datasources, defaultRules, datasourcesLoading, datasourcesError, datasourcesFilter, showDisabled } = this.state; @@ -379,15 +432,24 @@ GROUP BY 1`); filterable: false, Cell: row => { const { compaction } = row.original; + const compactionOpenOn: {datasource: string, configData: any} | null = { + datasource: row.original.datasource, + configData: compaction + }; let text: string; if (compaction) { text = `Target: ${formatBytes(compaction.targetCompactionSizeBytes)}`; } else { text = 'None'; } - return <span>{text} <a onClick={() => alert('ToDo')}>✎</a></span>; - }, - show: false // This feature is not ready, it will be enabled later + return <span + className={"clickable-cell"} + onClick={() => this.setState({compactionDialogOpenOn: compactionOpenOn})} + > + {text} + <a>✎</a> + </span>; + } }, { Header: 'Size', @@ -432,6 +494,7 @@ GROUP BY 1`); {this.renderEnableAction()} {this.renderKillAction()} {this.renderRetentionDialog()} + {this.renderCompactionDialog()} </>; } --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@druid.apache.org For additional commands, e-mail: commits-h...@druid.apache.org