mirror of
https://github.com/apache/druid.git
synced 2025-02-20 17:11:47 +00:00
No SQL mode in web console (#7493)
* Added no sql mode * Use status code * Add no sql mode to server view * add sql broker check to decide if no sql mode should be enabled * Fix historicals in home view * Name change * Add types for query result; improved functions * Fixed a conflict/bug * Fixed a bug * multiple fix * removed unused imports * terminate query manager * fix wording
This commit is contained in:
parent
8b1a4e18dd
commit
11a7e91a73
@ -24,8 +24,10 @@ import * as React from 'react';
|
|||||||
import { HashRouter, Route, Switch } from 'react-router-dom';
|
import { HashRouter, Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import { HeaderActiveTab, HeaderBar } from './components/header-bar';
|
import { HeaderActiveTab, HeaderBar } from './components/header-bar';
|
||||||
|
import {Loader} from './components/loader';
|
||||||
import { AppToaster } from './singletons/toaster';
|
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 { DatasourcesView } from './views/datasource-view';
|
||||||
import { HomeView } from './views/home-view';
|
import { HomeView } from './views/home-view';
|
||||||
import { LookupsView } from './views/lookups-view';
|
import { LookupsView } from './views/lookups-view';
|
||||||
@ -45,45 +47,54 @@ export interface ConsoleApplicationProps extends React.Props<any> {
|
|||||||
|
|
||||||
export interface ConsoleApplicationState {
|
export interface ConsoleApplicationState {
|
||||||
aboutDialogOpen: boolean;
|
aboutDialogOpen: boolean;
|
||||||
|
noSqlMode: boolean;
|
||||||
|
capabilitiesLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConsoleApplication extends React.Component<ConsoleApplicationProps, ConsoleApplicationState> {
|
export class ConsoleApplication extends React.Component<ConsoleApplicationProps, ConsoleApplicationState> {
|
||||||
static MESSAGE_KEY = 'druid-console-message';
|
static MESSAGE_KEY = 'druid-console-message';
|
||||||
static MESSAGE_DISMISSED = 'dismissed';
|
static MESSAGE_DISMISSED = 'dismissed';
|
||||||
|
private capabilitiesQueryManager: QueryManager<string, string>;
|
||||||
|
|
||||||
static async ensureSql() {
|
static async discoverCapabilities(): Promise<'working-with-sql' | 'working-without-sql' | 'broken'> {
|
||||||
try {
|
try {
|
||||||
await axios.post('/druid/v2/sql', { query: 'SELECT 1337' });
|
await axios.post('/druid/v2/sql', { query: 'SELECT 1337' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { response } = 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 {
|
try {
|
||||||
await axios.get('/status');
|
await axios.get('/status');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return true; // total failure
|
return 'broken'; // total failure
|
||||||
|
}
|
||||||
|
// Status works but SQL 405s => the SQL endpoint is disabled
|
||||||
|
return 'working-without-sql';
|
||||||
|
}
|
||||||
|
return 'working-with-sql';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status works but SQL 405s => the SQL endpoint is disabled
|
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({
|
AppToaster.show({
|
||||||
icon: IconNames.ERROR,
|
icon: IconNames.ERROR,
|
||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
/* tslint:disable:jsx-alignment */
|
message: message
|
||||||
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 true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async shownNotifications() {
|
|
||||||
await ConsoleApplication.ensureSql();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private taskId: string | null;
|
private taskId: string | null;
|
||||||
@ -95,7 +106,9 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||||||
constructor(props: ConsoleApplicationProps, context: any) {
|
constructor(props: ConsoleApplicationProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
aboutDialogOpen: false
|
aboutDialogOpen: false,
|
||||||
|
noSqlMode: false,
|
||||||
|
capabilitiesLoading: true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (props.baseURL) {
|
if (props.baseURL) {
|
||||||
@ -104,10 +117,30 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||||||
if (props.customHeaderName && props.customHeaderValue) {
|
if (props.customHeaderName && props.customHeaderValue) {
|
||||||
axios.defaults.headers.common[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 {
|
componentDidMount(): void {
|
||||||
ConsoleApplication.shownNotifications();
|
this.capabilitiesQueryManager.runQuery('dummy');
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
this.capabilitiesQueryManager.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetInitialsDelay() {
|
private resetInitialsDelay() {
|
||||||
@ -147,6 +180,7 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { hideLegacy } = this.props;
|
const { hideLegacy } = this.props;
|
||||||
|
const { noSqlMode, capabilitiesLoading } = this.state;
|
||||||
|
|
||||||
const wrapInViewContainer = (active: HeaderActiveTab, el: JSX.Element, scrollable = false) => {
|
const wrapInViewContainer = (active: HeaderActiveTab, el: JSX.Element, scrollable = false) => {
|
||||||
return <>
|
return <>
|
||||||
@ -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">
|
return <HashRouter hashType="noslash">
|
||||||
<div className="console-application">
|
<div className="console-application">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
path="/datasources"
|
path="/datasources"
|
||||||
component={() => {
|
component={() => {
|
||||||
return wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments}/>);
|
return wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/segments"
|
path="/segments"
|
||||||
component={() => {
|
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
|
<Route
|
||||||
path="/tasks"
|
path="/tasks"
|
||||||
component={() => {
|
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
|
<Route
|
||||||
path="/servers"
|
path="/servers"
|
||||||
component={() => {
|
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
|
<Route
|
||||||
@ -196,7 +239,7 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
component={() => {
|
component={() => {
|
||||||
return wrapInViewContainer(null, <HomeView/>);
|
return wrapInViewContainer(null, <HomeView noSqlMode={noSqlMode}/>);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -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_COMMUNITY = 'http://druid.io/community/';
|
||||||
export const DRUID_USER_GROUP = 'https://groups.google.com/forum/#!forum/druid-user';
|
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_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';
|
||||||
|
@ -44,10 +44,12 @@ import {
|
|||||||
import './datasource-view.scss';
|
import './datasource-view.scss';
|
||||||
|
|
||||||
const tableColumns: string[] = ['Datasource', 'Availability', 'Retention', 'Compaction', 'Size', 'Num rows', 'Actions'];
|
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> {
|
export interface DatasourcesViewProps extends React.Props<any> {
|
||||||
goToSql: (initSql: string) => void;
|
goToSql: (initSql: string) => void;
|
||||||
goToSegments: (datasource: string, onlyUnavailable?: boolean) => void;
|
goToSegments: (datasource: string, onlyUnavailable?: boolean) => void;
|
||||||
|
noSqlMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Datasource {
|
interface Datasource {
|
||||||
@ -56,6 +58,14 @@ interface Datasource {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DatasourceQueryResultRow {
|
||||||
|
datasource: string;
|
||||||
|
num_available_segments: number;
|
||||||
|
num_rows: number;
|
||||||
|
num_segments: number;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DatasourcesViewState {
|
export interface DatasourcesViewState {
|
||||||
datasourcesLoading: boolean;
|
datasourcesLoading: boolean;
|
||||||
datasources: Datasource[] | null;
|
datasources: Datasource[] | null;
|
||||||
@ -116,9 +126,28 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
const { noSqlMode } = this.props;
|
||||||
|
|
||||||
this.datasourceQueryManager = new QueryManager({
|
this.datasourceQueryManager = new QueryManager({
|
||||||
processQuery: async (query: string) => {
|
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 seen = countBy(datasources, (x: any) => x.datasource);
|
||||||
|
|
||||||
const disabledResp = await axios.get('/druid/coordinator/v1/metadata/datasources?includeDisabled');
|
const disabledResp = await axios.get('/druid/coordinator/v1/metadata/datasources?includeDisabled');
|
||||||
@ -133,7 +162,7 @@ export class DatasourcesView extends React.Component<DatasourcesViewProps, Datas
|
|||||||
const tiersResp = await axios.get('/druid/coordinator/v1/tiers');
|
const tiersResp = await axios.get('/druid/coordinator/v1/tiers');
|
||||||
const tiers = tiersResp.data;
|
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) => {
|
allDatasources.forEach((ds: any) => {
|
||||||
ds.rules = rules[ds.datasource] || [];
|
ds.rules = rules[ds.datasource] || [];
|
||||||
ds.compaction = compaction[ds.datasource];
|
ds.compaction = compaction[ds.datasource];
|
||||||
@ -354,7 +383,7 @@ GROUP BY 1`);
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderDatasourceTable() {
|
renderDatasourceTable() {
|
||||||
const { goToSegments } = this.props;
|
const { goToSegments, noSqlMode } = this.props;
|
||||||
const { datasources, defaultRules, datasourcesLoading, datasourcesError, datasourcesFilter, showDisabled } = this.state;
|
const { datasources, defaultRules, datasourcesLoading, datasourcesError, datasourcesFilter, showDisabled } = this.state;
|
||||||
const { tableColumnSelectionHandler } = this;
|
const { tableColumnSelectionHandler } = this;
|
||||||
let data = datasources || [];
|
let data = datasources || [];
|
||||||
@ -492,7 +521,7 @@ GROUP BY 1`);
|
|||||||
filterable: false,
|
filterable: false,
|
||||||
width: 100,
|
width: 100,
|
||||||
Cell: (row) => formatNumber(row.value),
|
Cell: (row) => formatNumber(row.value),
|
||||||
show: tableColumnSelectionHandler.showColumn('Num rows')
|
show: !noSqlMode && tableColumnSelectionHandler.showColumn('Num rows')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Actions',
|
Header: 'Actions',
|
||||||
@ -529,7 +558,7 @@ GROUP BY 1`);
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { goToSql } = this.props;
|
const { goToSql, noSqlMode } = this.props;
|
||||||
const { showDisabled } = this.state;
|
const { showDisabled } = this.state;
|
||||||
const { tableColumnSelectionHandler } = this;
|
const { tableColumnSelectionHandler } = this;
|
||||||
|
|
||||||
@ -540,18 +569,21 @@ GROUP BY 1`);
|
|||||||
text="Refresh"
|
text="Refresh"
|
||||||
onClick={() => this.datasourceQueryManager.rerunLastQuery()}
|
onClick={() => this.datasourceQueryManager.rerunLastQuery()}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
!noSqlMode &&
|
||||||
<Button
|
<Button
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="Go to SQL"
|
text="Go to SQL"
|
||||||
onClick={() => goToSql(this.datasourceQueryManager.getLastQuery())}
|
onClick={() => goToSql(this.datasourceQueryManager.getLastQuery())}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<Switch
|
<Switch
|
||||||
checked={showDisabled}
|
checked={showDisabled}
|
||||||
label="Show disabled"
|
label="Show disabled"
|
||||||
onChange={() => this.setState({ showDisabled: !showDisabled })}
|
onChange={() => this.setState({ showDisabled: !showDisabled })}
|
||||||
/>
|
/>
|
||||||
<TableColumnSelection
|
<TableColumnSelection
|
||||||
columns={tableColumns}
|
columns={noSqlMode ? tableColumnsNoSql : tableColumns}
|
||||||
onChange={(column) => tableColumnSelectionHandler.changeTableColumnSelection(column)}
|
onChange={(column) => tableColumnSelectionHandler.changeTableColumnSelection(column)}
|
||||||
tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
|
tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
|
||||||
/>
|
/>
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
import { Card, H5, Icon } from '@blueprintjs/core';
|
import { Card, H5, Icon } from '@blueprintjs/core';
|
||||||
import { IconName, IconNames } from '@blueprintjs/icons';
|
import { IconName, IconNames } from '@blueprintjs/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as classNames from 'classnames';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { getHeadProp, pluralIfNeeded, queryDruidSql, QueryManager } from '../utils';
|
import { getHeadProp, pluralIfNeeded, queryDruidSql, QueryManager } from '../utils';
|
||||||
@ -36,6 +35,7 @@ export interface CardOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeViewProps extends React.Props<any> {
|
export interface HomeViewProps extends React.Props<any> {
|
||||||
|
noSqlMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeViewState {
|
export interface HomeViewState {
|
||||||
@ -110,6 +110,8 @@ export class HomeView extends React.Component<HomeViewProps, HomeViewState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
const { noSqlMode } = this.props;
|
||||||
|
|
||||||
this.statusQueryManager = new QueryManager({
|
this.statusQueryManager = new QueryManager({
|
||||||
processQuery: async (query) => {
|
processQuery: async (query) => {
|
||||||
const statusResp = await axios.get('/status');
|
const statusResp = await axios.get('/status');
|
||||||
@ -130,7 +132,13 @@ export class HomeView extends React.Component<HomeViewProps, HomeViewState> {
|
|||||||
|
|
||||||
this.datasourceQueryManager = new QueryManager({
|
this.datasourceQueryManager = new QueryManager({
|
||||||
processQuery: async (query) => {
|
processQuery: async (query) => {
|
||||||
const datasources = await queryDruidSql({ query });
|
let datasources: string[];
|
||||||
|
if (!noSqlMode) {
|
||||||
|
datasources = await queryDruidSql({ query });
|
||||||
|
} else {
|
||||||
|
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
|
||||||
|
datasources = datasourcesResp.data;
|
||||||
|
}
|
||||||
return datasources.length;
|
return datasources.length;
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
@ -148,8 +156,26 @@ export class HomeView extends React.Component<HomeViewProps, HomeViewState> {
|
|||||||
|
|
||||||
this.segmentQueryManager = new QueryManager({
|
this.segmentQueryManager = new QueryManager({
|
||||||
processQuery: async (query) => {
|
processQuery: async (query) => {
|
||||||
|
if (!noSqlMode) {
|
||||||
const segments = await queryDruidSql({ query });
|
const segments = await queryDruidSql({ query });
|
||||||
return getHeadProp(segments, 'count') || 0;
|
return getHeadProp(segments, 'count') || 0;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
|
||||||
|
const loadstatus = loadstatusResp.data;
|
||||||
|
const unavailableSegmentNum = Object.keys(loadstatus).reduce((sum, key) => {
|
||||||
|
return sum + loadstatus[key];
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const datasourcesMetaResp = await axios.get('/druid/coordinator/v1/datasources?simple');
|
||||||
|
const datasourcesMeta = datasourcesMetaResp.data;
|
||||||
|
const availableSegmentNum = datasourcesMeta.reduce((sum: number, curr: any) => {
|
||||||
|
return sum + curr.properties.segments.count;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return availableSegmentNum + unavailableSegmentNum;
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -166,27 +192,27 @@ export class HomeView extends React.Component<HomeViewProps, HomeViewState> {
|
|||||||
|
|
||||||
this.taskQueryManager = new QueryManager({
|
this.taskQueryManager = new QueryManager({
|
||||||
processQuery: async (query) => {
|
processQuery: async (query) => {
|
||||||
const taskCountsFromSql = await queryDruidSql({ query });
|
let taskCountsFromQuery: {status: string, count: number}[] = [];
|
||||||
const taskCounts = {
|
if (!noSqlMode) {
|
||||||
successTaskCount: 0,
|
taskCountsFromQuery = await queryDruidSql({ query });
|
||||||
failedTaskCount: 0,
|
|
||||||
runningTaskCount: 0,
|
|
||||||
waitingTaskCount: 0,
|
|
||||||
pendingTaskCount: 0
|
|
||||||
};
|
|
||||||
for (const dataStatus of taskCountsFromSql) {
|
|
||||||
if (dataStatus.status === 'SUCCESS') {
|
|
||||||
taskCounts.successTaskCount = dataStatus.count;
|
|
||||||
} else if (dataStatus.status === 'FAILED') {
|
|
||||||
taskCounts.failedTaskCount = dataStatus.count;
|
|
||||||
} else if (dataStatus.status === 'RUNNING') {
|
|
||||||
taskCounts.runningTaskCount = dataStatus.count;
|
|
||||||
} else if (dataStatus.status === 'WAITING') {
|
|
||||||
taskCounts.waitingTaskCount = dataStatus.count;
|
|
||||||
} else {
|
} else {
|
||||||
taskCounts.pendingTaskCount = dataStatus.count;
|
const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
|
||||||
}
|
const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
|
||||||
|
const waitingTasksResp = await axios.get('/druid/indexer/v1/waitingTasks');
|
||||||
|
const pendingTasksResp = await axios.get('/druid/indexer/v1/pendingTasks');
|
||||||
|
taskCountsFromQuery.push(
|
||||||
|
{status: 'SUCCESS', count: completeTasksResp.data.filter((d: any) => d.status === 'SUCCESS').length},
|
||||||
|
{status: 'FAILED', count: completeTasksResp.data.filter((d: any) => d.status === 'FAILED').length},
|
||||||
|
{status: 'RUNNING', count: runningTasksResp.data.length},
|
||||||
|
{status: 'WAITING', count: waitingTasksResp.data.length},
|
||||||
|
{status: 'PENDING', count: pendingTasksResp.data.length}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
const taskCounts = taskCountsFromQuery.reduce((acc: any, curr: any) => {
|
||||||
|
const status = curr.status.toLowerCase();
|
||||||
|
const property = `${status}TaskCount`;
|
||||||
|
return {...acc, [property]: curr.count};
|
||||||
|
}, {});
|
||||||
return taskCounts;
|
return taskCounts;
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
@ -212,8 +238,19 @@ GROUP BY 1`);
|
|||||||
|
|
||||||
this.dataServerQueryManager = new QueryManager({
|
this.dataServerQueryManager = new QueryManager({
|
||||||
processQuery: async (query) => {
|
processQuery: async (query) => {
|
||||||
|
const getDataServerNum = async () => {
|
||||||
|
const allServerResp = await axios.get('/druid/coordinator/v1/servers?simple');
|
||||||
|
const allServers = allServerResp.data;
|
||||||
|
return allServers.filter((s: any) => s.type === 'historical').length;
|
||||||
|
};
|
||||||
|
if (!noSqlMode) {
|
||||||
const dataServerCounts = await queryDruidSql({ query });
|
const dataServerCounts = await queryDruidSql({ query });
|
||||||
return getHeadProp(dataServerCounts, 'count') || 0;
|
const serverNum = getHeadProp(dataServerCounts, 'count') || 0;
|
||||||
|
if (serverNum === 0) return await getDataServerNum();
|
||||||
|
return serverNum;
|
||||||
|
} else {
|
||||||
|
return await getDataServerNum();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -306,7 +343,8 @@ GROUP BY 1`);
|
|||||||
{Boolean(state.successTaskCount) && <p>{pluralIfNeeded(state.successTaskCount, 'successful task')}</p>}
|
{Boolean(state.successTaskCount) && <p>{pluralIfNeeded(state.successTaskCount, 'successful task')}</p>}
|
||||||
{Boolean(state.waitingTaskCount) && <p>{pluralIfNeeded(state.waitingTaskCount, 'waiting task')}</p>}
|
{Boolean(state.waitingTaskCount) && <p>{pluralIfNeeded(state.waitingTaskCount, 'waiting task')}</p>}
|
||||||
{Boolean(state.failedTaskCount) && <p>{pluralIfNeeded(state.failedTaskCount, 'failed task')}</p>}
|
{Boolean(state.failedTaskCount) && <p>{pluralIfNeeded(state.failedTaskCount, 'failed task')}</p>}
|
||||||
{ !(state.runningTaskCount + state.pendingTaskCount + state.successTaskCount + state.waitingTaskCount + state.failedTaskCount) &&
|
{!(Boolean(state.runningTaskCount) || Boolean(state.pendingTaskCount) || Boolean(state.successTaskCount) ||
|
||||||
|
Boolean(state.waitingTaskCount) || Boolean(state.failedTaskCount)) &&
|
||||||
<p>There are no tasks</p>
|
<p>There are no tasks</p>
|
||||||
}
|
}
|
||||||
</>,
|
</>,
|
||||||
|
@ -20,14 +20,12 @@ import { Button, Intent } from '@blueprintjs/core';
|
|||||||
import { H5 } from '@blueprintjs/core';
|
import { H5 } from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as classNames from 'classnames';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
import { Filter } from 'react-table';
|
import { Filter } from 'react-table';
|
||||||
|
|
||||||
import { TableColumnSelection } from '../components/table-column-selection';
|
import { TableColumnSelection } from '../components/table-column-selection';
|
||||||
import { ViewControlBar } from '../components/view-control-bar';
|
import { ViewControlBar } from '../components/view-control-bar';
|
||||||
import { AppToaster } from '../singletons/toaster';
|
|
||||||
import {
|
import {
|
||||||
addFilter,
|
addFilter,
|
||||||
formatBytes,
|
formatBytes,
|
||||||
@ -44,18 +42,21 @@ import './segments-view.scss';
|
|||||||
|
|
||||||
const tableColumns: string[] = ['Segment ID', 'Datasource', 'Start', 'End', 'Version', 'Partition',
|
const tableColumns: string[] = ['Segment ID', 'Datasource', 'Start', 'End', 'Version', 'Partition',
|
||||||
'Size', 'Num rows', 'Replicas', 'Is published', 'Is realtime', 'Is available'];
|
'Size', 'Num rows', 'Replicas', 'Is published', 'Is realtime', 'Is available'];
|
||||||
|
const tableColumnsNoSql: string[] = ['Segment ID', 'Datasource', 'Start', 'End', 'Version', 'Partition', 'Size'];
|
||||||
|
|
||||||
export interface SegmentsViewProps extends React.Props<any> {
|
export interface SegmentsViewProps extends React.Props<any> {
|
||||||
goToSql: (initSql: string) => void;
|
goToSql: (initSql: string) => void;
|
||||||
datasource: string | null;
|
datasource: string | null;
|
||||||
onlyUnavailable: boolean | null;
|
onlyUnavailable: boolean | null;
|
||||||
|
noSqlMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SegmentsViewState {
|
export interface SegmentsViewState {
|
||||||
segmentsLoading: boolean;
|
segmentsLoading: boolean;
|
||||||
segments: any[] | null;
|
segments: SegmentQueryResultRow[] | null;
|
||||||
segmentsError: string | null;
|
segmentsError: string | null;
|
||||||
segmentFilter: Filter[];
|
segmentFilter: Filter[];
|
||||||
|
allSegments?: SegmentQueryResultRow[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryAndSkip {
|
interface QueryAndSkip {
|
||||||
@ -63,8 +64,25 @@ interface QueryAndSkip {
|
|||||||
skip: number;
|
skip: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SegmentQueryResultRow {
|
||||||
|
datasource: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
segment_id: string;
|
||||||
|
version: string;
|
||||||
|
size: 0;
|
||||||
|
partition_num: number;
|
||||||
|
payload: any;
|
||||||
|
num_rows: number;
|
||||||
|
num_replicas: number;
|
||||||
|
is_available: number;
|
||||||
|
is_published: number;
|
||||||
|
is_realtime: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsViewState> {
|
export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsViewState> {
|
||||||
private segmentsQueryManager: QueryManager<QueryAndSkip, any[]>;
|
private segmentsSqlQueryManager: QueryManager<QueryAndSkip, SegmentQueryResultRow[]>;
|
||||||
|
private segmentsJsonQueryManager: QueryManager<any, SegmentQueryResultRow[]>;
|
||||||
private tableColumnSelectionHandler: TableColumnSelectionHandler;
|
private tableColumnSelectionHandler: TableColumnSelectionHandler;
|
||||||
|
|
||||||
constructor(props: SegmentsViewProps, context: any) {
|
constructor(props: SegmentsViewProps, context: any) {
|
||||||
@ -81,7 +99,7 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
|
|||||||
segmentFilter
|
segmentFilter
|
||||||
};
|
};
|
||||||
|
|
||||||
this.segmentsQueryManager = new QueryManager({
|
this.segmentsSqlQueryManager = new QueryManager({
|
||||||
processQuery: async (query: QueryAndSkip) => {
|
processQuery: async (query: QueryAndSkip) => {
|
||||||
const results: any[] = (await queryDruidSql({ query: query.query })).slice(query.skip);
|
const results: any[] = (await queryDruidSql({ query: query.query })).slice(query.skip);
|
||||||
results.forEach(result => {
|
results.forEach(result => {
|
||||||
@ -102,13 +120,58 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.segmentsJsonQueryManager = new QueryManager({
|
||||||
|
processQuery: async (query: any) => {
|
||||||
|
const datasourceList = (await axios.get('/druid/coordinator/v1/metadata/datasources')).data;
|
||||||
|
const nestedResults: SegmentQueryResultRow[][] = await Promise.all(datasourceList.map(async (d: string) => {
|
||||||
|
const segments = (await axios.get(`/druid/coordinator/v1/datasources/${d}?full`)).data.segments;
|
||||||
|
return segments.map((segment: any) => {
|
||||||
|
return {
|
||||||
|
segment_id: segment.identifier,
|
||||||
|
datasource: segment.dataSource,
|
||||||
|
start: segment.interval.split('/')[0],
|
||||||
|
end: segment.interval.split('/')[1],
|
||||||
|
version: segment.version,
|
||||||
|
partition_num: segment.shardSpec.partitionNum ? 0 : segment.shardSpec.partitionNum,
|
||||||
|
size: segment.size,
|
||||||
|
payload: segment,
|
||||||
|
num_rows: -1,
|
||||||
|
num_replicas: -1,
|
||||||
|
is_available: -1,
|
||||||
|
is_published: -1,
|
||||||
|
is_realtime: -1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
const results: SegmentQueryResultRow[] = [].concat.apply([], nestedResults).sort((d1: any, d2: any) => {
|
||||||
|
return d2.start.localeCompare(d1.start);
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
onStateChange: ({ result, loading, error }) => {
|
||||||
|
this.setState({
|
||||||
|
allSegments: result,
|
||||||
|
segments: result ? result.slice(0, 50) : null,
|
||||||
|
segmentsLoading: loading,
|
||||||
|
segmentsError: error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
|
this.tableColumnSelectionHandler = new TableColumnSelectionHandler(
|
||||||
LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION, () => this.setState({})
|
LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION, () => this.setState({})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
if (this.props.noSqlMode) {
|
||||||
|
this.segmentsJsonQueryManager.runQuery('init');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
this.segmentsQueryManager.terminate();
|
this.segmentsSqlQueryManager.terminate();
|
||||||
|
this.segmentsJsonQueryManager.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchData = (state: any, instance: any) => {
|
private fetchData = (state: any, instance: any) => {
|
||||||
@ -140,15 +203,42 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
|
|||||||
queryParts.push(`LIMIT ${totalQuerySize}`);
|
queryParts.push(`LIMIT ${totalQuerySize}`);
|
||||||
|
|
||||||
const query = queryParts.join('\n');
|
const query = queryParts.join('\n');
|
||||||
|
this.segmentsSqlQueryManager.runQuery({
|
||||||
this.segmentsQueryManager.runQuery({
|
|
||||||
query,
|
query,
|
||||||
skip: totalQuerySize - pageSize
|
skip: totalQuerySize - pageSize
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fecthClientSideData = (state: any, instance: any) => {
|
||||||
|
const { page, pageSize, filtered, sorted } = state;
|
||||||
|
const { allSegments } = this.state;
|
||||||
|
if (allSegments == null) return;
|
||||||
|
const startPage = page * pageSize;
|
||||||
|
const endPage = (page + 1) * pageSize;
|
||||||
|
const sortPivot = sorted[0].id;
|
||||||
|
const sortDesc = sorted[0].desc;
|
||||||
|
const selectedSegments = allSegments.sort((d1: any, d2: any) => {
|
||||||
|
const v1 = d1[sortPivot];
|
||||||
|
const v2 = d2[sortPivot];
|
||||||
|
if (typeof (d1[sortPivot]) === 'string') {
|
||||||
|
return sortDesc ? v2.localeCompare(v1) : v1.localeCompare(v2);
|
||||||
|
} else {
|
||||||
|
return sortDesc ? v2 - v1 : v1 - v2;
|
||||||
|
}
|
||||||
|
}).filter((d: any) => {
|
||||||
|
return filtered.every((f: any) => {
|
||||||
|
return d[f.id].includes(f.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const segments = selectedSegments.slice(startPage, endPage);
|
||||||
|
this.setState({
|
||||||
|
segments
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderSegmentsTable() {
|
renderSegmentsTable() {
|
||||||
const { segments, segmentsLoading, segmentsError, segmentFilter } = this.state;
|
const { segments, segmentsLoading, segmentsError, segmentFilter } = this.state;
|
||||||
|
const { noSqlMode } = this.props;
|
||||||
const { tableColumnSelectionHandler } = this;
|
const { tableColumnSelectionHandler } = this;
|
||||||
|
|
||||||
return <ReactTable
|
return <ReactTable
|
||||||
@ -163,7 +253,7 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
|
|||||||
onFilteredChange={(filtered, column) => {
|
onFilteredChange={(filtered, column) => {
|
||||||
this.setState({ segmentFilter: filtered });
|
this.setState({ segmentFilter: filtered });
|
||||||
}}
|
}}
|
||||||
onFetchData={this.fetchData}
|
onFetchData={noSqlMode ? this.fecthClientSideData : this.fetchData}
|
||||||
showPageJump={false}
|
showPageJump={false}
|
||||||
ofText=""
|
ofText=""
|
||||||
columns={[
|
columns={[
|
||||||
@ -232,7 +322,7 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
|
|||||||
filterable: false,
|
filterable: false,
|
||||||
defaultSortDesc: true,
|
defaultSortDesc: true,
|
||||||
Cell: row => formatNumber(row.value),
|
Cell: row => formatNumber(row.value),
|
||||||
show: tableColumnSelectionHandler.showColumn('Num rows')
|
show: !noSqlMode && tableColumnSelectionHandler.showColumn('Num rows')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Replicas',
|
Header: 'Replicas',
|
||||||
@ -240,28 +330,28 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
|
|||||||
width: 60,
|
width: 60,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
defaultSortDesc: true,
|
defaultSortDesc: true,
|
||||||
show: tableColumnSelectionHandler.showColumn('Replicas')
|
show: !noSqlMode && tableColumnSelectionHandler.showColumn('Replicas')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Is published',
|
Header: 'Is published',
|
||||||
id: 'is_published',
|
id: 'is_published',
|
||||||
accessor: (row) => String(Boolean(row.is_published)),
|
accessor: (row) => String(Boolean(row.is_published)),
|
||||||
Filter: makeBooleanFilter(),
|
Filter: makeBooleanFilter(),
|
||||||
show: tableColumnSelectionHandler.showColumn('Is published')
|
show: !noSqlMode && tableColumnSelectionHandler.showColumn('Is published')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Is realtime',
|
Header: 'Is realtime',
|
||||||
id: 'is_realtime',
|
id: 'is_realtime',
|
||||||
accessor: (row) => String(Boolean(row.is_realtime)),
|
accessor: (row) => String(Boolean(row.is_realtime)),
|
||||||
Filter: makeBooleanFilter(),
|
Filter: makeBooleanFilter(),
|
||||||
show: tableColumnSelectionHandler.showColumn('Is realtime')
|
show: !noSqlMode && tableColumnSelectionHandler.showColumn('Is realtime')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Is available',
|
Header: 'Is available',
|
||||||
id: 'is_available',
|
id: 'is_available',
|
||||||
accessor: (row) => String(Boolean(row.is_available)),
|
accessor: (row) => String(Boolean(row.is_available)),
|
||||||
Filter: makeBooleanFilter(),
|
Filter: makeBooleanFilter(),
|
||||||
show: tableColumnSelectionHandler.showColumn('Is available')
|
show: !noSqlMode && tableColumnSelectionHandler.showColumn('Is available')
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
defaultPageSize={50}
|
defaultPageSize={50}
|
||||||
@ -284,7 +374,7 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { goToSql } = this.props;
|
const { goToSql, noSqlMode } = this.props;
|
||||||
const { tableColumnSelectionHandler } = this;
|
const { tableColumnSelectionHandler } = this;
|
||||||
|
|
||||||
return <div className="segments-view app-view">
|
return <div className="segments-view app-view">
|
||||||
@ -292,15 +382,19 @@ export class SegmentsView extends React.Component<SegmentsViewProps, SegmentsVie
|
|||||||
<Button
|
<Button
|
||||||
icon={IconNames.REFRESH}
|
icon={IconNames.REFRESH}
|
||||||
text="Refresh"
|
text="Refresh"
|
||||||
onClick={() => this.segmentsQueryManager.rerunLastQuery()}
|
onClick={() => noSqlMode ? this.segmentsJsonQueryManager.rerunLastQuery() : this.segmentsSqlQueryManager.rerunLastQuery()}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
!noSqlMode &&
|
||||||
<Button
|
<Button
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="Go to SQL"
|
text="Go to SQL"
|
||||||
onClick={() => goToSql(this.segmentsQueryManager.getLastQuery().query)}
|
hidden={noSqlMode}
|
||||||
|
onClick={() => goToSql(this.segmentsSqlQueryManager.getLastQuery().query)}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<TableColumnSelection
|
<TableColumnSelection
|
||||||
columns={tableColumns}
|
columns={noSqlMode ? tableColumnsNoSql : tableColumns}
|
||||||
onChange={(column) => tableColumnSelectionHandler.changeTableColumnSelection(column)}
|
onChange={(column) => tableColumnSelectionHandler.changeTableColumnSelection(column)}
|
||||||
tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
|
tableColumnsHidden={tableColumnSelectionHandler.hiddenColumns}
|
||||||
/>
|
/>
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
import { Button, Switch } from '@blueprintjs/core';
|
import { Button, Switch } from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as classNames from 'classnames';
|
|
||||||
import { sum } from 'd3-array';
|
import { sum } from 'd3-array';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
@ -55,6 +54,7 @@ export interface ServersViewProps extends React.Props<any> {
|
|||||||
middleManager: string | null;
|
middleManager: string | null;
|
||||||
goToSql: (initSql: string) => void;
|
goToSql: (initSql: string) => void;
|
||||||
goToTask: (taskId: string) => void;
|
goToTask: (taskId: string) => void;
|
||||||
|
noSqlMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServersViewState {
|
export interface ServersViewState {
|
||||||
@ -70,9 +70,32 @@ export interface ServersViewState {
|
|||||||
middleManagerFilter: Filter[];
|
middleManagerFilter: Filter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ServerQueryResultRow {
|
||||||
|
curr_size: number;
|
||||||
|
host: string;
|
||||||
|
max_size: number;
|
||||||
|
plaintext_port: number;
|
||||||
|
server: string;
|
||||||
|
tier: string;
|
||||||
|
tls_port: number;
|
||||||
|
segmentsToDrop?: number;
|
||||||
|
segmentsToDropSize?: number;
|
||||||
|
segmentsToLoad?: number;
|
||||||
|
segmentsToLoadSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MiddleManagerQueryResultRow {
|
||||||
|
availabilityGroups: string[];
|
||||||
|
blacklistedUntil: string | null;
|
||||||
|
currCapacityUsed: number;
|
||||||
|
lastCompletedTaskTime: string;
|
||||||
|
runningTasks: string[];
|
||||||
|
worker: any;
|
||||||
|
}
|
||||||
|
|
||||||
export class ServersView extends React.Component<ServersViewProps, ServersViewState> {
|
export class ServersView extends React.Component<ServersViewProps, ServersViewState> {
|
||||||
private serverQueryManager: QueryManager<string, any[]>;
|
private serverQueryManager: QueryManager<string, ServerQueryResultRow[]>;
|
||||||
private middleManagerQueryManager: QueryManager<string, any[]>;
|
private middleManagerQueryManager: QueryManager<string, MiddleManagerQueryResultRow[]>;
|
||||||
private serverTableColumnSelectionHandler: TableColumnSelectionHandler;
|
private serverTableColumnSelectionHandler: TableColumnSelectionHandler;
|
||||||
private middleManagerTableColumnSelectionHandler: TableColumnSelectionHandler;
|
private middleManagerTableColumnSelectionHandler: TableColumnSelectionHandler;
|
||||||
|
|
||||||
@ -100,14 +123,37 @@ export class ServersView extends React.Component<ServersViewProps, ServersViewSt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getServers = async (): Promise<ServerQueryResultRow[]> => {
|
||||||
|
const allServerResp = await axios.get('/druid/coordinator/v1/servers?simple');
|
||||||
|
const allServers = allServerResp.data;
|
||||||
|
return allServers.filter((s: any) => s.type === 'historical').map((s: any) => {
|
||||||
|
return {
|
||||||
|
host: s.host.split(':')[0],
|
||||||
|
plaintext_port: parseInt(s.host.split(':')[1], 10),
|
||||||
|
server: s.host,
|
||||||
|
curr_size: s.currSize,
|
||||||
|
max_size: s.maxSize,
|
||||||
|
tier: s.tier,
|
||||||
|
tls_port: -1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
const { noSqlMode } = this.props;
|
||||||
this.serverQueryManager = new QueryManager({
|
this.serverQueryManager = new QueryManager({
|
||||||
processQuery: async (query: string) => {
|
processQuery: async (query: string) => {
|
||||||
const servers = await queryDruidSql({ query });
|
let servers: ServerQueryResultRow[];
|
||||||
|
if (!noSqlMode) {
|
||||||
|
servers = await queryDruidSql({ query });
|
||||||
|
if (servers.length === 0) {
|
||||||
|
servers = await ServersView.getServers();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
servers = await ServersView.getServers();
|
||||||
|
}
|
||||||
const loadQueueResponse = await axios.get('/druid/coordinator/v1/loadqueue?simple');
|
const loadQueueResponse = await axios.get('/druid/coordinator/v1/loadqueue?simple');
|
||||||
const loadQueues = loadQueueResponse.data;
|
const loadQueues = loadQueueResponse.data;
|
||||||
|
|
||||||
return servers.map((s: any) => {
|
return servers.map((s: any) => {
|
||||||
const loadQueueInfo = loadQueues[s.server];
|
const loadQueueInfo = loadQueues[s.server];
|
||||||
if (loadQueueInfo) {
|
if (loadQueueInfo) {
|
||||||
@ -366,7 +412,7 @@ WHERE "server_type" = 'historical'`);
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { goToSql } = this.props;
|
const { goToSql, noSqlMode } = this.props;
|
||||||
const { groupByTier } = this.state;
|
const { groupByTier } = this.state;
|
||||||
const { serverTableColumnSelectionHandler, middleManagerTableColumnSelectionHandler } = this;
|
const { serverTableColumnSelectionHandler, middleManagerTableColumnSelectionHandler } = this;
|
||||||
|
|
||||||
@ -377,11 +423,14 @@ WHERE "server_type" = 'historical'`);
|
|||||||
text="Refresh"
|
text="Refresh"
|
||||||
onClick={() => this.serverQueryManager.rerunLastQuery()}
|
onClick={() => this.serverQueryManager.rerunLastQuery()}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
!noSqlMode &&
|
||||||
<Button
|
<Button
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="Go to SQL"
|
text="Go to SQL"
|
||||||
onClick={() => goToSql(this.serverQueryManager.getLastQuery())}
|
onClick={() => goToSql(this.serverQueryManager.getLastQuery())}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<Switch
|
<Switch
|
||||||
checked={groupByTier}
|
checked={groupByTier}
|
||||||
label="Group by tier"
|
label="Group by tier"
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
import { Alert, Button, ButtonGroup, Intent, Label } from '@blueprintjs/core';
|
import { Alert, Button, ButtonGroup, Intent, Label } from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as classNames from 'classnames';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
import { Filter } from 'react-table';
|
import { Filter } from 'react-table';
|
||||||
@ -48,6 +47,7 @@ export interface TasksViewProps extends React.Props<any> {
|
|||||||
taskId: string | null;
|
taskId: string | null;
|
||||||
goToSql: (initSql: string) => void;
|
goToSql: (initSql: string) => void;
|
||||||
goToMiddleManager: (middleManager: string) => void;
|
goToMiddleManager: (middleManager: string) => void;
|
||||||
|
noSqlMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TasksViewState {
|
export interface TasksViewState {
|
||||||
@ -73,6 +73,23 @@ export interface TasksViewState {
|
|||||||
alertErrorMsg: string | null;
|
alertErrorMsg: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TaskQueryResultRow {
|
||||||
|
created_time: string;
|
||||||
|
datasource: string;
|
||||||
|
duration: number;
|
||||||
|
error_msg: string | null;
|
||||||
|
location: string | null;
|
||||||
|
rank: number;
|
||||||
|
status: string;
|
||||||
|
task_id: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SupervisorQueryResultRow {
|
||||||
|
id: string;
|
||||||
|
spec: any;
|
||||||
|
}
|
||||||
|
|
||||||
function statusToColor(status: string): string {
|
function statusToColor(status: string): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'RUNNING': return '#2167d5';
|
case 'RUNNING': return '#2167d5';
|
||||||
@ -85,11 +102,11 @@ function statusToColor(status: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
|
export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
|
||||||
private supervisorQueryManager: QueryManager<string, any[]>;
|
private supervisorQueryManager: QueryManager<string, SupervisorQueryResultRow[]>;
|
||||||
private taskQueryManager: QueryManager<string, any[]>;
|
private taskQueryManager: QueryManager<string, TaskQueryResultRow[]>;
|
||||||
private supervisorTableColumnSelectionHandler: TableColumnSelectionHandler;
|
private supervisorTableColumnSelectionHandler: TableColumnSelectionHandler;
|
||||||
private taskTableColumnSelectionHandler: TableColumnSelectionHandler;
|
private taskTableColumnSelectionHandler: TableColumnSelectionHandler;
|
||||||
private statusRanking = {RUNNING: 4, PENDING: 3, WAITING: 2, SUCCESS: 1, FAILED: 1};
|
static statusRanking = {RUNNING: 4, PENDING: 3, WAITING: 2, SUCCESS: 1, FAILED: 1};
|
||||||
|
|
||||||
constructor(props: TasksViewProps, context: any) {
|
constructor(props: TasksViewProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
@ -126,7 +143,24 @@ export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static parseTasks = (data: any[]): TaskQueryResultRow[] => {
|
||||||
|
return data.map((d: any) => {
|
||||||
|
return {
|
||||||
|
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,
|
||||||
|
rank: (TasksView.statusRanking as any)[d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode],
|
||||||
|
status: d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode,
|
||||||
|
task_id: d.id,
|
||||||
|
type: d.typTasksView
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
const { noSqlMode } = this.props;
|
||||||
this.supervisorQueryManager = new QueryManager({
|
this.supervisorQueryManager = new QueryManager({
|
||||||
processQuery: async (query: string) => {
|
processQuery: async (query: string) => {
|
||||||
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
|
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
|
||||||
@ -145,7 +179,16 @@ export class TasksView extends React.Component<TasksViewProps, TasksViewState> {
|
|||||||
|
|
||||||
this.taskQueryManager = new QueryManager({
|
this.taskQueryManager = new QueryManager({
|
||||||
processQuery: async (query: string) => {
|
processQuery: async (query: string) => {
|
||||||
|
if (!noSqlMode) {
|
||||||
return await queryDruidSql({ query });
|
return await queryDruidSql({ query });
|
||||||
|
} else {
|
||||||
|
const taskEndpoints: string[] = ['completeTasks', 'runningTasks', 'waitingTasks', 'pendingTasks'];
|
||||||
|
const result: TaskQueryResultRow[][] = await Promise.all(taskEndpoints.map(async (endpoint: string) => {
|
||||||
|
const resp = await axios.get(`/druid/indexer/v1/${endpoint}`);
|
||||||
|
return TasksView.parseTasks(resp.data);
|
||||||
|
}));
|
||||||
|
return [].concat.apply([], result);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -523,7 +566,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
|
|||||||
return <span>{Object.keys(previewCount).sort().map(v => `${v} (${previewCount[v]})`).join(', ')}</span>;
|
return <span>{Object.keys(previewCount).sort().map(v => `${v} (${previewCount[v]})`).join(', ')}</span>;
|
||||||
},
|
},
|
||||||
sortMethod: (d1, d2) => {
|
sortMethod: (d1, d2) => {
|
||||||
const statusRanking: any = this.statusRanking;
|
const statusRanking: any = TasksView.statusRanking;
|
||||||
return statusRanking[d1.status] - statusRanking[d2.status] || d1.created_time.localeCompare(d2.created_time);
|
return statusRanking[d1.status] - statusRanking[d2.status] || d1.created_time.localeCompare(d2.created_time);
|
||||||
},
|
},
|
||||||
filterMethod: (filter: Filter, row: any) => {
|
filterMethod: (filter: Filter, row: any) => {
|
||||||
@ -570,7 +613,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { goToSql } = this.props;
|
const { goToSql, noSqlMode } = this.props;
|
||||||
const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg } = this.state;
|
const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg } = this.state;
|
||||||
const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
|
const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
|
||||||
|
|
||||||
@ -609,11 +652,14 @@ ORDER BY "rank" DESC, "created_time" DESC`);
|
|||||||
text="Refresh"
|
text="Refresh"
|
||||||
onClick={() => this.taskQueryManager.rerunLastQuery()}
|
onClick={() => this.taskQueryManager.rerunLastQuery()}
|
||||||
/>
|
/>
|
||||||
|
{
|
||||||
|
!noSqlMode &&
|
||||||
<Button
|
<Button
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="Go to SQL"
|
text="Go to SQL"
|
||||||
onClick={() => goToSql(this.taskQueryManager.getLastQuery())}
|
onClick={() => goToSql(this.taskQueryManager.getLastQuery())}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
<Button
|
<Button
|
||||||
icon={IconNames.PLUS}
|
icon={IconNames.PLUS}
|
||||||
text="Submit task"
|
text="Submit task"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user