You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by fj...@apache.org on 2019/06/26 03:14:15 UTC

[incubator-druid] branch master updated: Web-console: Add action column to segments view (#7954)

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 a171b4a  Web-console: Add action column to segments view (#7954)
a171b4a is described below

commit a171b4a399d395b673a6f84854fef90d0f834a4e
Author: mcbrewster <37...@users.noreply.github.com>
AuthorDate: Tue Jun 25 20:14:06 2019 -0700

    Web-console: Add action column to segments view (#7954)
    
    * add actions column to segments view
    
    * add sements action column
---
 .idea/inspectionProfiles/Druid.xml                 |   2 +-
 .../segment-table-action-dialog.spec.tsx.snap      | 171 ++++++
 .../segment-table-action-dialog.spec.tsx}          |  28 +-
 .../segment-table-action-dialog.tsx                |  78 +++
 web-console/src/utils/index.tsx                    |   1 -
 .../src/utils/local-storage-backed-array.tsx       |  65 +++
 .../src/utils/table-column-selection-handler.tsx   |  61 ---
 .../src/views/datasource-view/datasource-view.tsx  |  36 +-
 .../src/views/lookups-view/lookups-view.tsx        |  45 +-
 .../__snapshots__/segments-view.spec.tsx.snap      | 595 +++++++++++----------
 .../src/views/segments-view/segments-view.tsx      | 214 +++++---
 .../src/views/servers-view/servers-view.tsx        |  53 +-
 web-console/src/views/task-view/tasks-view.tsx     |  82 +--
 13 files changed, 912 insertions(+), 519 deletions(-)

diff --git a/.idea/inspectionProfiles/Druid.xml b/.idea/inspectionProfiles/Druid.xml
index ed890c8..4de1e05 100644
--- a/.idea/inspectionProfiles/Druid.xml
+++ b/.idea/inspectionProfiles/Druid.xml
@@ -405,4 +405,4 @@
       <option name="ADD_NONJAVA_TO_ENTRIES" value="true" />
     </inspection_tool>
   </profile>
-</component>
+</component>
\ No newline at end of file
diff --git a/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap b/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
new file mode 100644
index 0000000..6e695a9
--- /dev/null
+++ b/web-console/src/dialogs/segments-table-action-dialog/__snapshots__/segment-table-action-dialog.spec.tsx.snap
@@ -0,0 +1,171 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`task table action dialog matches snapshot 1`] = `
+<div
+  class="bp3-portal"
+>
+  <div
+    class="bp3-overlay bp3-overlay-open bp3-overlay-scroll-container"
+  >
+    <div
+      class="bp3-overlay-backdrop bp3-overlay-appear bp3-overlay-appear-active"
+      tabindex="0"
+    />
+    <div
+      class="bp3-dialog-container bp3-overlay-content bp3-overlay-appear bp3-overlay-appear-active"
+      tabindex="0"
+    >
+      <div
+        class="bp3-dialog table-action-dialog"
+      >
+        <div
+          class="bp3-dialog-header"
+        >
+          <h4
+            class="bp3-heading"
+          >
+            Segment: test
+          </h4>
+          <button
+            aria-label="Close"
+            class="bp3-button bp3-minimal bp3-dialog-close-button"
+            type="button"
+          >
+            <span
+              class="bp3-icon bp3-icon-small-cross"
+              icon="small-cross"
+            >
+              <svg
+                data-icon="small-cross"
+                height="20"
+                viewBox="0 0 20 20"
+                width="20"
+              >
+                <desc>
+                  small-cross
+                </desc>
+                <path
+                  d="M11.41 10l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 0 0-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L11.41 10z"
+                  fill-rule="evenodd"
+                />
+              </svg>
+            </span>
+          </button>
+        </div>
+        <div
+          class="bp3-dialog-body"
+        >
+          <div
+            class="side-bar"
+          >
+            <button
+              class="bp3-button bp3-intent-primary tab-button"
+              type="button"
+            >
+              <span
+                class="bp3-icon bp3-icon-manually-entered-data"
+                icon="manually-entered-data"
+              >
+                <svg
+                  data-icon="manually-entered-data"
+                  height="20"
+                  viewBox="0 0 20 20"
+                  width="20"
+                >
+                  <desc>
+                    manually-entered-data
+                  </desc>
+                  <path
+                    d="M1 12h4.34l2-2H1c-.55 0-1 .45-1 1s.45 1 1 1zm16.77-3.94l1.65-1.65c.36-.36.58-.86.58-1.41 0-1.1-.9-2-2-2-.55 0-1.05.22-1.41.59l-1.65 1.65 2.83 2.82zM1 4h12.34l2-2H1c-.55 0-1 .45-1 1s.45 1 1 1zM0 15c0 .55.45 1 1 1h.34l2-2H1c-.55 0-1 .45-1 1zm1-7h8.34l2-2H1c-.55 0-1 .45-1 1s.45 1 1 1zm18 2h-.34l-2 2H19c.55 0 1-.45 1-1s-.45-1-1-1zm0 4h-4.34l-2 2H19c.55 0 1-.45 1-1s-.45-1-1-1zM4 19l4.41-1.59-2.81-2.79L4 19zM14.23 5.94l-7.65 7.65 2.83 2.83 7.65-7.65-2.83-2.83z"
+                    fill-rule="evenodd"
+                  />
+                </svg>
+              </span>
+              <span
+                class="bp3-button-text"
+              >
+                Metadata
+              </span>
+            </button>
+          </div>
+          <div
+            class="main-section"
+          >
+            <div
+              class="show-json"
+            >
+              <div
+                class="top-actions"
+              >
+                <div
+                  class="bp3-button-group right-buttons"
+                >
+                  <button
+                    class="bp3-button bp3-minimal"
+                    type="button"
+                  >
+                    <span
+                      class="bp3-button-text"
+                    >
+                      Save
+                    </span>
+                  </button>
+                  <button
+                    class="bp3-button bp3-minimal"
+                    type="button"
+                  >
+                    <span
+                      class="bp3-button-text"
+                    >
+                      Copy
+                    </span>
+                  </button>
+                  <button
+                    class="bp3-button bp3-minimal"
+                    type="button"
+                  >
+                    <span
+                      class="bp3-button-text"
+                    >
+                      View raw
+                    </span>
+                  </button>
+                </div>
+              </div>
+              <div
+                class="main-area"
+              >
+                <textarea
+                  class="bp3-input"
+                  readonly=""
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div
+          class="bp3-dialog-footer"
+        >
+          <div
+            class="footer-actions-left"
+          />
+          <div
+            class="bp3-dialog-footer-actions"
+          >
+            <button
+              class="bp3-button bp3-intent-primary"
+              type="button"
+            >
+              <span
+                class="bp3-button-text"
+              >
+                Close
+              </span>
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/web-console/src/utils/index.tsx b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.spec.tsx
similarity index 55%
copy from web-console/src/utils/index.tsx
copy to web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.spec.tsx
index 0a4c9f9..52decd2 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.spec.tsx
@@ -16,10 +16,24 @@
  * limitations under the License.
  */
 
-export * from './general';
-export * from './druid-query';
-export * from './query-manager';
-export * from './query-state';
-export * from './rune-decoder';
-export * from './table-column-selection-handler';
-export * from './local-storage-keys';
+import React from 'react';
+import { render } from 'react-testing-library';
+
+import { SegmentTableActionDialog } from './segment-table-action-dialog';
+
+const basicAction = { title: 'test', onAction: () => null };
+describe('task table action dialog', () => {
+  it('matches snapshot', () => {
+    const taskTableActionDialog = (
+      <SegmentTableActionDialog
+        dataSourceId="test"
+        segmentId="test"
+        actions={[]}
+        onClose={() => null}
+        isOpen
+      />
+    );
+    const { container } = render(taskTableActionDialog, { container: document.body });
+    expect(container.firstChild).toMatchSnapshot();
+  });
+});
diff --git a/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
new file mode 100644
index 0000000..1ae592b
--- /dev/null
+++ b/web-console/src/dialogs/segments-table-action-dialog/segment-table-action-dialog.tsx
@@ -0,0 +1,78 @@
+/*
+ * 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, TextArea } from '@blueprintjs/core';
+import React from 'react';
+
+import { ShowJson } from '../../components';
+import { BasicAction, basicActionsToButtons } from '../../utils/basic-action';
+import { SideButtonMetaData, TableActionDialog } from '../table-action-dialog/table-action-dialog';
+
+interface SegmentTableActionDialogProps extends IDialogProps {
+  segmentId: string | null;
+  dataSourceId: string | null;
+  actions: BasicAction[];
+  onClose: () => void;
+}
+
+interface SegmentTableActionDialogState {
+  activeTab: 'metadata';
+}
+
+export class SegmentTableActionDialog extends React.PureComponent<
+  SegmentTableActionDialogProps,
+  SegmentTableActionDialogState
+> {
+  constructor(props: SegmentTableActionDialogProps) {
+    super(props);
+    this.state = {
+      activeTab: 'metadata',
+    };
+  }
+
+  render(): React.ReactNode {
+    const { segmentId, onClose, dataSourceId, actions } = this.props;
+    const { activeTab } = this.state;
+
+    const taskTableSideButtonMetadata: SideButtonMetaData[] = [
+      {
+        icon: 'manually-entered-data',
+        text: 'Metadata',
+        active: activeTab === 'metadata',
+        onClick: () => this.setState({ activeTab: 'metadata' }),
+      },
+    ];
+
+    return (
+      <TableActionDialog
+        isOpen
+        sideButtonMetadata={taskTableSideButtonMetadata}
+        onClose={onClose}
+        title={`Segment: ${segmentId}`}
+        bottomButtons={basicActionsToButtons(actions)}
+      >
+        {activeTab === 'metadata' && (
+          <ShowJson
+            endpoint={`/druid/coordinator/v1/metadata/datasources/${dataSourceId}/segments/${segmentId}`}
+            downloadFilename={`Segment-metadata-${segmentId}.json`}
+          />
+        )}
+      </TableActionDialog>
+    );
+  }
+}
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index 0a4c9f9..f1d18b0 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -21,5 +21,4 @@ export * from './druid-query';
 export * from './query-manager';
 export * from './query-state';
 export * from './rune-decoder';
-export * from './table-column-selection-handler';
 export * from './local-storage-keys';
diff --git a/web-console/src/utils/local-storage-backed-array.tsx b/web-console/src/utils/local-storage-backed-array.tsx
new file mode 100644
index 0000000..daf227d
--- /dev/null
+++ b/web-console/src/utils/local-storage-backed-array.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 { localStorageGet, LocalStorageKeys, localStorageSet } from '../utils';
+
+export class LocalStorageBackedArray<T> {
+  key: LocalStorageKeys;
+  storedArray: T[];
+
+  constructor(key: LocalStorageKeys, array?: T[]) {
+    this.key = key;
+    if (!Array.isArray(array)) {
+      this.getDataFromStorage();
+    } else {
+      this.storedArray = array;
+      this.setDataInStorage();
+    }
+  }
+
+  private getDataFromStorage(): void {
+    let possibleArray: any;
+    try {
+      possibleArray = JSON.parse(String(localStorageGet(this.key)));
+    } catch {
+      // show all columns by default
+      possibleArray = [];
+    }
+    if (!Array.isArray(possibleArray)) possibleArray = [];
+
+    this.storedArray = possibleArray;
+  }
+
+  private setDataInStorage(): void {
+    localStorageSet(this.key, JSON.stringify(this.storedArray));
+  }
+
+  toggle(value: T): LocalStorageBackedArray<T> {
+    let toggledArray;
+    if (this.storedArray.includes(value)) {
+      toggledArray = this.storedArray.filter(c => c !== value);
+    } else {
+      toggledArray = this.storedArray.concat(value);
+    }
+    return new LocalStorageBackedArray<T>(this.key, toggledArray);
+  }
+
+  exists(value: T): boolean {
+    return !this.storedArray.includes(value);
+  }
+}
diff --git a/web-console/src/utils/table-column-selection-handler.tsx b/web-console/src/utils/table-column-selection-handler.tsx
deleted file mode 100644
index 4decef5..0000000
--- a/web-console/src/utils/table-column-selection-handler.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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 { localStorageGet, LocalStorageKeys, localStorageSet } from '../utils';
-
-export class TableColumnSelectionHandler {
-  tableName: LocalStorageKeys;
-  hiddenColumns: string[];
-  updateComponent: () => void;
-
-  constructor(tableName: LocalStorageKeys, updateComponent: () => void) {
-    this.tableName = tableName;
-    this.updateComponent = updateComponent;
-    this.getHiddenTableColumns();
-  }
-
-  getHiddenTableColumns(): void {
-    const stringValue: string | null = localStorageGet(this.tableName);
-    try {
-      const selections = JSON.parse(String(stringValue));
-      if (!Array.isArray(selections)) {
-        this.hiddenColumns = [];
-      } else {
-        this.hiddenColumns = selections;
-      }
-    } catch (e) {
-      this.hiddenColumns = [];
-    }
-  }
-
-  changeTableColumnSelector(column: string): void {
-    let newSelections: string[];
-    if (this.hiddenColumns.includes(column)) {
-      newSelections = this.hiddenColumns.filter(c => c !== column);
-    } else {
-      newSelections = this.hiddenColumns.concat(column);
-    }
-    this.hiddenColumns = newSelections;
-    this.updateComponent();
-    localStorageSet(this.tableName, JSON.stringify(newSelections));
-  }
-
-  showColumn(column: string): boolean {
-    return !this.hiddenColumns.includes(column);
-  }
-}
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx b/web-console/src/views/datasource-view/datasource-view.tsx
index bf7f60a..db14776 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -52,9 +52,9 @@ import {
   pluralIfNeeded,
   queryDruidSql,
   QueryManager,
-  TableColumnSelectionHandler,
 } from '../../utils';
 import { BasicAction } from '../../utils/basic-action';
+import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
 
 import './datasource-view.scss';
 
@@ -113,6 +113,7 @@ export interface DatasourcesViewState {
   dropReloadDatasource: string | null;
   dropReloadAction: 'drop' | 'reload';
   dropReloadInterval: string;
+  hiddenColumns: LocalStorageBackedArray<string>;
 }
 
 export class DatasourcesView extends React.PureComponent<
@@ -137,7 +138,6 @@ export class DatasourcesView extends React.PureComponent<
     string,
     { tiers: string[]; defaultRules: any[]; datasources: Datasource[] }
   >;
-  private tableColumnSelectionHandler: TableColumnSelectionHandler;
 
   constructor(props: DatasourcesViewProps, context: any) {
     super(props, context);
@@ -158,16 +158,15 @@ export class DatasourcesView extends React.PureComponent<
       dropReloadDatasource: null,
       dropReloadAction: 'drop',
       dropReloadInterval: '',
+      hiddenColumns: new LocalStorageBackedArray<string>(
+        LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
+      ),
     };
-
-    this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
-      LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
-      () => this.setState({}),
-    );
   }
 
   componentDidMount(): void {
     const { noSqlMode } = this.props;
+    const { hiddenColumns } = this.state;
 
     this.datasourceQueryManager = new QueryManager({
       processQuery: async (query: string) => {
@@ -564,8 +563,8 @@ GROUP BY 1`);
       datasourcesError,
       datasourcesFilter,
       showDisabled,
