Add table column selection in druid console to allow hiding/showing of columns (#7292)

* Add table column selections to all tables to allow user to hide/show columns

* Small change for re-rendering

* Use column selection handler class to process all column hiding/showing

* dereference table handler function at the start; use more specific file name for table.tsx
This commit is contained in:
Qi Shu 2019-03-20 13:11:00 -07:00 committed by Clint Wylie
parent 2814729d32
commit 30e646308a
9 changed files with 399 additions and 58 deletions

View File

@ -0,0 +1,31 @@
/*
* 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.
*/
.table-column-selection {
float: right;
.pt-popover-content {
padding: 10px 10px 1px 10px;
}
.form-group {
margin-bottom: 0;
}
}

View File

@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Checkbox, Popover, Position } from "@blueprintjs/core";
import * as React from 'react';
import { FormGroup, IconNames } from "./filler";
import "./table-column-selection.scss";
interface TableColumnSelectionProps extends React.Props<any> {
columns: string[];
onChange: (column: string) => void;
tableColumnsHidden: string[];
}
interface TableColumnSelectionState {
}
export class TableColumnSelection extends React.Component<TableColumnSelectionProps, TableColumnSelectionState> {
constructor(props: TableColumnSelectionProps) {
super(props);
this.state = {
};
}
render() {
const { columns, onChange, tableColumnsHidden } = this.props;
const checkboxes = <FormGroup>
{
columns.map(column => {
return <Checkbox
label={column}
key={column}
checked={!tableColumnsHidden.includes(column)}
onChange={() => onChange(column)}
/>;
})
}
</FormGroup>;
return <Popover
className={"table-column-selection"}
content={checkboxes}
position={Position.BOTTOM_RIGHT}
inline
>
<Button rightIconName={IconNames.CARET_DOWN} text={"Columns"} />
</Popover>;
}
}

View File

@ -20,3 +20,4 @@ export * from './general';
export * from './druid-query'; export * from './druid-query';
export * from './query-manager'; export * from './query-manager';
export * from './rune-decoder'; export * from './rune-decoder';
export * from './table-column-selection-handler';

View File

@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { localStorageGet, localStorageSet } from "./general";
export class TableColumnSelectionHandler {
tableName: string;
hiddenColumns: string[];
updateComponent: () => void;
constructor(tableName: string, 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 = [];
}
}
changeTableColumnSelection(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);
}
}

View File

