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

View File

@ -23,7 +23,9 @@ import { HeaderBar } from './header-bar';
describe('header bar', () => {
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();
});
});

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 { DoctorDialog } from '../../dialogs/doctor-dialog/doctor-dialog';
import { OverlordDynamicConfigDialog } from '../../dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
import { Capabilities } from '../../utils/capabilities';
import {
DRUID_ASF_SLACK,
DRUID_DOCS,
@ -130,10 +131,11 @@ function LegacyMenu() {
export interface HeaderBarProps {
active: HeaderActiveTab;
hideLegacy: boolean;
capabilities: Capabilities;
}
export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
const { active, hideLegacy } = props;
const { active, hideLegacy, capabilities } = props;
const [aboutDialogOpen, setAboutDialogOpen] = useState(false);
const [doctorDialogOpen, setDoctorDialogOpen] = useState(false);
const [coordinatorDynamicConfigDialogOpen, setCoordinatorDynamicConfigDialogOpen] = useState(
@ -198,6 +200,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
href="#load-data"
minimal={!loadDataPrimary}
intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE}
disabled={capabilities === 'no-proxy'}
/>
<NavbarDivider />
@ -241,12 +244,25 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
{!hideLegacy && (
<Popover content={<LegacyMenu />} position={Position.BOTTOM_RIGHT}>
<Button minimal icon={IconNames.SHARE} text="Legacy" />
<Popover
content={<LegacyMenu />}
position={Position.BOTTOM_RIGHT}
disabled={capabilities === 'no-proxy'}
>
<Button
minimal
icon={IconNames.SHARE}
text="Legacy"
disabled={capabilities === 'no-proxy'}
/>
</Popover>
)}
<Popover content={configMenu} position={Position.BOTTOM_RIGHT}>
<Button minimal icon={IconNames.COG} />
<Popover
content={configMenu}
position={Position.BOTTOM_RIGHT}
disabled={capabilities === 'no-proxy'}
>
<Button minimal icon={IconNames.COG} disabled={capabilities === 'no-proxy'} />
</Popover>
<Popover content={helpMenu} position={Position.BOTTOM_RIGHT}>
<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 { AppToaster } from './singletons/toaster';
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 {
DatasourcesView,
HomeView,
@ -40,15 +41,13 @@ import {
import './console-application.scss';
type Capabilities = 'working-with-sql' | 'working-without-sql' | 'broken';
export interface ConsoleApplicationProps {
hideLegacy: boolean;
exampleManifestsUrl?: string;
}
export interface ConsoleApplicationState {
noSqlMode: boolean;
capabilities: Capabilities;
capabilitiesLoading: boolean;
}
@ -64,6 +63,7 @@ export class ConsoleApplication extends React.PureComponent<
const capabilitiesOverride = localStorageGet(LocalStorageKeys.CAPABILITIES_OVERRIDE);
if (capabilitiesOverride) return capabilitiesOverride as Capabilities;
// Check SQL endpoint
try {
await axios.post(
'/druid/v2/sql',
@ -73,7 +73,7 @@ export class ConsoleApplication extends React.PureComponent<
} catch (e) {
const { response } = e;
if (response.status !== 405 || response.statusText !== 'Method Not Allowed') {
return 'working-with-sql'; // other failure
return 'full'; // other failure
}
try {
await axios.get('/status', { timeout: ConsoleApplication.STATUS_TIMEOUT });
@ -81,27 +81,65 @@ export class ConsoleApplication extends React.PureComponent<
return 'broken'; // total failure
}
// Status works but SQL 405s => the SQL endpoint is disabled
return 'working-without-sql';
return 'no-sql';
}
return 'working-with-sql';
// Check proxy
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';
}
return 'full';
}
static shownNotifications(capabilities: string) {
let message: JSX.Element = <></>;
static shownNotifications(capabilities: Capabilities) {
let message: JSX.Element;
switch (capabilities) {
case 'no-sql':
message = (
<>
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
in functionality. Look at{' '}
<ExternalLink href={DRUID_DOCS_SQL}>the SQL docs</ExternalLink> to enable the SQL
endpoint.
</>
);
break;
if (capabilities === 'working-without-sql') {
message = (
<>
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
in functionality. Look at <ExternalLink href={DRUID_DOCS_SQL}>the SQL docs</ExternalLink>{' '}
to enable the SQL endpoint.
</>
);
} else if (capabilities === 'broken') {
message = (
<>It appears that the Druid is not responding. Data cannot be retrieved right now</>
);
case 'no-proxy':
message = (
<>
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({
@ -123,21 +161,21 @@ export class ConsoleApplication extends React.PureComponent<
constructor(props: ConsoleApplicationProps, context: any) {
super(props, context);
this.state = {
noSqlMode: false,
capabilities: 'full',
capabilitiesLoading: true,
};
this.capabilitiesQueryManager = new QueryManager({
processQuery: async () => {
const capabilities = await ConsoleApplication.discoverCapabilities();
if (capabilities !== 'working-with-sql') {
if (capabilities !== 'full') {
ConsoleApplication.shownNotifications(capabilities);
}
return capabilities;
},
onStateChange: ({ result, loading }) => {
this.setState({
noSqlMode: result !== 'working-with-sql',
capabilities: result || 'full',
capabilitiesLoading: loading,
});
},
@ -216,18 +254,19 @@ export class ConsoleApplication extends React.PureComponent<
classType: 'normal' | 'narrow-pad' = 'normal',
) => {
const { hideLegacy } = this.props;
const { capabilities } = this.state;
return (
<>
<HeaderBar active={active} hideLegacy={hideLegacy} />
<HeaderBar active={active} hideLegacy={hideLegacy} capabilities={capabilities} />
<div className={classNames('view-container', classType)}>{el}</div>
</>
);
};
private wrappedHomeView = () => {
const { noSqlMode } = this.state;
return this.wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode} />);
const { capabilities } = this.state;
return this.wrapInViewContainer(null, <HomeView capabilities={capabilities} />);
};
private wrappedLoadDataView = () => {
@ -250,7 +289,7 @@ export class ConsoleApplication extends React.PureComponent<
};
private wrappedDatasourcesView = () => {
const { noSqlMode } = this.state;
const { capabilities } = this.state;
return this.wrapInViewContainer(
'datasources',
<DatasourcesView
@ -258,26 +297,26 @@ export class ConsoleApplication extends React.PureComponent<
goToQuery={this.goToQuery}
goToTask={this.goToTaskWithDatasource}
goToSegments={this.goToSegments}
noSqlMode={noSqlMode}
capabilities={capabilities}
/>,
);
};
private wrappedSegmentsView = () => {
const { noSqlMode } = this.state;
const { capabilities } = this.state;
return this.wrapInViewContainer(
'segments',
<SegmentsView
datasource={this.datasource}
onlyUnavailable={this.onlyUnavailable}
goToQuery={this.goToQuery}
noSqlMode={noSqlMode}
capabilities={capabilities}
/>,
);
};
private wrappedTasksView = () => {
const { noSqlMode } = this.state;
const { capabilities } = this.state;
return this.wrapInViewContainer(
'tasks',
<TasksView
@ -288,20 +327,20 @@ export class ConsoleApplication extends React.PureComponent<
goToQuery={this.goToQuery}
goToMiddleManager={this.goToMiddleManager}
goToLoadData={this.goToLoadData}
noSqlMode={noSqlMode}
capabilities={capabilities}
/>,
);
};
private wrappedServersView = () => {
const { noSqlMode } = this.state;
const { capabilities } = this.state;
return this.wrapInViewContainer(
'servers',
<ServersView
middleManager={this.middleManager}
goToQuery={this.goToQuery}
goToTask={this.goToTaskWithTaskId}
noSqlMode={noSqlMode}
capabilities={capabilities}
/>,
);
};
@ -316,7 +355,7 @@ export class ConsoleApplication extends React.PureComponent<
if (capabilitiesLoading) {
return (
<div className="loading-capabilities">
<Loader loadingText="" loading={capabilitiesLoading} />
<Loader loadingText="" loading />
</div>
);
}
@ -326,13 +365,14 @@ export class ConsoleApplication extends React.PureComponent<
<div className="console-application">
<Switch>
<Route path="/load-data" component={this.wrappedLoadDataView} />
<Route path="/query" component={this.wrappedQueryView} />
<Route path="/datasources" component={this.wrappedDatasourcesView} />
<Route path="/segments" component={this.wrappedSegmentsView} />
<Route path="/tasks" component={this.wrappedTasksView} />
<Route path="/servers" component={this.wrappedServersView} />
<Route path="/query" component={this.wrappedQueryView} />
<Route path="/lookups" component={this.wrappedLookupsView} />
<Route component={this.wrappedHomeView} />
</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={() => {}}
goToTask={() => null}
goToSegments={() => {}}
noSqlMode={false}
capabilities="full"
/>,
);
expect(dataSourceView).toMatchSnapshot();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,10 +20,13 @@ import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
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';
export interface SupervisorsCardProps {}
export interface SupervisorsCardProps {
capabilities: Capabilities;
}
export interface SupervisorsCardState {
supervisorCountLoading: boolean;
@ -36,7 +39,7 @@ export class SupervisorsCard extends React.PureComponent<
SupervisorsCardProps,
SupervisorsCardState
> {
private supervisorQueryManager: QueryManager<null, any>;
private supervisorQueryManager: QueryManager<Capabilities, any>;
constructor(props: SupervisorsCardProps, context: any) {
super(props, context);
@ -47,15 +50,25 @@ export class SupervisorsCard extends React.PureComponent<
};
this.supervisorQueryManager = new QueryManager({
processQuery: async () => {
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
const data = resp.data;
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
const suspendedSupervisorCount = data.filter((d: any) => d.spec.suspended === true).length;
return {
runningSupervisorCount,
suspendedSupervisorCount,
};
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 data = resp.data;
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
const suspendedSupervisorCount = data.filter((d: any) => d.spec.suspended === true)
.length;
return {
runningSupervisorCount,
suspendedSupervisorCount,
};
}
},
onStateChange: ({ result, loading, error }) => {
this.setState({
@ -69,7 +82,9 @@ export class SupervisorsCard extends React.PureComponent<
}
componentDidMount(): void {
this.supervisorQueryManager.runQuery(null);
const { capabilities } = this.props;
this.supervisorQueryManager.runQuery(capabilities);
}
componentWillUnmount(): void {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,12 +31,12 @@ exports[`tasks view matches snapshot 1`] = `
<Blueprint3.Menu>
<Blueprint3.MenuItem
disabled={false}
icon="cloud-upload"
icon="application"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="Go to data loader"
text="View SQL query for table"
/>
<Blueprint3.MenuItem
disabled={false}
@ -47,35 +47,6 @@ exports[`tasks view matches snapshot 1`] = `
shouldDismissPopover={true}
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
disabled={false}
icon="play"
@ -200,7 +171,7 @@ exports[`tasks view matches snapshot 1`] = `
Array [
Object {
"Header": "Datasource",
"accessor": "id",
"accessor": "supervisor_id",
"id": "datasource",
"show": true,
"width": 300,
@ -214,7 +185,7 @@ exports[`tasks view matches snapshot 1`] = `
Object {
"Header": "Topic/Stream",
"accessor": [Function],
"id": "topic",
"id": "source",
"show": true,
},
Object {
@ -228,7 +199,7 @@ exports[`tasks view matches snapshot 1`] = `
Object {
"Cell": [Function],
"Header": "Actions",
"accessor": "id",
"accessor": "supervisor_id",
"filterable": false,
"id": "actions",
"show": true,
@ -380,12 +351,12 @@ exports[`tasks view matches snapshot 1`] = `
<Blueprint3.Menu>
<Blueprint3.MenuItem
disabled={false}
icon="cloud-upload"
icon="application"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="Go to data loader"
text="View SQL query for table"
/>
<Blueprint3.MenuItem
disabled={false}
@ -414,44 +385,6 @@ exports[`tasks view matches snapshot 1`] = `
transitionDuration={300}
usePortal={true}
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
icon="more"

View File

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

View File

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