Skip to content
101 changes: 72 additions & 29 deletions web-console/src/console-application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import * as React from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';

import { HeaderActiveTab, HeaderBar } from './components/header-bar';
import {Loader} from './components/loader';
import { AppToaster } from './singletons/toaster';
import { DRUID_DOCS_SQL, LEGACY_COORDINATOR_CONSOLE, LEGACY_OVERLORD_CONSOLE } from './variables';
import {QueryManager} from './utils';
import {DRUID_DOCS_API, DRUID_DOCS_SQL, LEGACY_COORDINATOR_CONSOLE, LEGACY_OVERLORD_CONSOLE} from './variables';
import { DatasourcesView } from './views/datasource-view';
import { HomeView } from './views/home-view';
import { LookupsView } from './views/lookups-view';
Expand All @@ -45,45 +47,54 @@ export interface ConsoleApplicationProps extends React.Props<any> {

export interface ConsoleApplicationState {
aboutDialogOpen: boolean;
noSqlMode: boolean;
capabilitiesLoading: boolean;
}

export class ConsoleApplication extends React.Component<ConsoleApplicationProps, ConsoleApplicationState> {
static MESSAGE_KEY = 'druid-console-message';
static MESSAGE_DISMISSED = 'dismissed';
private capabilitiesQueryManager: QueryManager<string, string>;

static async ensureSql() {
static async discoverCapabilities(): Promise<'working-with-sql' | 'working-without-sql' | 'broken'> {
try {
await axios.post('/druid/v2/sql', { query: 'SELECT 1337' });
} catch (e) {
const { response } = e;
if (response.status !== 405 || response.statusText !== 'Method Not Allowed') return true; // other failure
if (response.status !== 405 || response.statusText !== 'Method Not Allowed') return 'working-with-sql'; // other failure
try {
await axios.get('/status');
} catch (e) {
return true; // total failure
return 'broken'; // total failure
}

// Status works but SQL 405s => the SQL endpoint is disabled
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
timeout: 120000,
/* tslint:disable:jsx-alignment */
message: <>
It appears that the SQL endpoint is disabled. Either <a
href={DRUID_DOCS_SQL}>enable the SQL endpoint</a> or use the old <a
href={LEGACY_COORDINATOR_CONSOLE}>coordinator</a> and <a
href={LEGACY_OVERLORD_CONSOLE}>overlord</a> consoles that do not rely on the SQL endpoint.
</>
/* tslint:enable:jsx-alignment */
});
return false;
return 'working-without-sql';
}
return true;
return 'working-with-sql';
}

static async shownNotifications() {
await ConsoleApplication.ensureSql();
static shownNotifications(capabilities: string) {
let message: JSX.Element = <></>;
/* tslint:disable:jsx-alignment */
if (capabilities === 'working-without-sql') {
message = <>
It appears that the SQL endpoint is disabled. The console will fall back
to <a href={DRUID_DOCS_API} target="_blank">native Druid APIs</a> and will be
limited in functionality. Look at <a href={DRUID_DOCS_SQL} target="_blank">the SQL docs</a> to
enable the SQL endpoint.
</>;
} else if (capabilities === 'broken') {
message = <>
It appears that the Druid is not responding. Data cannot be retrieved right now
</>;
}
/* tslint:enable:jsx-alignment */
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
timeout: 120000,
message: message
});
}

