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/05/06 10:20:20 UTC

[incubator-druid] branch master updated: Improved UI for actions in task/supervisor table (#7528)

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 83b38c0  Improved UI for actions in task/supervisor table (#7528)
83b38c0 is described below

commit 83b38c0911c6a040308c29dc88a3051d60c68b93
Author: Qi Shu <sh...@gmail.com>
AuthorDate: Mon May 6 06:20:12 2019 -0400

    Improved UI for actions in task/supervisor table (#7528)
    
    * Grouped actions in task/supervisor table to a dialog
    
    * Design change
    
    * fix divider
    
    * Removed await for onclick event on buttons
    
    * fix package.json
    
    * Better error handling
    
    * remove log
    
    * name change; better code organization
    
    * Code refactor; extrac table action dialog
    
    * minor change
    
    * tidy up actions
    
    * drop reload actions
---
 web-console/package-lock.json                      |  42 +++++++
 web-console/package.json                           |   4 +
 .../bootstrap/react-table-custom-pagination.tsx    |   2 +-
 web-console/src/components/action-cell.scss        |  34 +++++
 web-console/src/components/action-cell.tsx         |  36 ++++++
 web-console/src/components/show-json.scss          |  41 ++++++
 web-console/src/components/show-json.tsx           | 106 ++++++++++++++++
 web-console/src/components/show-log.scss           |  42 +++++++
 web-console/src/components/show-log.tsx            | 116 +++++++++++++++++
 web-console/src/components/sql-control.tsx         |   2 +-
 web-console/src/console-application.tsx            |   4 +-
 web-console/src/dialogs/async-action-dialog.tsx    |   5 +-
 web-console/src/dialogs/history-dialog.tsx         |   2 +-
 web-console/src/dialogs/snitch-dialog.tsx          |   2 +-
 .../src/dialogs/supervisor-table-action-dialog.tsx |  89 +++++++++++++
 web-console/src/dialogs/table-action-dialog.scss   |  94 ++++++++++++++
 web-console/src/dialogs/table-action-dialog.tsx    |  86 +++++++++++++
 .../src/dialogs/task-table-action-dialog.tsx       |  90 +++++++++++++
 web-console/src/utils/basic-action.tsx             |  61 +++++++++
 web-console/src/utils/general.tsx                  |  10 ++
 web-console/src/views/datasource-view.tsx          | 116 ++++++++++++++---
 web-console/src/views/tasks-view.tsx               | 140 ++++++++++++++++-----
 22 files changed, 1068 insertions(+), 56 deletions(-)

diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 8a6638c..e3ee018 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -498,6 +498,12 @@
       "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
       "dev": true
     },
+    "@types/file-saver": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.0.tgz",
+      "integrity": "sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ==",
+      "dev": true
+    },
     "@types/glob": {
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
@@ -591,6 +597,15 @@
         "csstype": "^2.2.0"
       }
     },
+    "@types/react-copy-to-clipboard": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.6.tgz",
+      "integrity": "sha512-v4/yLsuPf8GSFuTy9fA1ABpL5uuy04vwW7qs+cfxSe1UU/M/KK95rF3N3GRseismoK9tA28SvpwVsAg/GWoF3A==",
+      "dev": true,
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/react-dom": {
       "version": "16.8.4",
       "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.8.4.tgz",
@@ -2278,6 +2293,14 @@
       "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
       "dev": true
     },
+    "copy-to-clipboard": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz",
+      "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==",
+      "requires": {
+        "toggle-selection": "^1.0.6"
+      }
+    },
     "core-js": {
       "version": "1.2.7",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
@@ -3538,6 +3561,11 @@
         "flat-cache": "^2.0.1"
       }
     },
+    "file-saver": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.1.tgz",
+      "integrity": "sha512-dCB3K7/BvAcUmtmh1DzFdv0eXSVJ9IAFt1mw3XZfAexodNRoE29l3xB2EX4wH2q8m/UTzwzEPq/ArYk98kUkBQ=="
+    },
     "fileset": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz",
@@ -8595,6 +8623,15 @@
         "prop-types": "^15.6.2"
       }
     },
+    "react-copy-to-clipboard": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz",
+      "integrity": "sha512-ELKq31/E3zjFs5rDWNCfFL4NvNFQvGRoJdAKReD/rUPA+xxiLPQmZBZBvy2vgH7V0GE9isIQpT9WXbwIVErYdA==",
+      "requires": {
+        "copy-to-clipboard": "^3",
+        "prop-types": "^15.5.8"
+      }
+    },
     "react-dom": {
       "version": "16.8.6",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
@@ -10858,6 +10895,11 @@
         "repeat-string": "^1.6.1"
       }
     },
+    "toggle-selection": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+      "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
+    },
     "tough-cookie": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
diff --git a/web-console/package.json b/web-console/package.json
index ad9c0f8..93beecb 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -42,11 +42,13 @@
     "druid-console": "^0.0.2",
     "es6-shim": "^0.35.5",
     "es7-shim": "^6.0.0",
+    "file-saver": "^2.0.1",
     "hjson": "^3.1.2",
     "lodash.debounce": "^4.0.8",
     "numeral": "^2.0.6",
     "react": "^16.8.6",
     "react-ace": "^6.4.0",
+    "react-copy-to-clipboard": "^5.0.1",
     "react-dom": "^16.8.6",
     "react-router": "^5.0.0",
     "react-router-dom": "^5.0.0",