@ -23,6 +23,7 @@ import ReactTable, { Filter } from "react-table";
import { IconNames } from "../components/filler"; import { IconNames } from "../components/filler";
import { RuleEditor } from '../components/rule-editor'; import { RuleEditor } from '../components/rule-editor';
import { TableColumnSelection } from "../components/table-column-selection";
import { AsyncActionDialog } from '../dialogs/async-action-dialog'; import { AsyncActionDialog } from '../dialogs/async-action-dialog';
import { CompactionDialog } from "../dialogs/compaction-dialog"; import { CompactionDialog } from "../dialogs/compaction-dialog";
import { RetentionDialog } from '../dialogs/retention-dialog'; import { RetentionDialog } from '../dialogs/retention-dialog';
@ -34,11 +35,16 @@ import {
formatNumber, formatNumber,
getDruidErrorMessage, getDruidErrorMessage,
lookupBy, lookupBy,
pluralIfNeeded, queryDruidSql, QueryManager pluralIfNeeded,
queryDruidSql,
QueryManager, TableColumnSelectionHandler
} from "../utils"; } from "../utils";
import "./datasource-view.scss"; import "./datasource-view.scss";
const datasourceTableColumnSelection = "datasource-table-column-selection";
const tableColumns: string[] = ["Datasource", "Availability", "Retention", "Compaction", "Size", "Num rows", "Actions"];
export interface DatasourcesViewProps extends React.Props<any> { export interface DatasourcesViewProps extends React.Props<any> {
goToSql: (initSql: string) => void; goToSql: (initSql: string) => void;
goToSegments: (datasource: string, onlyUnavailable?: boolean) => void; goToSegments: (datasource: string, onlyUnavailable?: boolean) => void;
@ -64,6 +70,7 @@ export interface DatasourcesViewState {
dropDataDatasource: string | null; dropDataDatasource: string | null;
enableDatasource: string | null; enableDatasource: string | null;
killDatasource: string | null; killDatasource: string | null;
} }
export class DatasourcesView extends React.Component<DatasourcesViewProps, DatasourcesViewState> { export class DatasourcesView extends React.Component<DatasourcesViewProps, DatasourcesViewState> {
@ -82,6 +89,7 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
} }
private datasourceQueryManager: QueryManager<string, { tiers: string[], defaultRules: any[], datasources: Datasource[] }>; private datasourceQueryManager: QueryManager<string, { tiers: string[], defaultRules: any[], datasources: Datasource[] }>;
private tableColumnSelectionHandler: TableColumnSelectionHandler;
constructor(props: DatasourcesViewProps, context: any) { constructor(props: DatasourcesViewProps, context: any) {
super(props, context); super(props, context);
@ -99,7 +107,12 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
dropDataDatasource: null, dropDataDatasource: null,
enableDatasource: null, enableDatasource: null,
killDatasource: null killDatasource: null
}; };
this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
datasourceTableColumnSelection, () => this.setState({})
);
} }
componentDidMount(): void { componentDidMount(): void {
@ -151,6 +164,7 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
SUM("num_rows") AS num_rows SUM("num_rows") AS num_rows
FROM sys.segments FROM sys.segments
GROUP BY 1`); GROUP BY 1`);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -342,12 +356,11 @@ GROUP BY 1`);
renderDatasourceTable() { renderDatasourceTable() {
const { goToSegments } = this.props; const { goToSegments } = this.props;
const { datasources, defaultRules, datasourcesLoading, datasourcesError, datasourcesFilter, showDisabled } = this.state; const { datasources, defaultRules, datasourcesLoading, datasourcesError, datasourcesFilter, showDisabled } = this.state;
const { tableColumnSelectionHandler } = this;
let data = datasources || []; let data = datasources || [];
if (!showDisabled) { if (!showDisabled) {
data = data.filter(d => !d.disabled); data = data.filter(d => !d.disabled);
} }
return <> return <>
<ReactTable <ReactTable
data={data} data={data}
@ -366,7 +379,8 @@ GROUP BY 1`);
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
return <a onClick={() => { this.setState({ datasourcesFilter: addFilter(datasourcesFilter, 'datasource', value) }); }}>{value}</a>; return <a onClick={() => { this.setState({ datasourcesFilter: addFilter(datasourcesFilter, 'datasource', value) }); }}>{value}</a>;
} },
show: tableColumnSelectionHandler.showColumn("Datasource")
}, },
{ {
Header: "Availability", Header: "Availability",
@ -400,7 +414,8 @@ GROUP BY 1`);
</span>; </span>;
} }
} },
show: tableColumnSelectionHandler.showColumn("Availability")
}, },
{ {
Header: 'Retention', Header: 'Retention',
@ -423,7 +438,8 @@ GROUP BY 1`);
{text}&nbsp; {text}&nbsp;
<a>&#x270E;</a> <a>&#x270E;</a>
</span>; </span>;
} },
show: tableColumnSelectionHandler.showColumn("Retention")
}, },
{ {
Header: 'Compaction', Header: 'Compaction',
@ -449,21 +465,24 @@ GROUP BY 1`);
{text}&nbsp; {text}&nbsp;
<a>&#x270E;</a> <a>&#x270E;</a>
</span>; </span>;
} },
show: tableColumnSelectionHandler.showColumn("Compaction")
}, },
{ {
Header: 'Size', Header: 'Size',
accessor: 'size', accessor: 'size',
filterable: false, filterable: false,
width: 100, width: 100,
Cell: (row) => formatBytes(row.value) Cell: (row) => formatBytes(row.value),
show: tableColumnSelectionHandler.showColumn("Size")
}, },
{ {
Header: 'Num rows', Header: 'Num rows',
accessor: 'num_rows', accessor: 'num_rows',
filterable: false, filterable: false,
width: 100, width: 100,
Cell: (row) => formatNumber(row.value) Cell: (row) => formatNumber(row.value),
show: tableColumnSelectionHandler.showColumn("Num rows")
}, },
{ {
Header: 'Actions', Header: 'Actions',
@ -484,7 +503,8 @@ GROUP BY 1`);
<a onClick={() => this.setState({ dropDataDatasource: datasource })}>Drop data</a> <a onClick={() => this.setState({ dropDataDatasource: datasource })}>Drop data</a>
</div>; </div>;
} }
} },
show: tableColumnSelectionHandler.showColumn("Actions")
} }
]} ]}
defaultPageSize={50} defaultPageSize={50}
@ -501,6 +521,7 @@ GROUP BY 1`);
render() { render() {
const { goToSql } = this.props; const { goToSql } = this.props;
const { showDisabled } = this.state; const { showDisabled } = this.state;
const { tableColumnSelectionHandler } = this;
return <div className="data-sources-view app-view"> return <div className="data-sources-view app-view">
<div className="control-bar"> <div className="control-bar">
@ -520,6 +541,11 @@ GROUP BY 1`);
label="Show disabled" label="Show disabled"
onChange={() => this.setState({ showDisabled: !showDisabled })} onChange={() => this.setState({ showDisabled: !showDisabled })}
/> />
<TableColumnSelection
columns={tableColumns}
onChange={(column) => tableColumnSelectionHandler.changeTableColumnSelection(column)}
tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
/>
</div> </div>
{this.renderDatasourceTable()} {this.renderDatasourceTable()}
</div>; </div>;

View File

