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 18e5167  Add history dialog in snitch dialog to allow viewing the 
editing history (#7321)
18e5167 is described below

commit 18e51672452b5229e17a10e9c83d896bfd86e0f3
Author: Qi Shu <[email protected]>
AuthorDate: Wed Mar 27 17:52:44 2019 -0700

    Add history dialog in snitch dialog to allow viewing the editing history 
(#7321)
    
    * Add history dialog in snitch dialog to allow viewing the editing history
    
    * Improved CSS; better animation
    
    * Use position: absolute instead of float: right to position element
    
    * Removed author for history changes
---
 web-console/src/components/filler.tsx              | 54 +++++++++++--
 .../src/components/table-column-selection.scss     |  6 +-
 .../src/dialogs/coordinator-dynamic-config.scss    |  5 +-
 .../src/dialogs/coordinator-dynamic-config.tsx     | 31 ++++++--
 web-console/src/dialogs/history-dialog.scss        | 88 +++++++++++++++++++++
 web-console/src/dialogs/history-dialog.tsx         | 91 ++++++++++++++++++++++
 .../src/dialogs/overlord-dynamic-config.tsx        | 31 ++++++--
 web-console/src/dialogs/retention-dialog.tsx       | 32 ++++++--
 web-console/src/dialogs/snitch-dialog.tsx          | 78 +++++++++----------
 web-console/src/views/datasource-view.tsx          |  4 +-
 10 files changed, 353 insertions(+), 67 deletions(-)

diff --git a/web-console/src/components/filler.tsx 
b/web-console/src/components/filler.tsx
index decea1f..422ae00 100644
--- a/web-console/src/components/filler.tsx
+++ b/web-console/src/components/filler.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { Button } from '@blueprintjs/core';
+import { Button, Collapse } from '@blueprintjs/core';
 import classNames from 'classnames';
 import * as React from 'react';
 import AceEditor from "react-ace";
@@ -47,7 +47,7 @@ export const IconNames = {
   ARROW_LEFT: "arrow-left" as "arrow-left",
   CARET_RIGHT: "caret-right" as "caret-right",
   TICK: "tick" as "tick",
-  ARROW_RIGHT: "right-arrow" as "right-arrow",
+  ARROW_RIGHT: "arrow-right" as "arrow-right",
   TRASH: "trash" as "trash",
   CARET_DOWN: "caret-down" as "caret-down",
   ARROW_UP: "arrow-up" as "arrow-up",
@@ -152,13 +152,15 @@ export class HTMLSelect extends React.Component<{ key?: 
string; style?: any; onC
   }
 }
 
-export class TextArea extends React.Component<{ className?: string; onChange?: 
any; value?: string }, {}> {
+export class TextArea extends React.Component<{ className?: string; onChange?: 
any; value?: string, readOnly?: boolean, style?: any}, {}> {
   render() {
-    const { className, value, onChange } = this.props;
+    const { className, value, onChange, readOnly, style } = this.props;
     return <textarea
+      readOnly={readOnly}
       className={classNames("pt-input", className)}
       value={value}
       onChange={onChange}
+      style={style}
     />;
   }
 }
@@ -267,6 +269,7 @@ interface JSONInputProps extends React.Props<any> {
   onChange: (newJSONValue: any) => void;
   value: any;
   updateInputValidity: (valueValid: boolean) => void;
+  height?: string;
 }
 
 interface JSONInputState {
@@ -298,7 +301,7 @@ export class JSONInput extends 
React.Component<JSONInputProps, JSONInputState> {
   }
 
   render() {
-    const { onChange, updateInputValidity } = this.props;
+    const { onChange, updateInputValidity, height } = this.props;
     const { stringValue } = this.state;
     return <AceEditor
       className={"bp3-fill"}
@@ -314,7 +317,7 @@ export class JSONInput extends 
React.Component<JSONInputProps, JSONInputState> {
       focus
       fontSize={12}
       width={'100%'}
-      height={"8vh"}
+      height={height ? height : "8vh"}
       showPrintMargin={false}
       showGutter={false}
       value={stringValue}
@@ -330,3 +333,42 @@ export class JSONInput extends 
React.Component<JSONInputProps, JSONInputState> {
     />;
   }
 }
+
+interface JSONCollapseProps extends React.Props<any> {
+  stringValue: string;
+  buttonText: string;
+}
+
+interface JSONCollapseState {
+  isOpen: boolean;
+}
+
+export class JSONCollapse extends React.Component<JSONCollapseProps, 
JSONCollapseState> {
+  constructor(props: any) {
+    super(props);
+    this.state = {
+      isOpen: false
+    };
+  }
+
+  render() {
+    const { stringValue, buttonText} = this.props;
+    const { isOpen } = this.state;
+    const prettyValue = JSON.stringify(JSON.parse(stringValue), undefined, 2);
+    return <div className={"json-collapse"}>
+      <Button
+        className={`pt-minimal ${isOpen ? " pt-active" : ""}`}
+        onClick={() => this.setState({isOpen: !isOpen})}
+        text={buttonText}
+      />
+      <div>
+        <Collapse isOpen={isOpen}>
+          <TextArea
+            readOnly
+            value={prettyValue}
+          />
+        </Collapse>
+      </div>
+    </div>;
+  }
+}
diff --git a/web-console/src/components/table-column-selection.scss 
b/web-console/src/components/table-column-selection.scss
index ddce873..c651a4d 100644
--- a/web-console/src/components/table-column-selection.scss
+++ b/web-console/src/components/table-column-selection.scss
@@ -17,8 +17,10 @@
  */
 
 .table-column-selection {
-
-  float: right;
+  &.pt-popover-target{
+    position: absolute;
+    right: 0;
+  }
 
   .pt-popover-content {
     padding: 10px 10px 1px 10px;
diff --git a/web-console/src/dialogs/coordinator-dynamic-config.scss 
b/web-console/src/dialogs/coordinator-dynamic-config.scss
index 51eaa58..d4bb6d9 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config.scss
+++ b/web-console/src/dialogs/coordinator-dynamic-config.scss
@@ -17,7 +17,10 @@
  */
 
 .coordinator-dynamic-config {
-  margin-top: 5vh;
+  &.pt-dialog {
+    margin-top: 5vh;
+    top: 5%;
+  }
 
   .pt-dialog-body {
     max-height: 70vh;
diff --git a/web-console/src/dialogs/coordinator-dynamic-config.tsx 
b/web-console/src/dialogs/coordinator-dynamic-config.tsx
index 59ee165..b7b2b61 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config.tsx
+++ b/web-console/src/dialogs/coordinator-dynamic-config.tsx
@@ -23,7 +23,7 @@ import * as React from 'react';
 import { AutoForm } from '../components/auto-form';
 import { IconNames } from '../components/filler';
 import { AppToaster } from '../singletons/toaster';
-import { getDruidErrorMessage } from '../utils';
+import { getDruidErrorMessage, QueryManager } from '../utils';
 
 import { SnitchDialog } from './snitch-dialog';
 
@@ -35,18 +35,36 @@ export interface CoordinatorDynamicConfigDialogProps 
extends React.Props<any> {
 
 export interface CoordinatorDynamicConfigDialogState {
   dynamicConfig: Record<string, any> | null;
+  historyRecords: any[];
 }
 
 export class CoordinatorDynamicConfigDialog extends 
React.Component<CoordinatorDynamicConfigDialogProps, 
CoordinatorDynamicConfigDialogState> {
+  private historyQueryManager: QueryManager<string, any>;
+
   constructor(props: CoordinatorDynamicConfigDialogProps) {
     super(props);
     this.state = {
-      dynamicConfig: null
+      dynamicConfig: null,
+      historyRecords: []
     };
   }
 
-  componentDidMount(): void {
+  componentDidMount() {
     this.getClusterConfig();
+
+    this.historyQueryManager = new QueryManager({
+      processQuery: async (query) => {
+        const historyResp = await 
axios(`/druid/coordinator/v1/config/history?count=100`);
+        return historyResp.data;
+      },
+      onStateChange: ({ result, loading, error }) => {
+        this.setState({
+          historyRecords: result
+        });
+      }
+    });
+
+    this.historyQueryManager.runQuery(`dummy`);
   }
 
   async getClusterConfig() {
@@ -67,13 +85,13 @@ export class CoordinatorDynamicConfigDialog extends 
React.Component<CoordinatorD
     });
   }
 
-  private saveClusterConfig = async (author: string, comment: string) => {
+  private saveClusterConfig = async (comment: string) => {
     const { onClose } = this.props;
     const newState: any = this.state.dynamicConfig;
     try {
       await axios.post("/druid/coordinator/v1/config", newState, {
         headers: {
-          "X-Druid-Author": author,
+          "X-Druid-Author": "console",
           "X-Druid-Comment": comment
         }
       });
@@ -94,7 +112,7 @@ export class CoordinatorDynamicConfigDialog extends 
React.Component<CoordinatorD
 
   render() {
     const { onClose } = this.props;
-    const { dynamicConfig } = this.state;
+    const { dynamicConfig, historyRecords } = this.state;
 
     return <SnitchDialog
       className="coordinator-dynamic-config"
@@ -102,6 +120,7 @@ export class CoordinatorDynamicConfigDialog extends 
React.Component<CoordinatorD
       onSave={this.saveClusterConfig}
       onClose={onClose}
       title="Coordinator dynamic config"
+      historyRecords={historyRecords}
     >
       <p>
         Edit the coordinator dynamic configuration on the fly.
diff --git a/web-console/src/dialogs/history-dialog.scss 
b/web-console/src/dialogs/history-dialog.scss
new file mode 100644
index 0000000..b16ebd0
--- /dev/null
+++ b/web-console/src/dialogs/history-dialog.scss
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+.history-dialog {
+  &.pt-dialog {
+    width: 600px;
+    top: 5%;
+  }
+
+  .history-record-container {
+
+    padding: 15px 15px 0 15px;
+
+    h3 {
+      padding-left: 10px;
+    }
+
+    .no-record {
+      height: 25vh;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+
+    .history-record-entries {
+
+      margin-bottom: 10px;
+      max-height: 60vh;
+      overflow: scroll;
+
+      .history-record-entry {
+
+        padding: 5px;
+        border-style: dot-dash;
+        word-wrap: break-word;
+
+        hr {
+          margin: 5px 0 5px 0;
+        }
+
+        .pt-card {
+          padding-bottom: 10px;
+        }
+
+        .history-record-title {
+          justify-content: space-between;
+          display: flex;
+
+
+        }
+
+        .history-record-comment-title {
+          margin-bottom: 5px;
+        }
+
+        .json-collapse {
+          button {
+            position: relative;
+            left: 86%;
+            margin-bottom: 5px;
+          }
+
+          textarea {
+            width: 100%;
+            height: 30vh;
+            margin-bottom: 5px;
+          }
+        }
+      }
+    }
+  }
+}
+
diff --git a/web-console/src/dialogs/history-dialog.tsx 
b/web-console/src/dialogs/history-dialog.tsx
new file mode 100644
index 0000000..73ff6fe
--- /dev/null
+++ b/web-console/src/dialogs/history-dialog.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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 { Dialog } from "@blueprintjs/core";
+import * as React from "react";
+
+import { Card, JSONCollapse } from "../components/filler";
+
+import "./history-dialog.scss";
+
+interface HistoryDialogProps extends React.Props<any> {
+  historyRecords: any;
+}
+
+interface HistoryDialogState {
+
+}
+
+export class HistoryDialog extends React.Component<HistoryDialogProps, 
HistoryDialogState> {
+  constructor(props: HistoryDialogProps) {
+    super(props);
+    this.state = {
+
+    };
+  }
+
+  renderRecords() {
+    const {children, historyRecords} = this.props;
+    let content;
+    if (historyRecords.length === 0) {
+      content = <div className={"no-record"}>No history records 
available</div>;
+    } else {
+       content = <>
+          <h3>History</h3>
+          <div className={"history-record-entries"}>
+            {
+              historyRecords.map((record: any) => {
+                const auditInfo = record.auditInfo;
+                const auditTime = record.auditTime;
+                const formattedTime = auditTime.replace("T", " ").substring(0, 
auditTime.length - 5);
+
+                return <div key={record.auditTime} 
className={"history-record-entry"}>
+                  <Card>
+                    <div className={"history-record-title"}>
+                      <h5>Change</h5>
+                      <p>{formattedTime}</p>
+                    </div>
+                    <hr/>
+                    <p>{auditInfo.comment === "" ? "(No comment)" : 
auditInfo.comment}</p>
+                    <JSONCollapse
+                      stringValue={record.payload}
+                      buttonText={"Payload"}
+                    />
+                  </Card>
+                </div>;
+              })
+            }
+          </div>
+         </>;
+    }
+    return <div className={"history-record-container"}>
+      {content}
+      {children}
+    </div>;
+  }
+
+  render(): React.ReactNode {
+    return <Dialog
+      isOpen
+      {...this.props}
+      className={"history-dialog"}
+    >
+      {this.renderRecords()}
+    </Dialog>;
+  }
+}
diff --git a/web-console/src/dialogs/overlord-dynamic-config.tsx 
b/web-console/src/dialogs/overlord-dynamic-config.tsx
index f3be789..f4be905 100644
--- a/web-console/src/dialogs/overlord-dynamic-config.tsx
+++ b/web-console/src/dialogs/overlord-dynamic-config.tsx
@@ -23,7 +23,7 @@ import * as React from "react";
 import { AutoForm } from "../components/auto-form";
 import { IconNames } from "../components/filler";
 import { AppToaster } from "../singletons/toaster";
-import { getDruidErrorMessage } from "../utils";
+import { getDruidErrorMessage, QueryManager } from "../utils";
 
 import { SnitchDialog } from "./snitch-dialog";
 
@@ -36,19 +36,37 @@ export interface OverlordDynamicConfigDialogProps extends 
React.Props<any> {
 export interface OverlordDynamicConfigDialogState {
   dynamicConfig: Record<string, any> | null;
   allJSONValid: boolean;
+  historyRecords: any[];
 }
 
 export class OverlordDynamicConfigDialog extends 
React.Component<OverlordDynamicConfigDialogProps, 
OverlordDynamicConfigDialogState> {
+  private historyQueryManager: QueryManager<string, any>;
+
   constructor(props: OverlordDynamicConfigDialogProps) {
     super(props);
     this.state = {
       dynamicConfig: null,
-      allJSONValid: true
+      allJSONValid: true,
+      historyRecords: []
     };
   }
 
-  componentDidMount(): void {
+  componentDidMount() {
     this.getConfig();
+
+    this.historyQueryManager = new QueryManager({
+      processQuery: async (query) => {
+        const historyResp = await 
axios(`/druid/indexer/v1/worker/history?count=100`);
+        return historyResp.data;
+      },
+      onStateChange: ({ result, loading, error }) => {
+        this.setState({
+          historyRecords: result
+        });
+      }
+    });
+
+    this.historyQueryManager.runQuery(`dummy`);
   }
 
   async getConfig() {
@@ -69,13 +87,13 @@ export class OverlordDynamicConfigDialog extends 
React.Component<OverlordDynamic
     });
   }
 
-  private saveConfig = async (author: string, comment: string) => {
+  private saveConfig = async (comment: string) => {
     const { onClose } = this.props;
     const newState: any = this.state.dynamicConfig;
     try {
       await axios.post("/druid/indexer/v1/worker", newState, {
         headers: {
-          "X-Druid-Author": author,
+          "X-Druid-Author": "console",
           "X-Druid-Comment": comment
         }
       });
@@ -96,7 +114,7 @@ export class OverlordDynamicConfigDialog extends 
React.Component<OverlordDynamic
 
   render() {
     const { onClose } = this.props;
-    const { dynamicConfig, allJSONValid } = this.state;
+    const { dynamicConfig, allJSONValid, historyRecords } = this.state;
 
     return <SnitchDialog
       className="overlord-dynamic-config"
@@ -105,6 +123,7 @@ export class OverlordDynamicConfigDialog extends 
React.Component<OverlordDynamic
       onClose={onClose}
       title="Overlord dynamic config"
       saveDisabled={!allJSONValid}
+      historyRecords={historyRecords}
     >
       <p>
         Edit the overlord dynamic configuration on the fly.
diff --git a/web-console/src/dialogs/retention-dialog.tsx 
b/web-console/src/dialogs/retention-dialog.tsx
index 10d85f2..e427ce5 100644
--- a/web-console/src/dialogs/retention-dialog.tsx
+++ b/web-console/src/dialogs/retention-dialog.tsx
@@ -22,6 +22,7 @@ import * as React from 'react';
 
 import { FormGroup, IconNames } from '../components/filler';
 import { Rule, RuleEditor } from '../components/rule-editor';
+import { QueryManager } from "../utils";
 
 import { SnitchDialog } from './snitch-dialog';
 
@@ -43,28 +44,48 @@ export interface RetentionDialogProps extends 
React.Props<any> {
   tiers: string[];
   onEditDefaults: () => void;
   onCancel: () => void;
-  onSave: (datasource: string, newRules: any[], author: string, comment: 
string) => void;
+  onSave: (datasource: string, newRules: any[], comment: string) => void;
 }
 
 export interface RetentionDialogState {
   currentRules: any[];
+  historyRecords: any[];
 }
 
 export class RetentionDialog extends React.Component<RetentionDialogProps, 
RetentionDialogState> {
+  private historyQueryManager: QueryManager<string, any>;
 
   constructor(props: RetentionDialogProps) {
     super(props);
 
     this.state = {
-      currentRules: props.rules
+      currentRules: props.rules,
+      historyRecords: []
     };
   }
 
-  private save = (author: string, comment: string) => {
+  componentDidMount() {
+    const { datasource } = this.props;
+    this.historyQueryManager = new QueryManager({
+      processQuery: async (query) => {
+        const historyResp = await 
axios(`/druid/coordinator/v1/rules/${datasource}/history`);
+        return historyResp.data;
+      },
+      onStateChange: ({ result, loading, error }) => {
+        this.setState({
+          historyRecords: result
+        });
+      }
+    });
+
+    this.historyQueryManager.runQuery(`dummy`);
+  }
+
+  private save = (comment: string) => {
     const { datasource, onSave } = this.props;
     const { currentRules } = this.state;
 
-    onSave(datasource, currentRules, author, comment);
+    onSave(datasource, currentRules, comment);
   }
 
   private changeRule = (newRule: any, index: number) => {
@@ -140,7 +161,7 @@ export class RetentionDialog extends 
React.Component<RetentionDialogProps, Reten
 
   render() {
     const { datasource, onCancel, onEditDefaults } = this.props;
-    const { currentRules } = this.state;
+    const { currentRules, historyRecords } = this.state;
 
     return <SnitchDialog
       className="retention-dialog"
@@ -152,6 +173,7 @@ export class RetentionDialog extends 
React.Component<RetentionDialogProps, Reten
       title={`Edit retention rules: ${datasource}${datasource === '_default' ? 
' (cluster defaults)' : ''}`}
       onReset={this.reset}
       onSave={this.save}
+      historyRecords={historyRecords}
     >
       <p>
         Druid uses rules to determine what data should be retained in the 
cluster.
diff --git a/web-console/src/dialogs/snitch-dialog.tsx 
b/web-console/src/dialogs/snitch-dialog.tsx
index 46cb1f0..1d4cdb0 100644
--- a/web-console/src/dialogs/snitch-dialog.tsx
+++ b/web-console/src/dialogs/snitch-dialog.tsx
@@ -27,22 +27,23 @@ import {
 import * as React from 'react';
 
 import { FormGroup, IconNames } from '../components/filler';
-import { localStorageGet, localStorageSet } from "../utils";
 
-const druidEditingAuthor = "DRUID_EDITING_AUTHOR";
+import { HistoryDialog } from "./history-dialog";
 
 export interface SnitchDialogProps extends IDialogProps {
-  onSave: (author: string, comment: string) => void;
+  onSave: (comment: string) => void;
   saveDisabled?: boolean;
   onReset?: () => void;
+  historyRecords?: any[];
 }
 
 export interface SnitchDialogState {
-  author: string;
   comment: string;
 
   showFinalStep?: boolean;
   saveDisabled?: boolean;
+
+  showHistory?: boolean;
 }
 
 export class SnitchDialog extends React.Component<SnitchDialogProps, 
SnitchDialogState> {
@@ -51,48 +52,24 @@ export class SnitchDialog extends 
React.Component<SnitchDialogProps, SnitchDialo
 
     this.state = {
       comment: "",
-      author: "",
       saveDisabled: true
     };
   }
 
-  componentDidMount(): void {
-    this.getDefaultAuthor();
-  }
-
   save = () => {
     const { onSave, onClose } = this.props;
-    const { author, comment } = this.state;
+    const { comment } = this.state;
 
-    onSave(author, comment);
+    onSave(comment);
     if (onClose) onClose();
   }
 
-  getDefaultAuthor() {
-    const author: string | null = localStorageGet(druidEditingAuthor);
-    if (author) {
-      this.setState({
-        author
-      });
-    }
-  }
-
-  changeAuthor(newAuthor: string)  {
-    const { author, comment } = this.state;
-
-    this.setState({
-      author: newAuthor,
-      saveDisabled: !newAuthor || !comment
-    });
-    localStorageSet(druidEditingAuthor, newAuthor);
-  }
-
   changeComment(newComment: string)  {
-    const { author, comment } = this.state;
+    const { comment } = this.state;
 
     this.setState({
       comment: newComment,
-      saveDisabled: !author || !newComment
+      saveDisabled: !newComment
     });
   }
 
@@ -104,7 +81,8 @@ export class SnitchDialog extends 
React.Component<SnitchDialogProps, SnitchDialo
 
   back = () => {
     this.setState({
-      showFinalStep: false
+      showFinalStep: false,
+      showHistory: false
     });
   }
 
@@ -114,15 +92,18 @@ export class SnitchDialog extends 
React.Component<SnitchDialogProps, SnitchDialo
     });
   }
 
+  goToHistory = () => {
+    this.setState({
+      showHistory: true
+    });
+  }
+
   renderFinalStep() {
     const { onClose, children } = this.props;
-    const { saveDisabled, author, comment } = this.state;
+    const { saveDisabled, comment } = this.state;
 
     return <Dialog {...this.props}>
       <div className={`dialog-body ${Classes.DIALOG_BODY}`}>
-        <FormGroup label={"Who is making this change?"}>
-          <InputGroup value={author} onChange={(e: any) => 
this.changeAuthor(e.target.value)}/>
-        </FormGroup>
         <FormGroup label={"Why are you making this change?"} 
className={"comment"}>
           <InputGroup
             className="pt-large"
@@ -139,11 +120,28 @@ export class SnitchDialog extends 
React.Component<SnitchDialogProps, SnitchDialo
     </Dialog>;
   }
 
+  renderHistoryDialog() {
+    const { historyRecords } = this.props;
+    return <HistoryDialog
+      {...this.props}
+      historyRecords={historyRecords}
+    >
+      <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+        <Button onClick={this.back} 
iconName={IconNames.ARROW_LEFT}>Back</Button>
+      </div>
+    </HistoryDialog>;
+  }
+
   renderActions(saveDisabled?: boolean) {
-    const { onReset } = this.props;
+    const { onReset, historyRecords } = this.props;
     const { showFinalStep } = this.state;
 
     return <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+      {showFinalStep || historyRecords === undefined
+        ? null
+        : <Button style={{position: "absolute", left: "5px"}} 
className={"pt-minimal"} text="History" onClick={this.goToHistory}/>
+      }
+
       { showFinalStep
         ? <Button onClick={this.back} 
iconName={IconNames.ARROW_LEFT}>Back</Button>
         : onReset ? <Button onClick={this.reset} intent={"none" as 
any}>Reset</Button> : null
@@ -158,14 +156,16 @@ export class SnitchDialog extends 
React.Component<SnitchDialogProps, SnitchDialo
 
   render() {
     const { onClose, className, children, saveDisabled } = this.props;
-    const { showFinalStep } = this.state;
+    const { showFinalStep, showHistory } = this.state;
 
     if (showFinalStep) return this.renderFinalStep();
+    if (showHistory) return this.renderHistoryDialog();
 
     return <Dialog isOpen inline {...this.props}>
       <div className={Classes.DIALOG_BODY}>
         {children}
       </div>
+
       <div className={Classes.DIALOG_FOOTER}>
         {this.renderActions(saveDisabled)}
       </div>
diff --git a/web-console/src/views/datasource-view.tsx 
b/web-console/src/views/datasource-view.tsx
index 696a8f5..f8d38c4 100644
--- a/web-console/src/views/datasource-view.tsx
+++ b/web-console/src/views/datasource-view.tsx
@@ -249,11 +249,11 @@ GROUP BY 1`);
     </AsyncActionDialog>;
   }
 
-  private saveRules = async (datasource: string, rules: any[], author: string, 
comment: string) => {
+  private saveRules = async (datasource: string, rules: any[], comment: 
string) => {
     try {
       await axios.post(`/druid/coordinator/v1/rules/${datasource}`, rules, {
         headers: {
-          "X-Druid-Author": author,
+          "X-Druid-Author": "console",
           "X-Druid-Comment": comment
         }
       });


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to