+      hiddenColumns,
     } = this.state;
-    const { tableColumnSelectionHandler } = this;
     let data = datasources || [];
     if (!showDisabled) {
       data = data.filter(d => !d.disabled);
@@ -604,7 +603,7 @@ GROUP BY 1`);
                   </a>
                 );
               },
-              show: tableColumnSelectionHandler.showColumn('Datasource'),
+              show: hiddenColumns.exists('Datasource'),
             },
             {
               Header: 'Availability',
@@ -668,7 +667,7 @@ GROUP BY 1`);
                 const percentAvailable2 = d2.num_available / d2.num_total;
                 return percentAvailable1 - percentAvailable2 || d1.num_total - d2.num_total;
               },
-              show: tableColumnSelectionHandler.showColumn('Availability'),
+              show: hiddenColumns.exists('Availability'),
             },
             {
               Header: 'Retention',
@@ -701,7 +700,7 @@ GROUP BY 1`);
                   </span>
                 );
               },
-              show: tableColumnSelectionHandler.showColumn('Retention'),
+              show: hiddenColumns.exists('Retention'),
             },
             {
               Header: 'Compaction',
@@ -730,7 +729,7 @@ GROUP BY 1`);
                   </span>
                 );
               },
-              show: tableColumnSelectionHandler.showColumn('Compaction'),
+              show: hiddenColumns.exists('Compaction'),
             },
             {
               Header: 'Size',
@@ -738,7 +737,7 @@ GROUP BY 1`);
               filterable: false,
               width: 100,
               Cell: row => formatBytes(row.value),
-              show: tableColumnSelectionHandler.showColumn('Size'),
+              show: hiddenColumns.exists('Size'),
             },
             {
               Header: 'Num rows',
@@ -746,7 +745,7 @@ GROUP BY 1`);
               filterable: false,
               width: 100,
               Cell: row => formatNumber(row.value),
