2019-01-31 20:26:41 -05:00
|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2019-04-07 23:19:08 -04:00
|
|
|
import { Intent } from '@blueprintjs/core';
|
|
|
|
import { IconNames } from '@blueprintjs/icons';
|
2019-01-31 20:26:41 -05:00
|
|
|
import axios from 'axios';
|
|
|
|
import * as classNames from 'classnames';
|
2019-03-17 12:23:17 -04:00
|
|
|
import * as React from 'react';
|
2019-04-07 23:19:08 -04:00
|
|
|
import { HashRouter, Route, Switch } from 'react-router-dom';
|
2019-03-17 12:23:17 -04:00
|
|
|
|
2019-05-22 23:36:51 -04:00
|
|
|
import { ExternalLink } from './components/external-link/external-link';
|
|
|
|
import { HeaderActiveTab, HeaderBar } from './components/header-bar/header-bar';
|
|
|
|
import { Loader } from './components/loader/loader';
|
2019-01-31 20:26:41 -05:00
|
|
|
import { AppToaster } from './singletons/toaster';
|
2019-05-01 04:53:03 -04:00
|
|
|
import { UrlBaser } from './singletons/url-baser';
|
2019-05-03 20:14:57 -04:00
|
|
|
import { QueryManager } from './utils';
|
|
|
|
import { DRUID_DOCS_API, DRUID_DOCS_SQL } from './variables';
|
2019-05-22 23:36:51 -04:00
|
|
|
import { DatasourcesView } from './views/datasource-view/datasource-view';
|
|
|
|
import { HomeView } from './views/home-view/home-view';
|
|
|
|
import { LoadDataView } from './views/load-data-view/load-data-view';
|
|
|
|
import { LookupsView } from './views/lookups-view/lookups-view';
|
|
|
|
import { SegmentsView } from './views/segments-view/segments-view';
|
|
|
|
import { ServersView } from './views/servers-view/servers-view';
|
|
|
|
import { SqlView } from './views/sql-view/sql-view';
|
|
|
|
import { TasksView } from './views/task-view/tasks-view';
|
2019-03-17 12:23:17 -04:00
|
|
|
|
2019-04-07 23:19:08 -04:00
|
|
|
import './console-application.scss';
|
2019-01-31 20:26:41 -05:00
|
|
|
|
|
|
|
export interface ConsoleApplicationProps extends React.Props<any> {
|
2019-04-05 15:40:43 -04:00
|
|
|
hideLegacy: boolean;
|
|
|
|
baseURL?: string;
|
2019-04-12 00:58:09 -04:00
|
|
|
customHeaderName?: string;
|
|
|
|
customHeaderValue?: string;
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface ConsoleApplicationState {
|
|
|
|
aboutDialogOpen: boolean;
|
2019-04-23 19:15:02 -04:00
|
|
|
noSqlMode: boolean;
|
|
|
|
capabilitiesLoading: boolean;
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
2019-06-07 11:31:46 -04:00
|
|
|
export class ConsoleApplication extends React.PureComponent<ConsoleApplicationProps, ConsoleApplicationState> {
|
2019-01-31 20:26:41 -05:00
|
|
|
static MESSAGE_KEY = 'druid-console-message';
|
|
|
|
static MESSAGE_DISMISSED = 'dismissed';
|
2019-04-23 19:15:02 -04:00
|
|
|
private capabilitiesQueryManager: QueryManager<string, string>;
|
2019-01-31 20:26:41 -05:00
|
|
|
|
2019-04-23 19:15:02 -04:00
|
|
|
static async discoverCapabilities(): Promise<'working-with-sql' | 'working-without-sql' | 'broken'> {
|
2019-01-31 20:26:41 -05:00
|
|
|
try {
|
2019-04-07 23:19:08 -04:00
|
|
|
await axios.post('/druid/v2/sql', { query: 'SELECT 1337' });
|
2019-01-31 20:26:41 -05:00
|
|
|
} catch (e) {
|
|
|
|
const { response } = e;
|
2019-04-23 19:15:02 -04:00
|
|
|
if (response.status !== 405 || response.statusText !== 'Method Not Allowed') return 'working-with-sql'; // other failure
|
2019-01-31 20:26:41 -05:00
|
|
|
try {
|
2019-04-07 23:19:08 -04:00
|
|
|
await axios.get('/status');
|
2019-01-31 20:26:41 -05:00
|
|
|
} catch (e) {
|
2019-04-23 19:15:02 -04:00
|
|
|
return 'broken'; // total failure
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
// Status works but SQL 405s => the SQL endpoint is disabled
|
2019-04-23 19:15:02 -04:00
|
|
|
return 'working-without-sql';
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
2019-04-23 19:15:02 -04:00
|
|
|
return 'working-with-sql';
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
2019-04-23 19:15:02 -04:00
|
|
|
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
|
2019-05-03 20:14:57 -04:00
|
|
|
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
|
2019-04-23 19:15:02 -04:00
|
|
|
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
|
|
|
|
});
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
2019-05-25 01:52:26 -04:00
|
|
|
private supervisorId: string | null;
|
2019-01-31 20:26:41 -05:00
|
|
|
private taskId: string | null;
|
2019-05-17 17:01:27 -04:00
|
|
|
private openDialog: string | null;
|
2019-01-31 20:26:41 -05:00
|
|
|
private datasource: string | null;
|
|
|
|
private onlyUnavailable: boolean | null;
|
|
|
|
private initSql: string | null;
|
|
|
|
private middleManager: string | null;
|
|
|
|
|
|
|
|
constructor(props: ConsoleApplicationProps, context: any) {
|
|
|
|
super(props, context);
|
|
|
|
this.state = {
|
2019-04-23 19:15:02 -04:00
|
|
|
aboutDialogOpen: false,
|
|
|
|
noSqlMode: false,
|
|
|
|
capabilitiesLoading: true
|
2019-01-31 20:26:41 -05:00
|
|
|
};
|
2019-04-05 15:40:43 -04:00
|
|
|
|
|
|
|
if (props.baseURL) {
|
|
|
|
axios.defaults.baseURL = props.baseURL;
|
2019-05-01 04:53:03 -04:00
|
|
|
UrlBaser.baseURL = props.baseURL;
|
2019-04-05 15:40:43 -04:00
|
|
|
}
|
2019-04-12 00:58:09 -04:00
|
|
|
if (props.customHeaderName && props.customHeaderValue) {
|
|
|
|
axios.defaults.headers.common[props.customHeaderName] = props.customHeaderValue;
|
|
|
|
}
|
2019-04-23 19:15:02 -04:00
|
|
|
|
|
|
|
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
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount(): void {
|
2019-04-23 19:15:02 -04:00
|
|
|
this.capabilitiesQueryManager.runQuery('dummy');
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount(): void {
|
|
|
|
this.capabilitiesQueryManager.terminate();
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
2019-05-03 20:14:57 -04:00
|
|
|
private resetInitialsWithDelay() {
|
2019-01-31 20:26:41 -05:00
|
|
|
setTimeout(() => {
|
|
|
|
this.taskId = null;
|
2019-05-25 01:52:26 -04:00
|
|
|
this.supervisorId = null;
|
2019-05-17 17:01:27 -04:00
|
|
|
this.openDialog = null;
|
2019-01-31 20:26:41 -05:00
|
|
|
this.datasource = null;
|
|
|
|
this.onlyUnavailable = null;
|
|
|
|
this.initSql = null;
|
|
|
|
this.middleManager = null;
|
|
|
|
}, 50);
|
|
|
|
}
|
|
|
|
|
2019-05-25 01:52:26 -04:00
|
|
|
private goToLoadDataView = (supervisorId?: string, taskId?: string ) => {
|
|
|
|
if (taskId) this.taskId = taskId;
|
|
|
|
if (supervisorId) this.supervisorId = supervisorId;
|
2019-05-03 20:14:57 -04:00
|
|
|
window.location.hash = 'load-data';
|
|
|
|
this.resetInitialsWithDelay();
|
|
|
|
}
|
|
|
|
|
2019-05-17 17:01:27 -04:00
|
|
|
private goToTask = (taskId: string | null, openDialog?: string) => {
|
2019-01-31 20:26:41 -05:00
|
|
|
this.taskId = taskId;
|
2019-05-17 17:01:27 -04:00
|
|
|
if (openDialog) this.openDialog = openDialog;
|
2019-01-31 20:26:41 -05:00
|
|
|
window.location.hash = 'tasks';
|
2019-05-03 20:14:57 -04:00
|
|
|
this.resetInitialsWithDelay();
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
private goToSegments = (datasource: string, onlyUnavailable = false) => {
|
2019-04-19 18:51:45 -04:00
|
|
|
this.datasource = `"${datasource}"`;
|
2019-01-31 20:26:41 -05:00
|
|
|
this.onlyUnavailable = onlyUnavailable;
|
|
|
|
window.location.hash = 'segments';
|
2019-05-03 20:14:57 -04:00
|
|
|
this.resetInitialsWithDelay();
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
private goToMiddleManager = (middleManager: string) => {
|
|
|
|
this.middleManager = middleManager;
|
|
|
|
window.location.hash = 'servers';
|
2019-05-03 20:14:57 -04:00
|
|
|
this.resetInitialsWithDelay();
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
private goToSql = (initSql: string) => {
|
|
|
|
this.initSql = initSql;
|
2019-05-03 20:14:57 -04:00
|
|
|
window.location.hash = 'query';
|
|
|
|
this.resetInitialsWithDelay();
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
|
2019-05-24 18:01:45 -04:00
|
|
|
private wrapInViewContainer = (active: HeaderActiveTab, el: JSX.Element, classType: 'normal' | 'narrow-pad' = 'normal') => {
|
2019-04-05 15:40:43 -04:00
|
|
|
const { hideLegacy } = this.props;
|
|
|
|
|
2019-05-03 20:14:57 -04:00
|
|
|
return <>
|
2019-05-25 01:52:26 -04:00
|
|
|
<HeaderBar active={active} hideLegacy={hideLegacy}/>
|
2019-05-07 11:45:20 -04:00
|
|
|
<div className={classNames('view-container', classType)}>{el}</div>
|
2019-05-03 20:14:57 -04:00
|
|
|
</>;
|
|
|
|
}
|
|
|
|
|
|
|
|
private wrappedHomeView = () => {
|
|
|
|
const { noSqlMode } = this.state;
|
|
|
|
return this.wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode}/>);
|
|
|
|
}
|
|
|
|
|
|
|
|
private wrappedLoadDataView = () => {
|
2019-05-25 01:52:26 -04:00
|
|
|
|
|
|
|
return this.wrapInViewContainer('load-data', <LoadDataView initSupervisorId={this.supervisorId} initTaskId={this.taskId} goToTask={this.goToTask}/>, 'narrow-pad');
|
2019-05-03 20:14:57 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private wrappedSqlView = () => {
|
|
|
|
return this.wrapInViewContainer('query', <SqlView initSql={this.initSql}/>);
|
|
|
|
}
|
|
|
|
|
|
|
|
private wrappedDatasourcesView = () => {
|
|
|
|
const { noSqlMode } = this.state;
|
|
|
|
return this.wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
|
|
|
|
}
|
|
|
|
|
|
|
|
private wrappedSegmentsView = () => {
|
|
|
|
const { noSqlMode } = this.state;
|
|
|
|
return this.wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToSql={this.goToSql} noSqlMode={noSqlMode}/>);
|
|
|
|
}
|
|
|
|
|
|
|
|
private wrappedTasksView = () => {
|
|
|
|
const { noSqlMode } = this.state;
|
2019-05-24 18:01:45 -04:00
|
|
|
return this.wrapInViewContainer('tasks', <TasksView taskId={this.taskId} openDialog={this.openDialog} goToSql={this.goToSql} goToMiddleManager={this.goToMiddleManager} goToLoadDataView={this.goToLoadDataView} noSqlMode={noSqlMode}/>);
|
2019-05-03 20:14:57 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private wrappedServersView = () => {
|
|
|
|
const { noSqlMode } = this.state;
|
2019-05-24 18:01:45 -04:00
|
|
|
return this.wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToSql={this.goToSql} goToTask={this.goToTask} noSqlMode={noSqlMode}/>);
|
2019-05-03 20:14:57 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
private wrappedLookupsView = () => {
|
|
|
|
return this.wrapInViewContainer('lookups', <LookupsView/>);
|
|
|
|
}
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const { capabilitiesLoading } = this.state;
|
2019-01-31 20:26:41 -05:00
|
|
|
|
2019-04-23 19:15:02 -04:00
|
|
|
if (capabilitiesLoading) {
|
2019-05-06 06:20:12 -04:00
|
|
|
return <div className="loading-capabilities">
|
2019-04-23 19:15:02 -04:00
|
|
|
<Loader
|
2019-05-06 06:20:12 -04:00
|
|
|
loadingText=""
|
2019-04-23 19:15:02 -04:00
|
|
|
loading={capabilitiesLoading}
|
|
|
|
/>
|
|
|
|
</div>;
|
|
|
|
}
|
|
|
|
|
2019-01-31 20:26:41 -05:00
|
|
|
return <HashRouter hashType="noslash">
|
|
|
|
<div className="console-application">
|
|
|
|
<Switch>
|
2019-05-03 20:14:57 -04:00
|
|
|
<Route path="/load-data" component={this.wrappedLoadDataView}/>
|
|
|
|
<Route path="/query" component={this.wrappedSqlView}/>
|
|
|
|
<Route path="/sql" component={this.wrappedSqlView}/>
|
|
|
|
|
|
|
|
<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="/lookups" component={this.wrappedLookupsView}/>
|
|
|
|
<Route component={this.wrappedHomeView}/>
|
2019-01-31 20:26:41 -05:00
|
|
|
</Switch>
|
|
|
|
</div>
|
2019-02-25 23:54:56 -05:00
|
|
|
</HashRouter>;
|
2019-01-31 20:26:41 -05:00
|
|
|
}
|
|
|
|
}
|