@@ -56,11 +58,13 @@
   "devDependencies": {
     "@types/classnames": "^2.2.7",
     "@types/d3-array": "^2.0.0",
+    "@types/file-saver": "^2.0.0",
     "@types/hjson": "^2.4.1",
     "@types/jest": "^24.0.11",
     "@types/lodash.debounce": "^4.0.6",
     "@types/node": "^11.13.4",
     "@types/numeral": "^0.0.25",
+    "@types/react-copy-to-clipboard": "^4.2.6",
     "@types/react-dom": "^16.8.4",
     "@types/react-router-dom": "^4.3.2",
     "@types/react-table": "^6.8.1",
diff --git a/web-console/src/bootstrap/react-table-custom-pagination.tsx b/web-console/src/bootstrap/react-table-custom-pagination.tsx
index a55cf51..7225fb9 100644
--- a/web-console/src/bootstrap/react-table-custom-pagination.tsx
+++ b/web-console/src/bootstrap/react-table-custom-pagination.tsx
@@ -100,7 +100,7 @@ export class ReactTableCustomPagination extends React.Component<ReactTableCustom
     } = this.props;
 
     return (
-      <div className={'-pagination'} style={style}>
+      <div className="-pagination" style={style}>
         <div className="-previous">
           <Button
             fill
diff --git a/web-console/src/components/action-cell.scss b/web-console/src/components/action-cell.scss
new file mode 100644
index 0000000..db722e0
--- /dev/null
+++ b/web-console/src/components/action-cell.scss
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+.action-cell {
+  & > * {
+    margin-right: 10px;
+    color: #48aff0;
+    cursor: pointer;
+
+    &:hover {
+      color: #4891d2;
+    }
+
+    &:last-child {
+      margin-right: 0;
+    }
+  }
+}
+
diff --git a/web-console/src/components/action-cell.tsx b/web-console/src/components/action-cell.tsx
new file mode 100644
index 0000000..e37b2c9
--- /dev/null
+++ b/web-console/src/components/action-cell.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 * as React from 'react';
+
+import './action-cell.scss';
+
+export interface ActionCellProps extends React.Props<any> {
+}
+
+export class ActionCell extends React.Component<ActionCellProps, {}> {
+  constructor(props: ActionCellProps, context: any) {
+    super(props, context);
+  }
+
+  render() {
+    return <div className="action-cell">
+      {this.props.children}
+    </div>;
+  }
+}
diff --git a/web-console/src/components/show-json.scss b/web-console/src/components/show-json.scss
new file mode 100644
index 0000000..c3937a5
--- /dev/null
+++ b/web-console/src/components/show-json.scss
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+.show-json {
+  position: relative;
+  height: 100%;
+
+  .top-actions {
+    text-align: right;
+    padding-bottom: 10px;
+
+    & > * {
+      display: inline-block;
+    }
+  }
+
+  .main-area {
+    height: calc(100% - 40px);
+
+    textarea {
+      height: 100%;
+      width: 100%;
+      resize: none;
+    }
+  }
+}
diff --git a/web-console/src/components/show-json.tsx b/web-console/src/components/show-json.tsx
new file mode 100644
index 0000000..ebc4bfc
--- /dev/null
+++ b/web-console/src/components/show-json.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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, ButtonGroup, InputGroup, Intent, TextArea } from '@blueprintjs/core';
+import axios from 'axios';
+import * as React from 'react';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
+
+import { AppToaster } from '../singletons/toaster';
+import { UrlBaser } from '../singletons/url-baser';
+import { downloadFile } from '../utils';
+
+import './show-json.scss';
+
+export interface ShowJsonProps extends React.Props<any> {
+  endpoint: string;
+  downloadFilename?: string;
+}
+
+export interface ShowJsonState {
+  jsonValue: string;
+}
+
+export class ShowJson extends React.Component<ShowJsonProps, ShowJsonState> {
+  constructor(props: ShowJsonProps, context: any) {
+    super(props, context);
+    this.state = {
+      jsonValue: ''
+    };
+
+    this.getJsonInfo();
+  }
+
+  private getJsonInfo = async (): Promise<void> => {
+    const { endpoint } = this.props;
+    try {
+      const resp = await axios.get(endpoint);
+      const data = resp.data;
+      this.setState({
+        jsonValue: typeof (data) === 'string' ? data : JSON.stringify(data, undefined, 2)
+      });
+    } catch (e) {
+      this.setState({
+        jsonValue: `Error: ` + e.response.data
+      });
+    }
+  }
+
+  render() {
+    const { endpoint, downloadFilename } = this.props;
+    const { jsonValue } = this.state;
+
+    return <div className="show-json">
+      <div className="top-actions">
+        <ButtonGroup className="right-buttons">
+          {
+            downloadFilename &&
+            <Button
+              text="Save"
+              minimal
+              onClick={() => downloadFile(jsonValue, 'json', downloadFilename)}
+            />
+          }
+          <CopyToClipboard text={jsonValue}>
+            <Button
+              text="Copy"
+              minimal
+              onClick={() => {
+                AppToaster.show({
+                  message: 'Copied JSON to clipboard',
+                  intent: Intent.SUCCESS
+                });
+              }}
+            />
+          </CopyToClipboard>
+          <Button
+            text="View raw"
+            minimal
+            onClick={() => window.open(UrlBaser.base(endpoint), '_blank')}
+          />
+        </ButtonGroup>
+      </div>
+      <div className="main-area">
+        <TextArea
+          readOnly
+          value={jsonValue}
+        />
+      </div>
+    </div>;
+  }
+}
diff --git a/web-console/src/components/show-log.scss b/web-console/src/components/show-log.scss
new file mode 100644
index 0000000..58e7acf
--- /dev/null
+++ b/web-console/src/components/show-log.scss
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+.show-log {
+  position: relative;
+  height: 100%;
+
+  .top-actions {
+    text-align: right;
+    padding-bottom: 10px;
+
+    & > * {
+      display: inline-block;
+    }
+  }
+
+  .main-area {
+    height: calc(100% - 40px);
+
+    textarea {
+      height: 100%;
+      width: 100%;
+      resize: none;
+      white-space: pre;
+    }
+  }
+}
diff --git a/web-console/src/components/show-log.tsx b/web-console/src/components/show-log.tsx
new file mode 100644
index 0000000..c89e406
--- /dev/null
+++ b/web-console/src/components/show-log.tsx
@@ -0,0 +1,116 @@
+/*
+ * 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, ButtonGroup, InputGroup, Intent, TextArea } from '@blueprintjs/core';
+import axios from 'axios';
+import * as React from 'react';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
+
+import { AppToaster } from '../singletons/toaster';
+import { UrlBaser } from '../singletons/url-baser';
+import { downloadFile } from '../utils';
+
+import './show-log.scss';
+
+function removeFirstPartialLine(log: string): string {
+  const lines = log.split('\n');
+  if (lines.length > 1) {
+    lines.shift();
+  }
+  return lines.join('\n');
+}
+
+export interface ShowLogProps extends React.Props<any> {
+  endpoint: string;
+  downloadFilename?: string;
+  tailOffset?: number;
+}
+
+export interface ShowLogState {
+  logValue: string;
+}
+
+export class ShowLog extends React.Component<ShowLogProps, ShowLogState> {
+  constructor(props: ShowLogProps, context: any) {
+    super(props, context);
+    this.state = {
+      logValue: ''
+    };
+
+    this.getLogInfo();
+  }
+
+  private getLogInfo = async (): Promise<void> => {
+    const { endpoint, tailOffset } = this.props;
+    try {
+      const resp = await axios.get(endpoint + (tailOffset ? `?offset=-${tailOffset}` : ''));
+      const data = resp.data;
+
+      let logValue = typeof (data) === 'string' ? data : JSON.stringify(data, undefined, 2);
+      if (tailOffset) logValue = removeFirstPartialLine(logValue);
+      this.setState({ logValue });
+    } catch (e) {
+      this.setState({
+        logValue: `Error: ` + e.response.data
+      });
+    }
+  }
+
+  render() {
+    const { endpoint, downloadFilename } = this.props;
+    const { logValue } = this.state;
+
+    return <div className="show-log">
+      <div className="top-actions">
+        <ButtonGroup className="right-buttons">
+          {
+            downloadFilename &&
+            <Button
+              text="Save"
+              minimal
+              onClick={() => downloadFile(logValue, 'plain', downloadFilename)}
+            />
+          }
+          <CopyToClipboard text={logValue}>
+            <Button
+              text="Copy"
+              minimal
+              onClick={() => {
+                AppToaster.show({
+                  message: 'Copied log to clipboard',
+                  intent: Intent.SUCCESS
+                });
+              }}
+            />
+          </CopyToClipboard>
+          <Button
+            text="View full log"
+            minimal
+            onClick={() => window.open(UrlBaser.base(endpoint), '_blank')}
+          />
+        </ButtonGroup>
+      </div>
+      <div className="main-area">
+        <TextArea
+          readOnly
+          value={logValue}
+        />
+      </div>
+    </div>;
+  }
+}
diff --git a/web-console/src/components/sql-control.tsx b/web-console/src/components/sql-control.tsx
index d33f281..e34a3f3 100644
--- a/web-console/src/components/sql-control.tsx
+++ b/web-console/src/components/sql-control.tsx
@@ -302,7 +302,7 @@ export class SqlControl extends React.Component<SqlControlProps, SqlControlState
         </ButtonGroup>
         {
           queryElapsed &&
-          <span className={'query-elapsed'}> Last query took {(queryElapsed / 1000).toFixed(2)} seconds</span>
+          <span className="query-elapsed"> Last query took {(queryElapsed / 1000).toFixed(2)} seconds</span>
         }
       </div>
     </div>;
diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx
index 8691939..4f10977 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -240,9 +240,9 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
     const { capabilitiesLoading } = this.state;
 
     if (capabilitiesLoading) {
-      return <div className={'loading-capabilities'}>
+      return <div className="loading-capabilities">
         <Loader
-          loadingText={''}
+          loadingText=""
           loading={capabilitiesLoading}
         />
       </div>;
diff --git a/web-console/src/dialogs/async-action-dialog.tsx b/web-console/src/dialogs/async-action-dialog.tsx
index c29d058..1cfe144 100644
--- a/web-console/src/dialogs/async-action-dialog.tsx
+++ b/web-console/src/dialogs/async-action-dialog.tsx
@@ -34,6 +34,7 @@ export interface AsyncAlertDialogProps extends React.Props<any> {
   action: null | (() => Promise<void>);
   onClose: (success: boolean) => void;
   confirmButtonText: string;
+  confirmButtonDisabled?: boolean;
   cancelButtonText?: string;
   className?: string;
   icon?: IconName;
@@ -79,7 +80,7 @@ export class AsyncActionDialog extends React.Component<AsyncAlertDialogProps, As
   }
 
   render() {
-    const { action, onClose, className, icon, intent, confirmButtonText, cancelButtonText, children } = this.props;
+    const { action, onClose, className, icon, intent, confirmButtonText, cancelButtonText, confirmButtonDisabled, children } = this.props;
     const { working } = this.state;
     if (!action) return null;
 
@@ -99,7 +100,7 @@ export class AsyncActionDialog extends React.Component<AsyncAlertDialogProps, As
         working ?
           <ProgressBar/> :
           <div className={Classes.ALERT_FOOTER}>
-            <Button intent={intent} text={confirmButtonText} onClick={this.handleConfirm}/>
+            <Button intent={intent} text={confirmButtonText} onClick={this.handleConfirm} disabled={confirmButtonDisabled}/>
             <Button text={cancelButtonText || 'Cancel'} onClick={handleClose}/>
           </div>
       }
diff --git a/web-console/src/dialogs/history-dialog.tsx b/web-console/src/dialogs/history-dialog.tsx
index 9a860e6..5704176 100644
--- a/web-console/src/dialogs/history-dialog.tsx
+++ b/web-console/src/dialogs/history-dialog.tsx
@@ -46,7 +46,7 @@ export class HistoryDialog extends React.Component<HistoryDialogProps, HistoryDi
       content = <div className="no-record">No history records available</div>;
     } else {
        content = <>
-          <span className={'history-dialog-title'}>History</span>
+          <span className="history-dialog-title">History</span>
           <div className="history-record-entries">
             {
               historyRecords.map((record: any) => {
diff --git a/web-console/src/dialogs/snitch-dialog.tsx b/web-console/src/dialogs/snitch-dialog.tsx
index 91b94e7..bfa5a76 100644
--- a/web-console/src/dialogs/snitch-dialog.tsx
+++ b/web-console/src/dialogs/snitch-dialog.tsx
@@ -127,7 +127,7 @@ export class SnitchDialog extends React.Component<SnitchDialogProps, SnitchDialo
     const { historyRecords } = this.props;
     return <HistoryDialog
       {...this.props}
-      className={'history-dialog'}
+      className="history-dialog"
       historyRecords={historyRecords}
     >
       <div className={Classes.DIALOG_FOOTER_ACTIONS}>
diff --git a/web-console/src/dialogs/supervisor-table-action-dialog.tsx b/web-console/src/dialogs/supervisor-table-action-dialog.tsx
new file mode 100644
index 0000000..3c40f5d
--- /dev/null
+++ b/web-console/src/dialogs/supervisor-table-action-dialog.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 { IDialogProps } from '@blueprintjs/core';
+import * as React from 'react';
+
+import { ShowJson } from '../components/show-json';
+import { BasicAction, basicActionsToButtons } from '../utils/basic-action';
+
+import { SideButtonMetaData, TableActionDialog } from './table-action-dialog';
+
+interface SupervisorTableActionDialogProps extends IDialogProps {
+  supervisorId: string;
+  actions: BasicAction[];
+  onClose: () => void;
+}
+
+interface SupervisorTableActionDialogState {
+  activeTab: 'payload' | 'status' | 'stats' | 'history';
+}
+
+export class SupervisorTableActionDialog extends React.Component<SupervisorTableActionDialogProps, SupervisorTableActionDialogState> {
+  constructor(props: SupervisorTableActionDialogProps) {
+    super(props);
+    this.state = {
+      activeTab: 'payload'
+    };
+  }
+
+  render(): React.ReactNode {
+    const { supervisorId, actions, onClose } = this.props;
+    const { activeTab } = this.state;
+
+    const supervisorTableSideButtonMetadata: SideButtonMetaData[] = [
+      {
+        icon: 'align-left',
+        text: 'Payload',
+        active: activeTab === 'payload',
+        onClick: () => this.setState({ activeTab: 'payload' })
+      },
+      {
+        icon: 'dashboard',
+        text: 'Status',
+        active: activeTab === 'status',
+        onClick: () => this.setState({ activeTab: 'status' })
+      },
+      {
+        icon: 'chart',
+        text: 'Statistics',
+        active: activeTab === 'stats',
+        onClick: () => this.setState({ activeTab: 'stats' })
+      },
+      {
+        icon: 'history',
+        text: 'History',
+        active: activeTab === 'history',
+        onClick: () => this.setState({ activeTab: 'history' })
+      }
+    ];
+
+    return <TableActionDialog
+      isOpen
+      sideButtonMetadata={supervisorTableSideButtonMetadata}
+      onClose={onClose}
+      title={`Supervisor: ${supervisorId}`}
+      bottomButtons={basicActionsToButtons(actions)}
+    >
+      {activeTab === 'payload' && <ShowJson endpoint={`/druid/indexer/v1/supervisor/${supervisorId}`} downloadFilename={`supervisor-payload-${supervisorId}.json`}/>}
+      {activeTab === 'status' && <ShowJson endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/status`} downloadFilename={`supervisor-status-${supervisorId}.json`}/>}
+      {activeTab === 'stats' && <ShowJson endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/stats`} downloadFilename={`supervisor-stats-${supervisorId}.json`}/>}
+      {activeTab === 'history' && <ShowJson endpoint={`/druid/indexer/v1/supervisor/${supervisorId}/history`} downloadFilename={`supervisor-history-${supervisorId}.json`}/>}
+    </TableActionDialog>;
+  }
+}
diff --git a/web-console/src/dialogs/table-action-dialog.scss b/web-console/src/dialogs/table-action-dialog.scss
new file mode 100644
index 0000000..a1e5d49
--- /dev/null
+++ b/web-console/src/dialogs/table-action-dialog.scss
@@ -0,0 +1,94 @@
+/*
+ * 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.
+ */
+
+$side-bar-width: 120px;
+
+.table-action-dialog {
+  &.bp3-dialog {
+    position: relative;
+    width: 700px;
+    top: 5%;
+    height: 70vh;
+
+    &::after {
+      content: "";
+      position: absolute;
+      width: 0;
+      top: 40px;
+      bottom: 0;
+      left: $side-bar-width;
+      border-right: 1px solid #1f2832;
+    }
+  }
+
+  .bp3-dialog-body {
+    position: relative;
+    margin: 0;
+
+    .side-bar {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 120px;
+      height: 100%;
+
+      .tab-button {
+        width: 100%;
+        height: 10vh;
+        cursor: pointer;
+        display: flex;
+        flex-direction: column;
+        border-radius: 0;
+
+        &.active {
+          background-color: #2C74A8;
+        }
+
+        .bp3-icon {
+          margin-right: 0;
+        }
+
+        .bp3-button-text {
+          margin-top: 5px;
+        }
+      }
+    }
+
+    .main-section {
+      position: absolute;
+      top: 0;
+      left: $side-bar-width;
+      right: 0;
+      height: 100%;
+      padding: 10px 20px;
+    }
+  }
+
+  .bp3-dialog-footer {
+    position: relative;
+
+    .footer-actions-left {
+      position: absolute;
+      left: $side-bar-width;
+
+      & > * {
+        margin-right: 10px;
+      }
+    }
+  }
+}
diff --git a/web-console/src/dialogs/table-action-dialog.tsx b/web-console/src/dialogs/table-action-dialog.tsx
new file mode 100644
index 0000000..5870cea
--- /dev/null
+++ b/web-console/src/dialogs/table-action-dialog.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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, Icon, IconName, IDialogProps, Intent } from '@blueprintjs/core';
+import * as React from 'react';
+
+import './table-action-dialog.scss';
+
+export interface SideButtonMetaData {
+  icon: IconName;
+  text: string;
+  active?: boolean;
+  onClick?: () => void;
+}
+
+interface TableActionDialogProps extends IDialogProps {
+  sideButtonMetadata: SideButtonMetaData[];
+  onClose: () => void;
+  bottomButtons?: React.ReactNode;
+}
+
+export class TableActionDialog extends React.Component<TableActionDialogProps, {}> {
+  constructor(props: TableActionDialogProps) {
+    super(props);
+    this.state = {};
+  }
+
+  render() {
+    const { sideButtonMetadata, isOpen, onClose, title, bottomButtons } = this.props;
+
+    return <Dialog
+      className="table-action-dialog"
+      isOpen={isOpen}
+      onClose={onClose}
+      title={title}
+    >
+      <div className={Classes.DIALOG_BODY}>
+        <div className="side-bar">
+          {
+            sideButtonMetadata.map((d: SideButtonMetaData) => (
+              <Button
+                className="tab-button"
+                icon={<Icon icon={d.icon} iconSize={20}/>}
+                key={d.text}
+                text={d.text}
+                intent={d.active ? Intent.PRIMARY : Intent.NONE}
+                minimal={!d.active}
+                onClick={d.onClick}
+              />
+            ))
+          }
+        </div>
+        <div className="main-section">
+          {this.props.children}
+        </div>
+      </div>
+      <div className={Classes.DIALOG_FOOTER}>
+        <div className="footer-actions-left">
+          {bottomButtons}
+        </div>
+        <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+          <Button
+            text="Close"
+            intent={Intent.PRIMARY}
+            onClick={onClose}
+          />
+        </div>
+      </div>
+    </Dialog>;
+  }
+}
diff --git a/web-console/src/dialogs/task-table-action-dialog.tsx b/web-console/src/dialogs/task-table-action-dialog.tsx
new file mode 100644
index 0000000..b5518e6
--- /dev/null
+++ b/web-console/src/dialogs/task-table-action-dialog.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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 { IDialogProps } from '@blueprintjs/core';
+import * as React from 'react';
+
+import { ShowJson } from '../components/show-json';
+import { ShowLog } from '../components/show-log';
+import { BasicAction, basicActionsToButtons } from '../utils/basic-action';
+
+import { SideButtonMetaData, TableActionDialog } from './table-action-dialog';
+
+interface TaskTableActionDialogProps extends IDialogProps {
+  taskId: string;
+  actions: BasicAction[];
+  onClose: () => void;
+}
+
+interface TaskTableActionDialogState {
+  activeTab: 'payload' | 'status' | 'reports' | 'log';
+}
+
+export class TaskTableActionDialog extends React.Component<TaskTableActionDialogProps, TaskTableActionDialogState> {
+  constructor(props: TaskTableActionDialogProps) {
+    super(props);
+    this.state = {
+      activeTab: 'payload'
+    };
+  }
+
+  render(): React.ReactNode {
+    const { taskId, actions, onClose } = this.props;
+    const { activeTab } = this.state;
+
+    const taskTableSideButtonMetadata: SideButtonMetaData[] = [
+      {
+        icon: 'align-left',
+        text: 'Payload',
+        active: activeTab === 'payload',
+        onClick: () => this.setState({ activeTab: 'payload' })
+      },
+      {
+        icon: 'dashboard',
+        text: 'Status',
+        active: activeTab === 'status',
+        onClick: () => this.setState({ activeTab: 'status' })
+      },
+      {
+        icon: 'comparison',
+        text: 'Reports',
+        active: activeTab === 'reports',
+        onClick: () => this.setState({ activeTab: 'reports' })
+      },
+      {
+        icon: 'align-justify',
+        text: 'Logs',
+        active: activeTab === 'log',
+        onClick: () => this.setState({ activeTab: 'log' })
+      }
+    ];
+
+    return <TableActionDialog
+      isOpen
+      sideButtonMetadata={taskTableSideButtonMetadata}
+      onClose={onClose}
+      title={`Task: ${taskId}`}
+      bottomButtons={basicActionsToButtons(actions)}
+    >
+      {activeTab === 'payload' && <ShowJson endpoint={`/druid/indexer/v1/task/${taskId}`} downloadFilename={`task-payload-${taskId}.json`}/>}
+      {activeTab === 'status' && <ShowJson endpoint={`/druid/indexer/v1/task/${taskId}/status`} downloadFilename={`task-status-${taskId}.json`}/>}
+      {activeTab === 'reports' && <ShowJson endpoint={`/druid/indexer/v1/task/${taskId}/reports`} downloadFilename={`task-reports-${taskId}.json`}/>}
+      {activeTab === 'log' && <ShowLog endpoint={`/druid/indexer/v1/task/${taskId}/log`} downloadFilename={`task-log-${taskId}.json`} tailOffset={16000}/>}
+    </TableActionDialog>;
+  }
+}
diff --git a/web-console/src/utils/basic-action.tsx b/web-console/src/utils/basic-action.tsx
new file mode 100644
index 0000000..c5293d9
--- /dev/null
+++ b/web-console/src/utils/basic-action.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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, IconName, Intent, Menu, MenuItem } from '@blueprintjs/core';
+import * as React from 'react';
+
+export interface BasicAction {
+  icon?: IconName;
+  title: string;
+  intent?: Intent;
+  onAction: () => void;
+}
+
+export function basicActionsToMenu(basicActions: BasicAction[]) {
+  if (!basicActions.length) return null;
+  return <Menu>
+    {
+      basicActions.map((action) => (
+        <MenuItem
+          key={action.title}
+          icon={action.icon}
+          text={action.title}
+          intent={action.intent}
+          onClick={action.onAction}
+        />
+      ))
+    }
+  </Menu>;
+}
+
+export function basicActionsToButtons(basicActions: BasicAction[]) {
+  if (!basicActions.length) return null;
+  return <>
+    {
+      basicActions.map((action) => (
+        <Button
+          key={action.title}
+          icon={action.icon}
+          text={action.title}
+          intent={action.intent}
+          onClick={action.onAction}
+        />
+      ))
+    }
+  </>;
+}
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index c69ef32..599ce1c 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -18,6 +18,7 @@
 
 import { Button, HTMLSelect, InputGroup, Intent } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