-              show: !noSqlMode && tableColumnSelectionHandler.showColumn('Num rows'),
+              show: !noSqlMode && hiddenColumns.exists('Num rows'),
             },
             {
               Header: ActionCell.COLUMN_LABEL,
@@ -760,7 +759,7 @@ GROUP BY 1`);
                 const datasourceActions = this.getDatasourceActions(datasource, disabled);
                 return <ActionCell actions={datasourceActions} />;
               },
-              show: tableColumnSelectionHandler.showColumn(ActionCell.COLUMN_LABEL),
+              show: hiddenColumns.exists(ActionCell.COLUMN_LABEL),
             },
           ]}
           defaultPageSize={50}
@@ -777,8 +776,7 @@ GROUP BY 1`);
 
   render() {
     const { goToQuery, noSqlMode } = this.props;
-    const { showDisabled } = this.state;
-    const { tableColumnSelectionHandler } = this;
+    const { showDisabled, hiddenColumns } = this.state;
 
     return (
       <div className="data-sources-view app-view">
@@ -801,8 +799,8 @@ GROUP BY 1`);
           />
           <TableColumnSelector
             columns={noSqlMode ? tableColumnsNoSql : tableColumns}
-            onChange={column => tableColumnSelectionHandler.changeTableColumnSelector(column)}
-            tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
+            onChange={column => this.setState({ hiddenColumns: hiddenColumns.toggle(column) })}
+            tableColumnsHidden={hiddenColumns.storedArray}
           />
         </ViewControlBar>
         {this.renderDatasourceTable()}
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx b/web-console/src/views/lookups-view/lookups-view.tsx
index 8efc35a..182ea93 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -26,13 +26,9 @@ import ReactTable from 'react-table';
 import { ActionCell, TableColumnSelector, ViewControlBar } from '../../components';
 import { AsyncActionDialog, LookupEditDialog } from '../../dialogs/';
 import { AppToaster } from '../../singletons/toaster';
-import {
-  getDruidErrorMessage,
-  LocalStorageKeys,
-  QueryManager,
-  TableColumnSelectionHandler,
-} from '../../utils';
+import { getDruidErrorMessage, LocalStorageKeys, QueryManager } from '../../utils';
 import { BasicAction, basicActionsToMenu } from '../../utils/basic-action';
+import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
 
 import './lookups-view.scss';
 
@@ -57,11 +53,12 @@ export interface LookupsViewState {
 
   deleteLookupName: string | null;
   deleteLookupTier: string | null;
+
+  hiddenColumns: LocalStorageBackedArray<string>;
 }
 
 export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsViewState> {
   private lookupsGetQueryManager: QueryManager<string, { lookupEntries: any[]; tiers: string[] }>;
-  private tableColumnSelectionHandler: TableColumnSelectionHandler;
 
   constructor(props: LookupsViewProps, context: any) {
     super(props, context);
@@ -80,11 +77,11 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
 
       deleteLookupTier: null,
       deleteLookupName: null,
+
+      hiddenColumns: new LocalStorageBackedArray<string>(
+        LocalStorageKeys.LOOKUP_TABLE_COLUMN_SELECTION,
+      ),
     };
-    this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
-      LocalStorageKeys.LOOKUP_TABLE_COLUMN_SELECTION,
-      () => this.setState({}),
-    );
   }
 
   componentDidMount(): void {
@@ -266,8 +263,13 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
   }
 
   renderLookupsTable() {
-    const { lookups, loadingLookups, lookupsError, lookupsUninitialized } = this.state;
-    const { tableColumnSelectionHandler } = this;
+    const {
+      lookups,
+      loadingLookups,
+      lookupsError,
+      lookupsUninitialized,
+      hiddenColumns,
+    } = this.state;
 
     if (lookupsUninitialized) {
       return (
@@ -296,28 +298,28 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
               id: 'lookup_name',
               accessor: (row: any) => row.id,
               filterable: true,
-              show: tableColumnSelectionHandler.showColumn('Lookup name'),
+              show: hiddenColumns.exists('Lookup name'),
             },
             {
               Header: 'Tier',
               id: 'tier',
               accessor: (row: any) => row.tier,
               filterable: true,
-              show: tableColumnSelectionHandler.showColumn('Tier'),
+              show: hiddenColumns.exists('Tier'),
             },
             {
               Header: 'Type',
               id: 'type',
               accessor: (row: any) => row.spec.type,
               filterable: true,
-              show: tableColumnSelectionHandler.showColumn('Type'),
+              show: hiddenColumns.exists('Type'),
             },
             {
               Header: 'Version',
               id: 'version',
               accessor: (row: any) => row.version,
               filterable: true,
-              show: tableColumnSelectionHandler.showColumn('Version'),
+              show: hiddenColumns.exists('Version'),
             },
             {
               Header: ActionCell.COLUMN_LABEL,
@@ -331,7 +333,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
                 const lookupActions = this.getLookupActions(lookupTier, lookupId);
                 return <ActionCell actions={lookupActions} />;
               },
-              show: tableColumnSelectionHandler.showColumn(ActionCell.COLUMN_LABEL),
+              show: hiddenColumns.exists(ActionCell.COLUMN_LABEL),
             },
           ]}
           defaultPageSize={50}
@@ -368,8 +370,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
   }
 
   render() {
-    const { lookupsError } = this.state;
-    const { tableColumnSelectionHandler } = this;
+    const { lookupsError, hiddenColumns } = this.state;
 
     return (
       <div className="lookups-view app-view">
@@ -388,8 +389,8 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
           )}
           <TableColumnSelector
             columns={tableColumns}
-            onChange={column => tableColumnSelectionHandler.changeTableColumnSelector(column)}
-            tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
+            onChange={column => this.setState({ hiddenColumns: hiddenColumns.toggle(column) })}
+            tableColumnsHidden={hiddenColumns.storedArray}
           />
         </ViewControlBar>
         {this.renderLookupsTable()}
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 dae9fbe..9be7408 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
@@ -1,297 +1,324 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`segments-view matches snapshot 1`] = `
-<div
-  className="segments-view app-view"
->
-  <ViewControlBar
-    label="Segments"
+<Fragment>
+  <div
+    className="segments-view app-view"
   >
-    <RefreshButton
-      localStorageKey="segments-refresh-rate"
-      onRefresh={[Function]}
-    />
-    <Blueprint3.Button
-      hidden={false}
-      icon="application"
-      onClick={[Function]}
-      text="Go to SQL"
-    />
-    <TableColumnSelector
+    <ViewControlBar
+      label="Segments"
+    >
+      <RefreshButton
+        localStorageKey="segments-refresh-rate"
+        onRefresh={[Function]}
+      />
+      <Blueprint3.Button
+        hidden={false}
+        icon="application"
+        onClick={[Function]}
+        text="Go to SQL"
+      />
+      <TableColumnSelector
+        columns={
+          Array [
+            "Segment ID",
+            "Datasource",
+            "Start",
+            "End",
+            "Version",
+            "Partition",
+            "Size",
+            "Num rows",
+            "Replicas",
+            "Is published",
+            "Is realtime",
+            "Is available",
+            "Is overshadowed",
+            "Actions",
+          ]
+        }
+        onChange={[Function]}
+        tableColumnsHidden={Array []}
+      />
+    </ViewControlBar>
+    <ReactTable
+      AggregatedComponent={[Function]}
+      ExpanderComponent={[Function]}
+      FilterComponent={[Function]}
+      LoadingComponent={[Function]}
+      NoDataComponent={[Function]}
+      PadRowComponent={[Function]}
+      PaginationComponent={[Function]}
+      PivotValueComponent={[Function]}
+      ResizerComponent={[Function]}
+      TableComponent={[Function]}
+      TbodyComponent={[Function]}
+      TdComponent={[Function]}
+      TfootComponent={[Function]}
+      ThComponent={[Function]}
+      TheadComponent={[Function]}
+      TrComponent={[Function]}
+      TrGroupComponent={[Function]}
+      aggregatedKey="_aggregated"
+      className=""
+      collapseOnDataChange={true}
+      collapseOnPageChange={true}
+      collapseOnSortingChange={true}
+      column={
+        Object {
+          "Aggregated": undefined,
+          "Cell": undefined,
+          "Expander": undefined,
+          "Filter": undefined,
+          "Footer": undefined,
+          "Header": undefined,
+          "Pivot": undefined,
+          "PivotValue": undefined,
+          "Placeholder": undefined,
+          "aggregate": undefined,
+          "className": "",
+          "filterAll": false,
+          "filterMethod": undefined,
+          "filterable": undefined,
+          "footerClassName": "",
+          "footerStyle": Object {},
+          "getFooterProps": [Function],
+          "getHeaderProps": [Function],
+          "getProps": [Function],
+          "headerClassName": "",
+          "headerStyle": Object {},
+          "minResizeWidth": 11,
+          "minWidth": 100,
+          "resizable": undefined,
+          "show": true,
+          "sortMethod": undefined,
+          "sortable": undefined,
+          "style": Object {},
+        }
+      }
       columns={
         Array [
-          "Segment ID",
-          "Datasource",
-          "Start",
-          "End",
-          "Version",
-          "Partition",
-          "Size",
-          "Num rows",
-          "Replicas",
-          "Is published",
-          "Is realtime",
-          "Is available",
-          "Is overshadowed",
+          Object {
+            "Header": "Segment ID",
+            "accessor": "segment_id",
+            "show": true,
+            "width": 300,
+          },
+          Object {
+            "Cell": [Function],
+            "Header": "Datasource",
+            "accessor": "datasource",
+            "show": true,
+          },
+          Object {
+            "Cell": [Function],
+            "Header": "Start",
+            "accessor": "start",
+            "defaultSortDesc": true,
+            "show": true,
+            "width": 120,
+          },
+          Object {
+            "Cell": [Function],
+            "Header": "End",
+            "accessor": "end",
+            "defaultSortDesc": true,
+            "show": true,
+            "width": 120,
+          },
+          Object {
+            "Header": "Version",
+            "accessor": "version",
+            "defaultSortDesc": true,
+            "show": true,
+            "width": 120,
+          },
+          Object {
+            "Header": "Partition",
+            "accessor": "partition_num",
+            "filterable": false,
+            "show": true,
+            "width": 60,
+          },
+          Object {
+            "Cell": [Function],
+            "Header": "Size",
+            "accessor": "size",
+            "defaultSortDesc": true,
+            "filterable": false,
+            "show": true,
+          },
+          Object {
+            "Cell": [Function],
+            "Header": "Num rows",
+            "accessor": "num_rows",
+            "defaultSortDesc": true,
+            "filterable": false,
+            "show": true,
+          },
+          Object {
+            "Header": "Replicas",
+            "accessor": "num_replicas",
+            "defaultSortDesc": true,
+            "filterable": false,
+            "show": true,
+            "width": 60,
+          },
+          Object {
+            "Filter": [Function],
+            "Header": "Is published",
+            "accessor": [Function],
+            "id": "is_published",
+            "show": true,
+          },
+          Object {
+            "Filter": [Function],
+            "Header": "Is realtime",
+            "accessor": [Function],
+            "id": "is_realtime",
+            "show": true,
+          },
+          Object {
+            "Filter": [Function],
+            "Header": "Is available",
+            "accessor": [Function],
+            "id": "is_available",
+            "show": true,
+          },
+          Object {
+            "Filter": [Function],
+            "Header": "Is overshadowed",
+            "accessor": [Function],
+            "id": "is_overshadowed",
+            "show": true,
+          },
+          Object {
+            "Aggregated": [Function],
+            "Cell": [Function],
+            "Header": "Actions",
+            "accessor": "segment_id",
+            "filterable": false,
+            "id": "actions",
+            "show": true,
+            "width": 70,
+          },
         ]
       }
-      onChange={[Function]}
-      tableColumnsHidden={Array []}
-    />
-  </ViewControlBar>
-  <ReactTable
-    AggregatedComponent={[Function]}
-    ExpanderComponent={[Function]}
-    FilterComponent={[Function]}
-    LoadingComponent={[Function]}
-    NoDataComponent={[Function]}
-    PadRowComponent={[Function]}
-    PaginationComponent={[Function]}
-    PivotValueComponent={[Function]}
-    ResizerComponent={[Function]}
-    SubComponent={[Function]}
-    TableComponent={[Function]}
-    TbodyComponent={[Function]}
-    TdComponent={[Function]}
-    TfootComponent={[Function]}
-    ThComponent={[Function]}
-    TheadComponent={[Function]}
-    TrComponent={[Function]}
-    TrGroupComponent={[Function]}
-    aggregatedKey="_aggregated"
-    className=""
-    collapseOnDataChange={true}
-    collapseOnPageChange={true}
-    collapseOnSortingChange={true}
-    column={
-      Object {
-        "Aggregated": undefined,
-        "Cell": undefined,
-        "Expander": undefined,
-        "Filter": undefined,
-        "Footer": undefined,
-        "Header": undefined,
-        "Pivot": undefined,
-        "PivotValue": undefined,
-        "Placeholder": undefined,
-        "aggregate": undefined,
-        "className": "",
-        "filterAll": false,
-        "filterMethod": undefined,
-        "filterable": undefined,
-        "footerClassName": "",
-        "footerStyle": Object {},
-        "getFooterProps": [Function],
-        "getHeaderProps": [Function],
-        "getProps": [Function],
-        "headerClassName": "",
-        "headerStyle": Object {},
-        "minResizeWidth": 11,
-        "minWidth": 100,
-        "resizable": undefined,
-        "show": true,
-        "sortMethod": undefined,
-        "sortable": undefined,
-        "style": Object {},
+      data={Array []}
+      defaultExpanded={Object {}}
+      defaultFilterMethod={[Function]}
+      defaultFiltered={Array []}
+      defaultPage={0}
+      defaultPageSize={50}
+      defaultResized={Array []}
+      defaultSortDesc={false}
+      defaultSortMethod={[Function]}
+      defaultSorted={
+        Array [
+          Object {
+            "desc": true,
+            "id": "start",
+          },
+        ]
       }
-    }
-    columns={
-      Array [
-        Object {
-          "Header": "Segment ID",
-          "accessor": "segment_id",
-          "show": true,
-          "width": 300,
-        },
-        Object {
-          "Cell": [Function],
-          "Header": "Datasource",
-          "accessor": "datasource",
-          "show": true,
-        },
-        Object {
-          "Cell": [Function],
-          "Header": "Start",
-          "accessor": "start",
-          "defaultSortDesc": true,
-          "show": true,
-          "width": 120,
-        },
-        Object {
-          "Cell": [Function],
-          "Header": "End",
-          "accessor": "end",
-          "defaultSortDesc": true,
-          "show": true,
-          "width": 120,
-        },
-        Object {
-          "Header": "Version",
-          "accessor": "version",
-          "defaultSortDesc": true,
-          "show": true,
-          "width": 120,
-        },
-        Object {
-          "Header": "Partition",
-          "accessor": "partition_num",
-          "filterable": false,
-          "show": true,
-          "width": 60,
-        },
-        Object {
-          "Cell": [Function],
-          "Header": "Size",
-          "accessor": "size",
-          "defaultSortDesc": true,
-          "filterable": false,
-          "show": true,
-        },
-        Object {
-          "Cell": [Function],
-          "Header": "Num rows",
-          "accessor": "num_rows",
-          "defaultSortDesc": true,
-          "filterable": false,
-          "show": true,
-        },
+      expanderDefaults={
         Object {
-          "Header": "Replicas",
-          "accessor": "num_replicas",
-          "defaultSortDesc": true,
           "filterable": false,
-          "show": true,
-          "width": 60,
-        },
-        Object {
-          "Filter": [Function],
-          "Header": "Is published",
-          "accessor": [Function],
-          "id": "is_published",
-          "show": true,
-        },
-        Object {
-          "Filter": [Function],
-          "Header": "Is realtime",
-          "accessor": [Function],
-          "id": "is_realtime",
-          "show": true,
-        },
-        Object {
-          "Filter": [Function],
-          "Header": "Is available",
-          "accessor": [Function],
-          "id": "is_available",
-          "show": true,
-        },
-        Object {
-          "Filter": [Function],
-          "Header": "Is overshadowed",
-          "accessor": [Function],
-          "id": "is_overshadowed",
-          "show": true,
-        },
-      ]
-    }
-    data={Array []}
-    defaultExpanded={Object {}}
-    defaultFilterMethod={[Function]}
-    defaultFiltered={Array []}
-    defaultPage={0}
-    defaultPageSize={50}
-    defaultResized={Array []}
-    defaultSortDesc={false}
-    defaultSortMethod={[Function]}
-    defaultSorted={
-      Array [
-        Object {
-          "desc": true,
-          "id": "start",
-        },
-      ]
-    }
-    expanderDefaults={
-      Object {
-        "filterable": false,
-        "resizable": false,
-        "sortable": false,
-        "width": 35,
+          "resizable": false,
+          "sortable": false,
+          "width": 35,
+        }
       }
-    }
-    filterable={true}
-    filtered={
-      Array [
-        Object {
-          "id": "datasource",
-          "value": "test",
-        },
-      ]
-    }
-    freezeWhenExpanded={false}
-    getLoadingProps={[Function]}
-    getNoDataProps={[Function]}
-    getPaginationProps={[Function]}
-    getProps={[Function]}
-    getResizerProps={[Function]}
-    getTableProps={[Function]}
-    getTbodyProps={[Function]}
-    getTdProps={[Function]}
-    getTfootProps={[Function]}
-    getTfootTdProps={[Function]}
-    getTfootTrProps={[Function]}
-    getTheadFilterProps={[Function]}
-    getTheadFilterThProps={[Function]}
-    getTheadFilterTrProps={[Function]}
-    getTheadGroupProps={[Function]}
-    getTheadGroupThProps={[Function]}
-    getTheadGroupTrProps={[Function]}
-    getTheadProps={[Function]}
-    getTheadThProps={[Function]}
-    getTheadTrProps={[Function]}
-    getTrGroupProps={[Function]}
-    getTrProps={[Function]}
-    groupedByPivotKey="_groupedByPivot"
-    indexKey="_index"
-    loading={true}
-    loadingText="Loading..."
-    manual={true}
-    multiSort={true}
-    nestingLevelKey="_nestingLevel"
-    nextText="Next"
-    noDataText=""
-    ofText=""
-    onFetchData={[Function]}
-    onFilteredChange={[Function]}
-    originalKey="_original"
-    pageJumpText="jump to page"
-    pageSizeOptions={
-      Array [
-        5,
-        10,
-        20,
-        25,
-        50,
-        100,
-      ]
-    }
-    pageText="Page"
-    pages={10000000}
-    pivotDefaults={Object {}}
-    pivotIDKey="_pivotID"
-    pivotValKey="_pivotVal"
-    previousText="Previous"
-    resizable={true}
-    resolveData={[Function]}
-    rowsSelectorText="rows per page"
-    rowsText="rows"
-    showPageJump={false}
-    showPageSizeOptions={true}
-    showPagination={true}
-    showPaginationBottom={true}
-    showPaginationTop={false}
-    sortable={true}
-    style={Object {}}
-    subRowsKey="_subRows"
-  />
-</div>
+      filterable={true}
+      filtered={
+        Array [
+          Object {
+            "id": "datasource",
+            "value": "test",
+          },
+        ]
+      }
+      freezeWhenExpanded={false}
+      getLoadingProps={[Function]}
+      getNoDataProps={[Function]}
+      getPaginationProps={[Function]}
+      getProps={[Function]}
+      getResizerProps={[Function]}
+      getTableProps={[Function]}
+      getTbodyProps={[Function]}
+      getTdProps={[Function]}
+      getTfootProps={[Function]}
+      getTfootTdProps={[Function]}
+      getTfootTrProps={[Function]}
+      getTheadFilterProps={[Function]}
+      getTheadFilterThProps={[Function]}
+      getTheadFilterTrProps={[Function]}
+      getTheadGroupProps={[Function]}
+      getTheadGroupThProps={[Function]}
+      getTheadGroupTrProps={[Function]}
+      getTheadProps={[Function]}
+      getTheadThProps={[Function]}
+      getTheadTrProps={[Function]}
+      getTrGroupProps={[Function]}
+      getTrProps={[Function]}
+      groupedByPivotKey="_groupedByPivot"
+      indexKey="_index"
+      loading={true}
+      loadingText="Loading..."
+      manual={true}
+      multiSort={true}
+      nestingLevelKey="_nestingLevel"
+      nextText="Next"
+      noDataText=""
+      ofText=""
+      onFetchData={[Function]}
+      onFilteredChange={[Function]}
+      originalKey="_original"
+      pageJumpText="jump to page"
+      pageSizeOptions={
+        Array [
+          5,
+          10,
+          20,
+          25,
+          50,
+          100,
+        ]
+      }
+      pageText="Page"
+      pages={10000000}
+      pivotDefaults={Object {}}
+      pivotIDKey="_pivotID"
+      pivotValKey="_pivotVal"
+      previousText="Previous"
+      resizable={true}
+      resolveData={[Function]}
+      rowsSelectorText="rows per page"
+      rowsText="rows"
+      showPageJump={false}
+      showPageSizeOptions={true}
+      showPagination={true}
+      showPaginationBottom={true}
+      showPaginationTop={false}
+      sortable={true}
+      style={Object {}}
+      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 942caa2..5c0b397 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -17,14 +17,15 @@
  */
 
 import { Button, Intent } from '@blueprintjs/core';
-import { H5 } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import React from 'react';
 import ReactTable from 'react-table';
 import { Filter } from 'react-table';
 
-import { RefreshButton, TableColumnSelector, ViewControlBar } from '../../components';
+import { ActionCell, RefreshButton, TableColumnSelector, ViewControlBar } from '../../components';
+import { AsyncActionDialog } from '../../dialogs';
+import { SegmentTableActionDialog } from '../../dialogs/segments-table-action-dialog/segment-table-action-dialog';
 import {
   addFilter,
   formatBytes,
@@ -35,8 +36,9 @@ import {
   queryDruidSql,
   QueryManager,
   sqlQueryCustomTableFilter,
-  TableColumnSelectionHandler,
 } from '../../utils';
+import { BasicAction } from '../../utils/basic-action';
+import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
 
 import './segments-view.scss';
 
@@ -54,6 +56,7 @@ const tableColumns: string[] = [
   'Is realtime',
   'Is available',
   'Is overshadowed',
+  ActionCell.COLUMN_LABEL,
 ];
 const tableColumnsNoSql: string[] = [
   'Segment ID',
@@ -78,6 +81,12 @@ export interface SegmentsViewState {
   segmentsError: string | null;
   segmentFilter: Filter[];
   allSegments?: SegmentQueryResultRow[] | null;
+  segmentTableActionDialogId: string | null;
+  datasourceTableActionDialogId: string | null;
+  actions: BasicAction[];
+  terminateSegmentId: string | null;
+  terminateDatasourceId: string | null;
+  hiddenColumns: LocalStorageBackedArray<string>;
 }
 
 interface QueryAndSkip {
@@ -105,7 +114,6 @@ interface SegmentQueryResultRow {
 export class SegmentsView extends React.PureComponent<SegmentsViewProps, SegmentsViewState> {
   private segmentsSqlQueryManager: QueryManager<QueryAndSkip, SegmentQueryResultRow[]>;
   private segmentsJsonQueryManager: QueryManager<any, SegmentQueryResultRow[]>;
-  private tableColumnSelectionHandler: TableColumnSelectionHandler;
 
   constructor(props: SegmentsViewProps, context: any) {
     super(props, context);
@@ -115,10 +123,18 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
     if (props.onlyUnavailable) segmentFilter.push({ id: 'is_available', value: 'false' });
 
     this.state = {
+      segmentTableActionDialogId: null,
+      datasourceTableActionDialogId: null,
+      actions: [],
+      terminateSegmentId: null,
+      terminateDatasourceId: null,
       segmentsLoading: true,
       segments: null,
       segmentsError: null,
       segmentFilter,
+      hiddenColumns: new LocalStorageBackedArray<string>(
+        LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION,
+      ),
     };
 
     this.segmentsSqlQueryManager = new QueryManager({
@@ -185,11 +201,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
         });
       },
     });
-
-    this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
-      LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION,
-      () => this.setState({}),
-    );
   }
 
   componentDidMount(): void {
@@ -274,10 +285,20 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
     });
   };
 
+  private getSegmentActions(id: string, datasource: string): BasicAction[] {
+    const actions: BasicAction[] = [];
+    actions.push({
+      icon: IconNames.IMPORT,
+      title: 'Drop segment (disable)',
+      intent: Intent.DANGER,
+      onAction: () => this.setState({ terminateSegmentId: id, terminateDatasourceId: datasource }),
+    });
+    return actions;
+  }
+
   renderSegmentsTable() {
-    const { segments, segmentsLoading, segmentsError, segmentFilter } = this.state;
+    const { segments, segmentsLoading, segmentsError, segmentFilter, hiddenColumns } = this.state;
     const { noSqlMode } = this.props;
-    const { tableColumnSelectionHandler } = this;
 
     return (
       <ReactTable
@@ -302,7 +323,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             Header: 'Segment ID',
             accessor: 'segment_id',
             width: 300,
-            show: tableColumnSelectionHandler.showColumn('Segment ID'),
+            show: hiddenColumns.exists('Segment ID'),
           },
           {
             Header: 'Datasource',
@@ -319,7 +340,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                 </a>
               );
             },
-            show: tableColumnSelectionHandler.showColumn('Datasource'),
+            show: hiddenColumns.exists('Datasource'),
           },
           {
             Header: 'Start',
@@ -338,7 +359,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                 </a>
               );
             },
