Web console: use SQL for the supervisor view (#8796)

* use SQL for supervisor view

* home view sql also

* no proxy mode

* fix alert

* improve message
This commit is contained in:
Vadim Ogievetsky 2019-10-31 20:59:36 -07:00 committed by Clint Wylie
parent 27acdbd2b8
commit f6028de7a8
27 changed files with 488 additions and 384 deletions

View File

@ -15,6 +15,7 @@ exports[`header bar matches snapshot 1`] = `
<Blueprint3.NavbarDivider /> <Blueprint3.NavbarDivider />
<Blueprint3.AnchorButton <Blueprint3.AnchorButton
active={true} active={true}
disabled={false}
href="#load-data" href="#load-data"
icon="cloud-upload" icon="cloud-upload"
intent="none" intent="none"
@ -84,6 +85,7 @@ exports[`header bar matches snapshot 1`] = `
wrapperTagName="span" wrapperTagName="span"
> >
<Blueprint3.Button <Blueprint3.Button
disabled={false}
icon="share" icon="share"
minimal={true} minimal={true}
text="Legacy" text="Legacy"
@ -151,6 +153,7 @@ exports[`header bar matches snapshot 1`] = `
wrapperTagName="span" wrapperTagName="span"
> >
<Blueprint3.Button <Blueprint3.Button
disabled={false}
icon="cog" icon="cog"
minimal={true} minimal={true}
/> />

View File

@ -23,7 +23,9 @@ import { HeaderBar } from './header-bar';
describe('header bar', () => { describe('header bar', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const headerBar = shallow(<HeaderBar active={'load-data'} hideLegacy={false} />); const headerBar = shallow(
<HeaderBar active={'load-data'} hideLegacy={false} capabilities="full" />,
);
expect(headerBar).toMatchSnapshot(); expect(headerBar).toMatchSnapshot();
}); });
}); });

View File

@ -36,6 +36,7 @@ import { AboutDialog } from '../../dialogs/about-dialog/about-dialog';
import { CoordinatorDynamicConfigDialog } from '../../dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog'; import { CoordinatorDynamicConfigDialog } from '../../dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog';
import { DoctorDialog } from '../../dialogs/doctor-dialog/doctor-dialog'; import { DoctorDialog } from '../../dialogs/doctor-dialog/doctor-dialog';
import { OverlordDynamicConfigDialog } from '../../dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog'; import { OverlordDynamicConfigDialog } from '../../dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
import { Capabilities } from '../../utils/capabilities';
import { import {
DRUID_ASF_SLACK, DRUID_ASF_SLACK,
DRUID_DOCS, DRUID_DOCS,
@ -130,10 +131,11 @@ function LegacyMenu() {
export interface HeaderBarProps { export interface HeaderBarProps {
active: HeaderActiveTab; active: HeaderActiveTab;
hideLegacy: boolean; hideLegacy: boolean;
capabilities: Capabilities;
} }
export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) { export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
const { active, hideLegacy } = props; const { active, hideLegacy, capabilities } = props;
const [aboutDialogOpen, setAboutDialogOpen] = useState(false); const [aboutDialogOpen, setAboutDialogOpen] = useState(false);
const [doctorDialogOpen, setDoctorDialogOpen] = useState(false); const [doctorDialogOpen, setDoctorDialogOpen] = useState(false);
const [coordinatorDynamicConfigDialogOpen, setCoordinatorDynamicConfigDialogOpen] = useState( const [coordinatorDynamicConfigDialogOpen, setCoordinatorDynamicConfigDialogOpen] = useState(
@ -198,6 +200,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
href="#load-data" href="#load-data"
minimal={!loadDataPrimary} minimal={!loadDataPrimary}
intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE} intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE}
disabled={capabilities === 'no-proxy'}
/> />
<NavbarDivider /> <NavbarDivider />
@ -241,12 +244,25 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
</NavbarGroup> </NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}> <NavbarGroup align={Alignment.RIGHT}>
{!hideLegacy && ( {!hideLegacy && (
<Popover content={<LegacyMenu />} position={Position.BOTTOM_RIGHT}> <Popover
<Button minimal icon={IconNames.SHARE} text="Legacy" /> content={<LegacyMenu />}
position={Position.BOTTOM_RIGHT}
disabled={capabilities === 'no-proxy'}
>
<Button
minimal
icon={IconNames.SHARE}
text="Legacy"
disabled={capabilities === 'no-proxy'}
/>
</Popover> </Popover>
)} )}
<Popover content={configMenu} position={Position.BOTTOM_RIGHT}> <Popover
<Button minimal icon={IconNames.COG} /> content={configMenu}
position={Position.BOTTOM_RIGHT}
disabled={capabilities === 'no-proxy'}
>
<Button minimal icon={IconNames.COG} disabled={capabilities === 'no-proxy'} />
</Popover> </Popover>
<Popover content={helpMenu} position={Position.BOTTOM_RIGHT}> <Popover content={helpMenu} position={Position.BOTTOM_RIGHT}>
<Button minimal icon={IconNames.HELP} /> <Button minimal icon={IconNames.HELP} />

View File

@ -26,7 +26,8 @@ import { HashRouter, Route, Switch } from 'react-router-dom';
import { ExternalLink, HeaderActiveTab, HeaderBar, Loader } from './components'; import { ExternalLink, HeaderActiveTab, HeaderBar, Loader } from './components';
import { AppToaster } from './singletons/toaster'; import { AppToaster } from './singletons/toaster';
import { localStorageGet, LocalStorageKeys, QueryManager } from './utils'; import { localStorageGet, LocalStorageKeys, QueryManager } from './utils';
import { DRUID_DOCS_API, DRUID_DOCS_SQL } from './variables'; import { Capabilities } from './utils/capabilities';
import { DRUID_DOCS_API, DRUID_DOCS_SQL, DRUID_DOCS_VERSION } from './variables';
import { import {
DatasourcesView, DatasourcesView,
HomeView, HomeView,
@ -40,15 +41,13 @@ import {
import './console-application.scss'; import './console-application.scss';
type Capabilities = 'working-with-sql' | 'working-without-sql' | 'broken';
export interface ConsoleApplicationProps { export interface ConsoleApplicationProps {
hideLegacy: boolean; hideLegacy: boolean;
exampleManifestsUrl?: string; exampleManifestsUrl?: string;
} }
export interface ConsoleApplicationState { export interface ConsoleApplicationState {
noSqlMode: boolean; capabilities: Capabilities;
capabilitiesLoading: boolean; capabilitiesLoading: boolean;
} }
@ -64,6 +63,7 @@ export class ConsoleApplication extends React.PureComponent<
const capabilitiesOverride = localStorageGet(LocalStorageKeys.CAPABILITIES_OVERRIDE); const capabilitiesOverride = localStorageGet(LocalStorageKeys.CAPABILITIES_OVERRIDE);
if (capabilitiesOverride) return capabilitiesOverride as Capabilities; if (capabilitiesOverride) return capabilitiesOverride as Capabilities;
// Check SQL endpoint
try { try {
await axios.post( await axios.post(
'/druid/v2/sql', '/druid/v2/sql',
@ -73,7 +73,7 @@ export class ConsoleApplication extends React.PureComponent<
} catch (e) { } catch (e) {
const { response } = e; const { response } = e;
if (response.status !== 405 || response.statusText !== 'Method Not Allowed') { if (response.status !== 405 || response.statusText !== 'Method Not Allowed') {
return 'working-with-sql'; // other failure return 'full'; // other failure
} }
try { try {
await axios.get('/status', { timeout: ConsoleApplication.STATUS_TIMEOUT }); await axios.get('/status', { timeout: ConsoleApplication.STATUS_TIMEOUT });
@ -81,27 +81,65 @@ export class ConsoleApplication extends React.PureComponent<
return 'broken'; // total failure return 'broken'; // total failure
} }
// Status works but SQL 405s => the SQL endpoint is disabled // Status works but SQL 405s => the SQL endpoint is disabled
return 'working-without-sql'; return 'no-sql';
}
return 'working-with-sql';
} }
static shownNotifications(capabilities: string) { // Check proxy
let message: JSX.Element = <></>; try {
await axios.get('/proxy/coordinator/status', { timeout: ConsoleApplication.STATUS_TIMEOUT });
} catch (e) {
const { response } = e;
if (response.status !== 404) {
console.log('response.statusText', response.statusText);
return 'full'; // other failure
}
return 'no-proxy';
}
if (capabilities === 'working-without-sql') { return 'full';
}
static shownNotifications(capabilities: Capabilities) {
let message: JSX.Element;
switch (capabilities) {
case 'no-sql':
message = ( message = (
<> <>
It appears that the SQL endpoint is disabled. The console will fall back to{' '} It appears that the SQL endpoint is disabled. The console will fall back to{' '}
<ExternalLink href={DRUID_DOCS_API}>native Druid APIs</ExternalLink> and will be limited <ExternalLink href={DRUID_DOCS_API}>native Druid APIs</ExternalLink> and will be limited
in functionality. Look at <ExternalLink href={DRUID_DOCS_SQL}>the SQL docs</ExternalLink>{' '} in functionality. Look at{' '}
to enable the SQL endpoint. <ExternalLink href={DRUID_DOCS_SQL}>the SQL docs</ExternalLink> to enable the SQL
endpoint.
</> </>
); );
} else if (capabilities === 'broken') { break;
case 'no-proxy':
message = ( message = (
<>It appears that the Druid is not responding. Data cannot be retrieved right now</> <>
It appears that the management proxy is not enabled, the console will operate with
limited functionality. Look at{' '}
<ExternalLink
href={`https://druid.apache.org/docs/${DRUID_DOCS_VERSION}/operations/management-uis.html#druid-console`}
>
the console docs
</ExternalLink>{' '}
for more info on how to enable the management proxy.
</>
); );
break;
case 'broken':
message = (
<>
It appears that the the Router node is not responding. The console will not function at
the moment
</>
);
break;
default:
return;
} }
AppToaster.show({ AppToaster.show({
@ -123,21 +161,21 @@ export class ConsoleApplication extends React.PureComponent<
constructor(props: ConsoleApplicationProps, context: any) { constructor(props: ConsoleApplicationProps, context: any) {
super(props, context); super(props, context);
this.state = { this.state = {
noSqlMode: false, capabilities: 'full',
capabilitiesLoading: true, capabilitiesLoading: true,
}; };
this.capabilitiesQueryManager = new QueryManager({ this.capabilitiesQueryManager = new QueryManager({
processQuery: async () => { processQuery: async () => {
const capabilities = await ConsoleApplication.discoverCapabilities(); const capabilities = await ConsoleApplication.discoverCapabilities();
if (capabilities !== 'working-with-sql') { if (capabilities !== 'full') {
ConsoleApplication.shownNotifications(capabilities); ConsoleApplication.shownNotifications(capabilities);
} }
return capabilities; return capabilities;
}, },
onStateChange: ({ result, loading }) => { onStateChange: ({ result, loading }) => {
this.setState({ this.setState({
noSqlMode: result !== 'working-with-sql', capabilities: result || 'full',
capabilitiesLoading: loading, capabilitiesLoading: loading,
}); });
}, },
@ -216,18 +254,19 @@ export class ConsoleApplication extends React.PureComponent<
classType: 'normal' | 'narrow-pad' = 'normal', classType: 'normal' | 'narrow-pad' = 'normal',
) => { ) => {
const { hideLegacy } = this.props; const { hideLegacy } = this.props;
const { capabilities } = this.state;
return ( return (
<> <>
<HeaderBar active={active} hideLegacy={hideLegacy} /> <HeaderBar active={active} hideLegacy={hideLegacy} capabilities={capabilities} />
<div className={classNames('view-container', classType)}>{el}</div> <div className={classNames('view-container', classType)}>{el}</div>
</> </>
); );
}; };
private wrappedHomeView = () => { private wrappedHomeView = () => {
const { noSqlMode } = this.state; const { capabilities } = this.state;
return this.wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode} />); return this.wrapInViewContainer(null, <HomeView capabilities={capabilities} />);
}; };
private wrappedLoadDataView = () => { private wrappedLoadDataView = () => {
@ -250,7 +289,7 @@ export class ConsoleApplication extends React.PureComponent<
}; };
private wrappedDatasourcesView = () => { private wrappedDatasourcesView = () => {
const { noSqlMode } = this.state; const { capabilities } = this.state;
return this.wrapInViewContainer( return this.wrapInViewContainer(
'datasources', 'datasources',
<DatasourcesView <DatasourcesView
@ -258,26 +297,26 @@ export class ConsoleApplication extends React.PureComponent<
goToQuery={this.goToQuery} goToQuery={this.goToQuery}
goToTask={this.goToTaskWithDatasource} goToTask={this.goToTaskWithDatasource}
goToSegments={this.goToSegments} goToSegments={this.goToSegments}
noSqlMode={noSqlMode} capabilities={capabilities}
/>, />,
); );
}; };
private wrappedSegmentsView = () => { private wrappedSegmentsView = () => {
const { noSqlMode } = this.state; const { capabilities } = this.state;
return this.wrapInViewContainer( return this.wrapInViewContainer(
'segments', 'segments',
<SegmentsView <SegmentsView
datasource={this.datasource} datasource={this.datasource}
onlyUnavailable={this.onlyUnavailable} onlyUnavailable={this.onlyUnavailable}
goToQuery={this.goToQuery} goToQuery={this.goToQuery}
noSqlMode={noSqlMode} capabilities={capabilities}
/>, />,
); );
}; };
private wrappedTasksView = () => { private wrappedTasksView = () => {
const { noSqlMode } = this.state; const { capabilities } = this.state;
return this.wrapInViewContainer( return this.wrapInViewContainer(
'tasks', 'tasks',
<TasksView <TasksView
@ -288,20 +327,20 @@ export class ConsoleApplication extends React.PureComponent<
goToQuery={this.goToQuery} goToQuery={this.goToQuery}
goToMiddleManager={this.goToMiddleManager} goToMiddleManager={this.goToMiddleManager}
goToLoadData={this.goToLoadData} goToLoadData={this.goToLoadData}
noSqlMode={noSqlMode} capabilities={capabilities}
/>, />,
); );
}; };
private wrappedServersView = () => { private wrappedServersView = () => {
const { noSqlMode } = this.state; const { capabilities } = this.state;
return this.wrapInViewContainer( return this.wrapInViewContainer(
'servers', 'servers',
<ServersView <ServersView
middleManager={this.middleManager} middleManager={this.middleManager}
goToQuery={this.goToQuery} goToQuery={this.goToQuery}
goToTask={this.goToTaskWithTaskId} goToTask={this.goToTaskWithTaskId}
noSqlMode={noSqlMode} capabilities={capabilities}
/>, />,
); );
}; };
@ -316,7 +355,7 @@ export class ConsoleApplication extends React.PureComponent<
if (capabilitiesLoading) { if (capabilitiesLoading) {
return ( return (
<div className="loading-capabilities"> <div className="loading-capabilities">
<Loader loadingText="" loading={capabilitiesLoading} /> <Loader loadingText="" loading />
</div> </div>
); );
} }
@ -326,13 +365,14 @@ export class ConsoleApplication extends React.PureComponent<
<div className="console-application"> <div className="console-application">
<Switch> <Switch>
<Route path="/load-data" component={this.wrappedLoadDataView} /> <Route path="/load-data" component={this.wrappedLoadDataView} />
<Route path="/query" component={this.wrappedQueryView} />
<Route path="/datasources" component={this.wrappedDatasourcesView} /> <Route path="/datasources" component={this.wrappedDatasourcesView} />
<Route path="/segments" component={this.wrappedSegmentsView} /> <Route path="/segments" component={this.wrappedSegmentsView} />
<Route path="/tasks" component={this.wrappedTasksView} /> <Route path="/tasks" component={this.wrappedTasksView} />
<Route path="/servers" component={this.wrappedServersView} /> <Route path="/servers" component={this.wrappedServersView} />
<Route path="/query" component={this.wrappedQueryView} />
<Route path="/lookups" component={this.wrappedLookupsView} /> <Route path="/lookups" component={this.wrappedLookupsView} />
<Route component={this.wrappedHomeView} /> <Route component={this.wrappedHomeView} />
</Switch> </Switch>

View File

@ -0,0 +1,19 @@
/*
* 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.
*/
export type Capabilities = 'full' | 'no-sql' | 'no-proxy' | 'broken';

View File

@ -28,7 +28,7 @@ describe('data source view', () => {
goToQuery={() => {}} goToQuery={() => {}}
goToTask={() => null} goToTask={() => null}
goToSegments={() => {}} goToSegments={() => {}}
noSqlMode={false} capabilities="full"
/>, />,
); );
expect(dataSourceView).toMatchSnapshot(); expect(dataSourceView).toMatchSnapshot();

View File

@ -61,13 +61,15 @@ import {
QueryManager, QueryManager,
} from '../../utils'; } from '../../utils';
import { BasicAction } from '../../utils/basic-action'; import { BasicAction } from '../../utils/basic-action';
import { Capabilities } from '../../utils/capabilities';
import { RuleUtil } from '../../utils/load-rule'; import { RuleUtil } from '../../utils/load-rule';
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array'; import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
import { deepGet } from '../../utils/object-change'; import { deepGet } from '../../utils/object-change';
import './datasource-view.scss'; import './datasource-view.scss';
const tableColumns: string[] = [ const tableColumns: Record<Capabilities, string[]> = {
full: [
'Datasource', 'Datasource',
'Availability', 'Availability',
'Segment load/drop', 'Segment load/drop',
@ -78,8 +80,8 @@ const tableColumns: string[] = [
'Avg. segment size', 'Avg. segment size',
'Num rows', 'Num rows',
ACTION_COLUMN_LABEL, ACTION_COLUMN_LABEL,
]; ],
const tableColumnsNoSql: string[] = [ 'no-sql': [
'Datasource', 'Datasource',
'Availability', 'Availability',
'Segment load/drop', 'Segment load/drop',
@ -88,7 +90,19 @@ const tableColumnsNoSql: string[] = [
'Compaction', 'Compaction',
'Avg. segment size', 'Avg. segment size',
ACTION_COLUMN_LABEL, ACTION_COLUMN_LABEL,
]; ],
'no-proxy': [
'Datasource',
'Availability',
'Segment load/drop',
'Replicated size',
'Size',
'Avg. segment size',
'Num rows',
ACTION_COLUMN_LABEL,
],
broken: ['Datasource'],
};
function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string { function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string {
const loadDrop: string[] = []; const loadDrop: string[] = [];
@ -133,7 +147,7 @@ export interface DatasourcesViewProps {
goToQuery: (initSql: string) => void; goToQuery: (initSql: string) => void;
goToTask: (datasource?: string, openDialog?: string) => void; goToTask: (datasource?: string, openDialog?: string) => void;
goToSegments: (datasource: string, onlyUnavailable?: boolean) => void; goToSegments: (datasource: string, onlyUnavailable?: boolean) => void;
noSqlMode: boolean; capabilities: Capabilities;
initDatasource?: string; initDatasource?: string;
} }
@ -198,7 +212,7 @@ GROUP BY 1`;
} }
private datasourceQueryManager: QueryManager< private datasourceQueryManager: QueryManager<
boolean, Capabilities,
{ tiers: string[]; defaultRules: any[]; datasources: Datasource[] } { tiers: string[]; defaultRules: any[]; datasources: Datasource[] }
>; >;
@ -231,9 +245,9 @@ GROUP BY 1`;
}; };
this.datasourceQueryManager = new QueryManager({ this.datasourceQueryManager = new QueryManager({
processQuery: async noSqlMode => { processQuery: async capabilities => {
let datasources: DatasourceQueryResultRow[]; let datasources: DatasourceQueryResultRow[];
if (!noSqlMode) { if (capabilities !== 'no-sql') {
datasources = await queryDruidSql({ query: DatasourcesView.DATASOURCE_SQL }); datasources = await queryDruidSql({ query: DatasourcesView.DATASOURCE_SQL });
} else { } else {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple'); const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple');
@ -260,6 +274,17 @@ GROUP BY 1`;
); );
} }
if (capabilities === 'no-proxy') {
datasources.forEach((ds: any) => {
ds.rules = [];
});
return {
datasources,
tiers: [],
defaultRules: [],
};
}
const seen = countBy(datasources, (x: any) => x.datasource); const seen = countBy(datasources, (x: any) => x.datasource);
let disabled: string[] = []; let disabled: string[] = [];
@ -320,8 +345,8 @@ GROUP BY 1`;
}; };
componentDidMount(): void { componentDidMount(): void {
const { noSqlMode } = this.props; const { capabilities } = this.props;
this.datasourceQueryManager.runQuery(noSqlMode); this.datasourceQueryManager.runQuery(capabilities);
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
} }
@ -468,10 +493,10 @@ GROUP BY 1`;
} }
renderBulkDatasourceActions() { renderBulkDatasourceActions() {
const { goToQuery, noSqlMode } = this.props; const { goToQuery, capabilities } = this.props;
const bulkDatasourceActionsMenu = ( const bulkDatasourceActionsMenu = (
<Menu> <Menu>
{!noSqlMode && ( {capabilities !== 'no-sql' && (
<MenuItem <MenuItem
icon={IconNames.APPLICATION} icon={IconNames.APPLICATION}
text="View SQL query for table" text="View SQL query for table"
@ -581,7 +606,24 @@ GROUP BY 1`;
rules: any[], rules: any[],
compactionConfig: Record<string, any>, compactionConfig: Record<string, any>,
): BasicAction[] { ): BasicAction[] {
const { goToQuery, goToTask } = this.props; const { goToQuery, goToTask, capabilities } = this.props;
const goToActions: BasicAction[] = [
{
icon: IconNames.APPLICATION,
title: 'Query with SQL',
onAction: () => goToQuery(`SELECT * FROM ${escapeSqlIdentifier(datasource)}`),
},
{
icon: IconNames.GANTT_CHART,
title: 'Go to tasks',
onAction: () => goToTask(datasource),
},
];
if (capabilities === 'no-proxy') {
return goToActions;
}
if (disabled) { if (disabled) {
return [ return [
@ -598,12 +640,7 @@ GROUP BY 1`;
}, },
]; ];
} else { } else {
return [ return goToActions.concat([
{
icon: IconNames.APPLICATION,
title: 'Query with SQL',
onAction: () => goToQuery(`SELECT * FROM ${escapeSqlIdentifier(datasource)}`),
},
{ {
icon: IconNames.GANTT_CHART, icon: IconNames.GANTT_CHART,
title: 'Go to tasks', title: 'Go to tasks',
@ -657,7 +694,7 @@ GROUP BY 1`;
intent: Intent.DANGER, intent: Intent.DANGER,
onAction: () => this.setState({ killDatasource: datasource }), onAction: () => this.setState({ killDatasource: datasource }),
}, },
]; ]);
} }
} }
@ -694,7 +731,7 @@ GROUP BY 1`;
} }
renderDatasourceTable() { renderDatasourceTable() {
const { goToSegments, noSqlMode } = this.props; const { goToSegments, capabilities } = this.props;
const { const {
datasources, datasources,
defaultRules, defaultRules,
@ -850,7 +887,7 @@ GROUP BY 1`;
</span> </span>
); );
}, },
show: hiddenColumns.exists('Retention'), show: capabilities !== 'no-proxy' && hiddenColumns.exists('Retention'),
}, },
{ {
Header: 'Replicated size', Header: 'Replicated size',
@ -904,7 +941,7 @@ GROUP BY 1`;
</span> </span>
); );
}, },
show: hiddenColumns.exists('Compaction'), show: capabilities !== 'no-proxy' && hiddenColumns.exists('Compaction'),
}, },
{ {
Header: 'Avg. segment size', Header: 'Avg. segment size',
@ -920,7 +957,7 @@ GROUP BY 1`;
filterable: false, filterable: false,
width: 100, width: 100,
Cell: row => formatNumber(row.value), Cell: row => formatNumber(row.value),
show: !noSqlMode && hiddenColumns.exists('Num rows'), show: capabilities !== 'no-sql' && hiddenColumns.exists('Num rows'),
}, },
{ {
Header: ACTION_COLUMN_LABEL, Header: ACTION_COLUMN_LABEL,
@ -965,7 +1002,7 @@ GROUP BY 1`;
} }
render(): JSX.Element { render(): JSX.Element {
const { noSqlMode } = this.props; const { capabilities } = this.props;
const { const {
showDisabled, showDisabled,
hiddenColumns, hiddenColumns,
@ -993,13 +1030,15 @@ GROUP BY 1`;
label="Show segment timeline" label="Show segment timeline"
onChange={() => this.setState({ showChart: !showChart })} onChange={() => this.setState({ showChart: !showChart })}
/> />
{capabilities !== 'no-proxy' && (
<Switch <Switch
checked={showDisabled} checked={showDisabled}
label="Show disabled" label="Show disabled"
onChange={() => this.toggleDisabled(showDisabled)} onChange={() => this.toggleDisabled(showDisabled)}
/> />
)}
<TableColumnSelector <TableColumnSelector
columns={noSqlMode ? tableColumnsNoSql : tableColumns} columns={tableColumns[capabilities]}
onChange={column => onChange={column =>
this.setState(prevState => ({ this.setState(prevState => ({
hiddenColumns: prevState.hiddenColumns.toggle(column), hiddenColumns: prevState.hiddenColumns.toggle(column),

View File

@ -6,17 +6,19 @@ exports[`home view matches snapshot 1`] = `
> >
<StatusCard /> <StatusCard />
<DatasourcesCard <DatasourcesCard
noSqlMode={false} capabilities="full"
/> />
<SegmentsCard <SegmentsCard
noSqlMode={false} capabilities="full"
/>
<SupervisorsCard
capabilities="full"
/> />
<SupervisorsCard />
<TasksCard <TasksCard
noSqlMode={false} capabilities="full"
/> />
<ServersCard <ServersCard
noSqlMode={false} capabilities="full"
/> />
<LookupsCard /> <LookupsCard />
</div> </div>

View File

@ -23,7 +23,7 @@ import { DatasourcesCard } from './datasources-card';
describe('datasources card', () => { describe('datasources card', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const datasourcesCard = <DatasourcesCard noSqlMode={false} />; const datasourcesCard = <DatasourcesCard capabilities="full" />;
const { container } = render(datasourcesCard); const { container } = render(datasourcesCard);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();

View File

@ -21,10 +21,11 @@ import axios from 'axios';
import React from 'react'; import React from 'react';
import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils'; import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { Capabilities } from '../../../utils/capabilities';
import { HomeViewCard } from '../home-view-card/home-view-card'; import { HomeViewCard } from '../home-view-card/home-view-card';
export interface DatasourcesCardProps { export interface DatasourcesCardProps {
noSqlMode: boolean; capabilities: Capabilities;
} }
export interface DatasourcesCardState { export interface DatasourcesCardState {
@ -37,7 +38,7 @@ export class DatasourcesCard extends React.PureComponent<
DatasourcesCardProps, DatasourcesCardProps,
DatasourcesCardState DatasourcesCardState
> { > {
private datasourceQueryManager: QueryManager<boolean, any>; private datasourceQueryManager: QueryManager<Capabilities, any>;
constructor(props: DatasourcesCardProps, context: any) { constructor(props: DatasourcesCardProps, context: any) {
super(props, context); super(props, context);
@ -47,9 +48,9 @@ export class DatasourcesCard extends React.PureComponent<
}; };
this.datasourceQueryManager = new QueryManager({ this.datasourceQueryManager = new QueryManager({
processQuery: async noSqlMode => { processQuery: async capabilities => {
let datasources: string[]; let datasources: string[];
if (!noSqlMode) { if (capabilities !== 'no-sql') {
datasources = await queryDruidSql({ datasources = await queryDruidSql({
query: `SELECT datasource FROM sys.segments GROUP BY 1`, query: `SELECT datasource FROM sys.segments GROUP BY 1`,
}); });
@ -70,9 +71,9 @@ export class DatasourcesCard extends React.PureComponent<
} }
componentDidMount(): void { componentDidMount(): void {
const { noSqlMode } = this.props; const { capabilities } = this.props;
this.datasourceQueryManager.runQuery(noSqlMode); this.datasourceQueryManager.runQuery(capabilities);
} }
componentWillUnmount(): void { componentWillUnmount(): void {

View File

@ -23,7 +23,7 @@ import { HomeView } from './home-view';
describe('home view', () => { describe('home view', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const homeView = shallow(<HomeView noSqlMode={false} />); const homeView = shallow(<HomeView capabilities="full" />);
expect(homeView).toMatchSnapshot(); expect(homeView).toMatchSnapshot();
}); });
}); });

View File

@ -18,6 +18,8 @@
import React from 'react'; import React from 'react';
import { Capabilities } from '../../utils/capabilities';
import { DatasourcesCard } from './datasources-card/datasources-card'; import { DatasourcesCard } from './datasources-card/datasources-card';
import { LookupsCard } from './lookups-card/lookups-card'; import { LookupsCard } from './lookups-card/lookups-card';
import { SegmentsCard } from './segments-card/segments-card'; import { SegmentsCard } from './segments-card/segments-card';
@ -29,20 +31,20 @@ import { TasksCard } from './tasks-card/tasks-card';
import './home-view.scss'; import './home-view.scss';
export interface HomeViewProps { export interface HomeViewProps {
noSqlMode: boolean; capabilities: Capabilities;
} }
export const HomeView = React.memo(function HomeView(props: HomeViewProps) { export const HomeView = React.memo(function HomeView(props: HomeViewProps) {
const { noSqlMode } = props; const { capabilities } = props;
return ( return (
<div className="home-view app-view"> <div className="home-view app-view">
<StatusCard /> <StatusCard />
<DatasourcesCard noSqlMode={noSqlMode} /> <DatasourcesCard capabilities={capabilities} />
<SegmentsCard noSqlMode={noSqlMode} /> <SegmentsCard capabilities={capabilities} />
<SupervisorsCard /> <SupervisorsCard capabilities={capabilities} />
<TasksCard noSqlMode={noSqlMode} /> <TasksCard capabilities={capabilities} />
<ServersCard noSqlMode={noSqlMode} /> <ServersCard capabilities={capabilities} />
<LookupsCard /> <LookupsCard />
</div> </div>
); );

View File

@ -23,7 +23,7 @@ import { SegmentsCard } from './segments-card';
describe('segments card', () => { describe('segments card', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const segmentsCard = <SegmentsCard noSqlMode={false} />; const segmentsCard = <SegmentsCard capabilities="full" />;
const { container } = render(segmentsCard); const { container } = render(segmentsCard);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();

View File

@ -22,11 +22,12 @@ import { sum } from 'd3-array';
import React from 'react'; import React from 'react';
import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils'; import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { Capabilities } from '../../../utils/capabilities';
import { deepGet } from '../../../utils/object-change'; import { deepGet } from '../../../utils/object-change';
import { HomeViewCard } from '../home-view-card/home-view-card'; import { HomeViewCard } from '../home-view-card/home-view-card';
export interface SegmentsCardProps { export interface SegmentsCardProps {
noSqlMode: boolean; capabilities: Capabilities;
} }
export interface SegmentsCardState { export interface SegmentsCardState {
@ -37,7 +38,7 @@ export interface SegmentsCardState {
} }
export class SegmentsCard extends React.PureComponent<SegmentsCardProps, SegmentsCardState> { export class SegmentsCard extends React.PureComponent<SegmentsCardProps, SegmentsCardState> {
private segmentQueryManager: QueryManager<boolean, any>; private segmentQueryManager: QueryManager<Capabilities, any>;
constructor(props: SegmentsCardProps, context: any) { constructor(props: SegmentsCardProps, context: any) {
super(props, context); super(props, context);
@ -48,8 +49,8 @@ export class SegmentsCard extends React.PureComponent<SegmentsCardProps, Segment
}; };
this.segmentQueryManager = new QueryManager({ this.segmentQueryManager = new QueryManager({
processQuery: async noSqlMode => { processQuery: async capabilities => {
if (noSqlMode) { if (capabilities === 'no-sql') {
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple'); const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
const loadstatus = loadstatusResp.data; const loadstatus = loadstatusResp.data;
const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]); const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
@ -86,9 +87,9 @@ FROM sys.segments`,
} }
componentDidMount(): void { componentDidMount(): void {
const { noSqlMode } = this.props; const { capabilities } = this.props;
this.segmentQueryManager.runQuery(noSqlMode); this.segmentQueryManager.runQuery(capabilities);
} }
componentWillUnmount(): void { componentWillUnmount(): void {

View File

@ -23,7 +23,7 @@ import { ServersCard } from './servers-card';
describe('servers card', () => { describe('servers card', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const serversCard = <ServersCard noSqlMode={false} />; const serversCard = <ServersCard capabilities="full" />;
const { container } = render(serversCard); const { container } = render(serversCard);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();

View File

@ -21,10 +21,11 @@ import axios from 'axios';
import React from 'react'; import React from 'react';
import { compact, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils'; import { compact, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { Capabilities } from '../../../utils/capabilities';
import { HomeViewCard } from '../home-view-card/home-view-card'; import { HomeViewCard } from '../home-view-card/home-view-card';
export interface ServersCardProps { export interface ServersCardProps {
noSqlMode: boolean; capabilities: Capabilities;
} }
export interface ServersCardState { export interface ServersCardState {
@ -55,7 +56,7 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
return <p>{text}</p>; return <p>{text}</p>;
} }
private serverQueryManager: QueryManager<boolean, any>; private serverQueryManager: QueryManager<Capabilities, any>;
constructor(props: ServersCardProps, context: any) { constructor(props: ServersCardProps, context: any) {
super(props, context); super(props, context);
@ -72,8 +73,8 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
}; };
this.serverQueryManager = new QueryManager({ this.serverQueryManager = new QueryManager({
processQuery: async noSqlMode => { processQuery: async capabilities => {
if (noSqlMode) { if (capabilities === 'no-sql') {
const serversResp = await axios.get('/druid/coordinator/v1/servers?simple'); const serversResp = await axios.get('/druid/coordinator/v1/servers?simple');
const middleManagerResp = await axios.get('/druid/indexer/v1/workers'); const middleManagerResp = await axios.get('/druid/indexer/v1/workers');
return { return {
@ -109,9 +110,9 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
} }
componentDidMount(): void { componentDidMount(): void {
const { noSqlMode } = this.props; const { capabilities } = this.props;
this.serverQueryManager.runQuery(noSqlMode); this.serverQueryManager.runQuery(capabilities);
} }
componentWillUnmount(): void { componentWillUnmount(): void {

View File

@ -23,7 +23,7 @@ import { SupervisorsCard } from './supervisors-card';
describe('supervisors card', () => { describe('supervisors card', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const supervisorsCard = <SupervisorsCard />; const supervisorsCard = <SupervisorsCard capabilities="full" />;
const { container } = render(supervisorsCard); const { container } = render(supervisorsCard);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();

View File

@ -20,10 +20,13 @@ import { IconNames } from '@blueprintjs/icons';
import axios from 'axios'; import axios from 'axios';
import React from 'react'; import React from 'react';
import { pluralIfNeeded, QueryManager } from '../../../utils'; import { pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { Capabilities } from '../../../utils/capabilities';
import { HomeViewCard } from '../home-view-card/home-view-card'; import { HomeViewCard } from '../home-view-card/home-view-card';
export interface SupervisorsCardProps {} export interface SupervisorsCardProps {
capabilities: Capabilities;
}
export interface SupervisorsCardState { export interface SupervisorsCardState {
supervisorCountLoading: boolean; supervisorCountLoading: boolean;
@ -36,7 +39,7 @@ export class SupervisorsCard extends React.PureComponent<
SupervisorsCardProps, SupervisorsCardProps,
SupervisorsCardState SupervisorsCardState
> { > {
private supervisorQueryManager: QueryManager<null, any>; private supervisorQueryManager: QueryManager<Capabilities, any>;
constructor(props: SupervisorsCardProps, context: any) { constructor(props: SupervisorsCardProps, context: any) {
super(props, context); super(props, context);
@ -47,15 +50,25 @@ export class SupervisorsCard extends React.PureComponent<
}; };
this.supervisorQueryManager = new QueryManager({ this.supervisorQueryManager = new QueryManager({
processQuery: async () => { processQuery: async capabilities => {
if (capabilities !== 'no-sql') {
return (await queryDruidSql({
query: `SELECT
COUNT(*) FILTER (WHERE "suspended" = 0) AS "runningSupervisorCount",
COUNT(*) FILTER (WHERE "suspended" = 1) AS "suspendedSupervisorCount"
FROM sys.supervisors`,
}))[0];
} else {
const resp = await axios.get('/druid/indexer/v1/supervisor?full'); const resp = await axios.get('/druid/indexer/v1/supervisor?full');
const data = resp.data; const data = resp.data;
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length; const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
const suspendedSupervisorCount = data.filter((d: any) => d.spec.suspended === true).length; const suspendedSupervisorCount = data.filter((d: any) => d.spec.suspended === true)
.length;
return { return {
runningSupervisorCount, runningSupervisorCount,
suspendedSupervisorCount, suspendedSupervisorCount,
}; };
}
}, },
onStateChange: ({ result, loading, error }) => { onStateChange: ({ result, loading, error }) => {
this.setState({ this.setState({
@ -69,7 +82,9 @@ export class SupervisorsCard extends React.PureComponent<
} }
componentDidMount(): void { componentDidMount(): void {
this.supervisorQueryManager.runQuery(null); const { capabilities } = this.props;
this.supervisorQueryManager.runQuery(capabilities);
} }
componentWillUnmount(): void { componentWillUnmount(): void {

View File

@ -23,7 +23,7 @@ import { TasksCard } from './tasks-card';
describe('tasks card', () => { describe('tasks card', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const tasksCard = <TasksCard noSqlMode={false} />; const tasksCard = <TasksCard capabilities="full" />;
const { container } = render(tasksCard); const { container } = render(tasksCard);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();

View File

@ -21,10 +21,11 @@ import axios from 'axios';
import React from 'react'; import React from 'react';
import { lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils'; import { lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../../utils';
import { Capabilities } from '../../../utils/capabilities';
import { HomeViewCard } from '../home-view-card/home-view-card'; import { HomeViewCard } from '../home-view-card/home-view-card';
export interface TasksCardProps { export interface TasksCardProps {
noSqlMode: boolean; capabilities: Capabilities;
} }
export interface TasksCardState { export interface TasksCardState {
@ -38,7 +39,7 @@ export interface TasksCardState {
} }
export class TasksCard extends React.PureComponent<TasksCardProps, TasksCardState> { export class TasksCard extends React.PureComponent<TasksCardProps, TasksCardState> {
private taskQueryManager: QueryManager<boolean, any>; private taskQueryManager: QueryManager<Capabilities, any>;
constructor(props: TasksCardProps, context: any) { constructor(props: TasksCardProps, context: any) {
super(props, context); super(props, context);
@ -52,8 +53,8 @@ export class TasksCard extends React.PureComponent<TasksCardProps, TasksCardStat
}; };
this.taskQueryManager = new QueryManager({ this.taskQueryManager = new QueryManager({
processQuery: async noSqlMode => { processQuery: async capabilities => {
if (noSqlMode) { if (capabilities === 'no-sql') {
const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks'); const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks'); const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
const pendingTasksResp = await axios.get('/druid/indexer/v1/pendingTasks'); const pendingTasksResp = await axios.get('/druid/indexer/v1/pendingTasks');
@ -91,9 +92,9 @@ GROUP BY 1`,
} }
componentDidMount(): void { componentDidMount(): void {
const { noSqlMode } = this.props; const { capabilities } = this.props;
this.taskQueryManager.runQuery(noSqlMode); this.taskQueryManager.runQuery(capabilities);
} }
componentWillUnmount(): void { componentWillUnmount(): void {

View File

@ -28,7 +28,7 @@ describe('segments-view', () => {
datasource={'test'} datasource={'test'}
onlyUnavailable={false} onlyUnavailable={false}
goToQuery={() => {}} goToQuery={() => {}}
noSqlMode={false} capabilities="full"
/>, />,
); );
expect(segmentsView).toMatchSnapshot(); expect(segmentsView).toMatchSnapshot();

View File

@ -56,11 +56,13 @@ import {
sqlQueryCustomTableFilter, sqlQueryCustomTableFilter,
} from '../../utils'; } from '../../utils';
import { BasicAction } from '../../utils/basic-action'; import { BasicAction } from '../../utils/basic-action';
import { Capabilities } from '../../utils/capabilities';
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array'; import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
import './segments-view.scss'; import './segments-view.scss';
const tableColumns: string[] = [ const tableColumns: Record<Capabilities, string[]> = {
full: [
'Segment ID', 'Segment ID',
'Datasource', 'Datasource',
'Start', 'Start',
@ -75,8 +77,8 @@ const tableColumns: string[] = [
'Is available', 'Is available',
'Is overshadowed', 'Is overshadowed',
ACTION_COLUMN_LABEL, ACTION_COLUMN_LABEL,
]; ],
const tableColumnsNoSql: string[] = [ 'no-sql': [
'Segment ID', 'Segment ID',
'Datasource', 'Datasource',
'Start', 'Start',
@ -84,13 +86,31 @@ const tableColumnsNoSql: string[] = [
'Version', 'Version',
'Partition', 'Partition',
'Size', 'Size',
]; ACTION_COLUMN_LABEL,
],
'no-proxy': [
'Segment ID',
'Datasource',
'Start',
'End',
'Version',
'Partition',
'Size',
'Num rows',
'Replicas',
'Is published',
'Is realtime',
'Is available',
'Is overshadowed',
],
broken: ['Segment ID'],
};
export interface SegmentsViewProps { export interface SegmentsViewProps {
goToQuery: (initSql: string) => void; goToQuery: (initSql: string) => void;
datasource: string | undefined; datasource: string | undefined;
onlyUnavailable: boolean | undefined; onlyUnavailable: boolean | undefined;
noSqlMode: boolean; capabilities: Capabilities;
} }
export interface SegmentsViewState { export interface SegmentsViewState {
@ -318,8 +338,8 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
} }
componentDidMount(): void { componentDidMount(): void {
const { noSqlMode } = this.props; const { capabilities } = this.props;
if (noSqlMode) { if (capabilities === 'no-sql') {
this.segmentsNoSqlQueryManager.runQuery(null); this.segmentsNoSqlQueryManager.runQuery(null);
} }
} }
@ -389,7 +409,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
hiddenColumns, hiddenColumns,
groupByInterval, groupByInterval,
} = this.state; } = this.state;
const { noSqlMode } = this.props; const { capabilities } = this.props;
return ( return (
<ReactTable <ReactTable
@ -407,7 +427,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
this.setState({ segmentFilter: filtered }); this.setState({ segmentFilter: filtered });
}} }}
onFetchData={ onFetchData={
noSqlMode capabilities === 'no-sql'
? this.fetchClientSideData ? this.fetchClientSideData
: state => { : state => {
this.setState({ this.setState({
@ -536,7 +556,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
filterable: false, filterable: false,
defaultSortDesc: true, defaultSortDesc: true,
Cell: row => (row.original.is_available ? formatNumber(row.value) : <em>(unknown)</em>), Cell: row => (row.original.is_available ? formatNumber(row.value) : <em>(unknown)</em>),
show: !noSqlMode && hiddenColumns.exists('Num rows'), show: capabilities !== 'no-sql' && hiddenColumns.exists('Num rows'),
}, },
{ {
Header: 'Replicas', Header: 'Replicas',
@ -544,35 +564,35 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
width: 60, width: 60,
filterable: false, filterable: false,
defaultSortDesc: true, defaultSortDesc: true,
show: !noSqlMode && hiddenColumns.exists('Replicas'), show: capabilities !== 'no-sql' && hiddenColumns.exists('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: !noSqlMode && hiddenColumns.exists('Is published'), show: capabilities !== 'no-sql' && hiddenColumns.exists('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: !noSqlMode && hiddenColumns.exists('Is realtime'), show: capabilities !== 'no-sql' && hiddenColumns.exists('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: !noSqlMode && hiddenColumns.exists('Is available'), show: capabilities !== 'no-sql' && hiddenColumns.exists('Is available'),
}, },
{ {
Header: 'Is overshadowed', Header: 'Is overshadowed',
id: 'is_overshadowed', id: 'is_overshadowed',
accessor: row => String(Boolean(row.is_overshadowed)), accessor: row => String(Boolean(row.is_overshadowed)),
Filter: makeBooleanFilter(), Filter: makeBooleanFilter(),
show: !noSqlMode && hiddenColumns.exists('Is overshadowed'), show: capabilities !== 'no-sql' && hiddenColumns.exists('Is overshadowed'),
}, },
{ {
Header: ACTION_COLUMN_LABEL, Header: ACTION_COLUMN_LABEL,
@ -598,7 +618,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
); );
}, },
Aggregated: () => '', Aggregated: () => '',
show: hiddenColumns.exists(ACTION_COLUMN_LABEL), show: capabilities !== 'no-proxy' && hiddenColumns.exists(ACTION_COLUMN_LABEL),
}, },
]} ]}
defaultPageSize={SegmentsView.PAGE_SIZE} defaultPageSize={SegmentsView.PAGE_SIZE}
@ -638,12 +658,12 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
} }
renderBulkSegmentsActions() { renderBulkSegmentsActions() {
const { goToQuery, noSqlMode } = this.props; const { goToQuery, capabilities } = this.props;
const lastSegmentsQuery = this.segmentsSqlQueryManager.getLastIntermediateQuery(); const lastSegmentsQuery = this.segmentsSqlQueryManager.getLastIntermediateQuery();
const bulkSegmentsActionsMenu = ( const bulkSegmentsActionsMenu = (
<Menu> <Menu>
{!noSqlMode && ( {capabilities !== 'no-sql' && (
<MenuItem <MenuItem
icon={IconNames.APPLICATION} icon={IconNames.APPLICATION}
text="View SQL query for table" text="View SQL query for table"
@ -673,7 +693,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
actions, actions,
hiddenColumns, hiddenColumns,
} = this.state; } = this.state;
const { noSqlMode } = this.props; const { capabilities } = this.props;
const { groupByInterval } = this.state; const { groupByInterval } = this.state;
return ( return (
@ -682,7 +702,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
<ViewControlBar label="Segments"> <ViewControlBar label="Segments">
<RefreshButton <RefreshButton
onRefresh={auto => onRefresh={auto =>
noSqlMode capabilities
? this.segmentsNoSqlQueryManager.rerunLastQuery(auto) ? this.segmentsNoSqlQueryManager.rerunLastQuery(auto)
: this.segmentsSqlQueryManager.rerunLastQuery(auto) : this.segmentsSqlQueryManager.rerunLastQuery(auto)
} }
@ -694,7 +714,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
active={!groupByInterval} active={!groupByInterval}
onClick={() => { onClick={() => {
this.setState({ groupByInterval: false }); this.setState({ groupByInterval: false });
noSqlMode ? this.fetchClientSideData() : this.fetchData(false); capabilities === 'no-sql' ? this.fetchClientSideData() : this.fetchData(false);
}} }}
> >
None None
@ -711,7 +731,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
</ButtonGroup> </ButtonGroup>
{this.renderBulkSegmentsActions()} {this.renderBulkSegmentsActions()}
<TableColumnSelector <TableColumnSelector
columns={noSqlMode ? tableColumnsNoSql : tableColumns} columns={tableColumns[capabilities]}
onChange={column => onChange={column =>
this.setState(prevState => ({ this.setState(prevState => ({
hiddenColumns: prevState.hiddenColumns.toggle(column), hiddenColumns: prevState.hiddenColumns.toggle(column),

View File

@ -28,7 +28,7 @@ describe('servers view', () => {
middleManager={'test'} middleManager={'test'}
goToQuery={() => {}} goToQuery={() => {}}
goToTask={() => {}} goToTask={() => {}}
noSqlMode={false} capabilities="full"
/>, />,
); );
expect(serversView).toMatchSnapshot(); expect(serversView).toMatchSnapshot();

View File

@ -53,12 +53,13 @@ import {
QueryManager, QueryManager,
} from '../../utils'; } from '../../utils';
import { BasicAction } from '../../utils/basic-action'; import { BasicAction } from '../../utils/basic-action';
import { Capabilities } from '../../utils/capabilities';
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array'; import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
import { deepGet } from '../../utils/object-change'; import { deepGet } from '../../utils/object-change';
import './servers-view.scss'; import './servers-view.scss';
const serverTableColumns: string[] = [ const allColumns: string[] = [
'Server', 'Server',
'Type', 'Type',
'Tier', 'Tier',
@ -71,6 +72,13 @@ const serverTableColumns: string[] = [
ACTION_COLUMN_LABEL, ACTION_COLUMN_LABEL,
]; ];
const tableColumns: Record<Capabilities, string[]> = {
full: allColumns,
'no-sql': allColumns,
'no-proxy': ['Server', 'Type', 'Tier', 'Host', 'Port', 'Curr size', 'Max size', 'Usage'],
broken: ['Server'],
};
function formatQueues( function formatQueues(
segmentsToLoad: number, segmentsToLoad: number,
segmentsToLoadSize: number, segmentsToLoadSize: number,
@ -95,7 +103,7 @@ export interface ServersViewProps {
middleManager: string | undefined; middleManager: string | undefined;
goToQuery: (initSql: string) => void; goToQuery: (initSql: string) => void;
goToTask: (taskId: string) => void; goToTask: (taskId: string) => void;
noSqlMode: boolean; capabilities: Capabilities;
} }
export interface ServersViewState { export interface ServersViewState {
@ -150,7 +158,7 @@ interface ServerResultRow
Partial<MiddleManagerQueryResultRow> {} Partial<MiddleManagerQueryResultRow> {}
export class ServersView extends React.PureComponent<ServersViewProps, ServersViewState> { export class ServersView extends React.PureComponent<ServersViewProps, ServersViewState> {
private serverQueryManager: QueryManager<boolean, ServerResultRow[]>; private serverQueryManager: QueryManager<Capabilities, ServerResultRow[]>;
// Ranking // Ranking
// coordinator => 7 // coordinator => 7
@ -207,14 +215,18 @@ ORDER BY "rank" DESC, "server" DESC`;
}; };
this.serverQueryManager = new QueryManager({ this.serverQueryManager = new QueryManager({
processQuery: async noSqlMode => { processQuery: async capabilities => {
let servers: ServerQueryResultRow[]; let servers: ServerQueryResultRow[];
if (!noSqlMode) { if (capabilities !== 'no-sql') {
servers = await queryDruidSql({ query: ServersView.SERVER_SQL }); servers = await queryDruidSql({ query: ServersView.SERVER_SQL });
} else { } else {
servers = await ServersView.getServers(); servers = await ServersView.getServers();
} }
if (capabilities === 'no-proxy') {
return servers;
}
const loadQueueResponse = await axios.get('/druid/coordinator/v1/loadqueue?simple'); const loadQueueResponse = await axios.get('/druid/coordinator/v1/loadqueue?simple');
const loadQueues: Record<string, LoadQueueStatus> = loadQueueResponse.data; const loadQueues: Record<string, LoadQueueStatus> = loadQueueResponse.data;
servers = servers.map((s: any) => { servers = servers.map((s: any) => {
@ -264,8 +276,8 @@ ORDER BY "rank" DESC, "server" DESC`;
} }
componentDidMount(): void { componentDidMount(): void {
const { noSqlMode } = this.props; const { capabilities } = this.props;
this.serverQueryManager.runQuery(noSqlMode); this.serverQueryManager.runQuery(capabilities);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -273,6 +285,7 @@ ORDER BY "rank" DESC, "server" DESC`;
} }
renderServersTable() { renderServersTable() {
const { capabilities } = this.props;
const { const {
servers, servers,
serversLoading, serversLoading,
@ -528,7 +541,7 @@ ORDER BY "rank" DESC, "server" DESC`;
segmentsToDropSize, segmentsToDropSize,
); );
}, },
show: hiddenColumns.exists('Detail'), show: capabilities !== 'no-proxy' && hiddenColumns.exists('Detail'),
}, },
{ {
Header: ACTION_COLUMN_LABEL, Header: ACTION_COLUMN_LABEL,
@ -542,7 +555,7 @@ ORDER BY "rank" DESC, "server" DESC`;
const workerActions = this.getWorkerActions(row.value.host, disabled); const workerActions = this.getWorkerActions(row.value.host, disabled);
return <ActionCell actions={workerActions} />; return <ActionCell actions={workerActions} />;
}, },
show: hiddenColumns.exists(ACTION_COLUMN_LABEL), show: capabilities !== 'no-proxy' && hiddenColumns.exists(ACTION_COLUMN_LABEL),
}, },
]} ]}
/> />
@ -628,11 +641,11 @@ ORDER BY "rank" DESC, "server" DESC`;
} }
renderBulkServersActions() { renderBulkServersActions() {
const { goToQuery, noSqlMode } = this.props; const { goToQuery, capabilities } = this.props;
const bulkserversActionsMenu = ( const bulkserversActionsMenu = (
<Menu> <Menu>
{!noSqlMode && ( {capabilities !== 'no-sql' && (
<MenuItem <MenuItem
icon={IconNames.APPLICATION} icon={IconNames.APPLICATION}
text="View SQL query for table" text="View SQL query for table"
@ -652,6 +665,7 @@ ORDER BY "rank" DESC, "server" DESC`;
} }
render(): JSX.Element { render(): JSX.Element {
const { capabilities } = this.props;
const { groupServersBy, hiddenColumns } = this.state; const { groupServersBy, hiddenColumns } = this.state;
return ( return (
@ -684,7 +698,7 @@ ORDER BY "rank" DESC, "server" DESC`;
/> />
{this.renderBulkServersActions()} {this.renderBulkServersActions()}
<TableColumnSelector <TableColumnSelector
columns={serverTableColumns} columns={tableColumns[capabilities]}
onChange={column => onChange={column =>
this.setState(prevState => ({ this.setState(prevState => ({
hiddenColumns: prevState.hiddenColumns.toggle(column), hiddenColumns: prevState.hiddenColumns.toggle(column),

View File

@ -31,12 +31,12 @@ exports[`tasks view matches snapshot 1`] = `
<Blueprint3.Menu> <Blueprint3.Menu>
<Blueprint3.MenuItem <Blueprint3.MenuItem
disabled={false} disabled={false}
icon="cloud-upload" icon="application"
multiline={false} multiline={false}
onClick={[Function]} onClick={[Function]}
popoverProps={Object {}} popoverProps={Object {}}
shouldDismissPopover={true} shouldDismissPopover={true}
text="Go to data loader" text="View SQL query for table"
/> />
<Blueprint3.MenuItem <Blueprint3.MenuItem
disabled={false} disabled={false}
@ -47,35 +47,6 @@ exports[`tasks view matches snapshot 1`] = `
shouldDismissPopover={true} shouldDismissPopover={true}
text="Submit JSON supervisor" text="Submit JSON supervisor"
/> />
</Blueprint3.Menu>
}
defaultIsOpen={false}
disabled={false}
fill={false}
hasBackdrop={false}
hoverCloseDelay={300}
hoverOpenDelay={150}
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="bottom-left"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
icon="plus"
text="Submit supervisor"
/>
</Blueprint3.Popover>
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
content={
<Blueprint3.Menu>
<Blueprint3.MenuItem <Blueprint3.MenuItem
disabled={false} disabled={false}
icon="play" icon="play"
@ -200,7 +171,7 @@ exports[`tasks view matches snapshot 1`] = `
Array [ Array [
Object { Object {
"Header": "Datasource", "Header": "Datasource",
"accessor": "id", "accessor": "supervisor_id",
"id": "datasource", "id": "datasource",
"show": true, "show": true,
"width": 300, "width": 300,
@ -214,7 +185,7 @@ exports[`tasks view matches snapshot 1`] = `
Object { Object {
"Header": "Topic/Stream", "Header": "Topic/Stream",
"accessor": [Function], "accessor": [Function],
"id": "topic", "id": "source",
"show": true, "show": true,
}, },
Object { Object {
@ -228,7 +199,7 @@ exports[`tasks view matches snapshot 1`] = `
Object { Object {
"Cell": [Function], "Cell": [Function],
"Header": "Actions", "Header": "Actions",
"accessor": "id", "accessor": "supervisor_id",
"filterable": false, "filterable": false,
"id": "actions", "id": "actions",
"show": true, "show": true,
@ -380,12 +351,12 @@ exports[`tasks view matches snapshot 1`] = `
<Blueprint3.Menu> <Blueprint3.Menu>
<Blueprint3.MenuItem <Blueprint3.MenuItem
disabled={false} disabled={false}
icon="cloud-upload" icon="application"
multiline={false} multiline={false}
onClick={[Function]} onClick={[Function]}
popoverProps={Object {}} popoverProps={Object {}}
shouldDismissPopover={true} shouldDismissPopover={true}
text="Go to data loader" text="View SQL query for table"
/> />
<Blueprint3.MenuItem <Blueprint3.MenuItem
disabled={false} disabled={false}
@ -414,44 +385,6 @@ exports[`tasks view matches snapshot 1`] = `
transitionDuration={300} transitionDuration={300}
usePortal={true} usePortal={true}
wrapperTagName="span" wrapperTagName="span"
>
<Blueprint3.Button
icon="plus"
text="Submit task"
/>
</Blueprint3.Popover>
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
content={
<Blueprint3.Menu>
<Blueprint3.MenuItem
disabled={false}
icon="application"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="View SQL query for table"
/>
</Blueprint3.Menu>
}
defaultIsOpen={false}
disabled={false}
fill={false}
hasBackdrop={false}
hoverCloseDelay={300}
hoverOpenDelay={150}
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="bottom-left"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
> >
<Blueprint3.Button <Blueprint3.Button
icon="more" icon="more"

View File

@ -32,7 +32,7 @@ describe('tasks view', () => {
goToQuery={() => {}} goToQuery={() => {}}
goToMiddleManager={() => {}} goToMiddleManager={() => {}}
goToLoadData={() => {}} goToLoadData={() => {}}
noSqlMode={false} capabilities="full"
/>, />,
); );
expect(taskView).toMatchSnapshot(); expect(taskView).toMatchSnapshot();

View File

@ -63,7 +63,9 @@ import {
QueryManager, QueryManager,
} from '../../utils'; } from '../../utils';
import { BasicAction } from '../../utils/basic-action'; import { BasicAction } from '../../utils/basic-action';
import { Capabilities } from '../../utils/capabilities';
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array'; import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
import { deepGet } from '../../utils/object-change';
import './tasks-view.scss'; import './tasks-view.scss';
@ -86,6 +88,28 @@ const taskTableColumns: string[] = [
ACTION_COLUMN_LABEL, ACTION_COLUMN_LABEL,
]; ];
interface SupervisorQueryResultRow {
supervisor_id: string;
type: string;
source: string;
state: string;
detailed_state: string;
suspended: number;
}
interface TaskQueryResultRow {
task_id: string;
group_id: string;
type: string;
created_time: string;
datasource: string;
duration: number;
error_msg: string | null;
location: string | null;
status: string;
rank: number;
}
export interface TasksViewProps { export interface TasksViewProps {
taskId: string | undefined; taskId: string | undefined;
datasourceId: string | undefined; datasourceId: string | undefined;
@ -94,12 +118,12 @@ export interface TasksViewProps {
goToQuery: (initSql: string) => void; goToQuery: (initSql: string) => void;
goToMiddleManager: (middleManager: string) => void; goToMiddleManager: (middleManager: string) => void;
goToLoadData: (supervisorId?: string, taskId?: string) => void; goToLoadData: (supervisorId?: string, taskId?: string) => void;
noSqlMode: boolean; capabilities: Capabilities;
} }
export interface TasksViewState { export interface TasksViewState {
supervisorsLoading: boolean; supervisorsLoading: boolean;
supervisors: any[]; supervisors?: SupervisorQueryResultRow[];
supervisorsError?: string; supervisorsError?: string;
resumeSupervisorId?: string; resumeSupervisorId?: string;
@ -112,7 +136,7 @@ export interface TasksViewState {
showTerminateAllSupervisors: boolean; showTerminateAllSupervisors: boolean;
tasksLoading: boolean; tasksLoading: boolean;
tasks?: any[]; tasks?: TaskQueryResultRow[];
tasksError?: string; tasksError?: string;
taskFilter: Filter[]; taskFilter: Filter[];
@ -135,23 +159,6 @@ export interface TasksViewState {
hiddenSupervisorColumns: LocalStorageBackedArray<string>; hiddenSupervisorColumns: LocalStorageBackedArray<string>;
} }
interface TaskQueryResultRow {
created_time: string;
datasource: string;
duration: number;
error_msg: string | null;
location: string | null;
status: string;
task_id: string;
type: string;
rank: number;
}
interface SupervisorQueryResultRow {
id: string;
spec: any;
}
function statusToColor(status: string): string { function statusToColor(status: string): string {
switch (status) { switch (status) {
case 'RUNNING': case 'RUNNING':
@ -189,8 +196,8 @@ function stateToColor(status: string): string {
} }
export class TasksView extends React.PureComponent<TasksViewProps, TasksViewState> { export class TasksView extends React.PureComponent<TasksViewProps, TasksViewState> {
private supervisorQueryManager: QueryManager<null, SupervisorQueryResultRow[]>; private supervisorQueryManager: QueryManager<Capabilities, SupervisorQueryResultRow[]>;
private taskQueryManager: QueryManager<boolean, TaskQueryResultRow[]>; private taskQueryManager: QueryManager<Capabilities, TaskQueryResultRow[]>;
static statusRanking: Record<string, number> = { static statusRanking: Record<string, number> = {
RUNNING: 4, RUNNING: 4,
PENDING: 3, PENDING: 3,
@ -199,6 +206,9 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
FAILED: 1, FAILED: 1,
}; };
static SUPERVISOR_SQL = `SELECT "supervisor_id", "type", "source", "state", "detailed_state", "suspended"
FROM sys.supervisors`;
static TASK_SQL = `SELECT static TASK_SQL = `SELECT
"task_id", "group_id", "type", "datasource", "created_time", "location", "duration", "error_msg", "task_id", "group_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status", CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
@ -248,9 +258,28 @@ ORDER BY "rank" DESC, "created_time" DESC`;
}; };
this.supervisorQueryManager = new QueryManager({ this.supervisorQueryManager = new QueryManager({
processQuery: async () => { processQuery: async capabilities => {
const resp = await axios.get('/druid/indexer/v1/supervisor?full'); if (capabilities !== 'no-sql') {
return resp.data; return await queryDruidSql({
query: TasksView.SUPERVISOR_SQL,
});
} else {
const supervisors = (await axios.get('/druid/indexer/v1/supervisor?full')).data;
if (!Array.isArray(supervisors)) throw new Error(`Unexpected results`);
return supervisors.map((sup: any) => {
return {
supervisor_id: deepGet(sup, 'id'),
type: deepGet(sup, 'spec.tuningConfig.type'),
source:
deepGet(sup, 'spec.ioConfig.topic') ||
deepGet(sup, 'spec.ioConfig.stream') ||
'n/a',
state: deepGet(sup, 'state'),
detailed_state: deepGet(sup, 'detailedState'),
suspended: Number(deepGet(sup, 'suspended')),
};
});
}
}, },
onStateChange: ({ result, loading, error }) => { onStateChange: ({ result, loading, error }) => {
this.setState({ this.setState({
@ -262,8 +291,8 @@ ORDER BY "rank" DESC, "created_time" DESC`;
}); });
this.taskQueryManager = new QueryManager({ this.taskQueryManager = new QueryManager({
processQuery: async noSqlMode => { processQuery: async capabilities => {
if (!noSqlMode) { if (capabilities !== 'no-sql') {
return await queryDruidSql({ return await queryDruidSql({
query: TasksView.TASK_SQL, query: TasksView.TASK_SQL,
}); });
@ -280,7 +309,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
return TasksView.parseTasks(resp.data); return TasksView.parseTasks(resp.data);
}), }),
); );
return ([] as TaskQueryResultRow[]).concat.apply([], result); return result.flat();
} }
}, },
onStateChange: ({ result, loading, error }) => { onStateChange: ({ result, loading, error }) => {
@ -296,14 +325,15 @@ ORDER BY "rank" DESC, "created_time" DESC`;
static parseTasks = (data: any[]): TaskQueryResultRow[] => { static parseTasks = (data: any[]): TaskQueryResultRow[] => {
return data.map((d: any) => { return data.map((d: any) => {
return { return {
task_id: d.id,
group_id: d.groupId,
type: d.type,
created_time: d.createdTime, created_time: d.createdTime,
datasource: d.dataSource, datasource: d.dataSource,
duration: d.duration ? d.duration : 0, duration: d.duration ? d.duration : 0,
error_msg: d.errorMsg, error_msg: d.errorMsg,
location: d.location.host ? `${d.location.host}:${d.location.port}` : null, location: d.location.host ? `${d.location.host}:${d.location.port}` : null,
status: d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode, status: d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode,
task_id: d.id,
type: d.typTasksView,
rank: rank:
TasksView.statusRanking[d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode], TasksView.statusRanking[d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode],
}; };
@ -315,10 +345,10 @@ ORDER BY "rank" DESC, "created_time" DESC`;
} }
componentDidMount(): void { componentDidMount(): void {
const { noSqlMode } = this.props; const { capabilities } = this.props;
this.supervisorQueryManager.runQuery(null); this.supervisorQueryManager.runQuery(capabilities);
this.taskQueryManager.runQuery(noSqlMode); this.taskQueryManager.runQuery(capabilities);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -571,62 +601,45 @@ ORDER BY "rank" DESC, "created_time" DESC`;
{ {
Header: 'Datasource', Header: 'Datasource',
id: 'datasource', id: 'datasource',
accessor: 'id', accessor: 'supervisor_id',
width: 300, width: 300,
show: hiddenSupervisorColumns.exists('Datasource'), show: hiddenSupervisorColumns.exists('Datasource'),
}, },
{ {
Header: 'Type', Header: 'Type',
id: 'type', id: 'type',
accessor: row => { accessor: row => row.type,
const { spec } = row;
if (!spec) return '';
const { tuningConfig } = spec;
if (!tuningConfig) return '';
return tuningConfig.type;
},
show: hiddenSupervisorColumns.exists('Type'), show: hiddenSupervisorColumns.exists('Type'),
}, },
{ {
Header: 'Topic/Stream', Header: 'Topic/Stream',
id: 'topic', id: 'source',
accessor: row => { accessor: row => row.source,
const { spec } = row;
if (!spec) return '';
const { ioConfig } = spec;
if (!ioConfig) return '';
return ioConfig.topic || ioConfig.stream || '';
},
show: hiddenSupervisorColumns.exists('Topic/Stream'), show: hiddenSupervisorColumns.exists('Topic/Stream'),
}, },
{ {
Header: 'Status', Header: 'Status',
id: 'status', id: 'status',
width: 300, width: 300,
accessor: row => { accessor: row => row.detailed_state,
return row.detailedState; Cell: row => (
},
Cell: row => {
const value = row.original.detailedState;
return (
<span> <span>
<span style={{ color: stateToColor(row.original.state) }}>&#x25cf;&nbsp;</span> <span style={{ color: stateToColor(row.original.state) }}>&#x25cf;&nbsp;</span>
{value} {row.value}
</span> </span>
); ),
},
show: hiddenSupervisorColumns.exists('Status'), show: hiddenSupervisorColumns.exists('Status'),
}, },
{ {
Header: ACTION_COLUMN_LABEL, Header: ACTION_COLUMN_LABEL,
id: ACTION_COLUMN_ID, id: ACTION_COLUMN_ID,
accessor: 'id', accessor: 'supervisor_id',
width: ACTION_COLUMN_WIDTH, width: ACTION_COLUMN_WIDTH,
filterable: false, filterable: false,
Cell: row => { Cell: row => {
const id = row.value; const id = row.value;
const type = row.row.type; const type = row.original.type;
const supervisorSuspended = row.original.spec.suspended; const supervisorSuspended = row.original.suspended;
const supervisorActions = this.getSupervisorActions(id, supervisorSuspended, type); const supervisorActions = this.getSupervisorActions(id, supervisorSuspended, type);
return ( return (
<ActionCell <ActionCell
@ -917,8 +930,22 @@ ORDER BY "rank" DESC, "created_time" DESC`;
} }
renderBulkSupervisorActions() { renderBulkSupervisorActions() {
const { capabilities, goToQuery } = this.props;
const bulkSupervisorActionsMenu = ( const bulkSupervisorActionsMenu = (
<Menu> <Menu>
{capabilities !== 'no-sql' && (
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"
onClick={() => goToQuery(TasksView.SUPERVISOR_SQL)}
/>
)}
<MenuItem
icon={IconNames.MANUALLY_ENTERED_DATA}
text="Submit JSON supervisor"
onClick={() => this.setState({ supervisorSpecDialogOpen: true })}
/>
<MenuItem <MenuItem
icon={IconNames.PLAY} icon={IconNames.PLAY}
text="Resume all supervisors" text="Resume all supervisors"
@ -1029,17 +1056,22 @@ ORDER BY "rank" DESC, "created_time" DESC`;
} }
renderBulkTasksActions() { renderBulkTasksActions() {
const { goToQuery, noSqlMode } = this.props; const { goToQuery, capabilities } = this.props;
const bulkTaskActionsMenu = ( const bulkTaskActionsMenu = (
<Menu> <Menu>
{!noSqlMode && ( {capabilities !== 'no-sql' && (
<MenuItem <MenuItem
icon={IconNames.APPLICATION} icon={IconNames.APPLICATION}
text="View SQL query for table" text="View SQL query for table"
onClick={() => goToQuery(TasksView.TASK_SQL)} onClick={() => goToQuery(TasksView.TASK_SQL)}
/> />
)} )}
<MenuItem
icon={IconNames.MANUALLY_ENTERED_DATA}
text="Submit JSON task"
onClick={() => this.setState({ taskSpecDialogOpen: true })}
/>
</Menu> </Menu>
); );
@ -1053,7 +1085,6 @@ ORDER BY "rank" DESC, "created_time" DESC`;
} }
render(): JSX.Element { render(): JSX.Element {
const { goToLoadData } = this.props;
const { const {
groupTasksBy, groupTasksBy,
supervisorSpecDialogOpen, supervisorSpecDialogOpen,
@ -1068,36 +1099,6 @@ ORDER BY "rank" DESC, "created_time" DESC`;
hiddenTaskColumns, hiddenTaskColumns,
} = this.state; } = this.state;
const submitSupervisorMenu = (
<Menu>
<MenuItem
icon={IconNames.CLOUD_UPLOAD}
text="Go to data loader"
onClick={() => goToLoadData()}
/>
<MenuItem
icon={IconNames.MANUALLY_ENTERED_DATA}
text="Submit JSON supervisor"
onClick={() => this.setState({ supervisorSpecDialogOpen: true })}
/>
</Menu>
);
const submitTaskMenu = (
<Menu>
<MenuItem
icon={IconNames.CLOUD_UPLOAD}
text="Go to data loader"
onClick={() => goToLoadData()}
/>
<MenuItem
icon={IconNames.MANUALLY_ENTERED_DATA}
text="Submit JSON task"
onClick={() => this.setState({ taskSpecDialogOpen: true })}
/>
</Menu>
);
return ( return (
<> <>
<SplitterLayout <SplitterLayout
@ -1117,9 +1118,6 @@ ORDER BY "rank" DESC, "created_time" DESC`;
localStorageKey={LocalStorageKeys.SUPERVISORS_REFRESH_RATE} localStorageKey={LocalStorageKeys.SUPERVISORS_REFRESH_RATE}
onRefresh={auto => this.supervisorQueryManager.rerunLastQuery(auto)} onRefresh={auto => this.supervisorQueryManager.rerunLastQuery(auto)}
/> />
<Popover content={submitSupervisorMenu} position={Position.BOTTOM_LEFT}>
<Button icon={IconNames.PLUS} text="Submit supervisor" />
</Popover>
{this.renderBulkSupervisorActions()} {this.renderBulkSupervisorActions()}
<TableColumnSelector <TableColumnSelector
columns={supervisorTableColumns} columns={supervisorTableColumns}
@ -1172,9 +1170,6 @@ ORDER BY "rank" DESC, "created_time" DESC`;
localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE} localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE}
onRefresh={auto => this.taskQueryManager.rerunLastQuery(auto)} onRefresh={auto => this.taskQueryManager.rerunLastQuery(auto)}
/> />
<Popover content={submitTaskMenu} position={Position.BOTTOM_LEFT}>
<Button icon={IconNames.PLUS} text="Submit task" />
</Popover>
{this.renderBulkTasksActions()} {this.renderBulkTasksActions()}
<TableColumnSelector <TableColumnSelector
columns={taskTableColumns} columns={taskTableColumns}