private taskId: string | null;
Expand All @@ -95,7 +106,9 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
constructor(props: ConsoleApplicationProps, context: any) {
super(props, context);
this.state = {
aboutDialogOpen: false
aboutDialogOpen: false,
noSqlMode: false,
capabilitiesLoading: true
};

if (props.baseURL) {
Expand All @@ -104,10 +117,30 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
if (props.customHeaderName && props.customHeaderValue) {
axios.defaults.headers.common[props.customHeaderName] = props.customHeaderValue;
}

this.capabilitiesQueryManager = new QueryManager({
processQuery: async (query: string) => {
const capabilities = await ConsoleApplication.discoverCapabilities();
if (capabilities !== 'working-with-sql') {
ConsoleApplication.shownNotifications(capabilities);
}
return capabilities;
},
onStateChange: ({ result, loading, error }) => {
this.setState({
noSqlMode: result === 'working-with-sql' ? false : true,
capabilitiesLoading: loading
});
}
});
}

componentDidMount(): void {
ConsoleApplication.shownNotifications();
this.capabilitiesQueryManager.runQuery('dummy');
}

componentWillUnmount(): void {
this.capabilitiesQueryManager.terminate();
}

private resetInitialsDelay() {
Expand Down Expand Up @@ -147,6 +180,7 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,

render() {
const { hideLegacy } = this.props;
const { noSqlMode, capabilitiesLoading } = this.state;

const wrapInViewContainer = (active: HeaderActiveTab, el: JSX.Element, scrollable = false) => {
return <>
Expand All @@ -155,31 +189,40 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
</>;
};

if (capabilitiesLoading) {
return <div className={'loading-capabilities'}>
<Loader
loadingText={''}
loading={capabilitiesLoading}
/>
</div>;
}

return <HashRouter hashType="noslash">
<div className="console-application">
<Switch>
<Route
path="/datasources"
component={() => {
return wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments}/>);
return wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
}}
/>
<Route
path="/segments"
component={() => {
return wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToSql={this.goToSql}/>);
return wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToSql={this.goToSql} noSqlMode={noSqlMode}/>);
}}
/>
<Route
path="/tasks"
component={() => {
return wrapInViewContainer('tasks', <TasksView taskId={this.taskId} goToSql={this.goToSql} goToMiddleManager={this.goToMiddleManager}/>, true);
return wrapInViewContainer('tasks', <TasksView taskId={this.taskId} goToSql={this.goToSql} goToMiddleManager={this.goToMiddleManager} noSqlMode={noSqlMode}/>, true);
}}
/>
<Route
path="/servers"
component={() => {
return wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToSql={this.goToSql} goToTask={this.goToTask}/>, true);
return wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToSql={this.goToSql} goToTask={this.goToTask} noSqlMode={noSqlMode}/>, true);
}}
/>
<Route
Expand All @@ -196,7 +239,7 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
/>
<Route
component={() => {
return wrapInViewContainer(null, <HomeView/>);
return wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode}/>);
}}
/>
</Switch>
Expand Down
1 change: 1 addition & 0 deletions web-console/src/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export const DRUID_DOCS_SQL = 'http://druid.io/docs/latest/querying/sql.html';
export const DRUID_COMMUNITY = 'http://druid.io/community/';
export const DRUID_USER_GROUP = 'https://groups.google.com/forum/#!forum/druid-user';
export const DRUID_DEVELOPER_GROUP = 'https://lists.apache.org/list.html?dev@druid.apache.org';
export const DRUID_DOCS_API = 'http://druid.io/docs/latest/operations/api-reference.html';
54 changes: 43 additions & 11 deletions web-console/src/views/datasource-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ import {
import './datasource-view.scss';

const tableColumns: string[] = ['Datasource', 'Availability', 'Retention', 'Compaction', 'Size', 'Num rows', 'Actions'];
const tableColumnsNoSql: string[] = ['Datasource', 'Availability', 'Retention', 'Compaction', 'Size', 'Actions'];

export interface DatasourcesViewProps extends React.Props<any> {
goToSql: (initSql: string) => void;
goToSegments: (datasource: string, onlyUnavailable?: boolean) => void;
noSqlMode: boolean;
}

interface Datasource {
Expand All @@ -56,6 +58,14 @@ interface Datasource {
[key: string]: any;
}

interface DatasourceQueryResultRow {
datasource: string;
num_available_segments: number;
num_rows: number;
num_segments: number;
size: number;
}

export interface DatasourcesViewState {
datasourcesLoading: boolean;
datasources: Datasource[] | null;
Expand Down Expand Up @@ -116,9 +126,28 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
}

componentDidMount(): void {
const { noSqlMode } = this.props;

this.datasourceQueryManager = new QueryManager({
processQuery: async (query: string) => {
const datasources: any[] = await queryDruidSql({ query });
let datasources: DatasourceQueryResultRow[];
if (!noSqlMode) {
datasources = await queryDruidSql({ query });
} else {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple');
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
const loadstatus = loadstatusResp.data;
datasources = datasourcesResp.data.map((d: any) => {
return {
datasource: d.name,
num_available_segments: d.properties.segments.count,
size: d.properties.segments.size,
num_segments: d.properties.segments.count + loadstatus[d.name],
num_rows: -1
};
});
}

const seen = countBy(datasources, (x: any) => x.datasource);

const disabledResp = await axios.get('/druid/coordinator/v1/metadata/datasources?includeDisabled');
Expand All @@ -133,7 +162,7 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
const tiersResp = await axios.get('/druid/coordinator/v1/tiers');
const tiers = tiersResp.data;

const allDatasources = datasources.concat(disabled.map(d => ({ datasource: d, disabled: true })));
const allDatasources = (datasources as any).concat(disabled.map(d => ({ datasource: d, disabled: true })));
allDatasources.forEach((ds: any) => {
ds.rules = rules[ds.datasource] || [];
ds.compaction = compaction[ds.datasource];
Expand Down Expand Up @@ -354,7 +383,7 @@ GROUP BY 1`);
}

renderDatasourceTable() {
const { goToSegments } = this.props;
const { goToSegments, noSqlMode } = this.props;
const { datasources, defaultRules, datasourcesLoading, datasourcesError, datasourcesFilter, showDisabled } = this.state;
const { tableColumnSelectionHandler } = this;
let data = datasources || [];
Expand Down Expand Up @@ -492,7 +521,7 @@ GROUP BY 1`);
filterable: false,
width: 100,
Cell: (row) => formatNumber(row.value),
show: tableColumnSelectionHandler.showColumn('Num rows')
show: !noSqlMode && tableColumnSelectionHandler.showColumn('Num rows')
},
{
Header: 'Actions',
Expand Down Expand Up @@ -529,7 +558,7 @@ GROUP BY 1`);
}

render() {
const { goToSql } = this.props;
const { goToSql, noSqlMode } = this.props;
const { showDisabled } = this.state;
const { tableColumnSelectionHandler } = this;

Expand All @@ -540,18 +569,21 @@ GROUP BY 1`);
text="Refresh"
onClick={() => this.datasourceQueryManager.rerunLastQuery()}
/>
<Button
icon={IconNames.APPLICATION}
text="Go to SQL"
onClick={() => goToSql(this.datasourceQueryManager.getLastQuery())}
/>
{
!noSqlMode &&
<Button
icon={IconNames.APPLICATION}
text="Go to SQL"
onClick={() => goToSql(this.datasourceQueryManager.getLastQuery())}
/>
}
<Switch
checked={showDisabled}
label="Show disabled"
onChange={() => this.setState({ showDisabled: !showDisabled })}
/>
<TableColumnSelection
columns={tableColumns}
columns={noSqlMode ? tableColumnsNoSql : tableColumns}
onChange={(column) => tableColumnSelectionHandler.changeTableColumnSelection(column)}
tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
/>
Expand Down
Loading