You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by cw...@apache.org on 2019/03/18 02:21:30 UTC
[incubator-druid] branch master updated: Add compaction dialog in
druid console which allows users to add/edit data source compaction
configuration (#7242)
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 <sh...@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-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org