-            show: tableColumnSelectionHandler.showColumn('Start'),
+            show: hiddenColumns.exists('Start'),
           },
           {
             Header: 'End',
@@ -357,21 +378,21 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                 </a>
               );
             },
-            show: tableColumnSelectionHandler.showColumn('End'),
+            show: hiddenColumns.exists('End'),
           },
           {
             Header: 'Version',
             accessor: 'version',
             defaultSortDesc: true,
             width: 120,
-            show: tableColumnSelectionHandler.showColumn('Version'),
+            show: hiddenColumns.exists('Version'),
           },
           {
             Header: 'Partition',
             accessor: 'partition_num',
             width: 60,
             filterable: false,
-            show: tableColumnSelectionHandler.showColumn('Partition'),
+            show: hiddenColumns.exists('Partition'),
           },
           {
             Header: 'Size',
@@ -379,7 +400,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             filterable: false,
             defaultSortDesc: true,
             Cell: row => formatBytes(row.value),
-            show: tableColumnSelectionHandler.showColumn('Size'),
+            show: hiddenColumns.exists('Size'),
           },
           {
             Header: 'Num rows',
@@ -387,7 +408,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             filterable: false,
             defaultSortDesc: true,
             Cell: row => formatNumber(row.value),
-            show: !noSqlMode && tableColumnSelectionHandler.showColumn('Num rows'),
+            show: !noSqlMode && hiddenColumns.exists('Num rows'),
           },
           {
             Header: 'Replicas',
@@ -395,89 +416,152 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             width: 60,
             filterable: false,
             defaultSortDesc: true,
-            show: !noSqlMode && tableColumnSelectionHandler.showColumn('Replicas'),
+            show: !noSqlMode && hiddenColumns.exists('Replicas'),
           },
           {
             Header: 'Is published',
             id: 'is_published',
             accessor: row => String(Boolean(row.is_published)),
             Filter: makeBooleanFilter(),
-            show: !noSqlMode && tableColumnSelectionHandler.showColumn('Is published'),
+            show: !noSqlMode && hiddenColumns.exists('Is published'),
           },
           {
             Header: 'Is realtime',
             id: 'is_realtime',
             accessor: row => String(Boolean(row.is_realtime)),
             Filter: makeBooleanFilter(),
-            show: !noSqlMode && tableColumnSelectionHandler.showColumn('Is realtime'),
+            show: !noSqlMode && hiddenColumns.exists('Is realtime'),
           },
           {
             Header: 'Is available',
             id: 'is_available',
             accessor: row => String(Boolean(row.is_available)),
             Filter: makeBooleanFilter(),
-            show: !noSqlMode && tableColumnSelectionHandler.showColumn('Is available'),
+            show: !noSqlMode && hiddenColumns.exists('Is available'),
           },
           {
             Header: 'Is overshadowed',
             id: 'is_overshadowed',
             accessor: row => String(Boolean(row.is_overshadowed)),
             Filter: makeBooleanFilter(),
-            show: !noSqlMode && tableColumnSelectionHandler.showColumn('Is overshadowed'),
+            show: !noSqlMode && hiddenColumns.exists('Is overshadowed'),
+          },
+          {
+            Header: ActionCell.COLUMN_LABEL,
+            id: ActionCell.COLUMN_ID,
+            accessor: 'segment_id',
+            width: ActionCell.COLUMN_WIDTH,
+            filterable: false,
+            Cell: row => {
+              if (row.aggregated) return '';
+              const id = row.value;
+              const datasource = row.row.datasource;
+              const dimensions = parseList(row.original.payload.dimensions);
+              const metrics = parseList(row.original.payload.metrics);
+              return (
+                <ActionCell
+                  onDetail={() => {
+                    this.setState({
+                      segmentTableActionDialogId: id,
+                      datasourceTableActionDialogId: datasource,
+                      actions: this.getSegmentActions(id, datasource),
+                    });
+                  }}
+                  actions={this.getSegmentActions(id, datasource)}
+                />
+              );
+            },
+            Aggregated: row => '',
+            show: hiddenColumns.exists(ActionCell.COLUMN_LABEL),
           },
         ]}
         defaultPageSize={50}