+import * as FileSaver from 'file-saver';
 import * as numeral from 'numeral';
 import * as React from 'react';
 import { Filter, FilterRender } from 'react-table';
@@ -216,3 +217,12 @@ export function sortWithPrefixSuffix(things: string[], prefix: string[], suffix:
   const post = things.filter((x) => suffix.includes(x)).sort();
   return pre.concat(mid, post);
 }
+
+// ----------------------------
+
+export function downloadFile(text: string, type: string, fileName: string): void {
+  const blob = new Blob([text], {
+    type: `text/${type}`
+  });
+  FileSaver.saveAs(blob, fileName);
+}
diff --git a/web-console/src/views/datasource-view.tsx b/web-console/src/views/datasource-view.tsx
index e08cd45..1b4b1e3 100644
--- a/web-console/src/views/datasource-view.tsx
+++ b/web-console/src/views/datasource-view.tsx
@@ -16,12 +16,14 @@
  * limitations under the License.
  */
 
-import { Button, Intent, Switch } from '@blueprintjs/core';
+import { Button, Icon, InputGroup, Intent, Popover, Position, Switch } from '@blueprintjs/core';
+import { FormGroup } from '@blueprintjs/core/lib/esnext';
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import * as React from 'react';
 import ReactTable, { Filter } from 'react-table';
 