@ -23,12 +23,20 @@ import * as React from 'react';
import ReactTable from "react-table"; import ReactTable from "react-table";
import { Filter } from "react-table"; import { Filter } from "react-table";
import { TableColumnSelection } from "../components/table-column-selection";
import { LookupEditDialog } from "../dialogs/lookup-edit-dialog"; import { LookupEditDialog } from "../dialogs/lookup-edit-dialog";
import { AppToaster } from "../singletons/toaster"; import { AppToaster } from "../singletons/toaster";
import { getDruidErrorMessage, QueryManager } from "../utils"; import {
getDruidErrorMessage,
QueryManager,
TableColumnSelectionHandler
} from "../utils";
import "./lookups-view.scss"; import "./lookups-view.scss";
const lookupTableColumnSelection = "lookup-table-column-selection";
const tableColumns: string[] = ["Lookup Name", "Tier", "Type", "Version", "Config"];
export interface LookupsViewProps extends React.Props<any> { export interface LookupsViewProps extends React.Props<any> {
} }
@ -49,6 +57,7 @@ export interface LookupsViewState {
export class LookupsView extends React.Component<LookupsViewProps, LookupsViewState> { export class LookupsView extends React.Component<LookupsViewProps, LookupsViewState> {
private lookupsGetQueryManager: QueryManager<string, {lookupEntries: any[], tiers: string[]}>; private lookupsGetQueryManager: QueryManager<string, {lookupEntries: any[], tiers: string[]}>;
private lookupDeleteQueryManager: QueryManager<string, any[]>; private lookupDeleteQueryManager: QueryManager<string, any[]>;
private tableColumnSelectionHandler: TableColumnSelectionHandler;
constructor(props: LookupsViewProps, context: any) { constructor(props: LookupsViewProps, context: any) {
super(props, context); super(props, context);
@ -64,6 +73,9 @@ export class LookupsView extends React.Component<LookupsViewProps, LookupsViewSt
isEdit: false, isEdit: false,
allLookupTiers: [] allLookupTiers: []
}; };
this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
lookupTableColumnSelection, () => this.setState({})
);
} }
componentDidMount(): void { componentDidMount(): void {
@ -205,7 +217,9 @@ export class LookupsView extends React.Component<LookupsViewProps, LookupsViewSt
} }
renderLookupsTable() { renderLookupsTable() {
const { lookups, loadingLookups, lookupsError} = this.state; const { lookups, loadingLookups, lookupsError } = this.state;
const { tableColumnSelectionHandler } = this;
if (lookupsError) { if (lookupsError) {
return <div className={"init-div"}> return <div className={"init-div"}>
<Button <Button
@ -226,25 +240,29 @@ export class LookupsView extends React.Component<LookupsViewProps, LookupsViewSt
Header: "Lookup Name", Header: "Lookup Name",
id: "lookup_name", id: "lookup_name",
accessor: (row: any) => row.id, accessor: (row: any) => row.id,
filterable: true filterable: true,
show: tableColumnSelectionHandler.showColumn("Lookup Name")
}, },
{ {
Header: "Tier", Header: "Tier",
id: "tier", id: "tier",
accessor: (row: any) => row.tier, accessor: (row: any) => row.tier,
filterable: true filterable: true,
show: tableColumnSelectionHandler.showColumn("Tier")
}, },
{ {
Header: "Type", Header: "Type",
id: "type", id: "type",
accessor: (row: any) => row.spec.type, accessor: (row: any) => row.spec.type,
filterable: true filterable: true,
show: tableColumnSelectionHandler.showColumn("Type")
}, },
{ {
Header: "Version", Header: "Version",
id: "version", id: "version",
accessor: (row: any) => row.version, accessor: (row: any) => row.version,
filterable: true filterable: true,
show: tableColumnSelectionHandler.showColumn("Version")
}, },
{ {
Header: "Config", Header: "Config",
@ -259,7 +277,8 @@ export class LookupsView extends React.Component<LookupsViewProps, LookupsViewSt
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
<a onClick={() => this.deleteLookup(lookupTier, lookupId)}>Delete</a> <a onClick={() => this.deleteLookup(lookupTier, lookupId)}>Delete</a>
</div>; </div>;
} },
show: tableColumnSelectionHandler.showColumn("Config")
} }
]} ]}
defaultPageSize={50} defaultPageSize={50}
@ -286,6 +305,7 @@ export class LookupsView extends React.Component<LookupsViewProps, LookupsViewSt
} }
render() { render() {
const { tableColumnSelectionHandler } = this;
return <div className="lookups-view app-view"> return <div className="lookups-view app-view">
<div className="control-bar"> <div className="control-bar">
<div className="control-label">Lookups</div> <div className="control-label">Lookups</div>
@ -300,6 +320,11 @@ export class LookupsView extends React.Component<LookupsViewProps, LookupsViewSt
style={{display: this.state.lookupsError !== null ? 'none' : 'inline'}} style={{display: this.state.lookupsError !== null ? 'none' : 'inline'}}
onClick={() => this.openLookupEditDialog("", "")} onClick={() => this.openLookupEditDialog("", "")}
/> />
<TableColumnSelection
columns={tableColumns}
onChange={(column) => tableColumnSelectionHandler.changeTableColumnSelection(column)}
tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
/>
</div> </div>
{this.renderLookupsTable()} {this.renderLookupsTable()}
{this.renderLookupEditDialog()} {this.renderLookupEditDialog()}

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button } from "@blueprintjs/core"; import { Button, Intent } from "@blueprintjs/core";
import axios from 'axios'; import axios from 'axios';
import * as classNames from 'classnames'; import * as classNames from 'classnames';
import * as React from 'react'; import * as React from 'react';
@ -24,6 +24,8 @@ import ReactTable from "react-table";
import { Filter } from "react-table"; import { Filter } from "react-table";
import { H5, IconNames } from "../components/filler"; import { H5, IconNames } from "../components/filler";
import { TableColumnSelection } from "../components/table-column-selection";
import { AppToaster } from "../singletons/toaster";
import { import {
addFilter, addFilter,
formatBytes, formatBytes,
@ -31,11 +33,15 @@ import {
makeBooleanFilter, makeBooleanFilter,
parseList, parseList,
queryDruidSql, queryDruidSql,
QueryManager QueryManager, TableColumnSelectionHandler
} from "../utils"; } from "../utils";
import "./segments-view.scss"; import "./segments-view.scss";
const segmentTableColumnSelection = "segment-table-column-selection";
const tableColumns: string[] = ["Segment ID", "Datasource", "Start", "End", "Version", "Partition",
"Size", "Num rows", "Replicas", "Is published", "Is realtime", "Is available"];
export interface SegmentsViewProps extends React.Props<any> { export interface SegmentsViewProps extends React.Props<any> {
goToSql: (initSql: string) => void; goToSql: (initSql: string) => void;
datasource: string | null; datasource: string | null;
@ -56,6 +62,7 @@ interface QueryAndSkip {
export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsViewState> { export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsViewState> {
private segmentsQueryManager: QueryManager<QueryAndSkip, any[]>; private segmentsQueryManager: QueryManager<QueryAndSkip, any[]>;
private tableColumnSelectionHandler: TableColumnSelectionHandler;
constructor(props: SegmentsViewProps, context: any) { constructor(props: SegmentsViewProps, context: any) {
super(props, context); super(props, context);
@ -91,6 +98,10 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
}); });
} }
}); });
this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
segmentTableColumnSelection, () => this.setState({})
);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -135,6 +146,7 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
renderSegmentsTable() { renderSegmentsTable() {
const { segments, segmentsLoading, segmentsError, segmentFilter } = this.state; const { segments, segmentsLoading, segmentsError, segmentFilter } = this.state;
const { tableColumnSelectionHandler } = this;
return <ReactTable return <ReactTable
data={segments || []} data={segments || []}
@ -155,7 +167,8 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
{ {
Header: "Segment ID", Header: "Segment ID",
accessor: "segment_id", accessor: "segment_id",
width: 300 width: 300,
show: tableColumnSelectionHandler.showColumn("Segment ID")
}, },
{ {
Header: "Datasource", Header: "Datasource",
@ -163,7 +176,8 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
return <a onClick={() => { this.setState({ segmentFilter: addFilter(segmentFilter, 'datasource', value) }); }}>{value}</a>; return <a onClick={() => { this.setState({ segmentFilter: addFilter(segmentFilter, 'datasource', value) }); }}>{value}</a>;
} },
show: tableColumnSelectionHandler.showColumn("Datasource")
}, },
{ {
Header: "Start", Header: "Start",
@ -173,7 +187,8 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
return <a onClick={() => { this.setState({ segmentFilter: addFilter(segmentFilter, 'start', value) }); }}>{value}</a>; return <a onClick={() => { this.setState({ segmentFilter: addFilter(segmentFilter, 'start', value) }); }}>{value}</a>;
} },
show: tableColumnSelectionHandler.showColumn("Start")
}, },
{ {
Header: "End", Header: "End",
@ -183,58 +198,67 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
return <a onClick={() => { this.setState({ segmentFilter: addFilter(segmentFilter, 'end', value) }); }}>{value}</a>; return <a onClick={() => { this.setState({ segmentFilter: addFilter(segmentFilter, 'end', value) }); }}>{value}</a>;
} },
show: tableColumnSelectionHandler.showColumn("End")
}, },
{ {
Header: "Version", Header: "Version",
accessor: "version", accessor: "version",
defaultSortDesc: true, defaultSortDesc: true,
width: 120 width: 120,
show: tableColumnSelectionHandler.showColumn("Version")
}, },
{ {
Header: "Partition", Header: "Partition",
accessor: "partition_num", accessor: "partition_num",
width: 60, width: 60,
filterable: false filterable: false,
show: tableColumnSelectionHandler.showColumn("Partition")
}, },
{ {
Header: "Size", Header: "Size",
accessor: "size", accessor: "size",
filterable: false, filterable: false,
defaultSortDesc: true, defaultSortDesc: true,
Cell: row => formatBytes(row.value) Cell: row => formatBytes(row.value),
show: tableColumnSelectionHandler.showColumn("Size")
}, },
{ {
Header: "Num rows", Header: "Num rows",
accessor: "num_rows", accessor: "num_rows",
filterable: false, filterable: false,
defaultSortDesc: true, defaultSortDesc: true,
Cell: row => formatNumber(row.value) Cell: row => formatNumber(row.value),
show: tableColumnSelectionHandler.showColumn("Num rows")
}, },
{ {
Header: "Replicas", Header: "Replicas",
accessor: "num_replicas", accessor: "num_replicas",
width: 60, width: 60,
filterable: false, filterable: false,
defaultSortDesc: true defaultSortDesc: true,
show: tableColumnSelectionHandler.showColumn("Replicas")
}, },
{ {
Header: "Is published", Header: "Is published",
id: "is_published", id: "is_published",
accessor: (row) => String(Boolean(row.is_published)), accessor: (row) => String(Boolean(row.is_published)),
Filter: makeBooleanFilter() Filter: makeBooleanFilter(),
show: tableColumnSelectionHandler.showColumn("Is published")
}, },
{ {
Header: "Is realtime", Header: "Is realtime",
id: "is_realtime", id: "is_realtime",
accessor: (row) => String(Boolean(row.is_realtime)), accessor: (row) => String(Boolean(row.is_realtime)),
Filter: makeBooleanFilter() Filter: makeBooleanFilter(),
show: tableColumnSelectionHandler.showColumn("Is realtime")
}, },
{ {
Header: "Is available", Header: "Is available",
id: "is_available", id: "is_available",
accessor: (row) => String(Boolean(row.is_available)), accessor: (row) => String(Boolean(row.is_available)),
Filter: makeBooleanFilter() Filter: makeBooleanFilter(),
show: tableColumnSelectionHandler.showColumn("Is available")
} }
]} ]}
defaultPageSize={50} defaultPageSize={50}
@ -258,6 +282,7 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
render() { render() {
const { goToSql } = this.props; const { goToSql } = this.props;
const { tableColumnSelectionHandler } = this;
return <div className="segments-view app-view"> return <div className="segments-view app-view">
<div className="control-bar"> <div className="control-bar">
@ -272,6 +297,11 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
text="Go to SQL" text="Go to SQL"
onClick={() => goToSql(this.segmentsQueryManager.getLastQuery().query)} onClick={() => goToSql(this.segmentsQueryManager.getLastQuery().query)}
/> />
<TableColumnSelection
columns={tableColumns}
onChange={(column) => tableColumnSelectionHandler.changeTableColumnSelection(column)}
tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
/>
</div> </div>
{this.renderSegmentsTable()} {this.renderSegmentsTable()}
</div>; </div>;

View File

@ -25,10 +25,22 @@ import ReactTable from "react-table";
import { Filter } from "react-table"; import { Filter } from "react-table";
import { IconNames } from '../components/filler'; import { IconNames } from '../components/filler';
import { addFilter, formatBytes, formatBytesCompact, queryDruidSql, QueryManager } from "../utils"; import { TableColumnSelection } from "../components/table-column-selection";
import {
addFilter,
formatBytes,
formatBytesCompact,
queryDruidSql,
QueryManager, TableColumnSelectionHandler
} from "../utils";
import "./servers-view.scss"; import "./servers-view.scss";
const serverTableColumnSelection = "historical-table-column-selection";
const middleManagerTableColumnSelection = "middleManager-table-column-selection";
const serverTableColumns: string[] = ["Server", "Tier", "Curr size", "Max size", "Usage", "Load/drop queues", "Host", "Port"];
const middleManagerTableColumns: string[] = ["Host", "Usage", "Availability groups", "Last completed task time", "Blacklisted until"];
function formatQueues(segmentsToLoad: number, segmentsToLoadSize: number, segmentsToDrop: number, segmentsToDropSize: number): string { function formatQueues(segmentsToLoad: number, segmentsToLoadSize: number, segmentsToDrop: number, segmentsToDropSize: number): string {
const queueParts: string[] = []; const queueParts: string[] = [];
if (segmentsToLoad) { if (segmentsToLoad) {
@ -62,6 +74,8 @@ export interface ServersViewState {
export class ServersView extends React.Component<ServersViewProps, ServersViewState> { export class ServersView extends React.Component<ServersViewProps, ServersViewState> {
private serverQueryManager: QueryManager<string, any[]>; private serverQueryManager: QueryManager<string, any[]>;
private middleManagerQueryManager: QueryManager<string, any[]>; private middleManagerQueryManager: QueryManager<string, any[]>;
private serverTableColumnSelectionHandler: TableColumnSelectionHandler;
private middleManagerTableColumnSelectionHandler: TableColumnSelectionHandler;
constructor(props: ServersViewProps, context: any) { constructor(props: ServersViewProps, context: any) {
super(props, context); super(props, context);
@ -77,6 +91,14 @@ export class ServersView extends React.Component<ServersViewProps, ServersViewSt
middleManagersError: null, middleManagersError: null,
middleManagerFilter: props.middleManager ? [{ id: 'host', value: props.middleManager }] : [] middleManagerFilter: props.middleManager ? [{ id: 'host', value: props.middleManager }] : []
}; };
this.serverTableColumnSelectionHandler = new TableColumnSelectionHandler(
serverTableColumnSelection, () => this.setState({})
);
this.middleManagerTableColumnSelectionHandler = new TableColumnSelectionHandler(
middleManagerTableColumnSelection, () => this.setState({})
);
} }
componentDidMount(): void { componentDidMount(): void {
@ -124,6 +146,7 @@ WHERE "server_type" = 'historical'`);
}); });
this.middleManagerQueryManager.runQuery('dummy'); this.middleManagerQueryManager.runQuery('dummy');
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -133,6 +156,7 @@ WHERE "server_type" = 'historical'`);
renderServersTable() { renderServersTable() {
const { servers, serversLoading, serversError, serverFilter, groupByTier } = this.state; const { servers, serversLoading, serversError, serverFilter, groupByTier } = this.state;
const { serverTableColumnSelectionHandler } = this;
const fillIndicator = (value: number) => { const fillIndicator = (value: number) => {
return <div className="fill-indicator"> return <div className="fill-indicator">
@ -156,7 +180,8 @@ WHERE "server_type" = 'historical'`);
Header: "Server", Header: "Server",
accessor: "server", accessor: "server",
width: 300, width: 300,
Aggregated: row => '' Aggregated: row => '',
show: serverTableColumnSelectionHandler.showColumn("Server")
}, },
{ {
Header: "Tier", Header: "Tier",
@ -164,7 +189,8 @@ WHERE "server_type" = 'historical'`);
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
return <a onClick={() => { this.setState({ serverFilter: addFilter(serverFilter, 'tier', value) }); }}>{value}</a>; return <a onClick={() => { this.setState({ serverFilter: addFilter(serverFilter, 'tier', value) }); }}>{value}</a>;
} },
show: serverTableColumnSelectionHandler.showColumn("Tier")
}, },
{ {
Header: "Curr size", Header: "Curr size",
@ -181,7 +207,8 @@ WHERE "server_type" = 'historical'`);
if (row.aggregated) return ''; if (row.aggregated) return '';
if (row.value === null) return ''; if (row.value === null) return '';
return formatBytes(row.value); return formatBytes(row.value);
} },
show: serverTableColumnSelectionHandler.showColumn("Curr size")
}, },
{ {
Header: "Max size", Header: "Max size",
@ -198,7 +225,8 @@ WHERE "server_type" = 'historical'`);
if (row.aggregated) return ''; if (row.aggregated) return '';
if (row.value === null) return ''; if (row.value === null) return '';
return formatBytes(row.value); return formatBytes(row.value);
} },
show: serverTableColumnSelectionHandler.showColumn("Max size")
}, },
{ {
Header: "Usage", Header: "Usage",
@ -216,7 +244,8 @@ WHERE "server_type" = 'historical'`);
if (row.aggregated) return ''; if (row.aggregated) return '';
if (row.value === null) return ''; if (row.value === null) return '';
return fillIndicator(row.value); return fillIndicator(row.value);
} },
show: serverTableColumnSelectionHandler.showColumn("Usage")
}, },
{ {
Header: "Load/drop queues", Header: "Load/drop queues",
@ -236,12 +265,14 @@ WHERE "server_type" = 'historical'`);
const segmentsToDrop = sum(originals, s => s.segmentsToDrop); const segmentsToDrop = sum(originals, s => s.segmentsToDrop);
const segmentsToDropSize = sum(originals, s => s.segmentsToDropSize); const segmentsToDropSize = sum(originals, s => s.segmentsToDropSize);
return formatQueues(segmentsToLoad, segmentsToLoadSize, segmentsToDrop, segmentsToDropSize); return formatQueues(segmentsToLoad, segmentsToLoadSize, segmentsToDrop, segmentsToDropSize);
} },
show: serverTableColumnSelectionHandler.showColumn("Load/drop queues")
}, },
{ {
Header: "Host", Header: "Host",
accessor: "host", accessor: "host",
Aggregated: () => '' Aggregated: () => '',
show: serverTableColumnSelectionHandler.showColumn("Host")
}, },
{ {
Header: "Port", Header: "Port",
@ -256,7 +287,8 @@ WHERE "server_type" = 'historical'`);
} }
return ports.join(', ') || 'No port'; return ports.join(', ') || 'No port';
}, },
Aggregated: () => '' Aggregated: () => '',
show: serverTableColumnSelectionHandler.showColumn("Port")
} }
]} ]}
defaultPageSize={10} defaultPageSize={10}
@ -267,6 +299,7 @@ WHERE "server_type" = 'historical'`);
renderMiddleManagerTable() { renderMiddleManagerTable() {
const { goToTask } = this.props; const { goToTask } = this.props;
const { middleManagers, middleManagersLoading, middleManagersError, middleManagerFilter } = this.state; const { middleManagers, middleManagersLoading, middleManagersError, middleManagerFilter } = this.state;
const { middleManagerTableColumnSelectionHandler } = this;
return <ReactTable return <ReactTable
data={middleManagers || []} data={middleManagers || []}
@ -285,29 +318,34 @@ WHERE "server_type" = 'historical'`);
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
return <a onClick={() => { this.setState({ middleManagerFilter: addFilter(middleManagerFilter, 'host', value) }); }}>{value}</a>; return <a onClick={() => { this.setState({ middleManagerFilter: addFilter(middleManagerFilter, 'host', value) }); }}>{value}</a>;
} },
show: middleManagerTableColumnSelectionHandler.showColumn("Host")
}, },
{ {
Header: "Usage", Header: "Usage",
id: "usage", id: "usage",
width: 60, width: 60,
accessor: (row) => `${row.currCapacityUsed} / ${row.worker.capacity}`, accessor: (row) => `${row.currCapacityUsed} / ${row.worker.capacity}`,
filterable: false filterable: false,
show: middleManagerTableColumnSelectionHandler.showColumn("Usage")
}, },
{ {
Header: "Availability groups", Header: "Availability groups",
id: "availabilityGroups", id: "availabilityGroups",
width: 60, width: 60,
accessor: (row) => row.availabilityGroups.length, accessor: (row) => row.availabilityGroups.length,
filterable: false filterable: false,
show: middleManagerTableColumnSelectionHandler.showColumn("Availability groups")
}, },
{ {
Header: "Last completed task time", Header: "Last completed task time",
accessor: "lastCompletedTaskTime" accessor: "lastCompletedTaskTime",
show: middleManagerTableColumnSelectionHandler.showColumn("Last completed task time")
}, },
{ {
Header: "Blacklisted until", Header: "Blacklisted until",
accessor: "blacklistedUntil" accessor: "blacklistedUntil",
show: middleManagerTableColumnSelectionHandler.showColumn("Blacklisted until")
} }
]} ]}
defaultPageSize={10} defaultPageSize={10}
@ -331,6 +369,7 @@ WHERE "server_type" = 'historical'`);
render() { render() {
const { goToSql } = this.props; const { goToSql } = this.props;
const { groupByTier } = this.state; const { groupByTier } = this.state;
const { serverTableColumnSelectionHandler, middleManagerTableColumnSelectionHandler } = this;
return <div className="servers-view app-view"> return <div className="servers-view app-view">
<div className="control-bar"> <div className="control-bar">
@ -350,6 +389,11 @@ WHERE "server_type" = 'historical'`);
label="Group by tier" label="Group by tier"
onChange={() => this.setState({ groupByTier: !groupByTier })} onChange={() => this.setState({ groupByTier: !groupByTier })}
/> />
<TableColumnSelection
columns={serverTableColumns}
onChange={(column) => serverTableColumnSelectionHandler.changeTableColumnSelection(column)}
tableColumnsHidden={serverTableColumnSelectionHandler.hiddenColumns}
/>
</div> </div>
{this.renderServersTable()} {this.renderServersTable()}
@ -362,6 +406,11 @@ WHERE "server_type" = 'historical'`);
text="Refresh" text="Refresh"
onClick={() => this.middleManagerQueryManager.rerunLastQuery()} onClick={() => this.middleManagerQueryManager.rerunLastQuery()}
/> />
<TableColumnSelection
columns={middleManagerTableColumns}
onChange={(column) => middleManagerTableColumnSelectionHandler.changeTableColumnSelection(column)}
tableColumnsHidden={middleManagerTableColumnSelectionHandler.hiddenColumns}
/>
</div> </div>
{this.renderMiddleManagerTable()} {this.renderMiddleManagerTable()}
</div>; </div>;

View File

@ -24,13 +24,26 @@ import ReactTable from "react-table";
import { Filter } from "react-table"; import { Filter } from "react-table";
import { ButtonGroup, IconNames, Label } from "../components/filler"; import { ButtonGroup, IconNames, Label } from "../components/filler";
import { TableColumnSelection } from "../components/table-column-selection";
import { AsyncActionDialog } from "../dialogs/async-action-dialog"; import { AsyncActionDialog } from "../dialogs/async-action-dialog";
import { SpecDialog } from "../dialogs/spec-dialog"; import { SpecDialog } from "../dialogs/spec-dialog";
import { AppToaster } from '../singletons/toaster'; import { AppToaster } from '../singletons/toaster';
import { addFilter, countBy, formatDuration, getDruidErrorMessage, queryDruidSql, QueryManager } from "../utils"; import {
addFilter,
countBy,
formatDuration,
getDruidErrorMessage,
queryDruidSql,
QueryManager, TableColumnSelectionHandler
} from "../utils";
import "./tasks-view.scss"; import "./tasks-view.scss";
const supervisorTableColumnSelection = "supervisor-table-column-selection";
const taskTableColumnSelection = "task-table-column-selection";
const supervisorTableColumns: string[] = ["Datasource", "Type", "Topic/Stream", "Status", "Actions"];
const taskTableColumns: string[] = ["Task ID", "Type", "Datasource", "Created time", "Status", "Duration", "Actions"];
export interface TasksViewProps extends React.Props<any> { export interface TasksViewProps extends React.Props<any> {
taskId: string | null; taskId: string | null;
goToSql: (initSql: string) => void; goToSql: (initSql: string) => void;
@ -74,6 +87,8 @@ function statusToColor(status: string): string {
export class TasksView extends React.Component<TasksViewProps, TasksViewState> { export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
private supervisorQueryManager: QueryManager<string, any[]>; private supervisorQueryManager: QueryManager<string, any[]>;
private taskQueryManager: QueryManager<string, any[]>; private taskQueryManager: QueryManager<string, any[]>;
private supervisorTableColumnSelectionHandler: TableColumnSelectionHandler;
private taskTableColumnSelectionHandler: TableColumnSelectionHandler;
constructor(props: TasksViewProps, context: any) { constructor(props: TasksViewProps, context: any) {
super(props, context); super(props, context);
@ -98,7 +113,16 @@ export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
supervisorSpecDialogOpen: false, supervisorSpecDialogOpen: false,
taskSpecDialogOpen: false, taskSpecDialogOpen: false,
alertErrorMsg: null alertErrorMsg: null
}; };
this.supervisorTableColumnSelectionHandler = new TableColumnSelectionHandler(
supervisorTableColumnSelection, () => this.setState({})
);
this.taskTableColumnSelectionHandler = new TableColumnSelectionHandler(
taskTableColumnSelection, () => this.setState({})
);
} }
componentDidMount(): void { componentDidMount(): void {
@ -147,6 +171,7 @@ export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
"location", "duration", "error_msg" "location", "duration", "error_msg"
FROM sys.tasks FROM sys.tasks
ORDER BY "rank" DESC, "created_time" DESC`); ORDER BY "rank" DESC, "created_time" DESC`);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -295,6 +320,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
renderSupervisorTable() { renderSupervisorTable() {
const { supervisors, supervisorsLoading, supervisorsError } = this.state; const { supervisors, supervisorsLoading, supervisorsError } = this.state;
const { supervisorTableColumnSelectionHandler } = this;
return <> return <>
<ReactTable <ReactTable
@ -307,7 +333,8 @@ ORDER BY "rank" DESC, "created_time" DESC`);
Header: "Datasource", Header: "Datasource",
id: 'datasource', id: 'datasource',
accessor: "id", accessor: "id",
width: 300 width: 300,
show: supervisorTableColumnSelectionHandler.showColumn("Datasource")
}, },
{ {
Header: 'Type', Header: 'Type',
@ -318,7 +345,8 @@ ORDER BY "rank" DESC, "created_time" DESC`);
const { tuningConfig } = spec; const { tuningConfig } = spec;
if (!tuningConfig) return ''; if (!tuningConfig) return '';
return tuningConfig.type; return tuningConfig.type;
} },
show: supervisorTableColumnSelectionHandler.showColumn("Type")
}, },
{ {
Header: 'Topic/Stream', Header: 'Topic/Stream',
@ -329,7 +357,8 @@ ORDER BY "rank" DESC, "created_time" DESC`);
const { ioConfig } = spec; const { ioConfig } = spec;
if (!ioConfig) return ''; if (!ioConfig) return '';
return ioConfig.topic || ioConfig.stream || ''; return ioConfig.topic || ioConfig.stream || '';
} },
show: supervisorTableColumnSelectionHandler.showColumn("Topic/Stream")
}, },
{ {
Header: "Status", Header: "Status",
@ -345,7 +374,8 @@ ORDER BY "rank" DESC, "created_time" DESC`);
</span> </span>
{value} {value}
</span>; </span>;
} },
show: supervisorTableColumnSelectionHandler.showColumn("Status")
}, },
{ {
Header: 'Actions', Header: 'Actions',
@ -368,7 +398,8 @@ ORDER BY "rank" DESC, "created_time" DESC`);
<a onClick={() => this.setState({ resetSupervisorId: id })}>Reset</a>&nbsp;&nbsp;&nbsp; <a onClick={() => this.setState({ resetSupervisorId: id })}>Reset</a>&nbsp;&nbsp;&nbsp;
<a onClick={() => this.setState({ terminateSupervisorId: id })}>Terminate</a> <a onClick={() => this.setState({ terminateSupervisorId: id })}>Terminate</a>
</div>; </div>;
} },
show: supervisorTableColumnSelectionHandler.showColumn("Actions")
} }
]} ]}
defaultPageSize={10} defaultPageSize={10}
@ -411,6 +442,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
renderTaskTable() { renderTaskTable() {
const { goToMiddleManager } = this.props; const { goToMiddleManager } = this.props;
const { tasks, tasksLoading, tasksError, taskFilter, groupTasksBy } = this.state; const { tasks, tasksLoading, tasksError, taskFilter, groupTasksBy } = this.state;
const { taskTableColumnSelectionHandler } = this;
return <> return <>
<ReactTable <ReactTable
@ -429,7 +461,8 @@ ORDER BY "rank" DESC, "created_time" DESC`);
Header: "Task ID", Header: "Task ID",
accessor: "task_id", accessor: "task_id",
width: 300, width: 300,
Aggregated: row => '' Aggregated: row => '',
show: taskTableColumnSelectionHandler.showColumn("Task ID")
}, },
{ {
Header: "Type", Header: "Type",
@ -437,7 +470,8 @@ ORDER BY "rank" DESC, "created_time" DESC`);
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
return <a onClick={() => { this.setState({ taskFilter: addFilter(taskFilter, 'type', value) }); }}>{value}</a>; return <a onClick={() => { this.setState({ taskFilter: addFilter(taskFilter, 'type', value) }); }}>{value}</a>;
} },
show: taskTableColumnSelectionHandler.showColumn("Type")
}, },
{ {
Header: "Datasource", Header: "Datasource",
@ -445,13 +479,15 @@ ORDER BY "rank" DESC, "created_time" DESC`);
Cell: row => { Cell: row => {
const value = row.value; const value = row.value;
return <a onClick={() => { this.setState({ taskFilter: addFilter(taskFilter, 'datasource', value) }); }}>{value}</a>; return <a onClick={() => { this.setState({ taskFilter: addFilter(taskFilter, 'datasource', value) }); }}>{value}</a>;
} },
show: taskTableColumnSelectionHandler.showColumn("Datasource")
}, },
{ {
Header: "Created time", Header: "Created time",
accessor: "created_time", accessor: "created_time",
width: 120, width: 120,
Aggregated: row => '' Aggregated: row => '',
show: taskTableColumnSelectionHandler.showColumn("Created time")
}, },
{ {
Header: "Status", Header: "Status",
@ -484,14 +520,16 @@ ORDER BY "rank" DESC, "created_time" DESC`);
const previewValues = subRows.filter((d: any) => typeof d[column.id] !== 'undefined').map((row: any) => row._original[column.id]); const previewValues = subRows.filter((d: any) => typeof d[column.id] !== 'undefined').map((row: any) => row._original[column.id]);
const previewCount = countBy(previewValues); const previewCount = countBy(previewValues);
return <span>{Object.keys(previewCount).sort().map(v => `${v} (${previewCount[v]})`).join(', ')}</span>; return <span>{Object.keys(previewCount).sort().map(v => `${v} (${previewCount[v]})`).join(', ')}</span>;
} },
show: taskTableColumnSelectionHandler.showColumn("Status")
}, },
{ {
Header: "Duration", Header: "Duration",
accessor: "duration", accessor: "duration",
filterable: false, filterable: false,
Cell: (row) => row.value > 0 ? formatDuration(row.value) : '', Cell: (row) => row.value > 0 ? formatDuration(row.value) : '',
Aggregated: () => '' Aggregated: () => '',
show: taskTableColumnSelectionHandler.showColumn("Duration")
}, },
{ {
Header: 'Actions', Header: 'Actions',
@ -512,7 +550,8 @@ ORDER BY "rank" DESC, "created_time" DESC`);
{(status === 'RUNNING' || status === 'WAITING' || status === 'PENDING') && <a onClick={() => this.setState({ killTaskId: id })}>Kill</a>} {(status === 'RUNNING' || status === 'WAITING' || status === 'PENDING') && <a onClick={() => this.setState({ killTaskId: id })}>Kill</a>}
</div>; </div>;
}, },
Aggregated: row => '' Aggregated: row => '',
show: taskTableColumnSelectionHandler.showColumn("Actions")
} }
]} ]}
defaultPageSize={20} defaultPageSize={20}
@ -525,6 +564,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
render() { render() {
const { goToSql } = this.props; const { goToSql } = this.props;
const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg } = this.state; const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg } = this.state;
const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
return <div className="tasks-view app-view"> return <div className="tasks-view app-view">
<div className="control-bar"> <div className="control-bar">
@ -539,6 +579,11 @@ ORDER BY "rank" DESC, "created_time" DESC`);
text="Submit supervisor" text="Submit supervisor"
onClick={() => this.setState({ supervisorSpecDialogOpen: true })} onClick={() => this.setState({ supervisorSpecDialogOpen: true })}
/> />
<TableColumnSelection
columns={supervisorTableColumns}
onChange={(column) => supervisorTableColumnSelectionHandler.changeTableColumnSelection(column)}
tableColumnsHidden={supervisorTableColumnSelectionHandler.hiddenColumns}
/>
</div> </div>
{this.renderSupervisorTable()} {this.renderSupervisorTable()}
@ -568,6 +613,11 @@ ORDER BY "rank" DESC, "created_time" DESC`);
text="Submit task" text="Submit task"
onClick={() => this.setState({ taskSpecDialogOpen: true })} onClick={() => this.setState({ taskSpecDialogOpen: true })}
/> />
<TableColumnSelection
columns={taskTableColumns}
onChange={(column) => taskTableColumnSelectionHandler.changeTableColumnSelection(column)}
tableColumnsHidden={taskTableColumnSelectionHandler.hiddenColumns}
/>
</div> </div>
{this.renderTaskTable()} {this.renderTaskTable()}
{ supervisorSpecDialogOpen ? <SpecDialog { supervisorSpecDialogOpen ? <SpecDialog