-        SubComponent={rowInfo => {
-          const { original } = rowInfo;
-          const { payload } = rowInfo.original;
-          const dimensions = parseList(payload.dimensions);
-          const metrics = parseList(payload.metrics);
-          return (
-            <div className="segment-detail">
-              <H5>Segment ID</H5>
-              <p>{original.segment_id}</p>
-              <H5>{`Dimensions (${dimensions.length})`}</H5>
-              <p>{dimensions.join(', ') || 'No dimension'}</p>
-              <H5>{`Metrics (${metrics.length})`}</H5>
-              <p>{metrics.join(', ') || 'No metrics'}</p>
-            </div>
-          );
-        }}
       />
     );
   }
 
+  renderTerminateSegmentAction() {
+    const { terminateSegmentId, terminateDatasourceId } = this.state;
+
+    return (
+      <AsyncActionDialog
+        action={
+          terminateSegmentId
+            ? async () => {
+                const resp = await axios.delete(
+                  `/druid/coordinator/v1/datasources/${terminateDatasourceId}/segments/${terminateSegmentId}`,
+                  {},
+                );
+                return resp.data;
+              }
+            : null
+        }
+        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 => {
+          this.setState({ terminateSegmentId: null });
+          if (success) {
+            this.segmentsJsonQueryManager.rerunLastQuery();
+            this.segmentsSqlQueryManager.rerunLastQuery();
+          }
+        }}
+      >
+        <p>{`Are you sure you want to drop segment '${terminateSegmentId}'?`}</p>
+        <p>This action is not reversible.</p>
+      </AsyncActionDialog>
+    );
+  }
+
   render() {
+    const {
+      segmentTableActionDialogId,
+      datasourceTableActionDialogId,
+      actions,
+      hiddenColumns,
+    } = this.state;
     const { goToQuery, noSqlMode } = this.props;
-    const { tableColumnSelectionHandler } = this;
 
     return (
-      <div className="segments-view app-view">
-        <ViewControlBar label="Segments">
-          <RefreshButton
-            onRefresh={auto =>
-              noSqlMode
-                ? this.segmentsJsonQueryManager.rerunLastQueryInBackground(auto)
-                : this.segmentsSqlQueryManager.rerunLastQueryInBackground(auto)
-            }
-            localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE}
-          />
-          {!noSqlMode && (
-            <Button
-              icon={IconNames.APPLICATION}
-              text="Go to SQL"
-              hidden={noSqlMode}
-              onClick={() => goToQuery(this.segmentsSqlQueryManager.getLastQuery().query)}
+      <>
+        <div className="segments-view app-view">
+          <ViewControlBar label="Segments">
+            <RefreshButton
+              onRefresh={auto =>
+                noSqlMode
+                  ? this.segmentsJsonQueryManager.rerunLastQueryInBackground(auto)
+                  : this.segmentsSqlQueryManager.rerunLastQueryInBackground(auto)
+              }
+              localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE}
+            />
+            {!noSqlMode && (
+              <Button
+                icon={IconNames.APPLICATION}
+                text="Go to SQL"
+                hidden={noSqlMode}
+                onClick={() => goToQuery(this.segmentsSqlQueryManager.getLastQuery().query)}
+              />
+            )}
+            <TableColumnSelector
+              columns={noSqlMode ? tableColumnsNoSql : tableColumns}
+              onChange={column => this.setState({ hiddenColumns: hiddenColumns.toggle(column) })}
+              tableColumnsHidden={hiddenColumns.storedArray}
             />
-          )}
-          <TableColumnSelector
-            columns={noSqlMode ? tableColumnsNoSql : tableColumns}
-            onChange={column => tableColumnSelectionHandler.changeTableColumnSelector(column)}
-            tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
+          </ViewControlBar>
+          {this.renderSegmentsTable()}
+        </div>
+        {this.renderTerminateSegmentAction()}
+        {segmentTableActionDialogId && (
+          <SegmentTableActionDialog
+            segmentId={segmentTableActionDialogId}
+            dataSourceId={datasourceTableActionDialogId}
+            actions={actions}
+            onClose={() => this.setState({ segmentTableActionDialogId: null })}
+            isOpen
           />
-        </ViewControlBar>
-        {this.renderSegmentsTable()}
-      </div>
+        )}
+      </>
     );
   }
 }