+import { ActionCell } from '../components/action-cell';
 import { RuleEditor } from '../components/rule-editor';
 import { TableColumnSelection } from '../components/table-column-selection';
 import { ViewControlBar } from '../components/view-control-bar';
@@ -40,6 +42,7 @@ import {
   queryDruidSql,
   QueryManager, TableColumnSelectionHandler
 } from '../utils';
+import { BasicAction, basicActionsToMenu } from '../utils/basic-action';
 
 import './datasource-view.scss';
 
@@ -80,7 +83,9 @@ export interface DatasourcesViewState {
   dropDataDatasource: string | null;
   enableDatasource: string | null;
   killDatasource: string | null;
-
+  dropReloadDatasource: string | null;
+  dropReloadAction: 'drop' | 'reload';
+  dropReloadInterval: string;
 }
 
 export class DatasourcesView extends React.Component<DatasourcesViewProps, DatasourcesViewState> {
@@ -116,8 +121,10 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
       compactionDialogOpenOn: null,
       dropDataDatasource: null,
       enableDatasource: null,
-      killDatasource: null
-
+      killDatasource: null,
+      dropReloadDatasource: null,
+      dropReloadAction: 'drop',
+      dropReloadInterval: ''
     };
 
     this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
@@ -250,6 +257,46 @@ GROUP BY 1`);
     </AsyncActionDialog>;
   }
 
+  renderDropReloadAction() {
+    const { dropReloadDatasource, dropReloadAction, dropReloadInterval } = this.state;
+    const isDrop = dropReloadAction === 'drop';
+
+    return <AsyncActionDialog
+      action={
+        dropReloadDatasource ? async () => {
+          if (!dropReloadInterval) return;
+          const resp = await axios.post(`/druid/coordinator/v1/datasources/${dropReloadDatasource}/${isDrop ? 'markUnused' : 'markUsed'}`, {
+            interval: dropReloadInterval
+          });
+          return resp.data;
+        } : null
+      }
+      confirmButtonText={`${isDrop ? 'Drop' : 'Reload'} selected data`}
+      confirmButtonDisabled={!/.\/./.test(dropReloadInterval)}
+      successText={`${isDrop ? 'Drop' : 'Reload'} request submitted`}
+      failText={`Could not ${isDrop ? 'drop' : 'reload'} data`}
+      intent={Intent.PRIMARY}
+      onClose={(success) => {
+        this.setState({ dropReloadDatasource: null });
+        if (success) this.datasourceQueryManager.rerunLastQuery();
+      }}
+    >
+      <p>
+        {`Please select the interval that you want to ${isDrop ? 'drop' : 'reload'}?`}
+      </p>
+      <FormGroup>
+        <InputGroup
+          value={dropReloadInterval}
+          onChange={(e: any) => {
+            const v = e.target.value;
+            this.setState({ dropReloadInterval: v.toUpperCase() });
+          }}
+          placeholder="2018-01-01T00:00:00/2018-01-03T00:00:00"
+        />
+      </FormGroup>
+    </AsyncActionDialog>;
+  }
+
   renderKillAction() {
     const { killDatasource } = this.state;
 
@@ -354,6 +401,43 @@ GROUP BY 1`);
     });
   }
 
