This is an automated email from the ASF dual-hosted git repository.

fjy 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 8493bd5  Web console: added bulk supervisor actions (#8028)
8493bd5 is described below

commit 8493bd5c1ccbd54c2c6d873591b1a44179655f5e
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Tue Jul 9 17:31:23 2019 -0700

    Web console: added bulk supervisor actions (#8028)
    
    * added bulk supervisor actions to the web console
    
    * improve async action dialog
    
    * fix tests
---
 web-console/package.json                           |   1 -
 web-console/src/console-application.tsx            |   4 +-
 .../async-action-dialog.spec.tsx.snap              |   2 +-
 .../async-action-dialog/async-action-dialog.scss   |  23 ++
 .../async-action-dialog/async-action-dialog.tsx    | 110 ++++++----
 web-console/src/utils/general.tsx                  |   6 +
 web-console/src/utils/query-manager.tsx            |  11 +-
 .../__snapshots__/datasource-view.spec.tsx.snap    |  59 -----
 .../src/views/datasource-view/datasource-view.tsx  | 112 +++++-----
 .../__snapshots__/lookups-view.spec.tsx.snap       |  12 -
 .../src/views/lookups-view/lookups-view.tsx        |  21 +-
 .../__snapshots__/segments-view.spec.tsx.snap      |  15 --
 .../src/views/segments-view/segments-view.tsx      |  33 ++-
 .../__snapshots__/servers-view.spec.tsx.snap       |  24 --
 .../src/views/servers-view/servers-view.tsx        |  52 +++--
 .../__snapshots__/tasks-view.spec.tsx.snap         | 118 +++++-----
 web-console/src/views/task-view/tasks-view.tsx     | 244 +++++++++++++++------
 17 files changed, 448 insertions(+), 399 deletions(-)

diff --git a/web-console/package.json b/web-console/package.json
index edb85f2..0ec61cb 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -33,7 +33,6 @@
     "endOfLine": "lf"
   },
   "scripts": {
-    "watch": "./script/watch",
     "compile": "./script/build",
     "pretest": "./script/build",
     "run": "./script/run",
diff --git a/web-console/src/console-application.tsx 
b/web-console/src/console-application.tsx
index 8485a07..268fb92 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -62,7 +62,7 @@ export class ConsoleApplication extends React.PureComponent<
 > {
   static MESSAGE_KEY = 'druid-console-message';
   static MESSAGE_DISMISSED = 'dismissed';
-  private capabilitiesQueryManager: QueryManager<string, Capabilities>;
+  private capabilitiesQueryManager: QueryManager<null, Capabilities>;
 
   static async discoverCapabilities(): Promise<Capabilities> {
     try {
@@ -151,7 +151,7 @@ export class ConsoleApplication extends React.PureComponent<
   }
 
   componentDidMount(): void {
-    this.capabilitiesQueryManager.runQuery('dummy');
+    this.capabilitiesQueryManager.runQuery(null);
   }
 
   componentWillUnmount(): void {
diff --git 
a/web-console/src/dialogs/async-action-dialog/__snapshots__/async-action-dialog.spec.tsx.snap
 
b/web-console/src/dialogs/async-action-dialog/__snapshots__/async-action-dialog.spec.tsx.snap
index a2df6e9..811a405 100644
--- 
a/web-console/src/dialogs/async-action-dialog/__snapshots__/async-action-dialog.spec.tsx.snap
+++ 
b/web-console/src/dialogs/async-action-dialog/__snapshots__/async-action-dialog.spec.tsx.snap
@@ -16,7 +16,7 @@ exports[`async action dialog matches snapshot 1`] = `
       tabindex="0"
     >
       <div
-        class="bp3-dialog bp3-alert async-alert-dialog"
+        class="bp3-dialog bp3-alert async-action-dialog"
       >
         <div
           class="bp3-alert-body"
diff --git 
a/web-console/src/dialogs/async-action-dialog/async-action-dialog.scss 
b/web-console/src/dialogs/async-action-dialog/async-action-dialog.scss
new file mode 100644
index 0000000..3e53727
--- /dev/null
+++ b/web-console/src/dialogs/async-action-dialog/async-action-dialog.scss
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+.async-action-dialog {
+  .progress-group {
+    width: 100%;
+  }
+}
diff --git 
a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx 
b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
index e5a26c3..9891d1c 100644
--- a/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
+++ b/web-console/src/dialogs/async-action-dialog/async-action-dialog.tsx
@@ -16,16 +16,28 @@
  * limitations under the License.
  */
 
-import { Button, Classes, Dialog, Icon, Intent, ProgressBar } from 
'@blueprintjs/core';
-import { IconName } from '@blueprintjs/icons';
+import {
+  Button,
+  Classes,
+  Dialog,
+  FormGroup,
+  Icon,
+  IconName,
+  Intent,
+  ProgressBar,
+} from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
 import classNames from 'classnames';
 import React from 'react';
 
 import { AppToaster } from '../../singletons/toaster';
 
-export interface AsyncAlertDialogProps {
-  action: null | (() => Promise<void>);
-  onClose: (success: boolean) => void;
+import './async-action-dialog.scss';
+
+export interface AsyncActionDialogProps {
+  action: () => Promise<void>;
+  onClose: () => void;
+  onSuccess?: () => void;
   confirmButtonText: string;
   confirmButtonDisabled?: boolean;
   cancelButtonText?: string;
@@ -36,24 +48,33 @@ export interface AsyncAlertDialogProps {
   failText: string;
 }
 
-export interface AsyncAlertDialogState {
+export interface AsyncActionDialogState {
   working: boolean;
 }
 
 export class AsyncActionDialog extends React.PureComponent<
-  AsyncAlertDialogProps,
-  AsyncAlertDialogState
+  AsyncActionDialogProps,
+  AsyncActionDialogState
 > {
-  constructor(props: AsyncAlertDialogProps) {
+  private mounted = false;
+
+  constructor(props: AsyncActionDialogProps) {
     super(props);
     this.state = {
       working: false,
     };
   }
 
+  componentDidMount(): void {
+    this.mounted = true;
+  }
+
+  componentWillUnmount(): void {
+    this.mounted = false;
+  }
+
   private handleConfirm = async () => {
-    const { action, onClose, successText, failText } = this.props;
-    if (!action) throw new Error('should never get here');
+    const { action, onClose, onSuccess, successText, failText } = this.props;
 
     this.setState({ working: true });
     try {
@@ -63,22 +84,30 @@ export class AsyncActionDialog extends React.PureComponent<
         message: `${failText}: ${e.message}`,
         intent: Intent.DANGER,
       });
-      this.setState({ working: false });
-      onClose(false);
+      if (this.mounted) {
+        this.setState({ working: false });
+        onClose();
+      }
       return;
     }
     AppToaster.show({
       message: successText,
       intent: Intent.SUCCESS,
     });
-    this.setState({ working: false });
-    onClose(true);
+    if (this.mounted) {
+      this.setState({ working: false });
+    }
+    if (onSuccess) onSuccess();
+    onClose();
+  };
+
+  private handleClose = () => {
+    const { onClose } = this.props;
+    onClose();
   };
 
   render() {
     const {
-      action,
-      onClose,
       className,
       icon,
       intent,
@@ -88,34 +117,41 @@ export class AsyncActionDialog extends React.PureComponent<
       children,
     } = this.props;
     const { working } = this.state;
-    if (!action) return null;
-
-    const handleClose = () => onClose(false);
 
     return (
       <Dialog
         isOpen
-        className={classNames(Classes.ALERT, 'async-alert-dialog', className)}
+        className={classNames(Classes.ALERT, 'async-action-dialog', className)}
         canEscapeKeyClose={!working}
-        onClose={handleClose}
+        onClose={this.handleClose}
       >
         <div className={Classes.ALERT_BODY}>
-          {icon && <Icon icon={icon} />}
-          {!working && <div 
className={Classes.ALERT_CONTENTS}>{children}</div>}
+          {working ? (
+            <FormGroup className="progress-group" label="Processing action...">
+              <ProgressBar intent={intent || Intent.PRIMARY} />
+            </FormGroup>
+          ) : (
+            <>
+              {icon && <Icon icon={icon} />}
+              <div className={Classes.ALERT_CONTENTS}>{children}</div>
+            </>
+          )}
+        </div>
+        <div className={Classes.ALERT_FOOTER}>
+          {working ? (
+            <Button icon={IconNames.EYE_OFF} text="Run in background" 
onClick={this.handleClose} />
+          ) : (
+            <>
+              <Button
+                intent={intent}
+                text={confirmButtonText}
+                onClick={this.handleConfirm}
+                disabled={confirmButtonDisabled}
+              />
+              <Button text={cancelButtonText || 'Cancel'} 
onClick={this.handleClose} />
+            </>
+          )}
         </div>
-        {working ? (
-          <ProgressBar />
-        ) : (
-          <div className={Classes.ALERT_FOOTER}>
-            <Button
-              intent={intent}
-              text={confirmButtonText}
-              onClick={this.handleConfirm}
-              disabled={confirmButtonDisabled}
-            />
-            <Button text={cancelButtonText || 'Cancel'} onClick={handleClose} 
/>
-          </div>
-        )}
       </Dialog>
     );
   }
diff --git a/web-console/src/utils/general.tsx 
b/web-console/src/utils/general.tsx
index 7f13b04..6f834e8 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -24,6 +24,12 @@ import numeral from 'numeral';
 import React from 'react';
 import { Filter, FilterRender } from 'react-table';
 
+export function wait(ms: number): Promise<void> {
+  return new Promise(resolve => {
+    setTimeout(resolve, ms);
+  });
+}
+
 export function addFilter(filters: Filter[], id: string, value: string): 
Filter[] {
   value = `"${value}"`;
   const currentFilter = filters.find(f => f.id === id);
diff --git a/web-console/src/utils/query-manager.tsx 
b/web-console/src/utils/query-manager.tsx
index 8938067..1317c7b 100644
--- a/web-console/src/utils/query-manager.tsx
+++ b/web-console/src/utils/query-manager.tsx
@@ -117,18 +117,15 @@ export class QueryManager<Q, R> {
   }
 
   public runQuery(query: Q): void {
+    if (this.terminated) return;
     this.nextQuery = query;
     this.trigger();
   }
 
-  public rerunLastQuery(): void {
+  public rerunLastQuery(runInBackground = false): void {
+    if (this.terminated) return;
     this.nextQuery = this.lastQuery;
-    this.trigger();
-  }
-
-  public rerunLastQueryInBackground(auto: boolean): void {
-    this.nextQuery = this.lastQuery;
-    if (auto) {
+    if (runInBackground) {
       this.runWhenIdle();
     } else {
       this.trigger();
diff --git 
a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
 
b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
index dac1512..c2f33f2 100755
--- 
a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
+++ 
b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
@@ -246,64 +246,5 @@ exports[`data source view matches snapshot 1`] = `
     style={Object {}}
     subRowsKey="_subRows"
   />
-  <AsyncActionDialog
-    action={null}
-    confirmButtonText="Drop data"
-    failText="Could not drop data"
-    intent="danger"
-    onClose={[Function]}
-    successText="Data drop request acknowledged, next time the coordinator 
runs data will be dropped"
-  >
-    <p>
-      Are you sure you want to drop all the data for datasource 'null'?
-    </p>
-  </AsyncActionDialog>
-  <AsyncActionDialog
-    action={null}
-    confirmButtonText="Enable datasource"
-    failText="Could not enable datasource"
-    intent="primary"
-    onClose={[Function]}
-    successText="Datasource has been enabled"
-  >
-    <p>
-      Are you sure you want to enable datasource 'null'?
-    </p>
-  </AsyncActionDialog>
-  <AsyncActionDialog
-    action={null}
-    confirmButtonDisabled={true}
-    confirmButtonText="Drop selected data"
-    failText="Could not drop data"
-    intent="primary"
-    onClose={[Function]}
-    successText="Drop request submitted"
-  >
-    <p>
-      Please select the interval that you want to drop?
-    </p>
-    <Blueprint3.FormGroup>
-      <Blueprint3.InputGroup
-        onChange={[Function]}
-        placeholder="2018-01-01T00:00:00/2018-01-03T00:00:00"
-        value=""
-      />
-    </Blueprint3.FormGroup>
-  </AsyncActionDialog>
-  <AsyncActionDialog
-    action={null}
-    confirmButtonText="Permanently delete data"
-    failText="Could not submit kill task"
-    intent="danger"
-    onClose={[Function]}
-    successText="Kill task was issued. Datasource will be deleted"
-  >
-    <p>
-      Are you sure you want to permanently delete the data in datasource 
'null'?
-    </p>
-    <p>
-      This action can not be undone.
-    </p>
-  </AsyncActionDialog>
 </div>
 `;
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx 
b/web-console/src/views/datasource-view/datasource-view.tsx
index ab6f1b4..9a4ae18 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -266,27 +266,26 @@ GROUP BY 1`;
 
   renderDropDataAction() {
     const { dropDataDatasource } = this.state;
+    if (!dropDataDatasource) return;
 
     return (
       <AsyncActionDialog
-        action={
-          dropDataDatasource
-            ? async () => {
-                const resp = await axios.delete(
-                  `/druid/coordinator/v1/datasources/${dropDataDatasource}`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.delete(
+            `/druid/coordinator/v1/datasources/${dropDataDatasource}`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Drop data"
         successText="Data drop request acknowledged, next time the coordinator 
runs data will be dropped"
         failText="Could not drop data"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ dropDataDatasource: null });
-          if (success) this.datasourceQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.datasourceQueryManager.rerunLastQuery();
         }}
       >
         <p>
@@ -298,27 +297,26 @@ GROUP BY 1`;
 
   renderEnableAction() {
     const { enableDatasource } = this.state;
+    if (!enableDatasource) return;
 
     return (
       <AsyncActionDialog
-        action={
-          enableDatasource
-            ? async () => {
-                const resp = await axios.post(
-                  `/druid/coordinator/v1/datasources/${enableDatasource}`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.post(
+            `/druid/coordinator/v1/datasources/${enableDatasource}`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Enable datasource"
         successText="Datasource has been enabled"
         failText="Could not enable datasource"
         intent={Intent.PRIMARY}
-        onClose={success => {
+        onClose={() => {
           this.setState({ enableDatasource: null });
-          if (success) this.datasourceQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.datasourceQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to enable datasource 
'${enableDatasource}'?`}</p>
@@ -328,34 +326,33 @@ GROUP BY 1`;
 
   renderDropReloadAction() {
     const { dropReloadDatasource, dropReloadAction, dropReloadInterval } = 
this.state;
+    if (!dropReloadDatasource) return;
     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
-        }
+        action={async () => {
+          if (!dropReloadInterval) return;
+          const resp = await axios.post(
+            `/druid/coordinator/v1/datasources/${dropReloadDatasource}/${
+              isDrop ? 'markUnused' : 'markUsed'
+            }`,
+            {
+              interval: dropReloadInterval,
+            },
+          );
+          return resp.data;
+        }}
         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 => {
+        onClose={() => {
           this.setState({ dropReloadDatasource: null });
-          if (success) this.datasourceQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.datasourceQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Please select the interval that you want to ${isDrop ? 'drop' : 
'reload'}?`}</p>
@@ -375,27 +372,26 @@ GROUP BY 1`;
 
   renderKillAction() {
     const { killDatasource } = this.state;
+    if (!killDatasource) return;
 
     return (
       <AsyncActionDialog
-        action={
-          killDatasource
-            ? async () => {
-                const resp = await axios.delete(
-                  
`/druid/coordinator/v1/datasources/${killDatasource}?kill=true&interval=1000/3000`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.delete(
+            
`/druid/coordinator/v1/datasources/${killDatasource}?kill=true&interval=1000/3000`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Permanently delete data"
         successText="Kill task was issued. Datasource will be deleted"
         failText="Could not submit kill task"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ killDatasource: null });
-          if (success) this.datasourceQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.datasourceQueryManager.rerunLastQuery();
         }}
       >
         <p>
@@ -808,7 +804,7 @@ GROUP BY 1`;
       <div className="data-sources-view app-view">
         <ViewControlBar label="Datasources">
           <RefreshButton
-            onRefresh={auto => 
this.datasourceQueryManager.rerunLastQueryInBackground(auto)}
+            onRefresh={auto => 
this.datasourceQueryManager.rerunLastQuery(auto)}
             localStorageKey={LocalStorageKeys.DATASOURCES_REFRESH_RATE}
           />
           {!noSqlMode && (
diff --git 
a/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap 
b/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
index 228fed1..a57aea4 100755
--- 
a/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
+++ 
b/web-console/src/views/lookups-view/__snapshots__/lookups-view.spec.tsx.snap
@@ -220,17 +220,5 @@ exports[`lookups view matches snapshot 1`] = `
     onClose={[Function]}
     onSubmit={[Function]}
   />
-  <AsyncActionDialog
-    action={null}
-    confirmButtonText="Delete lookup"
-    failText="Could not delete lookup"
-    intent="danger"
-    onClose={[Function]}
-    successText="Lookup was deleted"
-  >
-    <p>
-      Are you sure you want to delete the lookup 'null'?
-    </p>
-  </AsyncActionDialog>
 </div>
 `;
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx 
b/web-console/src/views/lookups-view/lookups-view.tsx
index a792cbb..2b6bb19 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -235,25 +235,24 @@ export class LookupsView extends 
React.PureComponent<LookupsViewProps, LookupsVi
 
   renderDeleteLookupAction() {
     const { deleteLookupTier, deleteLookupName } = this.state;
+    if (!deleteLookupTier) return;
 
     return (
       <AsyncActionDialog
-        action={
-          deleteLookupTier
-            ? async () => {
-                await axios.delete(
-                  
`/druid/coordinator/v1/lookups/config/${deleteLookupTier}/${deleteLookupName}`,
-                );
-              }
-            : null
-        }
+        action={async () => {
+          await axios.delete(
+            
`/druid/coordinator/v1/lookups/config/${deleteLookupTier}/${deleteLookupName}`,
+          );
+        }}
         confirmButtonText="Delete lookup"
         successText="Lookup was deleted"
         failText="Could not delete lookup"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ deleteLookupTier: null, deleteLookupName: null });
-          if (success) this.lookupsQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.lookupsQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to delete the lookup 
'${deleteLookupName}'?`}</p>
diff --git 
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap 
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index efd5139..5179ad4 100755
--- 
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++ 
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -305,20 +305,5 @@ exports[`segments-view matches snapshot 1`] = `
       subRowsKey="_subRows"
     />
   </div>
-  <AsyncActionDialog
-    action={null}
-    confirmButtonText="Drop Segment"
-    failText="Could not drop segment"
-    intent="danger"
-    onClose={[Function]}
-    successText="Segment drop request acknowledged, next time the coordinator 
runs segment will be dropped"
-  >
-    <p>
-      Are you sure you want to drop segment 'null'?
-    </p>
-    <p>
-      This action is not reversible.
-    </p>
-  </AsyncActionDialog>
 </Fragment>
 `;
diff --git a/web-console/src/views/segments-view/segments-view.tsx 
b/web-console/src/views/segments-view/segments-view.tsx
index 8be5c05..2a65636 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -479,30 +479,27 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
 
   renderTerminateSegmentAction() {
     const { terminateSegmentId, terminateDatasourceId } = this.state;
+    if (!terminateDatasourceId || !terminateSegmentId) return;
 
     return (
       <AsyncActionDialog
-        action={
-          terminateSegmentId
-            ? async () => {
-                const resp = await axios.delete(
-                  
`/druid/coordinator/v1/datasources/${terminateDatasourceId}/segments/${terminateSegmentId}`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.delete(
+            
`/druid/coordinator/v1/datasources/${terminateDatasourceId}/segments/${terminateSegmentId}`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Drop Segment"
         successText="Segment drop request acknowledged, next time the 
coordinator runs segment will be dropped"
         failText="Could not drop segment"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ terminateSegmentId: null });
-          if (success) {
-            this.segmentsNoSqlQueryManager.rerunLastQuery();
-            this.segmentsSqlQueryManager.rerunLastQuery();
-          }
+        }}
+        onSuccess={() => {
+          this.segmentsNoSqlQueryManager.rerunLastQuery();
+          this.segmentsSqlQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to drop segment 
'${terminateSegmentId}'?`}</p>
@@ -528,8 +525,8 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
             <RefreshButton
               onRefresh={auto =>
                 noSqlMode
-                  ? 
this.segmentsNoSqlQueryManager.rerunLastQueryInBackground(auto)
-                  : 
this.segmentsSqlQueryManager.rerunLastQueryInBackground(auto)
+                  ? this.segmentsNoSqlQueryManager.rerunLastQuery(auto)
+                  : this.segmentsSqlQueryManager.rerunLastQuery(auto)
               }
               localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE}
             />
diff --git 
a/web-console/src/views/servers-view/__snapshots__/servers-view.spec.tsx.snap 
b/web-console/src/views/servers-view/__snapshots__/servers-view.spec.tsx.snap
index 520323d..315424a 100755
--- 
a/web-console/src/views/servers-view/__snapshots__/servers-view.spec.tsx.snap
+++ 
b/web-console/src/views/servers-view/__snapshots__/servers-view.spec.tsx.snap
@@ -284,29 +284,5 @@ exports[`servers view action servers view 1`] = `
     style={Object {}}
     subRowsKey="_subRows"
   />
-  <AsyncActionDialog
-    action={null}
-    confirmButtonText="Disable worker"
-    failText="Could not disable worker"
-    intent="danger"
-    onClose={[Function]}
-    successText="Worker has been disabled"
-  >
-    <p>
-      Are you sure you want to disable worker 'null'?
-    </p>
-  </AsyncActionDialog>
-  <AsyncActionDialog
-    action={null}
-    confirmButtonText="Enable worker"
-    failText="Could not enable worker"
-    intent="primary"
-    onClose={[Function]}
-    successText="Worker has been enabled"
-  >
-    <p>
-      Are you sure you want to enable worker 'null'?
-    </p>
-  </AsyncActionDialog>
 </div>
 `;
diff --git a/web-console/src/views/servers-view/servers-view.tsx 
b/web-console/src/views/servers-view/servers-view.tsx
index 01c8319..142faa0 100644
--- a/web-console/src/views/servers-view/servers-view.tsx
+++ b/web-console/src/views/servers-view/servers-view.tsx
@@ -560,27 +560,26 @@ ORDER BY "rank" DESC, "server" DESC`;
 
   renderDisableWorkerAction() {
     const { middleManagerDisableWorkerHost } = this.state;
+    if (!middleManagerDisableWorkerHost) return;
 
     return (
       <AsyncActionDialog
-        action={
-          middleManagerDisableWorkerHost
-            ? async () => {
-                const resp = await axios.post(
-                  
`/druid/indexer/v1/worker/${middleManagerDisableWorkerHost}/disable`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.post(
+            
`/druid/indexer/v1/worker/${middleManagerDisableWorkerHost}/disable`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Disable worker"
         successText="Worker has been disabled"
         failText="Could not disable worker"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ middleManagerDisableWorkerHost: null });
-          if (success) this.serverQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.serverQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to disable worker 
'${middleManagerDisableWorkerHost}'?`}</p>
@@ -590,27 +589,26 @@ ORDER BY "rank" DESC, "server" DESC`;
 
   renderEnableWorkerAction() {
     const { middleManagerEnableWorkerHost } = this.state;
+    if (!middleManagerEnableWorkerHost) return;
 
     return (
       <AsyncActionDialog
-        action={
-          middleManagerEnableWorkerHost
-            ? async () => {
-                const resp = await axios.post(
-                  
`/druid/indexer/v1/worker/${middleManagerEnableWorkerHost}/enable`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.post(
+            `/druid/indexer/v1/worker/${middleManagerEnableWorkerHost}/enable`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Enable worker"
         successText="Worker has been enabled"
         failText="Could not enable worker"
         intent={Intent.PRIMARY}
-        onClose={success => {
+        onClose={() => {
           this.setState({ middleManagerEnableWorkerHost: null });
-          if (success) this.serverQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.serverQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to enable worker 
'${middleManagerEnableWorkerHost}'?`}</p>
@@ -647,7 +645,7 @@ ORDER BY "rank" DESC, "server" DESC`;
             </Button>
           </ButtonGroup>
           <RefreshButton
-            onRefresh={auto => 
this.serverQueryManager.rerunLastQueryInBackground(auto)}
+            onRefresh={auto => this.serverQueryManager.rerunLastQuery(auto)}
             localStorageKey={LocalStorageKeys.SERVERS_REFRESH_RATE}
           />
           {!noSqlMode && (
diff --git 
a/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap 
b/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
index 9ced81f..33ddf2f 100644
--- a/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
+++ b/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
@@ -70,6 +70,61 @@ exports[`tasks view matches snapshot 1`] = `
             text="Submit supervisor"
           />
         </Blueprint3.Popover>
+        <Blueprint3.Popover
+          boundary="scrollParent"
+          captureDismiss={false}
+          content={
+            <Blueprint3.Menu>
+              <Blueprint3.MenuItem
+                disabled={false}
+                icon="play"
+                multiline={false}
+                onClick={[Function]}
+                popoverProps={Object {}}
+                shouldDismissPopover={true}
+                text="Resume all supervisors"
+              />
+              <Blueprint3.MenuItem
+                disabled={false}
+                icon="pause"
+                multiline={false}
+                onClick={[Function]}
+                popoverProps={Object {}}
+                shouldDismissPopover={true}
+                text="Suspend all supervisors"
+              />
+              <Blueprint3.MenuItem
+                disabled={false}
+                icon="cross"
+                intent="danger"
+                multiline={false}
+                onClick={[Function]}
+                popoverProps={Object {}}
+                shouldDismissPopover={true}
+                text="Terminate all supervisors"
+              />
+            </Blueprint3.Menu>
+          }
+          defaultIsOpen={false}
+          disabled={false}
+          hasBackdrop={false}
+          hoverCloseDelay={300}
+          hoverOpenDelay={150}
+          inheritDarkTheme={true}
+          interactionKind="click"
+          minimal={false}
+          modifiers={Object {}}
+          openOnTargetFocus={true}
+          position="bottom-left"
+          targetTagName="span"
+          transitionDuration={300}
+          usePortal={true}
+          wrapperTagName="span"
+        >
+          <Blueprint3.Button
+            icon="more"
+          />
+        </Blueprint3.Popover>
         <TableColumnSelector
           columns={
             Array [
@@ -261,57 +316,6 @@ exports[`tasks view matches snapshot 1`] = `
         style={Object {}}
         subRowsKey="_subRows"
       />
-      <AsyncActionDialog
-        action={null}
-        confirmButtonText="Resume supervisor"
-        failText="Could not resume supervisor"
-        intent="primary"
-        onClose={[Function]}
-        successText="Supervisor has been resumed"
-      >
-        <p>
-          Are you sure you want to resume supervisor 'null'?
-        </p>
-      </AsyncActionDialog>
-      <AsyncActionDialog
-        action={null}
-        confirmButtonText="Suspend supervisor"
-        failText="Could not suspend supervisor"
-        intent="danger"
-        onClose={[Function]}
-        successText="Supervisor has been suspended"
-      >
-        <p>
-          Are you sure you want to suspend supervisor 'null'?
-        </p>
-      </AsyncActionDialog>
-      <AsyncActionDialog
-        action={null}
-        confirmButtonText="Reset supervisor"
-        failText="Could not reset supervisor"
-        intent="danger"
-        onClose={[Function]}
-        successText="Supervisor has been reset"
-      >
-        <p>
-          Are you sure you want to reset supervisor 'null'?
-        </p>
-      </AsyncActionDialog>
-      <AsyncActionDialog
-        action={null}
-        confirmButtonText="Terminate supervisor"
-        failText="Could not terminate supervisor"
-        intent="danger"
-        onClose={[Function]}
-        successText="Supervisor has been terminated"
-      >
-        <p>
-          Are you sure you want to terminate supervisor 'null'?
-        </p>
-        <p>
-          This action is not reversible.
-        </p>
-      </AsyncActionDialog>
     </div>
     <div
       className="bottom-pane"
@@ -639,18 +643,6 @@ exports[`tasks view matches snapshot 1`] = `
         style={Object {}}
         subRowsKey="_subRows"
       />
-      <AsyncActionDialog
-        action={null}
-        confirmButtonText="Kill task"
-        failText="Could not kill task"
-        intent="danger"
-        onClose={[Function]}
-        successText="Task was killed"
-      >
-        <p>
-          Are you sure you want to kill task 'null'?
-        </p>
-      </AsyncActionDialog>
     </div>
   </t>
   <Blueprint3.Alert
diff --git a/web-console/src/views/task-view/tasks-view.tsx 
b/web-console/src/views/task-view/tasks-view.tsx
index 39bb7f9..28c0541 100644
--- a/web-console/src/views/task-view/tasks-view.tsx
+++ b/web-console/src/views/task-view/tasks-view.tsx
@@ -95,6 +95,10 @@ export interface TasksViewState {
   resetSupervisorId: string | null;
   terminateSupervisorId: string | null;
 
+  showResumeAllSupervisors: boolean;
+  showSuspendAllSupervisors: boolean;
+  showTerminateAllSupervisors: boolean;
+
   tasksLoading: boolean;
   tasks: any[] | null;
   tasksError: string | null;
@@ -206,6 +210,10 @@ ORDER BY "rank" DESC, "created_time" DESC`;
       supervisorTableActionDialogId: null,
       terminateSupervisorId: null,
 
+      showResumeAllSupervisors: false,
+      showSuspendAllSupervisors: false,
+      showTerminateAllSupervisors: false,
+
       tasksLoading: true,
       tasks: null,
       tasksError: null,
@@ -394,27 +402,26 @@ ORDER BY "rank" DESC, "created_time" DESC`;
 
   renderResumeSupervisorAction() {
     const { resumeSupervisorId } = this.state;
+    if (!resumeSupervisorId) return;
 
     return (
       <AsyncActionDialog
-        action={
-          resumeSupervisorId
-            ? async () => {
-                const resp = await axios.post(
-                  `/druid/indexer/v1/supervisor/${resumeSupervisorId}/resume`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.post(
+            `/druid/indexer/v1/supervisor/${resumeSupervisorId}/resume`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Resume supervisor"
         successText="Supervisor has been resumed"
         failText="Could not resume supervisor"
         intent={Intent.PRIMARY}
-        onClose={success => {
+        onClose={() => {
           this.setState({ resumeSupervisorId: null });
-          if (success) this.supervisorQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.supervisorQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to resume supervisor 
'${resumeSupervisorId}'?`}</p>
@@ -424,27 +431,26 @@ ORDER BY "rank" DESC, "created_time" DESC`;
 
   renderSuspendSupervisorAction() {
     const { suspendSupervisorId } = this.state;
+    if (!suspendSupervisorId) return;
 
     return (
       <AsyncActionDialog
-        action={
-          suspendSupervisorId
-            ? async () => {
-                const resp = await axios.post(
-                  
`/druid/indexer/v1/supervisor/${suspendSupervisorId}/suspend`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.post(
+            `/druid/indexer/v1/supervisor/${suspendSupervisorId}/suspend`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Suspend supervisor"
         successText="Supervisor has been suspended"
         failText="Could not suspend supervisor"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ suspendSupervisorId: null });
-          if (success) this.supervisorQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.supervisorQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to suspend supervisor 
'${suspendSupervisorId}'?`}</p>
@@ -454,27 +460,26 @@ ORDER BY "rank" DESC, "created_time" DESC`;
 
   renderResetSupervisorAction() {
     const { resetSupervisorId } = this.state;
+    if (!resetSupervisorId) return;
 
     return (
       <AsyncActionDialog
-        action={
-          resetSupervisorId
-            ? async () => {
-                const resp = await axios.post(
-                  `/druid/indexer/v1/supervisor/${resetSupervisorId}/reset`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.post(
+            `/druid/indexer/v1/supervisor/${resetSupervisorId}/reset`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Reset supervisor"
         successText="Supervisor has been reset"
         failText="Could not reset supervisor"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ resetSupervisorId: null });
-          if (success) this.supervisorQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.supervisorQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to reset supervisor 
'${resetSupervisorId}'?`}</p>
@@ -484,27 +489,26 @@ ORDER BY "rank" DESC, "created_time" DESC`;
 
   renderTerminateSupervisorAction() {
     const { terminateSupervisorId } = this.state;
+    if (!terminateSupervisorId) return;
 
     return (
       <AsyncActionDialog
-        action={
-          terminateSupervisorId
-            ? async () => {
-                const resp = await axios.post(
-                  
`/druid/indexer/v1/supervisor/${terminateSupervisorId}/terminate`,
-                  {},
-                );
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await axios.post(
+            `/druid/indexer/v1/supervisor/${terminateSupervisorId}/terminate`,
+            {},
+          );
+          return resp.data;
+        }}
         confirmButtonText="Terminate supervisor"
         successText="Supervisor has been terminated"
         failText="Could not terminate supervisor"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ terminateSupervisorId: null });
-          if (success) this.supervisorQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.supervisorQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to terminate supervisor 
'${terminateSupervisorId}'?`}</p>
@@ -638,24 +642,23 @@ ORDER BY "rank" DESC, "created_time" DESC`;
 
   renderKillTaskAction() {
     const { killTaskId } = this.state;
+    if (!killTaskId) return;
 
     return (
       <AsyncActionDialog
-        action={
-          killTaskId
-            ? async () => {
-                const resp = await 
axios.post(`/druid/indexer/v1/task/${killTaskId}/shutdown`, {});
-                return resp.data;
-              }
-            : null
-        }
+        action={async () => {
+          const resp = await 
axios.post(`/druid/indexer/v1/task/${killTaskId}/shutdown`, {});
+          return resp.data;
+        }}
         confirmButtonText="Kill task"
         successText="Task was killed"
         failText="Could not kill task"
         intent={Intent.DANGER}
-        onClose={success => {
+        onClose={() => {
           this.setState({ killTaskId: null });
-          if (success) this.taskQueryManager.rerunLastQuery();
+        }}
+        onSuccess={() => {
+          this.taskQueryManager.rerunLastQuery();
         }}
       >
         <p>{`Are you sure you want to kill task '${killTaskId}'?`}</p>
@@ -849,6 +852,118 @@ ORDER BY "rank" DESC, "created_time" DESC`;
     );
   }
 
+  renderBulkSupervisorActions() {
+    const bulkSupervisorActionsMenu = (
+      <Menu>
+        <MenuItem
+          icon={IconNames.PLAY}
+          text="Resume all supervisors"
+          onClick={() => this.setState({ showResumeAllSupervisors: true })}
+        />
+        <MenuItem
+          icon={IconNames.PAUSE}
+          text="Suspend all supervisors"
+          onClick={() => this.setState({ showSuspendAllSupervisors: true })}
+        />
+        <MenuItem
+          icon={IconNames.CROSS}
+          text="Terminate all supervisors"
+          intent={Intent.DANGER}
+          onClick={() => this.setState({ showTerminateAllSupervisors: true })}
+        />
+      </Menu>
+    );
+
+    return (
+      <>
+        <Popover content={bulkSupervisorActionsMenu} 
position={Position.BOTTOM_LEFT}>
+          <Button icon={IconNames.MORE} />
+        </Popover>
+        {this.renderResumeAllSupervisorAction()}
+        {this.renderSuspendAllSupervisorAction()}
+        {this.renderTerminateAllSupervisorAction()}
+      </>
+    );
+  }
+
+  renderResumeAllSupervisorAction() {
+    const { showResumeAllSupervisors } = this.state;
+    if (!showResumeAllSupervisors) return;
+
+    return (
+      <AsyncActionDialog
+        action={async () => {
+          const resp = await 
axios.post(`/druid/indexer/v1/supervisor/resumeAll`, {});
+          return resp.data;
+        }}
+        confirmButtonText="Resume all supervisors"
+        successText="All supervisors have been resumed"
+        failText="Could not resume all supervisors"
+        intent={Intent.PRIMARY}
+        onClose={() => {
+          this.setState({ showResumeAllSupervisors: false });
+        }}
+        onSuccess={() => {
+          this.supervisorQueryManager.rerunLastQuery();
+        }}
+      >
+        <p>Are you sure you want to resume all the supervisors?</p>
+      </AsyncActionDialog>
+    );
+  }
+
+  renderSuspendAllSupervisorAction() {
+    const { showSuspendAllSupervisors } = this.state;
+    if (!showSuspendAllSupervisors) return;
+
+    return (
+      <AsyncActionDialog
+        action={async () => {
+          const resp = await 
axios.post(`/druid/indexer/v1/supervisor/suspendAll`, {});
+          return resp.data;
+        }}
+        confirmButtonText="Suspend all supervisors"
+        successText="All supervisors have been suspended"
+        failText="Could not suspend all supervisors"
+        intent={Intent.DANGER}
+        onClose={() => {
+          this.setState({ showSuspendAllSupervisors: false });
+        }}
+        onSuccess={() => {
+          this.supervisorQueryManager.rerunLastQuery();
+        }}
+      >
+        <p>Are you sure you want to suspend all the supervisors?</p>
+      </AsyncActionDialog>
+    );
+  }
+
+  renderTerminateAllSupervisorAction() {
+    const { showTerminateAllSupervisors } = this.state;
+    if (!showTerminateAllSupervisors) return;
+
+    return (
+      <AsyncActionDialog
+        action={async () => {
+          const resp = await 
axios.post(`/druid/indexer/v1/supervisor/terminateAll`, {});
+          return resp.data;
+        }}
+        confirmButtonText="Terminate all supervisors"
+        successText="All supervisors have been terminated"
+        failText="Could not terminate all supervisors"
+        intent={Intent.DANGER}
+        onClose={() => {
+          this.setState({ showTerminateAllSupervisors: false });
+        }}
+        onSuccess={() => {
+          this.supervisorQueryManager.rerunLastQuery();
+        }}
+      >
+        <p>Are you sure you want to terminate all the supervisors?</p>
+      </AsyncActionDialog>
+    );
+  }
+
   render() {
     const { goToQuery, goToLoadDataView, noSqlMode } = this.props;
     const {
@@ -912,11 +1027,12 @@ ORDER BY "rank" DESC, "created_time" DESC`;
             <ViewControlBar label="Supervisors">
               <RefreshButton
                 localStorageKey={LocalStorageKeys.SUPERVISORS_REFRESH_RATE}
-                onRefresh={auto => 
this.supervisorQueryManager.rerunLastQueryInBackground(auto)}
+                onRefresh={auto => 
this.supervisorQueryManager.rerunLastQuery(auto)}
               />
               <Popover content={submitSupervisorMenu} 
position={Position.BOTTOM_LEFT}>
                 <Button icon={IconNames.PLUS} text="Submit supervisor" />
               </Popover>
+              {this.renderBulkSupervisorActions()}
               <TableColumnSelector
                 columns={supervisorTableColumns}
                 onChange={column =>
@@ -958,7 +1074,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
               </ButtonGroup>
               <RefreshButton
                 localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE}
-                onRefresh={auto => 
this.taskQueryManager.rerunLastQueryInBackground(auto)}
+                onRefresh={auto => this.taskQueryManager.rerunLastQuery(auto)}
               />
               {!noSqlMode && (
                 <Button


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

Reply via email to