diff --git a/web-console/src/views/servers-view/servers-view.tsx b/web-console/src/views/servers-view/servers-view.tsx
index af02784..e8a0433 100644
--- a/web-console/src/views/servers-view/servers-view.tsx
+++ b/web-console/src/views/servers-view/servers-view.tsx
@@ -34,9 +34,9 @@ import {
   lookupBy,
   queryDruidSql,
   QueryManager,
-  TableColumnSelectionHandler,
 } from '../../utils';
 import { BasicAction } from '../../utils/basic-action';
+import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
 import { deepGet } from '../../utils/object-change';
 
 import './servers-view.scss';
@@ -90,6 +90,8 @@ export interface ServersViewState {
 
   middleManagerDisableWorkerHost: string | null;
   middleManagerEnableWorkerHost: string | null;
+
+  hiddenColumns: LocalStorageBackedArray<string>;
 }
 
 interface ServerQueryResultRow {
@@ -132,7 +134,6 @@ interface ServerResultRow
 
 export class ServersView extends React.PureComponent<ServersViewProps, ServersViewState> {
   private serverQueryManager: QueryManager<string, ServerQueryResultRow[]>;
-  private serverTableColumnSelectionHandler: TableColumnSelectionHandler;
 
   constructor(props: ServersViewProps, context: any) {
     super(props, context);
@@ -145,12 +146,11 @@ export class ServersView extends React.PureComponent<ServersViewProps, ServersVi
 
       middleManagerDisableWorkerHost: null,
       middleManagerEnableWorkerHost: null,
-    };
 
-    this.serverTableColumnSelectionHandler = new TableColumnSelectionHandler(
-      LocalStorageKeys.SERVER_TABLE_COLUMN_SELECTION,
-      () => this.setState({}),
-    );
+      hiddenColumns: new LocalStorageBackedArray<string>(
+        LocalStorageKeys.SERVER_TABLE_COLUMN_SELECTION,
+      ),
+    };
   }
 
   static async getServers(): Promise<ServerQueryResultRow[]> {
@@ -172,6 +172,8 @@ export class ServersView extends React.PureComponent<ServersViewProps, ServersVi
 
   componentDidMount(): void {
     const { noSqlMode } = this.props;
+    const { hiddenColumns } = this.state;
+
     this.serverQueryManager = new QueryManager({
       processQuery: async (query: string) => {
         let servers: ServerQueryResultRow[];
@@ -260,8 +262,14 @@ ORDER BY "rank" DESC, "server" DESC`);
   }
 
   renderServersTable() {
-    const { servers, serversLoading, serversError, serverFilter, groupServersBy } = this.state;
-    const { serverTableColumnSelectionHandler } = this;
+    const {
+      servers,
+      serversLoading,
+      serversError,
+      serverFilter,
+      groupServersBy,
+      hiddenColumns,
+    } = this.state;
 
     const fillIndicator = (value: number) => {
       let formattedValue = (value * 100).toFixed(1);
@@ -294,7 +302,7 @@ ORDER BY "rank" DESC, "server" DESC`);
             accessor: 'server',
             width: 300,
             Aggregated: row => '',
-            show: serverTableColumnSelectionHandler.showColumn('Server'),
+            show: hiddenColumns.exists('Server'),
           },
           {
             Header: 'Type',
@@ -312,7 +320,7 @@ ORDER BY "rank" DESC, "server" DESC`);
                 </a>
               );
             },
-            show: serverTableColumnSelectionHandler.showColumn('Type'),
+            show: hiddenColumns.exists('Type'),
           },
           {
             Header: 'Tier',
@@ -329,13 +337,13 @@ ORDER BY "rank" DESC, "server" DESC`);
                 </a>
               );
             },
-            show: serverTableColumnSelectionHandler.showColumn('Tier'),
+            show: hiddenColumns.exists('Tier'),
           },
           {
             Header: 'Host',
             accessor: 'host',
             Aggregated: () => '',
-            show: serverTableColumnSelectionHandler.showColumn('Host'),
+            show: hiddenColumns.exists('Host'),
           },
           {
             Header: 'Port',
@@ -351,7 +359,7 @@ ORDER BY "rank" DESC, "server" DESC`);
               return ports.join(', ') || 'No port';
             },
             Aggregated: () => '',
-            show: serverTableColumnSelectionHandler.showColumn('Port'),
+            show: hiddenColumns.exists('Port'),
           },
           {
             Header: 'Curr size',
@@ -370,7 +378,7 @@ ORDER BY "rank" DESC, "server" DESC`);
               if (row.value === null) return '';
               return formatBytes(row.value);
             },