+  getDatasourceActions(datasource: string, disabled: boolean): BasicAction[] {
+    if (disabled) {
+      return [
+        {
+          icon: IconNames.EXPORT,
+          title: 'Enable',
+          onAction: () => this.setState({ enableDatasource: datasource })
+        },
+        {
+          icon: IconNames.TRASH,
+          title: 'Permanently delete (kill task)',
+          intent: Intent.DANGER,
+          onAction: () => this.setState({ killDatasource: datasource })
+        }
+      ];
+    } else {
+      return [
+        {
+          icon: IconNames.EXPORT,
+          title: 'Reload data by interval',
+          onAction: () => this.setState({ dropReloadDatasource: datasource, dropReloadAction: 'reload' })
+        },
+        {
+          icon: IconNames.IMPORT,
+          title: 'Drop data by interval',
+          onAction: () => this.setState({ dropReloadDatasource: datasource, dropReloadAction: 'drop' })
+        },
+        {
+          icon: IconNames.IMPORT,
+          title: 'Drop datasource (disable)',
+          intent: Intent.DANGER,
+          onAction: () => this.setState({ dropDataDatasource: datasource })
+        }
+      ];
+    }
+  }
+
   renderRetentionDialog() {
     const { retentionDialogOpenOn, tiers } = this.state;
     if (!retentionDialogOpenOn) return null;
@@ -527,21 +611,22 @@ GROUP BY 1`);
             Header: 'Actions',
             accessor: 'datasource',
             id: 'actions',
-            width: 160,
+            width: 70,
             filterable: false,
             Cell: row => {
               const datasource = row.value;
               const { disabled } = row.original;
-              if (disabled) {
-                return <div>
-                  <a onClick={() => this.setState({ enableDatasource: datasource })}>Enable</a>&nbsp;&nbsp;&nbsp;
-                  <a onClick={() => this.setState({ killDatasource: datasource })}>Permanently delete</a>
-                </div>;
-              } else {
-                return <div>
-                  <a onClick={() => this.setState({ dropDataDatasource: datasource })}>Drop data</a>
-                </div>;
-              }
+              const datasourceActions = this.getDatasourceActions(datasource, disabled);
+              const datasourceMenu = basicActionsToMenu(datasourceActions);
+
+              return <ActionCell>
+                {
+                  datasourceMenu &&
+                  <Popover content={datasourceMenu} position={Position.BOTTOM_RIGHT}>
+                    <Icon icon={IconNames.WRENCH}/>
+                  </Popover>
+                }
+              </ActionCell>;
             },
             show: tableColumnSelectionHandler.showColumn('Actions')
           }
@@ -551,6 +636,7 @@ GROUP BY 1`);
       />
       {this.renderDropDataAction()}
       {this.renderEnableAction()}
