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')}>&#x270E;</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}&nbsp;
+                <a>&#x270E;</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

Reply via email to