-            show: serverTableColumnSelectionHandler.showColumn('Curr size'),
+            show: hiddenColumns.exists('Curr size'),
           },
           {
             Header: 'Max size',
@@ -389,7 +397,7 @@ ORDER BY "rank" DESC, "server" DESC`);
               if (row.value === null) return '';
               return formatBytes(row.value);
             },
-            show: serverTableColumnSelectionHandler.showColumn('Max size'),
+            show: hiddenColumns.exists('Max size'),
           },
           {
             Header: 'Usage',
@@ -447,7 +455,7 @@ ORDER BY "rank" DESC, "server" DESC`);
                   return '';
               }
             },
-            show: serverTableColumnSelectionHandler.showColumn('Usage'),
+            show: hiddenColumns.exists('Usage'),
           },
           {
             Header: 'Detail',
@@ -509,7 +517,7 @@ ORDER BY "rank" DESC, "server" DESC`);
                 segmentsToDropSize,
               );
             },
-            show: serverTableColumnSelectionHandler.showColumn('Detail'),
+            show: hiddenColumns.exists('Detail'),
           },
           {
             Header: ActionCell.COLUMN_LABEL,
@@ -523,7 +531,7 @@ ORDER BY "rank" DESC, "server" DESC`);
               const workerActions = this.getWorkerActions(row.value.host, disabled);
               return <ActionCell actions={workerActions} />;
             },
-            show: serverTableColumnSelectionHandler.showColumn(ActionCell.COLUMN_LABEL),
+            show: hiddenColumns.exists(ActionCell.COLUMN_LABEL),
           },
         ]}
       />
@@ -612,8 +620,7 @@ ORDER BY "rank" DESC, "server" DESC`);
 
   render() {
     const { goToQuery, noSqlMode } = this.props;
-    const { groupServersBy } = this.state;
-    const { serverTableColumnSelectionHandler } = this;
+    const { groupServersBy, hiddenColumns } = this.state;
 
     return (
       <div className="servers-view app-view">
@@ -652,8 +659,8 @@ ORDER BY "rank" DESC, "server" DESC`);
           )}
           <TableColumnSelector
             columns={serverTableColumns}
-            onChange={column => serverTableColumnSelectionHandler.changeTableColumnSelector(column)}
-            tableColumnsHidden={serverTableColumnSelectionHandler.hiddenColumns}
+            onChange={column => this.setState({ hiddenColumns: hiddenColumns.toggle(column) })}
+            tableColumnsHidden={hiddenColumns.storedArray}
           />
         </ViewControlBar>
         {this.renderServersTable()}
diff --git a/web-console/src/views/task-view/tasks-view.tsx b/web-console/src/views/task-view/tasks-view.tsx
index 0f7505d..3ab7293 100644
--- a/web-console/src/views/task-view/tasks-view.tsx
+++ b/web-console/src/views/task-view/tasks-view.tsx
@@ -53,9 +53,9 @@ import {
   localStorageSet,
   queryDruidSql,
   QueryManager,
-  TableColumnSelectionHandler,
 } from '../../utils';
 import { BasicAction } from '../../utils/basic-action';
+import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
 
 import './tasks-view.scss';
 