+      {this.renderDropReloadAction()}
       {this.renderKillAction()}
       {this.renderRetentionDialog()}
       {this.renderCompactionDialog()}
diff --git a/web-console/src/views/tasks-view.tsx b/web-console/src/views/tasks-view.tsx
index b5a412a..8acee10 100644
--- a/web-console/src/views/tasks-view.tsx
+++ b/web-console/src/views/tasks-view.tsx
@@ -16,31 +16,31 @@
  * limitations under the License.
  */
 
-import { Alert, Button, ButtonGroup, Intent, Label, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
+import { Alert, Button, ButtonGroup, Icon, Intent, Label, Menu, MenuDivider, MenuItem, Popover, Position } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import * as React from 'react';
 import ReactTable from 'react-table';
 import { Filter } from 'react-table';
 
+import { ActionCell } from '../components/action-cell';
 import { TableColumnSelection } from '../components/table-column-selection';
 import { ViewControlBar } from '../components/view-control-bar';
 import { AsyncActionDialog } from '../dialogs/async-action-dialog';
 import { SpecDialog } from '../dialogs/spec-dialog';
+import { SupervisorTableActionDialog } from '../dialogs/supervisor-table-action-dialog';
+import { TaskTableActionDialog } from '../dialogs/task-table-action-dialog';
 import { AppToaster } from '../singletons/toaster';
-import { UrlBaser } from '../singletons/url-baser';
 import {
   addFilter,
   booleanCustomTableFilter,
   countBy,
   formatDuration,
-  getDruidErrorMessage, localStorageGet, LocalStorageKeys,
+  getDruidErrorMessage, LocalStorageKeys,
   queryDruidSql,
   QueryManager, TableColumnSelectionHandler
 } from '../utils';
-import { IngestionType } from '../utils/ingestion-spec';
-
-import { LoadDataViewSeed } from './load-data-view';
+import { BasicAction, basicActionsToMenu } from '../utils/basic-action';
 
 import './tasks-view.scss';
 
@@ -77,6 +77,11 @@ export interface TasksViewState {
   taskSpecDialogOpen: boolean;
   initSpec: any;
   alertErrorMsg: string | null;
+
+  taskTableActionDialogId: string | null;
+  taskTableActionDialogActions: BasicAction[];
+  supervisorTableActionDialogId: string | null;
+  supervisorTableActionDialogActions: BasicAction[];
 }
 
 interface TaskQueryResultRow {
@@ -137,8 +142,12 @@ export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
       supervisorSpecDialogOpen: false,
       taskSpecDialogOpen: false,
       initSpec: null,
-      alertErrorMsg: null
+      alertErrorMsg: null,
 
+      taskTableActionDialogId: null,
+      taskTableActionDialogActions: [],
+      supervisorTableActionDialogId: null,
+      supervisorTableActionDialogActions: []
     };
 
     this.supervisorTableColumnSelectionHandler = new TableColumnSelectionHandler(
@@ -274,6 +283,27 @@ ORDER BY "rank" DESC, "created_time" DESC`);
     this.taskQueryManager.rerunLastQuery();
   }
 
+  private getSupervisorActions(id: string, supervisorSuspended: boolean): BasicAction[] {
+    return [
+      {
+        icon: IconNames.STEP_BACKWARD,
+        title: 'Reset',
+        onAction: () => this.setState({ resetSupervisorId: id })
+      },
+      {
+        icon: supervisorSuspended ? IconNames.PLAY : IconNames.PAUSE,
+        title: supervisorSuspended ? 'Resume' : 'Suspend',
+        onAction: () => supervisorSuspended ? this.setState({ resumeSupervisorId: id }) : this.setState({ suspendSupervisorId: id })
+      },
+      {
+        icon: IconNames.CROSS,
+        title: 'Terminate',
+        intent: Intent.DANGER,
+        onAction: () => this.setState({ terminateSupervisorId: id })
+      }
+    ];
+  }
+
   renderResumeSupervisorAction() {
     const { resumeSupervisorId } = this.state;
 
@@ -440,23 +470,29 @@ ORDER BY "rank" DESC, "created_time" DESC`);
             Header: 'Actions',
             id: 'actions',
             accessor: 'id',
-            width: 420,
+            width: 70,
             filterable: false,
             Cell: row => {
               const id = row.value;
-              const suspendResume = row.original.spec.suspended ?
-                <a onClick={() => this.setState({ resumeSupervisorId: id })}>Resume</a> :
-                <a onClick={() => this.setState({ suspendSupervisorId: id })}>Suspend</a>;
-
-              return <div>
-                <a href={UrlBaser.base(`/druid/indexer/v1/supervisor/${id}`)} target="_blank">Payload</a>&nbsp;&nbsp;&nbsp;
-                <a href={UrlBaser.base(`/druid/indexer/v1/supervisor/${id}/status`)} target="_blank">Status</a>&nbsp;&nbsp;&nbsp;
-                <a href={UrlBaser.base(`/druid/indexer/v1/supervisor/${id}/stats`)} target="_blank">Stats</a>&nbsp;&nbsp;&nbsp;
-                <a href={UrlBaser.base(`/druid/indexer/v1/supervisor/${id}/history`)} target="_blank">History</a>&nbsp;&nbsp;&nbsp;
-                {suspendResume}&nbsp;&nbsp;&nbsp;
-                <a onClick={() => this.setState({ resetSupervisorId: id })}>Reset</a>&nbsp;&nbsp;&nbsp;
-                <a onClick={() => this.setState({ terminateSupervisorId: id })}>Terminate</a>
-              </div>;
+              const supervisorSuspended = row.original.spec.suspended;
+              const supervisorActions = this.getSupervisorActions(id, supervisorSuspended);
+              const supervisorMenu = basicActionsToMenu(supervisorActions);
+
+              return <ActionCell>
+                <Icon
+                  icon={IconNames.SEARCH_TEMPLATE}
+                  onClick={() => this.setState({
+                    supervisorTableActionDialogId: id,
+                    supervisorTableActionDialogActions: supervisorActions
+                  })}
+                />
+                {
+                  supervisorMenu &&
+                  <Popover content={supervisorMenu} position={Position.BOTTOM_RIGHT}>
+                    <Icon icon={IconNames.WRENCH}/>
+                  </Popover>
+                }
+              </ActionCell>;
             },
             show: supervisorTableColumnSelectionHandler.showColumn('Actions')
           }
@@ -473,6 +509,18 @@ ORDER BY "rank" DESC, "created_time" DESC`);
 
   // --------------------------------------
 
+  private getTaskActions(id: string, status: string): BasicAction[] {
+    if (status !== 'RUNNING' && status !== 'WAITING' && status !== 'PENDING') return [];
+    return [
+      {
+        icon: IconNames.CROSS,
+        title: 'Kill',
+        intent: Intent.DANGER,
+        onAction: () => this.setState({ killTaskId: id })
+      }
+    ];
+  }
+
   renderKillTaskAction() {
     const { killTaskId } = this.state;
 
@@ -601,20 +649,30 @@ ORDER BY "rank" DESC, "created_time" DESC`);
             Header: 'Actions',
             id: 'actions',
             accessor: 'task_id',
-            width: 360,
+            width: 70,
             filterable: false,
             Cell: row => {
               if (row.aggregated) return '';
               const id = row.value;
               const { status } = row.original;
-              return <div>
-                <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}`)} target="_blank">Payload</a>&nbsp;&nbsp;&nbsp;
-                <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}/status`)} target="_blank">Status</a>&nbsp;&nbsp;&nbsp;
-                <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}/reports`)} target="_blank">Reports</a>&nbsp;&nbsp;&nbsp;
-                <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}/log`)} target="_blank">Log (all)</a>&nbsp;&nbsp;&nbsp;
-                <a href={UrlBaser.base(`/druid/indexer/v1/task/${id}/log?offset=-8192`)} target="_blank">Log (last 8kb)</a>&nbsp;&nbsp;&nbsp;
-                {(status === 'RUNNING' || status === 'WAITING' || status === 'PENDING') && <a onClick={() => this.setState({ killTaskId: id })}>Kill</a>}
-              </div>;
+              const taskActions = this.getTaskActions(id, status);
+              const taskMenu = basicActionsToMenu(taskActions);
+
+              return <ActionCell>
+                <Icon
+                  icon={IconNames.SEARCH_TEMPLATE}
+                  onClick={() => this.setState({
+                    taskTableActionDialogId: id,
+                    taskTableActionDialogActions: taskActions
+                  })}
+                />
+                {
+                  taskMenu &&
+                  <Popover content={taskMenu} position={Position.BOTTOM_RIGHT}>
+                    <Icon icon={IconNames.WRENCH}/>
+                  </Popover>
+                }
+              </ActionCell>;
             },
             Aggregated: row => '',
             show: taskTableColumnSelectionHandler.showColumn('Actions')