@@ -114,6 +114,8 @@ export interface TasksViewState {
   taskTableActionDialogActions: BasicAction[];
   supervisorTableActionDialogId: string | null;
   supervisorTableActionDialogActions: BasicAction[];
+  hiddenTaskColumns: LocalStorageBackedArray<string>;
+  hiddenSupervisorColumns: LocalStorageBackedArray<string>;
 }
 
 interface TaskQueryResultRow {
@@ -172,8 +174,6 @@ function stateToColor(status: string): string {
 export class TasksView extends React.PureComponent<TasksViewProps, TasksViewState> {
   private supervisorQueryManager: QueryManager<string, SupervisorQueryResultRow[]>;
   private taskQueryManager: QueryManager<string, TaskQueryResultRow[]>;
-  private supervisorTableColumnSelectionHandler: TableColumnSelectionHandler;
-  private taskTableColumnSelectionHandler: TableColumnSelectionHandler;
   static statusRanking: Record<string, number> = {
     RUNNING: 4,
     PENDING: 3,
@@ -192,6 +192,7 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
       resumeSupervisorId: null,
       suspendSupervisorId: null,
       resetSupervisorId: null,
+      supervisorTableActionDialogId: null,
       terminateSupervisorId: null,
 
       tasksLoading: true,
@@ -210,19 +211,15 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
       taskTableActionDialogId: null,
       taskTableActionDialogStatus: null,
       taskTableActionDialogActions: [],
-      supervisorTableActionDialogId: null,
       supervisorTableActionDialogActions: [],
-    };
 
-    this.supervisorTableColumnSelectionHandler = new TableColumnSelectionHandler(
-      LocalStorageKeys.SUPERVISOR_TABLE_COLUMN_SELECTION,
-      () => this.setState({}),
-    );
-
-    this.taskTableColumnSelectionHandler = new TableColumnSelectionHandler(
-      LocalStorageKeys.TASK_TABLE_COLUMN_SELECTION,
-      () => this.setState({}),
-    );
+      hiddenTaskColumns: new LocalStorageBackedArray<string>(
+        LocalStorageKeys.TASK_TABLE_COLUMN_SELECTION,
+      ),
+      hiddenSupervisorColumns: new LocalStorageBackedArray<string>(
+        LocalStorageKeys.SUPERVISOR_TABLE_COLUMN_SELECTION,
+      ),
+    };
   }
 
   static parseTasks = (data: any[]): TaskQueryResultRow[] => {
@@ -248,6 +245,7 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
 
   componentDidMount(): void {
     const { noSqlMode } = this.props;
+
     this.supervisorQueryManager = new QueryManager({
       processQuery: async (query: string) => {
         const resp = await axios.get('/druid/indexer/v1/supervisor?full');
@@ -521,8 +519,12 @@ ORDER BY "rank" DESC, "created_time" DESC`);
   }
 
   renderSupervisorTable() {
-    const { supervisors, supervisorsLoading, supervisorsError } = this.state;
-    const { supervisorTableColumnSelectionHandler } = this;
+    const {
+      supervisors,
+      supervisorsLoading,
+      supervisorsError,
+      hiddenSupervisorColumns,
+    } = this.state;
     return (
       <>
         <ReactTable
@@ -540,7 +542,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               id: 'datasource',
               accessor: 'id',
               width: 300,
-              show: supervisorTableColumnSelectionHandler.showColumn('Datasource'),
+              show: hiddenSupervisorColumns.exists('Datasource'),
             },
             {
               Header: 'Type',
@@ -552,7 +554,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                 if (!tuningConfig) return '';
                 return tuningConfig.type;
               },
-              show: supervisorTableColumnSelectionHandler.showColumn('Type'),
+              show: hiddenSupervisorColumns.exists('Type'),
             },
             {
               Header: 'Topic/Stream',
@@ -564,7 +566,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                 if (!ioConfig) return '';
                 return ioConfig.topic || ioConfig.stream || '';
               },
-              show: supervisorTableColumnSelectionHandler.showColumn('Topic/Stream'),
+              show: hiddenSupervisorColumns.exists('Topic/Stream'),
             },
             {
               Header: 'Status',
@@ -582,7 +584,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                   </span>
                 );
               },
-              show: supervisorTableColumnSelectionHandler.showColumn('Status'),
+              show: hiddenSupervisorColumns.exists('Status'),
             },
             {
               Header: ActionCell.COLUMN_LABEL,
@@ -607,7 +609,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                   />
                 );
               },
-              show: supervisorTableColumnSelectionHandler.showColumn(ActionCell.COLUMN_LABEL),
+              show: hiddenSupervisorColumns.exists(ActionCell.COLUMN_LABEL),
             },
           ]}
         />
@@ -668,8 +670,14 @@ ORDER BY "rank" DESC, "created_time" DESC`);
 
   renderTaskTable() {
     const { goToMiddleManager } = this.props;
-    const { tasks, tasksLoading, tasksError, taskFilter, groupTasksBy } = this.state;
-    const { taskTableColumnSelectionHandler } = this;
+    const {
+      tasks,
+      tasksLoading,
+      tasksError,
+      taskFilter,
+      groupTasksBy,
+      hiddenTaskColumns,
+    } = this.state;
 
     return (
       <>
@@ -690,7 +698,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               accessor: 'task_id',
               width: 300,
               Aggregated: row => '',
-              show: taskTableColumnSelectionHandler.showColumn('Task ID'),
+              show: hiddenTaskColumns.exists('Task ID'),
             },
             {
               Header: 'Type',
@@ -707,7 +715,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                   </a>
                 );
               },
-              show: taskTableColumnSelectionHandler.showColumn('Type'),
+              show: hiddenTaskColumns.exists('Type'),
             },
             {
               Header: 'Datasource',
@@ -724,8 +732,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                   </a>
                 );
               },
-              show: taskTableColumnSelectionHandler.showColumn('Datasource'),
+              show: hiddenTaskColumns.exists('Datasource'),
             },
+
             {
               Header: 'Location',
               accessor: 'location',
@@ -733,14 +742,14 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               filterMethod: (filter: Filter, row: any) => {
                 return booleanCustomTableFilter(filter, row.location);
               },
-              show: taskTableColumnSelectionHandler.showColumn('Location'),
+              show: hiddenTaskColumns.exists('Location'),
             },
             {
               Header: 'Created time',
               accessor: 'created_time',
               width: 120,
               Aggregated: row => '',
-              show: taskTableColumnSelectionHandler.showColumn('Created time'),
+              show: hiddenTaskColumns.exists('Created time'),
             },
             {
               Header: 'Status',
@@ -800,7 +809,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               filterMethod: (filter: Filter, row: any) => {
                 return booleanCustomTableFilter(filter, row.status.status);
               },
-              show: taskTableColumnSelectionHandler.showColumn('Status'),
+              show: hiddenTaskColumns.exists('Status'),
             },
             {
               Header: 'Duration',
@@ -808,7 +817,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               filterable: false,
               Cell: row => (row.value > 0 ? formatDuration(row.value) : ''),
               Aggregated: () => '',
-              show: taskTableColumnSelectionHandler.showColumn('Duration'),
+              show: hiddenTaskColumns.exists('Duration'),
             },
             {
               Header: ActionCell.COLUMN_LABEL,
@@ -836,7 +845,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
                 );
               },
               Aggregated: row => '',
-              show: taskTableColumnSelectionHandler.showColumn(ActionCell.COLUMN_LABEL),
+              show: hiddenTaskColumns.exists(ActionCell.COLUMN_LABEL),
             },
           ]}
         />
@@ -857,8 +866,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
       supervisorTableActionDialogId,
       supervisorTableActionDialogActions,
       taskTableActionDialogStatus,
+      hiddenSupervisorColumns,
+      hiddenTaskColumns,
     } = this.state;
-    const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
 
     const submitSupervisorMenu = (
       <Menu>
@@ -915,9 +925,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               <TableColumnSelector
                 columns={supervisorTableColumns}
                 onChange={column =>
-                  supervisorTableColumnSelectionHandler.changeTableColumnSelector(column)
+                  this.setState({ hiddenSupervisorColumns: hiddenSupervisorColumns.toggle(column) })
                 }
-                tableColumnsHidden={supervisorTableColumnSelectionHandler.hiddenColumns}
+                tableColumnsHidden={hiddenSupervisorColumns.storedArray}
               />
             </ViewControlBar>
             {this.renderSupervisorTable()}
@@ -968,9 +978,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               <TableColumnSelector
                 columns={taskTableColumns}
                 onChange={column =>
-                  taskTableColumnSelectionHandler.changeTableColumnSelector(column)
+                  this.setState({ hiddenTaskColumns: hiddenTaskColumns.toggle(column) })
                 }
-                tableColumnsHidden={taskTableColumnSelectionHandler.hiddenColumns}
+                tableColumnsHidden={hiddenTaskColumns.storedArray}
               />
             </ViewControlBar>
             {this.renderTaskTable()}


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