@@ -629,7 +687,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
 
   render() {
     const { goToSql, goToLoadDataView, noSqlMode } = this.props;
-    const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, initSpec, alertErrorMsg } = this.state;
+    const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg, taskTableActionDialogId, taskTableActionDialogActions, supervisorTableActionDialogId, supervisorTableActionDialogActions } = this.state;
     const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
 
     const submitTaskMenu = <Menu>
@@ -702,7 +760,6 @@ ORDER BY "rank" DESC, "created_time" DESC`);
           onClose={this.closeSpecDialogs}
           onSubmit={this.submitSupervisor}
           title="Submit supervisor"
-          initSpec={initSpec}
         />
       }
       {
@@ -711,7 +768,6 @@ ORDER BY "rank" DESC, "created_time" DESC`);
           onClose={this.closeSpecDialogs}
           onSubmit={this.submitTask}
           title="Submit task"
-          initSpec={initSpec}
         />
       }
       <Alert
@@ -723,6 +779,24 @@ ORDER BY "rank" DESC, "created_time" DESC`);
       >
         <p>{alertErrorMsg}</p>
       </Alert>
+      {
+        supervisorTableActionDialogId &&
+        <SupervisorTableActionDialog
+          isOpen
+          supervisorId={supervisorTableActionDialogId}
+          actions={supervisorTableActionDialogActions}
+          onClose={() => this.setState({supervisorTableActionDialogId: null})}
+        />
+      }
+      {
+        taskTableActionDialogId &&
+        <TaskTableActionDialog
+          isOpen
+          taskId={taskTableActionDialogId}
+          actions={taskTableActionDialogActions}
+          onClose={() => this.setState({taskTableActionDialogId: null})}
+        />
+      }
     </div>;
   }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org