mirror of https://github.com/apache/druid.git
Web console: fine grained capabilities / graceful degradation (#8805)
* fine grained capabilities * fix tests * configure all cards * better detection * update tests * rename server to service * node -> service * remove console log * better loader in data loader
This commit is contained in:
parent
6f7fbeb63a
commit
7addfc27da
|
@ -10,7 +10,7 @@ exports[`header bar matches snapshot 1`] = `
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
>
|
>
|
||||||
<Logo />
|
<Memo(DruidLogo) />
|
||||||
</a>
|
</a>
|
||||||
<Blueprint3.NavbarDivider />
|
<Blueprint3.NavbarDivider />
|
||||||
<Blueprint3.AnchorButton
|
<Blueprint3.AnchorButton
|
||||||
|
@ -46,14 +46,15 @@ exports[`header bar matches snapshot 1`] = `
|
||||||
/>
|
/>
|
||||||
<Blueprint3.AnchorButton
|
<Blueprint3.AnchorButton
|
||||||
active={false}
|
active={false}
|
||||||
href="#servers"
|
href="#services"
|
||||||
icon="database"
|
icon="database"
|
||||||
minimal={true}
|
minimal={true}
|
||||||
text="Servers"
|
text="Services"
|
||||||
/>
|
/>
|
||||||
<Blueprint3.NavbarDivider />
|
<Blueprint3.NavbarDivider />
|
||||||
<Blueprint3.AnchorButton
|
<Blueprint3.AnchorButton
|
||||||
active={false}
|
active={false}
|
||||||
|
disabled={false}
|
||||||
href="#query"
|
href="#query"
|
||||||
icon="application"
|
icon="application"
|
||||||
minimal={true}
|
minimal={true}
|
||||||
|
@ -66,7 +67,17 @@ exports[`header bar matches snapshot 1`] = `
|
||||||
<Blueprint3.Popover
|
<Blueprint3.Popover
|
||||||
boundary="scrollParent"
|
boundary="scrollParent"
|
||||||
captureDismiss={false}
|
captureDismiss={false}
|
||||||
content={<LegacyMenu />}
|
content={
|
||||||
|
<Memo(LegacyMenu)
|
||||||
|
capabilities={
|
||||||
|
Capabilities {
|
||||||
|
"coordinator": true,
|
||||||
|
"overlord": true,
|
||||||
|
"queryType": "nativeAndSql",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
defaultIsOpen={false}
|
defaultIsOpen={false}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
fill={false}
|
fill={false}
|
||||||
|
@ -85,7 +96,6 @@ exports[`header bar matches snapshot 1`] = `
|
||||||
wrapperTagName="span"
|
wrapperTagName="span"
|
||||||
>
|
>
|
||||||
<Blueprint3.Button
|
<Blueprint3.Button
|
||||||
disabled={false}
|
|
||||||
icon="share"
|
icon="share"
|
||||||
minimal={true}
|
minimal={true}
|
||||||
text="Legacy"
|
text="Legacy"
|
||||||
|
@ -153,7 +163,6 @@ exports[`header bar matches snapshot 1`] = `
|
||||||
wrapperTagName="span"
|
wrapperTagName="span"
|
||||||
>
|
>
|
||||||
<Blueprint3.Button
|
<Blueprint3.Button
|
||||||
disabled={false}
|
|
||||||
icon="cog"
|
icon="cog"
|
||||||
minimal={true}
|
minimal={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
.header-bar {
|
.header-bar {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.logo {
|
.druid-logo {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: $header-bar-height;
|
height: $header-bar-height;
|
||||||
|
|
|
@ -19,12 +19,14 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../utils/capabilities';
|
||||||
|
|
||||||
import { HeaderBar } from './header-bar';
|
import { HeaderBar } from './header-bar';
|
||||||
|
|
||||||
describe('header bar', () => {
|
describe('header bar', () => {
|
||||||
it('matches snapshot', () => {
|
it('matches snapshot', () => {
|
||||||
const headerBar = shallow(
|
const headerBar = shallow(
|
||||||
<HeaderBar active={'load-data'} hideLegacy={false} capabilities="full" />,
|
<HeaderBar active={'load-data'} hideLegacy={false} capabilities={Capabilities.FULL} />,
|
||||||
);
|
);
|
||||||
expect(headerBar).toMatchSnapshot();
|
expect(headerBar).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,12 +55,12 @@ export type HeaderActiveTab =
|
||||||
| 'datasources'
|
| 'datasources'
|
||||||
| 'segments'
|
| 'segments'
|
||||||
| 'tasks'
|
| 'tasks'
|
||||||
| 'servers'
|
| 'services'
|
||||||
| 'lookups';
|
| 'lookups';
|
||||||
|
|
||||||
function Logo() {
|
const DruidLogo = React.memo(function DruidLogo() {
|
||||||
return (
|
return (
|
||||||
<div className="logo">
|
<div className="druid-logo">
|
||||||
<svg
|
<svg
|
||||||
version="1.1"
|
version="1.1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -113,9 +113,15 @@ function Logo() {
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface LegacyMenuProps {
|
||||||
|
capabilities: Capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LegacyMenu() {
|
const LegacyMenu = React.memo(function LegacyMenu(props: LegacyMenuProps) {
|
||||||
|
const { capabilities } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -123,16 +129,18 @@ function LegacyMenu() {
|
||||||
text="Legacy coordinator console"
|
text="Legacy coordinator console"
|
||||||
href={LEGACY_COORDINATOR_CONSOLE}
|
href={LEGACY_COORDINATOR_CONSOLE}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
disabled={!capabilities.hasCoordinatorAccess()}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.MAP}
|
icon={IconNames.MAP}
|
||||||
text="Legacy overlord console"
|
text="Legacy overlord console"
|
||||||
href={LEGACY_OVERLORD_CONSOLE}
|
href={LEGACY_OVERLORD_CONSOLE}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
disabled={!capabilities.hasOverlordAccess()}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
export interface HeaderBarProps {
|
export interface HeaderBarProps {
|
||||||
active: HeaderActiveTab;
|
active: HeaderActiveTab;
|
||||||
|
@ -171,22 +179,26 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
|
||||||
icon={IconNames.PULSE}
|
icon={IconNames.PULSE}
|
||||||
text="Druid Doctor"
|
text="Druid Doctor"
|
||||||
onClick={() => setDoctorDialogOpen(true)}
|
onClick={() => setDoctorDialogOpen(true)}
|
||||||
|
disabled={!capabilities.hasEverything()}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.SETTINGS}
|
icon={IconNames.SETTINGS}
|
||||||
text="Coordinator dynamic config"
|
text="Coordinator dynamic config"
|
||||||
onClick={() => setCoordinatorDynamicConfigDialogOpen(true)}
|
onClick={() => setCoordinatorDynamicConfigDialogOpen(true)}
|
||||||
|
disabled={!capabilities.hasCoordinatorAccess()}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.WRENCH}
|
icon={IconNames.WRENCH}
|
||||||
text="Overlord dynamic config"
|
text="Overlord dynamic config"
|
||||||
onClick={() => setOverlordDynamicConfigDialogOpen(true)}
|
onClick={() => setOverlordDynamicConfigDialogOpen(true)}
|
||||||
|
disabled={!capabilities.hasOverlordAccess()}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.PROPERTIES}
|
icon={IconNames.PROPERTIES}
|
||||||
active={active === 'lookups'}
|
active={active === 'lookups'}
|
||||||
text="Lookups"
|
text="Lookups"
|
||||||
href="#lookups"
|
href="#lookups"
|
||||||
|
disabled={!capabilities.hasCoordinatorAccess()}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
@ -195,7 +207,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
|
||||||
<Navbar className="header-bar">
|
<Navbar className="header-bar">
|
||||||
<NavbarGroup align={Alignment.LEFT}>
|
<NavbarGroup align={Alignment.LEFT}>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<Logo />
|
<DruidLogo />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
|
@ -206,7 +218,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
|
||||||
href="#load-data"
|
href="#load-data"
|
||||||
minimal={!loadDataPrimary}
|
minimal={!loadDataPrimary}
|
||||||
intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE}
|
intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE}
|
||||||
disabled={capabilities === 'no-proxy'}
|
disabled={!capabilities.hasEverything()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
|
@ -233,10 +245,10 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
|
||||||
/>
|
/>
|
||||||
<AnchorButton
|
<AnchorButton
|
||||||
minimal
|
minimal
|
||||||
active={active === 'servers'}
|
active={active === 'services'}
|
||||||
icon={IconNames.DATABASE}
|
icon={IconNames.DATABASE}
|
||||||
text="Servers"
|
text="Services"
|
||||||
href="#servers"
|
href="#services"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
|
@ -246,29 +258,20 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="Query"
|
text="Query"
|
||||||
href="#query"
|
href="#query"
|
||||||
|
disabled={!capabilities.hasQuerying()}
|
||||||
/>
|
/>
|
||||||
</NavbarGroup>
|
</NavbarGroup>
|
||||||
<NavbarGroup align={Alignment.RIGHT}>
|
<NavbarGroup align={Alignment.RIGHT}>
|
||||||
{!hideLegacy && (
|
{!hideLegacy && (
|
||||||
<Popover
|
<Popover
|
||||||
content={<LegacyMenu />}
|
content={<LegacyMenu capabilities={capabilities} />}
|
||||||
position={Position.BOTTOM_RIGHT}
|
position={Position.BOTTOM_RIGHT}
|
||||||
disabled={capabilities === 'no-proxy'}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button minimal icon={IconNames.SHARE} text="Legacy" />
|
||||||
minimal
|
|
||||||
icon={IconNames.SHARE}
|
|
||||||
text="Legacy"
|
|
||||||
disabled={capabilities === 'no-proxy'}
|
|
||||||
/>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
<Popover
|
<Popover content={configMenu} position={Position.BOTTOM_RIGHT}>
|
||||||
content={configMenu}
|
<Button minimal icon={IconNames.COG} />
|
||||||
position={Position.BOTTOM_RIGHT}
|
|
||||||
disabled={capabilities === 'no-proxy'}
|
|
||||||
>
|
|
||||||
<Button minimal icon={IconNames.COG} disabled={capabilities === 'no-proxy'} />
|
|
||||||
</Popover>
|
</Popover>
|
||||||
<Popover content={helpMenu} position={Position.BOTTOM_RIGHT}>
|
<Popover content={helpMenu} position={Position.BOTTOM_RIGHT}>
|
||||||
<Button minimal icon={IconNames.HELP} />
|
<Button minimal icon={IconNames.HELP} />
|
||||||
|
|
|
@ -18,14 +18,13 @@
|
||||||
|
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import axios from 'axios';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { HashRouter, Route, Switch } from 'react-router-dom';
|
import { HashRouter, Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import { ExternalLink, HeaderActiveTab, HeaderBar, Loader } from './components';
|
import { ExternalLink, HeaderActiveTab, HeaderBar, Loader } from './components';
|
||||||
import { AppToaster } from './singletons/toaster';
|
import { AppToaster } from './singletons/toaster';
|
||||||
import { localStorageGet, LocalStorageKeys, QueryManager } from './utils';
|
import { QueryManager } from './utils';
|
||||||
import { Capabilities } from './utils/capabilities';
|
import { Capabilities } from './utils/capabilities';
|
||||||
import { DRUID_DOCS_API, DRUID_DOCS_SQL, DRUID_DOCS_VERSION } from './variables';
|
import { DRUID_DOCS_API, DRUID_DOCS_SQL, DRUID_DOCS_VERSION } from './variables';
|
||||||
import {
|
import {
|
||||||
|
@ -35,7 +34,7 @@ import {
|
||||||
LookupsView,
|
LookupsView,
|
||||||
QueryView,
|
QueryView,
|
||||||
SegmentsView,
|
SegmentsView,
|
||||||
ServersView,
|
ServicesView,
|
||||||
TasksView,
|
TasksView,
|
||||||
} from './views';
|
} from './views';
|
||||||
|
|
||||||
|
@ -55,91 +54,49 @@ export class ConsoleApplication extends React.PureComponent<
|
||||||
ConsoleApplicationProps,
|
ConsoleApplicationProps,
|
||||||
ConsoleApplicationState
|
ConsoleApplicationState
|
||||||
> {
|
> {
|
||||||
static STATUS_TIMEOUT = 2000;
|
|
||||||
|
|
||||||
private capabilitiesQueryManager: QueryManager<null, Capabilities>;
|
private capabilitiesQueryManager: QueryManager<null, Capabilities>;
|
||||||
|
|
||||||
static async discoverCapabilities(): Promise<Capabilities> {
|
static shownNotifications(capabilities: Capabilities | undefined) {
|
||||||
const capabilitiesOverride = localStorageGet(LocalStorageKeys.CAPABILITIES_OVERRIDE);
|
|
||||||
if (capabilitiesOverride) return capabilitiesOverride as Capabilities;
|
|
||||||
|
|
||||||
// Check SQL endpoint
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
'/druid/v2/sql',
|
|
||||||
{ query: 'SELECT 1337', context: { timeout: ConsoleApplication.STATUS_TIMEOUT } },
|
|
||||||
{ timeout: ConsoleApplication.STATUS_TIMEOUT },
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
const { response } = e;
|
|
||||||
if (response.status !== 405 || response.statusText !== 'Method Not Allowed') {
|
|
||||||
return 'full'; // other failure
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await axios.get('/status', { timeout: ConsoleApplication.STATUS_TIMEOUT });
|
|
||||||
} catch (e) {
|
|
||||||
return 'broken'; // total failure
|
|
||||||
}
|
|
||||||
// Status works but SQL 405s => the SQL endpoint is disabled
|
|
||||||
return 'no-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: Capabilities) {
|
|
||||||
let message: JSX.Element;
|
let message: JSX.Element;
|
||||||
switch (capabilities) {
|
if (!capabilities) {
|
||||||
case 'no-sql':
|
message = (
|
||||||
message = (
|
<>
|
||||||
<>
|
It appears that the the service serving this console is not responding. The console will
|
||||||
It appears that the SQL endpoint is disabled. The console will fall back to{' '}
|
not function at the moment
|
||||||
<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
|
} else {
|
||||||
endpoint.
|
switch (capabilities.getMode()) {
|
||||||
</>
|
case 'no-sql':
|
||||||
);
|
message = (
|
||||||
break;
|
<>
|
||||||
|
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;
|
||||||
|
|
||||||
case 'no-proxy':
|
case 'no-proxy':
|
||||||
message = (
|
message = (
|
||||||
<>
|
<>
|
||||||
It appears that the management proxy is not enabled, the console will operate with
|
It appears that the management proxy is not enabled, the console will operate with
|
||||||
limited functionality. Look at{' '}
|
limited functionality. Look at{' '}
|
||||||
<ExternalLink
|
<ExternalLink
|
||||||
href={`https://druid.apache.org/docs/${DRUID_DOCS_VERSION}/operations/management-uis.html#druid-console`}
|
href={`https://druid.apache.org/docs/${DRUID_DOCS_VERSION}/operations/management-uis.html#druid-console`}
|
||||||
>
|
>
|
||||||
the console docs
|
the console docs
|
||||||
</ExternalLink>{' '}
|
</ExternalLink>{' '}
|
||||||
for more info on how to enable the management proxy.
|
for more info on how to enable the management proxy.
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'broken':
|
default:
|
||||||
message = (
|
return;
|
||||||
<>
|
}
|
||||||
It appears that the the Router node is not responding. The console will not function at
|
|
||||||
the moment
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AppToaster.show({
|
AppToaster.show({
|
||||||
|
@ -161,21 +118,19 @@ export class ConsoleApplication extends React.PureComponent<
|
||||||
constructor(props: ConsoleApplicationProps, context: any) {
|
constructor(props: ConsoleApplicationProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
capabilities: 'full',
|
capabilities: Capabilities.FULL,
|
||||||
capabilitiesLoading: true,
|
capabilitiesLoading: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.capabilitiesQueryManager = new QueryManager({
|
this.capabilitiesQueryManager = new QueryManager({
|
||||||
processQuery: async () => {
|
processQuery: async () => {
|
||||||
const capabilities = await ConsoleApplication.discoverCapabilities();
|
const capabilities = await Capabilities.detectCapabilities();
|
||||||
if (capabilities !== 'full') {
|
ConsoleApplication.shownNotifications(capabilities);
|
||||||
ConsoleApplication.shownNotifications(capabilities);
|
return capabilities || Capabilities.FULL;
|
||||||
}
|
|
||||||
return capabilities;
|
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading }) => {
|
onStateChange: ({ result, loading }) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
capabilities: result || 'full',
|
capabilities: result || Capabilities.FULL,
|
||||||
capabilitiesLoading: loading,
|
capabilitiesLoading: loading,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -238,7 +193,7 @@ export class ConsoleApplication extends React.PureComponent<
|
||||||
|
|
||||||
private goToMiddleManager = (middleManager: string) => {
|
private goToMiddleManager = (middleManager: string) => {
|
||||||
this.middleManager = middleManager;
|
this.middleManager = middleManager;
|
||||||
window.location.hash = 'servers';
|
window.location.hash = 'services';
|
||||||
this.resetInitialsWithDelay();
|
this.resetInitialsWithDelay();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -332,11 +287,11 @@ export class ConsoleApplication extends React.PureComponent<
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private wrappedServersView = () => {
|
private wrappedServicesView = () => {
|
||||||
const { capabilities } = this.state;
|
const { capabilities } = this.state;
|
||||||
return this.wrapInViewContainer(
|
return this.wrapInViewContainer(
|
||||||
'servers',
|
'services',
|
||||||
<ServersView
|
<ServicesView
|
||||||
middleManager={this.middleManager}
|
middleManager={this.middleManager}
|
||||||
goToQuery={this.goToQuery}
|
goToQuery={this.goToQuery}
|
||||||
goToTask={this.goToTaskWithTaskId}
|
goToTask={this.goToTaskWithTaskId}
|
||||||
|
@ -369,7 +324,7 @@ export class ConsoleApplication extends React.PureComponent<
|
||||||
<Route path="/datasources" component={this.wrappedDatasourcesView} />
|
<Route path="/datasources" component={this.wrappedDatasourcesView} />
|
||||||
<Route path="/segments" component={this.wrappedSegmentsView} />
|
<Route path="/segments" component={this.wrappedSegmentsView} />
|
||||||
<Route path="/tasks" component={this.wrappedTasksView} />
|
<Route path="/tasks" component={this.wrappedTasksView} />
|
||||||
<Route path="/servers" component={this.wrappedServersView} />
|
<Route path="/services" component={this.wrappedServicesView} />
|
||||||
|
|
||||||
<Route path="/query" component={this.wrappedQueryView} />
|
<Route path="/query" component={this.wrappedQueryView} />
|
||||||
|
|
||||||
|
|
|
@ -261,9 +261,9 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
|
||||||
type: 'string-array',
|
type: 'string-array',
|
||||||
info: (
|
info: (
|
||||||
<>
|
<>
|
||||||
List of historical servers to 'decommission'. Coordinator will not assign new
|
List of historical services to 'decommission'. Coordinator will not assign new
|
||||||
segments to 'decommissioning' servers, and segments will be moved away from them
|
segments to 'decommissioning' services, and segments will be moved away from them
|
||||||
to be placed on non-decommissioning servers at the maximum rate specified by{' '}
|
to be placed on non-decommissioning services at the maximum rate specified by{' '}
|
||||||
<Code>decommissioningMaxPercentOfMaxSegmentsToMove</Code>.
|
<Code>decommissioningMaxPercentOfMaxSegmentsToMove</Code>.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
@ -275,15 +275,15 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
|
||||||
info: (
|
info: (
|
||||||
<>
|
<>
|
||||||
The maximum number of segments that may be moved away from 'decommissioning'
|
The maximum number of segments that may be moved away from 'decommissioning'
|
||||||
servers to non-decommissioning (that is, active) servers during one Coordinator
|
services to non-decommissioning (that is, active) services during one Coordinator
|
||||||
run. This value is relative to the total maximum segment movements allowed during
|
run. This value is relative to the total maximum segment movements allowed during
|
||||||
one run which is determined by <Code>maxSegmentsToMove</Code>. If
|
one run which is determined by <Code>maxSegmentsToMove</Code>. If
|
||||||
<Code>decommissioningMaxPercentOfMaxSegmentsToMove</Code> is 0, segments will
|
<Code>decommissioningMaxPercentOfMaxSegmentsToMove</Code> is 0, segments will
|
||||||
neither be moved from or to 'decommissioning' servers, effectively putting them in
|
neither be moved from or to 'decommissioning' services, effectively putting them
|
||||||
a sort of "maintenance" mode that will not participate in balancing or assignment
|
in a sort of "maintenance" mode that will not participate in balancing or
|
||||||
by load rules. Decommissioning can also become stalled if there are no available
|
assignment by load rules. Decommissioning can also become stalled if there are no
|
||||||
active servers to place the segments. By leveraging the maximum percent of
|
available active services to place the segments. By leveraging the maximum percent
|
||||||
decommissioning segment movements, an operator can prevent active servers from
|
of decommissioning segment movements, an operator can prevent active services from
|
||||||
overload by prioritizing balancing, or decrease decommissioning time instead. The
|
overload by prioritizing balancing, or decrease decommissioning time instead. The
|
||||||
value should be between 0 and 100.
|
value should be between 0 and 100.
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -38,7 +38,7 @@ const RUNTIME_PROPERTIES_ALL_NODES_MUST_AGREE_ON: string[] = [
|
||||||
'druid.zk.service.host',
|
'druid.zk.service.host',
|
||||||
];
|
];
|
||||||
|
|
||||||
// In the future (when we can query other nodes) is will also be cool to check:
|
// In the future (when we can query other services) is will also be cool to check:
|
||||||
// 'druid.storage.type' <=> historicals, overlords, mm
|
// 'druid.storage.type' <=> historicals, overlords, mm
|
||||||
// 'druid.indexer.logs.type' <=> overlord, mm, + peons
|
// 'druid.indexer.logs.type' <=> overlord, mm, + peons
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ export const DOCTOR_CHECKS: DoctorCheck[] = [
|
||||||
status = (await axios.get(`/status`)).data;
|
status = (await axios.get(`/status`)).data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
controls.addIssue(
|
controls.addIssue(
|
||||||
`Did not get a /status response from the Router node. Try confirming that it is running and accessible. Got: ${e.message}`,
|
`Did not get a /status response from the Router service. Try confirming that it is running and accessible. Got: ${e.message}`,
|
||||||
);
|
);
|
||||||
controls.terminateChecks();
|
controls.terminateChecks();
|
||||||
return;
|
return;
|
||||||
|
@ -137,7 +137,7 @@ export const DOCTOR_CHECKS: DoctorCheck[] = [
|
||||||
coordinatorStatus = (await axios.get(`/proxy/coordinator/status`)).data;
|
coordinatorStatus = (await axios.get(`/proxy/coordinator/status`)).data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
controls.addIssue(
|
controls.addIssue(
|
||||||
'Did not get a /status response from the Coordinator node. Try confirming that it is running and accessible.',
|
'Did not get a /status response from the Coordinator service. Try confirming that it is running and accessible.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -147,20 +147,20 @@ export const DOCTOR_CHECKS: DoctorCheck[] = [
|
||||||
overlordStatus = (await axios.get(`/proxy/overlord/status`)).data;
|
overlordStatus = (await axios.get(`/proxy/overlord/status`)).data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
controls.addIssue(
|
controls.addIssue(
|
||||||
'Did not get a /status response from the Overlord node. Try confirming that it is running and accessible.',
|
'Did not get a /status response from the Overlord service. Try confirming that it is running and accessible.',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myStatus.version !== coordinatorStatus.version) {
|
if (myStatus.version !== coordinatorStatus.version) {
|
||||||
controls.addSuggestion(
|
controls.addSuggestion(
|
||||||
`It looks like the Router and Coordinator nodes are on different versions of Druid. This may indicate a problem if you are not in the middle of a rolling upgrade.`,
|
`It looks like the Router and Coordinator services are on different versions of Druid. This may indicate a problem if you are not in the middle of a rolling upgrade.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myStatus.version !== overlordStatus.version) {
|
if (myStatus.version !== overlordStatus.version) {
|
||||||
controls.addSuggestion(
|
controls.addSuggestion(
|
||||||
`It looks like the Router and Overlord nodes are on different versions of Druid. This may indicate a problem if you are not in the middle of a rolling upgrade.`,
|
`It looks like the Router and Overlord services are on different versions of Druid. This may indicate a problem if you are not in the middle of a rolling upgrade.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -282,7 +282,7 @@ export const DOCTOR_CHECKS: DoctorCheck[] = [
|
||||||
sqlResult = await queryDruidSql({ query: `SELECT 1 + 1 AS "two"` });
|
sqlResult = await queryDruidSql({ query: `SELECT 1 + 1 AS "two"` });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
controls.addIssue(
|
controls.addIssue(
|
||||||
`Could not query SQL ensure that "druid.sql.enable" is set to "true" and that there is a Broker node running. Got: ${e.message}`,
|
`Could not query SQL ensure that "druid.sql.enable" is set to "true" and that there is a Broker service running. Got: ${e.message}`,
|
||||||
);
|
);
|
||||||
controls.terminateChecks();
|
controls.terminateChecks();
|
||||||
return;
|
return;
|
||||||
|
@ -294,9 +294,9 @@ export const DOCTOR_CHECKS: DoctorCheck[] = [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Verify that there are historical nodes',
|
name: 'Verify that there are historical services',
|
||||||
check: async controls => {
|
check: async controls => {
|
||||||
// Make sure that there are broker and historical nodes reported from sys.servers
|
// Make sure that there are broker and historical services reported from sys.servers
|
||||||
let sqlResult: any[];
|
let sqlResult: any[];
|
||||||
try {
|
try {
|
||||||
sqlResult = await queryDruidSql({
|
sqlResult = await queryDruidSql({
|
||||||
|
@ -311,19 +311,19 @@ WHERE "server_type" = 'historical'`,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sqlResult.length === 1 && sqlResult[0]['historicals'] === 0) {
|
if (sqlResult.length === 1 && sqlResult[0]['historicals'] === 0) {
|
||||||
controls.addIssue(`There do not appear to be any historical nodes.`);
|
controls.addIssue(`There do not appear to be any historical services.`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Verify that the historicals are not overfilled',
|
name: 'Verify that the historicals are not overfilled',
|
||||||
check: async controls => {
|
check: async controls => {
|
||||||
// Make sure that no nodes are reported that are over 95% capacity
|
// Make sure that no services are reported that are over 95% capacity
|
||||||
let sqlResult: any[];
|
let sqlResult: any[];
|
||||||
try {
|
try {
|
||||||
sqlResult = await queryDruidSql({
|
sqlResult = await queryDruidSql({
|
||||||
query: `SELECT
|
query: `SELECT
|
||||||
"server",
|
"server" AS "service",
|
||||||
"curr_size" * 1.0 / "max_size" AS "fill"
|
"curr_size" * 1.0 / "max_size" AS "fill"
|
||||||
FROM sys.servers
|
FROM sys.servers
|
||||||
WHERE "server_type" = 'historical' AND "curr_size" * 1.0 / "max_size" > 0.9
|
WHERE "server_type" = 'historical' AND "curr_size" * 1.0 / "max_size" > 0.9
|
||||||
|
@ -334,21 +334,21 @@ ORDER BY "server" DESC`,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPercent(server: any): string {
|
function formatPercent(service: any): string {
|
||||||
return (server['fill'] * 100).toFixed(2);
|
return (service['fill'] * 100).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const server of sqlResult) {
|
for (const service of sqlResult) {
|
||||||
if (server['fill'] > 0.95) {
|
if (service['fill'] > 0.95) {
|
||||||
controls.addIssue(
|
controls.addIssue(
|
||||||
`Server "${server['server']}" appears to be over 95% full (is ${formatPercent(
|
`Historical "${service['service']}" appears to be over 95% full (is ${formatPercent(
|
||||||
server,
|
service,
|
||||||
)}%). Increase capacity.`,
|
)}%). Increase capacity.`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
controls.addSuggestion(
|
controls.addSuggestion(
|
||||||
`Server "${server['server']}" appears to be over 90% full (is ${formatPercent(
|
`Historical "${service['service']}" appears to be over 90% full (is ${formatPercent(
|
||||||
server,
|
service,
|
||||||
)}%)`,
|
)}%)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,4 +16,142 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type Capabilities = 'full' | 'no-sql' | 'no-proxy' | 'broken';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { localStorageGetJson, LocalStorageKeys } from './local-storage-keys';
|
||||||
|
|
||||||
|
export type CapabilitiesMode = 'full' | 'no-sql' | 'no-proxy';
|
||||||
|
|
||||||
|
export interface CapabilitiesOptions {
|
||||||
|
queryType: QueryType;
|
||||||
|
coordinator: boolean;
|
||||||
|
overlord: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryType = 'none' | 'nativeOnly' | 'nativeAndSql';
|
||||||
|
|
||||||
|
export class Capabilities {
|
||||||
|
static STATUS_TIMEOUT = 2000;
|
||||||
|
static FULL: Capabilities;
|
||||||
|
|
||||||
|
private queryType: QueryType;
|
||||||
|
private coordinator: boolean;
|
||||||
|
private overlord: boolean;
|
||||||
|
|
||||||
|
static async detectQueryType(): Promise<QueryType | undefined> {
|
||||||
|
// Check SQL endpoint
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
'/druid/v2/sql',
|
||||||
|
{ query: 'SELECT 1337', context: { timeout: Capabilities.STATUS_TIMEOUT } },
|
||||||
|
{ timeout: Capabilities.STATUS_TIMEOUT },
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
const { response } = e;
|
||||||
|
if (response.status !== 405 && response.status !== 404) {
|
||||||
|
return; // other failure
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await axios.get('/status', { timeout: Capabilities.STATUS_TIMEOUT });
|
||||||
|
} catch (e) {
|
||||||
|
return; // total failure
|
||||||
|
}
|
||||||
|
// Status works but SQL 405s => the SQL endpoint is disabled
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(
|
||||||
|
'/druid/v2',
|
||||||
|
{
|
||||||
|
queryType: 'dataSourceMetadata',
|
||||||
|
dataSource: '__web_console_probe__',
|
||||||
|
context: { timeout: Capabilities.STATUS_TIMEOUT },
|
||||||
|
},
|
||||||
|
{ timeout: Capabilities.STATUS_TIMEOUT },
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (response.status !== 405 && response.status !== 404) {
|
||||||
|
return; // other failure
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'nativeOnly';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'nativeAndSql';
|
||||||
|
}
|
||||||
|
|
||||||
|
static async detectNode(node: 'coordinator' | 'overlord'): Promise<boolean | undefined> {
|
||||||
|
try {
|
||||||
|
await axios.get(`/druid/${node === 'overlord' ? 'indexer' : node}/v1/isLeader`, {
|
||||||
|
timeout: Capabilities.STATUS_TIMEOUT,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const { response } = e;
|
||||||
|
if (response.status !== 404) {
|
||||||
|
return; // other failure
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async detectCapabilities(): Promise<Capabilities | undefined> {
|
||||||
|
const capabilitiesOverride = localStorageGetJson(LocalStorageKeys.CAPABILITIES_OVERRIDE);
|
||||||
|
if (capabilitiesOverride) return new Capabilities(capabilitiesOverride as any);
|
||||||
|
|
||||||
|
const queryType = await Capabilities.detectQueryType();
|
||||||
|
if (typeof queryType === 'undefined') return;
|
||||||
|
|
||||||
|
const coordinator = await Capabilities.detectNode('coordinator');
|
||||||
|
if (typeof coordinator === 'undefined') return;
|
||||||
|
|
||||||
|
const overlord = await Capabilities.detectNode('overlord');
|
||||||
|
if (typeof overlord === 'undefined') return;
|
||||||
|
|
||||||
|
return new Capabilities({
|
||||||
|
queryType,
|
||||||
|
coordinator,
|
||||||
|
overlord,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: CapabilitiesOptions) {
|
||||||
|
this.queryType = options.queryType;
|
||||||
|
this.coordinator = options.coordinator;
|
||||||
|
this.overlord = options.overlord;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMode(): CapabilitiesMode {
|
||||||
|
if (!this.hasSql()) return 'no-sql';
|
||||||
|
if (!this.hasCoordinatorAccess()) return 'no-proxy';
|
||||||
|
return 'full';
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasEverything(): boolean {
|
||||||
|
return this.queryType === 'nativeAndSql' && this.coordinator && this.overlord;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasQuerying(): boolean {
|
||||||
|
return this.queryType !== 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasSql(): boolean {
|
||||||
|
return this.queryType === 'nativeAndSql';
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasCoordinatorAccess(): boolean {
|
||||||
|
return this.coordinator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasOverlordAccess(): boolean {
|
||||||
|
return this.overlord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Capabilities.FULL = new Capabilities({
|
||||||
|
queryType: 'nativeAndSql',
|
||||||
|
coordinator: true,
|
||||||
|
overlord: true,
|
||||||
|
});
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const LocalStorageKeys = {
|
||||||
SEGMENT_TABLE_COLUMN_SELECTION: 'segment-table-column-selection' as 'segment-table-column-selection',
|
SEGMENT_TABLE_COLUMN_SELECTION: 'segment-table-column-selection' as 'segment-table-column-selection',
|
||||||
SUPERVISOR_TABLE_COLUMN_SELECTION: 'supervisor-table-column-selection' as 'supervisor-table-column-selection',
|
SUPERVISOR_TABLE_COLUMN_SELECTION: 'supervisor-table-column-selection' as 'supervisor-table-column-selection',
|
||||||
TASK_TABLE_COLUMN_SELECTION: 'task-table-column-selection' as 'task-table-column-selection',
|
TASK_TABLE_COLUMN_SELECTION: 'task-table-column-selection' as 'task-table-column-selection',
|
||||||
SERVER_TABLE_COLUMN_SELECTION: 'historical-table-column-selection' as 'historical-table-column-selection',
|
SERVICE_TABLE_COLUMN_SELECTION: 'service-table-column-selection' as 'service-table-column-selection',
|
||||||
LOOKUP_TABLE_COLUMN_SELECTION: 'lookup-table-column-selection' as 'lookup-table-column-selection',
|
LOOKUP_TABLE_COLUMN_SELECTION: 'lookup-table-column-selection' as 'lookup-table-column-selection',
|
||||||
QUERY_KEY: 'druid-console-query' as 'druid-console-query',
|
QUERY_KEY: 'druid-console-query' as 'druid-console-query',
|
||||||
QUERY_CONTEXT: 'query-context' as 'query-context',
|
QUERY_CONTEXT: 'query-context' as 'query-context',
|
||||||
|
@ -32,7 +32,7 @@ export const LocalStorageKeys = {
|
||||||
TASKS_REFRESH_RATE: 'task-refresh-rate' as 'task-refresh-rate',
|
TASKS_REFRESH_RATE: 'task-refresh-rate' as 'task-refresh-rate',
|
||||||
DATASOURCES_REFRESH_RATE: 'datasources-refresh-rate' as 'datasources-refresh-rate',
|
DATASOURCES_REFRESH_RATE: 'datasources-refresh-rate' as 'datasources-refresh-rate',
|
||||||
SEGMENTS_REFRESH_RATE: 'segments-refresh-rate' as 'segments-refresh-rate',
|
SEGMENTS_REFRESH_RATE: 'segments-refresh-rate' as 'segments-refresh-rate',
|
||||||
SERVERS_REFRESH_RATE: 'servers-refresh-rate' as 'servers-refresh-rate',
|
SERVICES_REFRESH_RATE: 'services-refresh-rate' as 'services-refresh-rate',
|
||||||
SUPERVISORS_REFRESH_RATE: 'supervisors-refresh-rate' as 'supervisors-refresh-rate',
|
SUPERVISORS_REFRESH_RATE: 'supervisors-refresh-rate' as 'supervisors-refresh-rate',
|
||||||
LOOKUPS_REFRESH_RATE: 'lookups-refresh-rate' as 'lookups-refresh-rate',
|
LOOKUPS_REFRESH_RATE: 'lookups-refresh-rate' as 'lookups-refresh-rate',
|
||||||
QUERY_HISTORY: 'query-history' as 'query-history',
|
QUERY_HISTORY: 'query-history' as 'query-history',
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../utils/capabilities';
|
||||||
|
|
||||||
import { DatasourcesView } from './datasource-view';
|
import { DatasourcesView } from './datasource-view';
|
||||||
|
|
||||||
describe('data source view', () => {
|
describe('data source view', () => {
|
||||||
|
@ -28,7 +30,7 @@ describe('data source view', () => {
|
||||||
goToQuery={() => {}}
|
goToQuery={() => {}}
|
||||||
goToTask={() => null}
|
goToTask={() => null}
|
||||||
goToSegments={() => {}}
|
goToSegments={() => {}}
|
||||||
capabilities="full"
|
capabilities={Capabilities.FULL}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(dataSourceView).toMatchSnapshot();
|
expect(dataSourceView).toMatchSnapshot();
|
||||||
|
|
|
@ -61,14 +61,14 @@ import {
|
||||||
QueryManager,
|
QueryManager,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { BasicAction } from '../../utils/basic-action';
|
import { BasicAction } from '../../utils/basic-action';
|
||||||
import { Capabilities } from '../../utils/capabilities';
|
import { Capabilities, CapabilitiesMode } from '../../utils/capabilities';
|
||||||
import { RuleUtil } from '../../utils/load-rule';
|
import { RuleUtil } from '../../utils/load-rule';
|
||||||
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
|
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
|
||||||
import { deepGet } from '../../utils/object-change';
|
import { deepGet } from '../../utils/object-change';
|
||||||
|
|
||||||
import './datasource-view.scss';
|
import './datasource-view.scss';
|
||||||
|
|
||||||
const tableColumns: Record<Capabilities, string[]> = {
|
const tableColumns: Record<CapabilitiesMode, string[]> = {
|
||||||
full: [
|
full: [
|
||||||
'Datasource',
|
'Datasource',
|
||||||
'Availability',
|
'Availability',
|
||||||
|
@ -101,7 +101,6 @@ const tableColumns: Record<Capabilities, string[]> = {
|
||||||
'Num rows',
|
'Num rows',
|
||||||
ACTION_COLUMN_LABEL,
|
ACTION_COLUMN_LABEL,
|
||||||
],
|
],
|
||||||
broken: ['Datasource'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string {
|
function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string {
|
||||||
|
@ -247,9 +246,9 @@ GROUP BY 1`;
|
||||||
this.datasourceQueryManager = new QueryManager({
|
this.datasourceQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
let datasources: DatasourceQueryResultRow[];
|
let datasources: DatasourceQueryResultRow[];
|
||||||
if (capabilities !== 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
datasources = await queryDruidSql({ query: DatasourcesView.DATASOURCE_SQL });
|
datasources = await queryDruidSql({ query: DatasourcesView.DATASOURCE_SQL });
|
||||||
} else {
|
} else if (capabilities.hasCoordinatorAccess()) {
|
||||||
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple');
|
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple');
|
||||||
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
|
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
|
||||||
const loadstatus = loadstatusResp.data;
|
const loadstatus = loadstatusResp.data;
|
||||||
|
@ -272,9 +271,11 @@ GROUP BY 1`;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(`must have SQL or coordinator access`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capabilities === 'no-proxy') {
|
if (!capabilities.hasCoordinatorAccess()) {
|
||||||
datasources.forEach((ds: any) => {
|
datasources.forEach((ds: any) => {
|
||||||
ds.rules = [];
|
ds.rules = [];
|
||||||
});
|
});
|
||||||
|
@ -496,7 +497,7 @@ GROUP BY 1`;
|
||||||
const { goToQuery, capabilities } = this.props;
|
const { goToQuery, capabilities } = this.props;
|
||||||
const bulkDatasourceActionsMenu = (
|
const bulkDatasourceActionsMenu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
{capabilities !== 'no-sql' && (
|
{capabilities.hasSql() && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="View SQL query for table"
|
text="View SQL query for table"
|
||||||
|
@ -621,7 +622,7 @@ GROUP BY 1`;
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (capabilities === 'no-proxy') {
|
if (!capabilities.hasCoordinatorAccess()) {
|
||||||
return goToActions;
|
return goToActions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -887,7 +888,7 @@ GROUP BY 1`;
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
show: capabilities !== 'no-proxy' && hiddenColumns.exists('Retention'),
|
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Retention'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Replicated size',
|
Header: 'Replicated size',
|
||||||
|
@ -941,7 +942,7 @@ GROUP BY 1`;
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
show: capabilities !== 'no-proxy' && hiddenColumns.exists('Compaction'),
|
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Compaction'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Avg. segment size',
|
Header: 'Avg. segment size',
|
||||||
|
@ -957,7 +958,7 @@ GROUP BY 1`;
|
||||||
filterable: false,
|
filterable: false,
|
||||||
width: 100,
|
width: 100,
|
||||||
Cell: row => formatNumber(row.value),
|
Cell: row => formatNumber(row.value),
|
||||||
show: capabilities !== 'no-sql' && hiddenColumns.exists('Num rows'),
|
show: capabilities.hasSql() && hiddenColumns.exists('Num rows'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: ACTION_COLUMN_LABEL,
|
Header: ACTION_COLUMN_LABEL,
|
||||||
|
@ -1030,7 +1031,7 @@ GROUP BY 1`;
|
||||||
label="Show segment timeline"
|
label="Show segment timeline"
|
||||||
onChange={() => this.setState({ showChart: !showChart })}
|
onChange={() => this.setState({ showChart: !showChart })}
|
||||||
/>
|
/>
|
||||||
{capabilities !== 'no-proxy' && (
|
{capabilities.hasCoordinatorAccess() && (
|
||||||
<Switch
|
<Switch
|
||||||
checked={showDisabled}
|
checked={showDisabled}
|
||||||
label="Show disabled"
|
label="Show disabled"
|
||||||
|
@ -1038,7 +1039,7 @@ GROUP BY 1`;
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TableColumnSelector
|
<TableColumnSelector
|
||||||
columns={tableColumns[capabilities]}
|
columns={tableColumns[capabilities.getMode()]}
|
||||||
onChange={column =>
|
onChange={column =>
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
hiddenColumns: prevState.hiddenColumns.toggle(column),
|
hiddenColumns: prevState.hiddenColumns.toggle(column),
|
||||||
|
|
|
@ -6,19 +6,49 @@ exports[`home view matches snapshot 1`] = `
|
||||||
>
|
>
|
||||||
<StatusCard />
|
<StatusCard />
|
||||||
<DatasourcesCard
|
<DatasourcesCard
|
||||||
capabilities="full"
|
capabilities={
|
||||||
|
Capabilities {
|
||||||
|
"coordinator": true,
|
||||||
|
"overlord": true,
|
||||||
|
"queryType": "nativeAndSql",
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<SegmentsCard
|
<SegmentsCard
|
||||||
capabilities="full"
|
capabilities={
|
||||||
|
Capabilities {
|
||||||
|
"coordinator": true,
|
||||||
|
"overlord": true,
|
||||||
|
"queryType": "nativeAndSql",
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<SupervisorsCard
|
<SupervisorsCard
|
||||||
capabilities="full"
|
capabilities={
|
||||||
|
Capabilities {
|
||||||
|
"coordinator": true,
|
||||||
|
"overlord": true,
|
||||||
|
"queryType": "nativeAndSql",
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<TasksCard
|
<TasksCard
|
||||||
capabilities="full"
|
capabilities={
|
||||||
|
Capabilities {
|
||||||
|
"coordinator": true,
|
||||||
|
"overlord": true,
|
||||||
|
"queryType": "nativeAndSql",
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ServersCard
|
<ServicesCard
|
||||||
capabilities="full"
|
capabilities={
|
||||||
|
Capabilities {
|
||||||
|
"coordinator": true,
|
||||||
|
"overlord": true,
|
||||||
|
"queryType": "nativeAndSql",
|
||||||
|
}
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<LookupsCard />
|
<LookupsCard />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,11 +19,13 @@
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../../utils/capabilities';
|
||||||
|
|
||||||
import { DatasourcesCard } from './datasources-card';
|
import { DatasourcesCard } from './datasources-card';
|
||||||
|
|
||||||
describe('datasources card', () => {
|
describe('datasources card', () => {
|
||||||
it('matches snapshot', () => {
|
it('matches snapshot', () => {
|
||||||
const datasourcesCard = <DatasourcesCard capabilities="full" />;
|
const datasourcesCard = <DatasourcesCard capabilities={Capabilities.FULL} />;
|
||||||
|
|
||||||
const { container } = render(datasourcesCard);
|
const { container } = render(datasourcesCard);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
|
|
@ -50,14 +50,17 @@ export class DatasourcesCard extends React.PureComponent<
|
||||||
this.datasourceQueryManager = new QueryManager({
|
this.datasourceQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
let datasources: string[];
|
let datasources: string[];
|
||||||
if (capabilities !== 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
datasources = await queryDruidSql({
|
datasources = await queryDruidSql({
|
||||||
query: `SELECT datasource FROM sys.segments GROUP BY 1`,
|
query: `SELECT datasource FROM sys.segments GROUP BY 1`,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (capabilities.hasCoordinatorAccess()) {
|
||||||
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
|
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
|
||||||
datasources = datasourcesResp.data;
|
datasources = datasourcesResp.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(`must have SQL or coordinator access`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return datasources.length;
|
return datasources.length;
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
.home-view {
|
.home-view {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: $standard-padding;
|
gap: $standard-padding;
|
||||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
|
||||||
& > a {
|
& > a {
|
||||||
|
|
|
@ -19,11 +19,13 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../utils/capabilities';
|
||||||
|
|
||||||
import { HomeView } from './home-view';
|
import { HomeView } from './home-view';
|
||||||
|
|
||||||
describe('home view', () => {
|
describe('home view', () => {
|
||||||
it('matches snapshot', () => {
|
it('matches snapshot', () => {
|
||||||
const homeView = shallow(<HomeView capabilities="full" />);
|
const homeView = shallow(<HomeView capabilities={Capabilities.FULL} />);
|
||||||
expect(homeView).toMatchSnapshot();
|
expect(homeView).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { Capabilities } from '../../utils/capabilities';
|
||||||
import { DatasourcesCard } from './datasources-card/datasources-card';
|
import { DatasourcesCard } from './datasources-card/datasources-card';
|
||||||
import { LookupsCard } from './lookups-card/lookups-card';
|
import { LookupsCard } from './lookups-card/lookups-card';
|
||||||
import { SegmentsCard } from './segments-card/segments-card';
|
import { SegmentsCard } from './segments-card/segments-card';
|
||||||
import { ServersCard } from './servers-card/servers-card';
|
import { ServicesCard } from './services-card/services-card';
|
||||||
import { StatusCard } from './status-card/status-card';
|
import { StatusCard } from './status-card/status-card';
|
||||||
import { SupervisorsCard } from './supervisors-card/supervisors-card';
|
import { SupervisorsCard } from './supervisors-card/supervisors-card';
|
||||||
import { TasksCard } from './tasks-card/tasks-card';
|
import { TasksCard } from './tasks-card/tasks-card';
|
||||||
|
@ -44,7 +44,7 @@ export const HomeView = React.memo(function HomeView(props: HomeViewProps) {
|
||||||
<SegmentsCard capabilities={capabilities} />
|
<SegmentsCard capabilities={capabilities} />
|
||||||
<SupervisorsCard capabilities={capabilities} />
|
<SupervisorsCard capabilities={capabilities} />
|
||||||
<TasksCard capabilities={capabilities} />
|
<TasksCard capabilities={capabilities} />
|
||||||
<ServersCard capabilities={capabilities} />
|
<ServicesCard capabilities={capabilities} />
|
||||||
<LookupsCard />
|
<LookupsCard />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,11 +19,13 @@
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../../utils/capabilities';
|
||||||
|
|
||||||
import { SegmentsCard } from './segments-card';
|
import { SegmentsCard } from './segments-card';
|
||||||
|
|
||||||
describe('segments card', () => {
|
describe('segments card', () => {
|
||||||
it('matches snapshot', () => {
|
it('matches snapshot', () => {
|
||||||
const segmentsCard = <SegmentsCard capabilities="full" />;
|
const segmentsCard = <SegmentsCard capabilities={Capabilities.FULL} />;
|
||||||
|
|
||||||
const { container } = render(segmentsCard);
|
const { container } = render(segmentsCard);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
|
|
@ -50,7 +50,15 @@ export class SegmentsCard extends React.PureComponent<SegmentsCardProps, Segment
|
||||||
|
|
||||||
this.segmentQueryManager = new QueryManager({
|
this.segmentQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
if (capabilities === 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
|
const segments = await queryDruidSql({
|
||||||
|
query: `SELECT
|
||||||
|
COUNT(*) as "count",
|
||||||
|
COUNT(*) FILTER (WHERE is_available = 0) as "unavailable"
|
||||||
|
FROM sys.segments`,
|
||||||
|
});
|
||||||
|
return segments.length === 1 ? segments[0] : null;
|
||||||
|
} else if (capabilities.hasCoordinatorAccess()) {
|
||||||
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
|
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
|
||||||
const loadstatus = loadstatusResp.data;
|
const loadstatus = loadstatusResp.data;
|
||||||
const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
|
const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
|
||||||
|
@ -66,13 +74,7 @@ export class SegmentsCard extends React.PureComponent<SegmentsCardProps, Segment
|
||||||
unavailable: unavailableSegmentNum,
|
unavailable: unavailableSegmentNum,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const segments = await queryDruidSql({
|
throw new Error(`must have SQL or coordinator access`);
|
||||||
query: `SELECT
|
|
||||||
COUNT(*) as "count",
|
|
||||||
COUNT(*) FILTER (WHERE is_available = 0) as "unavailable"
|
|
||||||
FROM sys.segments`,
|
|
||||||
});
|
|
||||||
return segments.length === 1 ? segments[0] : null;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`servers card matches snapshot 1`] = `
|
exports[`services card matches snapshot 1`] = `
|
||||||
<a
|
<a
|
||||||
class="home-view-card servers-card"
|
class="home-view-card services-card"
|
||||||
href="#servers"
|
href="#services"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bp3-card bp3-interactive bp3-elevation-0"
|
class="bp3-card bp3-interactive bp3-elevation-0"
|
||||||
|
@ -32,7 +32,7 @@ exports[`servers card matches snapshot 1`] = `
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
Servers
|
Services
|
||||||
</h5>
|
</h5>
|
||||||
<p>
|
<p>
|
||||||
Loading...
|
Loading...
|
|
@ -19,13 +19,15 @@
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ServersCard } from './servers-card';
|
import { Capabilities } from '../../../utils/capabilities';
|
||||||
|
|
||||||
describe('servers card', () => {
|
import { ServicesCard } from './services-card';
|
||||||
|
|
||||||
|
describe('services card', () => {
|
||||||
it('matches snapshot', () => {
|
it('matches snapshot', () => {
|
||||||
const serversCard = <ServersCard capabilities="full" />;
|
const servicesCard = <ServicesCard capabilities={Capabilities.FULL} />;
|
||||||
|
|
||||||
const { container } = render(serversCard);
|
const { container } = render(servicesCard);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -24,12 +24,12 @@ import { compact, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '
|
||||||
import { Capabilities } from '../../../utils/capabilities';
|
import { Capabilities } from '../../../utils/capabilities';
|
||||||
import { HomeViewCard } from '../home-view-card/home-view-card';
|
import { HomeViewCard } from '../home-view-card/home-view-card';
|
||||||
|
|
||||||
export interface ServersCardProps {
|
export interface ServicesCardProps {
|
||||||
capabilities: Capabilities;
|
capabilities: Capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServersCardState {
|
export interface ServicesCardState {
|
||||||
serverCountLoading: boolean;
|
serviceCountLoading: boolean;
|
||||||
coordinatorCount: number;
|
coordinatorCount: number;
|
||||||
overlordCount: number;
|
overlordCount: number;
|
||||||
routerCount: number;
|
routerCount: number;
|
||||||
|
@ -38,10 +38,10 @@ export interface ServersCardState {
|
||||||
middleManagerCount: number;
|
middleManagerCount: number;
|
||||||
peonCount: number;
|
peonCount: number;
|
||||||
indexerCount: number;
|
indexerCount: number;
|
||||||
serverCountError?: string;
|
serviceCountError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServersCard extends React.PureComponent<ServersCardProps, ServersCardState> {
|
export class ServicesCard extends React.PureComponent<ServicesCardProps, ServicesCardState> {
|
||||||
static renderPluralIfNeededPair(
|
static renderPluralIfNeededPair(
|
||||||
count1: number,
|
count1: number,
|
||||||
singular1: string,
|
singular1: string,
|
||||||
|
@ -56,12 +56,12 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
|
||||||
return <p>{text}</p>;
|
return <p>{text}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private serverQueryManager: QueryManager<Capabilities, any>;
|
private serviceQueryManager: QueryManager<Capabilities, any>;
|
||||||
|
|
||||||
constructor(props: ServersCardProps, context: any) {
|
constructor(props: ServicesCardProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
serverCountLoading: false,
|
serviceCountLoading: false,
|
||||||
coordinatorCount: 0,
|
coordinatorCount: 0,
|
||||||
overlordCount: 0,
|
overlordCount: 0,
|
||||||
routerCount: 0,
|
routerCount: 0,
|
||||||
|
@ -72,29 +72,37 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
|
||||||
indexerCount: 0,
|
indexerCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.serverQueryManager = new QueryManager({
|
this.serviceQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
if (capabilities === 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
const serversResp = await axios.get('/druid/coordinator/v1/servers?simple');
|
const serviceCountsFromQuery: {
|
||||||
const middleManagerResp = await axios.get('/druid/indexer/v1/workers');
|
service_type: string;
|
||||||
return {
|
|
||||||
historical: serversResp.data.filter((s: any) => s.type === 'historical').length,
|
|
||||||
middle_manager: middleManagerResp.data.length,
|
|
||||||
peon: serversResp.data.filter((s: any) => s.type === 'indexer-executor').length,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const serverCountsFromQuery: {
|
|
||||||
server_type: string;
|
|
||||||
count: number;
|
count: number;
|
||||||
}[] = await queryDruidSql({
|
}[] = await queryDruidSql({
|
||||||
query: `SELECT server_type, COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
|
query: `SELECT server_type AS "service_type", COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
|
||||||
});
|
});
|
||||||
return lookupBy(serverCountsFromQuery, x => x.server_type, x => x.count);
|
return lookupBy(serviceCountsFromQuery, x => x.service_type, x => x.count);
|
||||||
|
} else if (capabilities.hasCoordinatorAccess() || capabilities.hasOverlordAccess()) {
|
||||||
|
const services = capabilities.hasCoordinatorAccess()
|
||||||
|
? (await axios.get('/druid/coordinator/v1/servers?simple')).data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const middleManager = capabilities.hasOverlordAccess()
|
||||||
|
? (await axios.get('/druid/indexer/v1/workers')).data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
historical: services.filter((s: any) => s.type === 'historical').length,
|
||||||
|
middle_manager: middleManager.length,
|
||||||
|
peon: services.filter((s: any) => s.type === 'indexer-executor').length,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(`must have SQL or coordinator/overlord access`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
serverCountLoading: loading,
|
serviceCountLoading: loading,
|
||||||
coordinatorCount: result ? result.coordinator : 0,
|
coordinatorCount: result ? result.coordinator : 0,
|
||||||
overlordCount: result ? result.overlord : 0,
|
overlordCount: result ? result.overlord : 0,
|
||||||
routerCount: result ? result.router : 0,
|
routerCount: result ? result.router : 0,
|
||||||
|
@ -103,7 +111,7 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
|
||||||
middleManagerCount: result ? result.middle_manager : 0,
|
middleManagerCount: result ? result.middle_manager : 0,
|
||||||
peonCount: result ? result.peon : 0,
|
peonCount: result ? result.peon : 0,
|
||||||
indexerCount: result ? result.indexer : 0,
|
indexerCount: result ? result.indexer : 0,
|
||||||
serverCountError: error,
|
serviceCountError: error,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -112,16 +120,16 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
const { capabilities } = this.props;
|
const { capabilities } = this.props;
|
||||||
|
|
||||||
this.serverQueryManager.runQuery(capabilities);
|
this.serviceQueryManager.runQuery(capabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
this.serverQueryManager.terminate();
|
this.serviceQueryManager.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
serverCountLoading,
|
serviceCountLoading,
|
||||||
coordinatorCount,
|
coordinatorCount,
|
||||||
overlordCount,
|
overlordCount,
|
||||||
routerCount,
|
routerCount,
|
||||||
|
@ -130,31 +138,31 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
|
||||||
middleManagerCount,
|
middleManagerCount,
|
||||||
peonCount,
|
peonCount,
|
||||||
indexerCount,
|
indexerCount,
|
||||||
serverCountError,
|
serviceCountError,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
return (
|
return (
|
||||||
<HomeViewCard
|
<HomeViewCard
|
||||||
className="servers-card"
|
className="services-card"
|
||||||
href={'#servers'}
|
href={'#services'}
|
||||||
icon={IconNames.DATABASE}
|
icon={IconNames.DATABASE}
|
||||||
title={'Servers'}
|
title={'Services'}
|
||||||
loading={serverCountLoading}
|
loading={serviceCountLoading}
|
||||||
error={serverCountError}
|
error={serviceCountError}
|
||||||
>
|
>
|
||||||
{ServersCard.renderPluralIfNeededPair(
|
{ServicesCard.renderPluralIfNeededPair(
|
||||||
overlordCount,
|
overlordCount,
|
||||||
'overlord',
|
'overlord',
|
||||||
coordinatorCount,
|
coordinatorCount,
|
||||||
'coordinator',
|
'coordinator',
|
||||||
)}
|
)}
|
||||||
{ServersCard.renderPluralIfNeededPair(routerCount, 'router', brokerCount, 'broker')}
|
{ServicesCard.renderPluralIfNeededPair(routerCount, 'router', brokerCount, 'broker')}
|
||||||
{ServersCard.renderPluralIfNeededPair(
|
{ServicesCard.renderPluralIfNeededPair(
|
||||||
historicalCount,
|
historicalCount,
|
||||||
'historical',
|
'historical',
|
||||||
middleManagerCount,
|
middleManagerCount,
|
||||||
'middle manager',
|
'middle manager',
|
||||||
)}
|
)}
|
||||||
{ServersCard.renderPluralIfNeededPair(peonCount, 'peon', indexerCount, 'indexer')}
|
{ServicesCard.renderPluralIfNeededPair(peonCount, 'peon', indexerCount, 'indexer')}
|
||||||
</HomeViewCard>
|
</HomeViewCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -19,11 +19,13 @@
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../../utils/capabilities';
|
||||||
|
|
||||||
import { SupervisorsCard } from './supervisors-card';
|
import { SupervisorsCard } from './supervisors-card';
|
||||||
|
|
||||||
describe('supervisors card', () => {
|
describe('supervisors card', () => {
|
||||||
it('matches snapshot', () => {
|
it('matches snapshot', () => {
|
||||||
const supervisorsCard = <SupervisorsCard capabilities="full" />;
|
const supervisorsCard = <SupervisorsCard capabilities={Capabilities.FULL} />;
|
||||||
|
|
||||||
const { container } = render(supervisorsCard);
|
const { container } = render(supervisorsCard);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
|
|
@ -51,14 +51,14 @@ export class SupervisorsCard extends React.PureComponent<
|
||||||
|
|
||||||
this.supervisorQueryManager = new QueryManager({
|
this.supervisorQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
if (capabilities !== 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
return (await queryDruidSql({
|
return (await queryDruidSql({
|
||||||
query: `SELECT
|
query: `SELECT
|
||||||
COUNT(*) FILTER (WHERE "suspended" = 0) AS "runningSupervisorCount",
|
COUNT(*) FILTER (WHERE "suspended" = 0) AS "runningSupervisorCount",
|
||||||
COUNT(*) FILTER (WHERE "suspended" = 1) AS "suspendedSupervisorCount"
|
COUNT(*) FILTER (WHERE "suspended" = 1) AS "suspendedSupervisorCount"
|
||||||
FROM sys.supervisors`,
|
FROM sys.supervisors`,
|
||||||
}))[0];
|
}))[0];
|
||||||
} else {
|
} else if (capabilities.hasOverlordAccess()) {
|
||||||
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
|
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
|
||||||
const data = resp.data;
|
const data = resp.data;
|
||||||
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
|
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
|
||||||
|
@ -68,6 +68,8 @@ FROM sys.supervisors`,
|
||||||
runningSupervisorCount,
|
runningSupervisorCount,
|
||||||
suspendedSupervisorCount,
|
suspendedSupervisorCount,
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(`must have SQL or overlord access`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
|
|
|
@ -19,11 +19,13 @@
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../../utils/capabilities';
|
||||||
|
|
||||||
import { TasksCard } from './tasks-card';
|
import { TasksCard } from './tasks-card';
|
||||||
|
|
||||||
describe('tasks card', () => {
|
describe('tasks card', () => {
|
||||||
it('matches snapshot', () => {
|
it('matches snapshot', () => {
|
||||||
const tasksCard = <TasksCard capabilities="full" />;
|
const tasksCard = <TasksCard capabilities={Capabilities.FULL} />;
|
||||||
|
|
||||||
const { container } = render(tasksCard);
|
const { container } = render(tasksCard);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
|
|
@ -54,7 +54,16 @@ export class TasksCard extends React.PureComponent<TasksCardProps, TasksCardStat
|
||||||
|
|
||||||
this.taskQueryManager = new QueryManager({
|
this.taskQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
if (capabilities === 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
|
const taskCountsFromQuery: { status: string; count: number }[] = await queryDruidSql({
|
||||||
|
query: `SELECT
|
||||||
|
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
|
||||||
|
COUNT (*) AS "count"
|
||||||
|
FROM sys.tasks
|
||||||
|
GROUP BY 1`,
|
||||||
|
});
|
||||||
|
return lookupBy(taskCountsFromQuery, x => x.status, x => x.count);
|
||||||
|
} else if (capabilities.hasOverlordAccess()) {
|
||||||
const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
|
const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
|
||||||
const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
|
const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
|
||||||
const pendingTasksResp = await axios.get('/druid/indexer/v1/pendingTasks');
|
const pendingTasksResp = await axios.get('/druid/indexer/v1/pendingTasks');
|
||||||
|
@ -67,14 +76,7 @@ export class TasksCard extends React.PureComponent<TasksCardProps, TasksCardStat
|
||||||
WAITING: waitingTasksResp.data.length,
|
WAITING: waitingTasksResp.data.length,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const taskCountsFromQuery: { status: string; count: number }[] = await queryDruidSql({
|
throw new Error(`must have SQL or overlord access`);
|
||||||
query: `SELECT
|
|
||||||
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
|
|
||||||
COUNT (*) AS "count"
|
|
||||||
FROM sys.tasks
|
|
||||||
GROUP BY 1`,
|
|
||||||
});
|
|
||||||
return lookupBy(taskCountsFromQuery, x => x.status, x => x.count);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
|
|
|
@ -21,6 +21,6 @@ export * from './home-view/home-view';
|
||||||
export * from './load-data-view/load-data-view';
|
export * from './load-data-view/load-data-view';
|
||||||
export * from './lookups-view/lookups-view';
|
export * from './lookups-view/lookups-view';
|
||||||
export * from './segments-view/segments-view';
|
export * from './segments-view/segments-view';
|
||||||
export * from './servers-view/servers-view';
|
export * from './services-view/services-view';
|
||||||
export * from './query-view/query-view';
|
export * from './query-view/query-view';
|
||||||
export * from './task-view/tasks-view';
|
export * from './task-view/tasks-view';
|
||||||
|
|
|
@ -326,7 +326,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
let spec = parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
|
let spec = parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
|
||||||
if (!spec || typeof spec !== 'object') spec = {};
|
if (!spec || typeof spec !== 'object') spec = {};
|
||||||
this.state = {
|
this.state = {
|
||||||
step: 'welcome',
|
step: 'loading',
|
||||||
spec,
|
spec,
|
||||||
specPreview: spec,
|
specPreview: spec,
|
||||||
|
|
||||||
|
@ -534,6 +534,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
return (
|
return (
|
||||||
<div className={classNames('load-data-view', 'app-view', step)}>
|
<div className={classNames('load-data-view', 'app-view', step)}>
|
||||||
{this.renderStepNav()}
|
{this.renderStepNav()}
|
||||||
|
{step === 'loading' && <Loader loading />}
|
||||||
|
|
||||||
{step === 'welcome' && this.renderWelcomeStep()}
|
{step === 'welcome' && this.renderWelcomeStep()}
|
||||||
{step === 'connect' && this.renderConnectStep()}
|
{step === 'connect' && this.renderConnectStep()}
|
||||||
{step === 'parser' && this.renderParserStep()}
|
{step === 'parser' && this.renderParserStep()}
|
||||||
|
@ -548,7 +550,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
{step === 'publish' && this.renderPublishStep()}
|
{step === 'publish' && this.renderPublishStep()}
|
||||||
|
|
||||||
{step === 'spec' && this.renderSpecStep()}
|
{step === 'spec' && this.renderSpecStep()}
|
||||||
{step === 'loading' && this.renderLoading()}
|
|
||||||
|
|
||||||
{this.renderResetConfirm()}
|
{this.renderResetConfirm()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1071,7 +1072,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
{deepGet(spec, 'ioConfig.firehose.type') === 'local' && (
|
{deepGet(spec, 'ioConfig.firehose.type') === 'local' && (
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<Callout intent={Intent.WARNING}>
|
<Callout intent={Intent.WARNING}>
|
||||||
This path must be available on the local filesystem of all Druid servers.
|
This path must be available on the local filesystem of all Druid services.
|
||||||
</Callout>
|
</Callout>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
)}
|
)}
|
||||||
|
@ -3000,10 +3001,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderLoading() {
|
|
||||||
return <Loader loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSpecStep() {
|
renderSpecStep() {
|
||||||
const { spec, submitting } = this.state;
|
const { spec, submitting } = this.state;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../utils/capabilities';
|
||||||
import { SegmentsView } from '../segments-view/segments-view';
|
import { SegmentsView } from '../segments-view/segments-view';
|
||||||
|
|
||||||
describe('segments-view', () => {
|
describe('segments-view', () => {
|
||||||
|
@ -28,7 +29,7 @@ describe('segments-view', () => {
|
||||||
datasource={'test'}
|
datasource={'test'}
|
||||||
onlyUnavailable={false}
|
onlyUnavailable={false}
|
||||||
goToQuery={() => {}}
|
goToQuery={() => {}}
|
||||||
capabilities="full"
|
capabilities={Capabilities.FULL}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(segmentsView).toMatchSnapshot();
|
expect(segmentsView).toMatchSnapshot();
|
||||||
|
|
|
@ -56,12 +56,12 @@ import {
|
||||||
sqlQueryCustomTableFilter,
|
sqlQueryCustomTableFilter,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { BasicAction } from '../../utils/basic-action';
|
import { BasicAction } from '../../utils/basic-action';
|
||||||
import { Capabilities } from '../../utils/capabilities';
|
import { Capabilities, CapabilitiesMode } from '../../utils/capabilities';
|
||||||
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
|
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
|
||||||
|
|
||||||
import './segments-view.scss';
|
import './segments-view.scss';
|
||||||
|
|
||||||
const tableColumns: Record<Capabilities, string[]> = {
|
const tableColumns: Record<CapabilitiesMode, string[]> = {
|
||||||
full: [
|
full: [
|
||||||
'Segment ID',
|
'Segment ID',
|
||||||
'Datasource',
|
'Datasource',
|
||||||
|
@ -103,7 +103,6 @@ const tableColumns: Record<Capabilities, string[]> = {
|
||||||
'Is available',
|
'Is available',
|
||||||
'Is overshadowed',
|
'Is overshadowed',
|
||||||
],
|
],
|
||||||
broken: ['Segment ID'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SegmentsViewProps {
|
export interface SegmentsViewProps {
|
||||||
|
@ -339,7 +338,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
const { capabilities } = this.props;
|
const { capabilities } = this.props;
|
||||||
if (capabilities === 'no-sql') {
|
if (!capabilities.hasSql() && capabilities.hasCoordinatorAccess()) {
|
||||||
this.segmentsNoSqlQueryManager.runQuery(null);
|
this.segmentsNoSqlQueryManager.runQuery(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -426,21 +425,21 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
||||||
onFilteredChange={filtered => {
|
onFilteredChange={filtered => {
|
||||||
this.setState({ segmentFilter: filtered });
|
this.setState({ segmentFilter: filtered });
|
||||||
}}
|
}}
|
||||||
onFetchData={
|
onFetchData={state => {
|
||||||
capabilities === 'no-sql'
|
if (capabilities.hasSql()) {
|
||||||
? this.fetchClientSideData
|
this.setState({
|
||||||
: state => {
|
page: state.page,
|
||||||
this.setState({
|
pageSize: state.pageSize,
|
||||||
page: state.page,
|
filtered: state.filtered,
|
||||||
pageSize: state.pageSize,
|
sorted: state.sorted,
|
||||||
filtered: state.filtered,
|
});
|
||||||
sorted: state.sorted,
|
if (this.segmentsSqlQueryManager.getLastQuery) {
|
||||||
});
|
this.fetchData(groupByInterval, state);
|
||||||
if (this.segmentsSqlQueryManager.getLastQuery) {
|
}
|
||||||
this.fetchData(groupByInterval, state);
|
} else if (capabilities.hasCoordinatorAccess()) {
|
||||||
}
|
this.fetchClientSideData(state);
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
showPageJump={false}
|
showPageJump={false}
|
||||||
ofText=""
|
ofText=""
|
||||||
pivotBy={groupByInterval ? ['interval'] : []}
|
pivotBy={groupByInterval ? ['interval'] : []}
|
||||||
|
@ -556,7 +555,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
||||||
filterable: false,
|
filterable: false,
|
||||||
defaultSortDesc: true,
|
defaultSortDesc: true,
|
||||||
Cell: row => (row.original.is_available ? formatNumber(row.value) : <em>(unknown)</em>),
|
Cell: row => (row.original.is_available ? formatNumber(row.value) : <em>(unknown)</em>),
|
||||||
show: capabilities !== 'no-sql' && hiddenColumns.exists('Num rows'),
|
show: capabilities.hasSql() && hiddenColumns.exists('Num rows'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Replicas',
|
Header: 'Replicas',
|
||||||
|
@ -564,35 +563,35 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
||||||
width: 60,
|
width: 60,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
defaultSortDesc: true,
|
defaultSortDesc: true,
|
||||||
show: capabilities !== 'no-sql' && hiddenColumns.exists('Replicas'),
|
show: capabilities.hasSql() && hiddenColumns.exists('Replicas'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Is published',
|
Header: 'Is published',
|
||||||
id: 'is_published',
|
id: 'is_published',
|
||||||
accessor: row => String(Boolean(row.is_published)),
|
accessor: row => String(Boolean(row.is_published)),
|
||||||
Filter: makeBooleanFilter(),
|
Filter: makeBooleanFilter(),
|
||||||
show: capabilities !== 'no-sql' && hiddenColumns.exists('Is published'),
|
show: capabilities.hasSql() && hiddenColumns.exists('Is published'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Is realtime',
|
Header: 'Is realtime',
|
||||||
id: 'is_realtime',
|
id: 'is_realtime',
|
||||||
accessor: row => String(Boolean(row.is_realtime)),
|
accessor: row => String(Boolean(row.is_realtime)),
|
||||||
Filter: makeBooleanFilter(),
|
Filter: makeBooleanFilter(),
|
||||||
show: capabilities !== 'no-sql' && hiddenColumns.exists('Is realtime'),
|
show: capabilities.hasSql() && hiddenColumns.exists('Is realtime'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Is available',
|
Header: 'Is available',
|
||||||
id: 'is_available',
|
id: 'is_available',
|
||||||
accessor: row => String(Boolean(row.is_available)),
|
accessor: row => String(Boolean(row.is_available)),
|
||||||
Filter: makeBooleanFilter(),
|
Filter: makeBooleanFilter(),
|
||||||
show: capabilities !== 'no-sql' && hiddenColumns.exists('Is available'),
|
show: capabilities.hasSql() && hiddenColumns.exists('Is available'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Is overshadowed',
|
Header: 'Is overshadowed',
|
||||||
id: 'is_overshadowed',
|
id: 'is_overshadowed',
|
||||||
accessor: row => String(Boolean(row.is_overshadowed)),
|
accessor: row => String(Boolean(row.is_overshadowed)),
|
||||||
Filter: makeBooleanFilter(),
|
Filter: makeBooleanFilter(),
|
||||||
show: capabilities !== 'no-sql' && hiddenColumns.exists('Is overshadowed'),
|
show: capabilities.hasSql() && hiddenColumns.exists('Is overshadowed'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: ACTION_COLUMN_LABEL,
|
Header: ACTION_COLUMN_LABEL,
|
||||||
|
@ -618,7 +617,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
Aggregated: () => '',
|
Aggregated: () => '',
|
||||||
show: capabilities !== 'no-proxy' && hiddenColumns.exists(ACTION_COLUMN_LABEL),
|
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists(ACTION_COLUMN_LABEL),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
defaultPageSize={SegmentsView.PAGE_SIZE}
|
defaultPageSize={SegmentsView.PAGE_SIZE}
|
||||||
|
@ -663,7 +662,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
||||||
|
|
||||||
const bulkSegmentsActionsMenu = (
|
const bulkSegmentsActionsMenu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
{capabilities !== 'no-sql' && (
|
{capabilities.hasSql() && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="View SQL query for table"
|
text="View SQL query for table"
|
||||||
|
@ -714,7 +713,11 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
||||||
active={!groupByInterval}
|
active={!groupByInterval}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({ groupByInterval: false });
|
this.setState({ groupByInterval: false });
|
||||||
capabilities === 'no-sql' ? this.fetchClientSideData() : this.fetchData(false);
|
if (capabilities.hasSql()) {
|
||||||
|
this.fetchData(false);
|
||||||
|
} else {
|
||||||
|
this.fetchClientSideData();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
None
|
None
|
||||||
|
@ -731,7 +734,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
{this.renderBulkSegmentsActions()}
|
{this.renderBulkSegmentsActions()}
|
||||||
<TableColumnSelector
|
<TableColumnSelector
|
||||||
columns={tableColumns[capabilities]}
|
columns={tableColumns[capabilities.getMode()]}
|
||||||
onChange={column =>
|
onChange={column =>
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
hiddenColumns: prevState.hiddenColumns.toggle(column),
|
hiddenColumns: prevState.hiddenColumns.toggle(column),
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`servers view action servers view 1`] = `
|
exports[`services view action services view 1`] = `
|
||||||
<div
|
<div
|
||||||
className="servers-view app-view"
|
className="services-view app-view"
|
||||||
>
|
>
|
||||||
<Memo(ViewControlBar)
|
<Memo(ViewControlBar)
|
||||||
label="Servers"
|
label="Services"
|
||||||
>
|
>
|
||||||
<Component>
|
<Component>
|
||||||
Group by
|
Group by
|
||||||
|
@ -31,7 +31,7 @@ exports[`servers view action servers view 1`] = `
|
||||||
</Blueprint3.Button>
|
</Blueprint3.Button>
|
||||||
</Blueprint3.ButtonGroup>
|
</Blueprint3.ButtonGroup>
|
||||||
<Memo(RefreshButton)
|
<Memo(RefreshButton)
|
||||||
localStorageKey="servers-refresh-rate"
|
localStorageKey="services-refresh-rate"
|
||||||
onRefresh={[Function]}
|
onRefresh={[Function]}
|
||||||
/>
|
/>
|
||||||
<Blueprint3.Popover
|
<Blueprint3.Popover
|
||||||
|
@ -74,7 +74,7 @@ exports[`servers view action servers view 1`] = `
|
||||||
<Memo(TableColumnSelector)
|
<Memo(TableColumnSelector)
|
||||||
columns={
|
columns={
|
||||||
Array [
|
Array [
|
||||||
"Server",
|
"Service",
|
||||||
"Type",
|
"Type",
|
||||||
"Tier",
|
"Tier",
|
||||||
"Host",
|
"Host",
|
||||||
|
@ -149,15 +149,15 @@ exports[`servers view action servers view 1`] = `
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
"Aggregated": [Function],
|
"Aggregated": [Function],
|
||||||
"Header": "Server",
|
"Header": "Service",
|
||||||
"accessor": "server",
|
"accessor": "service",
|
||||||
"show": true,
|
"show": true,
|
||||||
"width": 300,
|
"width": 300,
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"Cell": [Function],
|
"Cell": [Function],
|
||||||
"Header": "Type",
|
"Header": "Type",
|
||||||
"accessor": "server_type",
|
"accessor": "service_type",
|
||||||
"show": true,
|
"show": true,
|
||||||
"width": 150,
|
"width": 150,
|
||||||
},
|
},
|
|
@ -19,18 +19,20 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ServersView } from './servers-view';
|
import { Capabilities } from '../../utils/capabilities';
|
||||||
|
|
||||||
describe('servers view', () => {
|
import { ServicesView } from './services-view';
|
||||||
it('action servers view', () => {
|
|
||||||
const serversView = shallow(
|
describe('services view', () => {
|
||||||
<ServersView
|
it('action services view', () => {
|
||||||
|
const servicesView = shallow(
|
||||||
|
<ServicesView
|
||||||
middleManager={'test'}
|
middleManager={'test'}
|
||||||
goToQuery={() => {}}
|
goToQuery={() => {}}
|
||||||
goToTask={() => {}}
|
goToTask={() => {}}
|
||||||
capabilities="full"
|
capabilities={Capabilities.FULL}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(serversView).toMatchSnapshot();
|
expect(servicesView).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -53,14 +53,14 @@ import {
|
||||||
QueryManager,
|
QueryManager,
|
||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { BasicAction } from '../../utils/basic-action';
|
import { BasicAction } from '../../utils/basic-action';
|
||||||
import { Capabilities } from '../../utils/capabilities';
|
import { Capabilities, CapabilitiesMode } from '../../utils/capabilities';
|
||||||
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
|
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
|
||||||
import { deepGet } from '../../utils/object-change';
|
import { deepGet } from '../../utils/object-change';
|
||||||
|
|
||||||
import './servers-view.scss';
|
import './services-view.scss';
|
||||||
|
|
||||||
const allColumns: string[] = [
|
const allColumns: string[] = [
|
||||||
'Server',
|
'Service',
|
||||||
'Type',
|
'Type',
|
||||||
'Tier',
|
'Tier',
|
||||||
'Host',
|
'Host',
|
||||||
|
@ -72,11 +72,10 @@ const allColumns: string[] = [
|
||||||
ACTION_COLUMN_LABEL,
|
ACTION_COLUMN_LABEL,
|
||||||
];
|
];
|
||||||
|
|
||||||
const tableColumns: Record<Capabilities, string[]> = {
|
const tableColumns: Record<CapabilitiesMode, string[]> = {
|
||||||
full: allColumns,
|
full: allColumns,
|
||||||
'no-sql': allColumns,
|
'no-sql': allColumns,
|
||||||
'no-proxy': ['Server', 'Type', 'Tier', 'Host', 'Port', 'Curr size', 'Max size', 'Usage'],
|
'no-proxy': ['Service', 'Type', 'Tier', 'Host', 'Port', 'Curr size', 'Max size', 'Usage'],
|
||||||
broken: ['Server'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatQueues(
|
function formatQueues(
|
||||||
|
@ -99,19 +98,19 @@ function formatQueues(
|
||||||
return queueParts.join(', ') || 'Empty load/drop queues';
|
return queueParts.join(', ') || 'Empty load/drop queues';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServersViewProps {
|
export interface ServicesViewProps {
|
||||||
middleManager: string | undefined;
|
middleManager: string | undefined;
|
||||||
goToQuery: (initSql: string) => void;
|
goToQuery: (initSql: string) => void;
|
||||||
goToTask: (taskId: string) => void;
|
goToTask: (taskId: string) => void;
|
||||||
capabilities: Capabilities;
|
capabilities: Capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServersViewState {
|
export interface ServicesViewState {
|
||||||
serversLoading: boolean;
|
servicesLoading: boolean;
|
||||||
servers?: any[];
|
services?: any[];
|
||||||
serversError?: string;
|
servicesError?: string;
|
||||||
serverFilter: Filter[];
|
serviceFilter: Filter[];
|
||||||
groupServersBy?: 'server_type' | 'tier';
|
groupServicesBy?: 'service_type' | 'tier';
|
||||||
|
|
||||||
middleManagerDisableWorkerHost?: string;
|
middleManagerDisableWorkerHost?: string;
|
||||||
middleManagerEnableWorkerHost?: string;
|
middleManagerEnableWorkerHost?: string;
|
||||||
|
@ -119,9 +118,9 @@ export interface ServersViewState {
|
||||||
hiddenColumns: LocalStorageBackedArray<string>;
|
hiddenColumns: LocalStorageBackedArray<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerQueryResultRow {
|
interface ServiceQueryResultRow {
|
||||||
server: string;
|
service: string;
|
||||||
server_type: string;
|
service_type: string;
|
||||||
tier: string;
|
tier: string;
|
||||||
curr_size: number;
|
curr_size: number;
|
||||||
host: string;
|
host: string;
|
||||||
|
@ -152,13 +151,13 @@ interface MiddleManagerQueryResultRow {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServerResultRow
|
interface ServiceResultRow
|
||||||
extends ServerQueryResultRow,
|
extends ServiceQueryResultRow,
|
||||||
Partial<LoadQueueStatus>,
|
Partial<LoadQueueStatus>,
|
||||||
Partial<MiddleManagerQueryResultRow> {}
|
Partial<MiddleManagerQueryResultRow> {}
|
||||||
|
|
||||||
export class ServersView extends React.PureComponent<ServersViewProps, ServersViewState> {
|
export class ServicesView extends React.PureComponent<ServicesViewProps, ServicesViewState> {
|
||||||
private serverQueryManager: QueryManager<Capabilities, ServerResultRow[]>;
|
private serviceQueryManager: QueryManager<Capabilities, ServiceResultRow[]>;
|
||||||
|
|
||||||
// Ranking
|
// Ranking
|
||||||
// coordinator => 7
|
// coordinator => 7
|
||||||
|
@ -169,8 +168,8 @@ export class ServersView extends React.PureComponent<ServersViewProps, ServersVi
|
||||||
// middle_manager => 2
|
// middle_manager => 2
|
||||||
// peon => 1
|
// peon => 1
|
||||||
|
|
||||||
static SERVER_SQL = `SELECT
|
static SERVICE_SQL = `SELECT
|
||||||
"server", "server_type", "tier", "host", "plaintext_port", "tls_port", "curr_size", "max_size",
|
"server" AS "service", "server_type" AS "service_type", "tier", "host", "plaintext_port", "tls_port", "curr_size", "max_size",
|
||||||
(
|
(
|
||||||
CASE "server_type"
|
CASE "server_type"
|
||||||
WHEN 'coordinator' THEN 7
|
WHEN 'coordinator' THEN 7
|
||||||
|
@ -184,15 +183,15 @@ export class ServersView extends React.PureComponent<ServersViewProps, ServersVi
|
||||||
END
|
END
|
||||||
) AS "rank"
|
) AS "rank"
|
||||||
FROM sys.servers
|
FROM sys.servers
|
||||||
ORDER BY "rank" DESC, "server" DESC`;
|
ORDER BY "rank" DESC, "service" DESC`;
|
||||||
|
|
||||||
static async getServers(): Promise<ServerQueryResultRow[]> {
|
static async getServices(): Promise<ServiceQueryResultRow[]> {
|
||||||
const allServerResp = await axios.get('/druid/coordinator/v1/servers?simple');
|
const allServiceResp = await axios.get('/druid/coordinator/v1/servers?simple');
|
||||||
const allServers = allServerResp.data;
|
const allServices = allServiceResp.data;
|
||||||
return allServers.map((s: any) => {
|
return allServices.map((s: any) => {
|
||||||
return {
|
return {
|
||||||
server: s.host,
|
service: s.host,
|
||||||
server_type: s.type === 'indexer-executor' ? 'peon' : s.type,
|
service_type: s.type === 'indexer-executor' ? 'peon' : s.type,
|
||||||
tier: s.tier,
|
tier: s.tier,
|
||||||
host: s.host.split(':')[0],
|
host: s.host.split(':')[0],
|
||||||
plaintext_port: parseInt(s.host.split(':')[1], 10),
|
plaintext_port: parseInt(s.host.split(':')[1], 10),
|
||||||
|
@ -203,73 +202,77 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: ServersViewProps, context: any) {
|
constructor(props: ServicesViewProps, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
serversLoading: true,
|
servicesLoading: true,
|
||||||
serverFilter: [],
|
serviceFilter: [],
|
||||||
|
|
||||||
hiddenColumns: new LocalStorageBackedArray<string>(
|
hiddenColumns: new LocalStorageBackedArray<string>(
|
||||||
LocalStorageKeys.SERVER_TABLE_COLUMN_SELECTION,
|
LocalStorageKeys.SERVICE_TABLE_COLUMN_SELECTION,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.serverQueryManager = new QueryManager({
|
this.serviceQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
let servers: ServerQueryResultRow[];
|
let services: ServiceQueryResultRow[];
|
||||||
if (capabilities !== 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
servers = await queryDruidSql({ query: ServersView.SERVER_SQL });
|
services = await queryDruidSql({ query: ServicesView.SERVICE_SQL });
|
||||||
|
} else if (capabilities.hasCoordinatorAccess()) {
|
||||||
|
services = await ServicesView.getServices();
|
||||||
} else {
|
} else {
|
||||||
servers = await ServersView.getServers();
|
throw new Error(`must have SQL or coordinator access`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capabilities === 'no-proxy') {
|
if (capabilities.hasCoordinatorAccess()) {
|
||||||
return servers;
|
const loadQueueResponse = await axios.get('/druid/coordinator/v1/loadqueue?simple');
|
||||||
|
const loadQueues: Record<string, LoadQueueStatus> = loadQueueResponse.data;
|
||||||
|
services = services.map(s => {
|
||||||
|
const loadQueueInfo = loadQueues[s.service];
|
||||||
|
if (loadQueueInfo) {
|
||||||
|
s = Object.assign(s, loadQueueInfo);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadQueueResponse = await axios.get('/druid/coordinator/v1/loadqueue?simple');
|
if (capabilities.hasOverlordAccess()) {
|
||||||
const loadQueues: Record<string, LoadQueueStatus> = loadQueueResponse.data;
|
let middleManagers: MiddleManagerQueryResultRow[];
|
||||||
servers = servers.map((s: any) => {
|
try {
|
||||||
const loadQueueInfo = loadQueues[s.server];
|
const middleManagerResponse = await axios.get('/druid/indexer/v1/workers');
|
||||||
if (loadQueueInfo) {
|
middleManagers = middleManagerResponse.data;
|
||||||
s = Object.assign(s, loadQueueInfo);
|
} catch (e) {
|
||||||
|
if (
|
||||||
|
e.response &&
|
||||||
|
typeof e.response.data === 'object' &&
|
||||||
|
e.response.data.error === 'Task Runner does not support worker listing'
|
||||||
|
) {
|
||||||
|
// Swallow this error because it simply a reflection of a local task runner.
|
||||||
|
middleManagers = [];
|
||||||
|
} else {
|
||||||
|
// Otherwise re-throw.
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return s;
|
|
||||||
});
|
|
||||||
|
|
||||||
let middleManagers: MiddleManagerQueryResultRow[];
|
const middleManagersLookup = lookupBy(middleManagers, m => m.worker.host);
|
||||||
try {
|
|
||||||
const middleManagerResponse = await axios.get('/druid/indexer/v1/workers');
|
services = services.map(s => {
|
||||||
middleManagers = middleManagerResponse.data;
|
const middleManagerInfo = middleManagersLookup[s.service];
|
||||||
} catch (e) {
|
if (middleManagerInfo) {
|
||||||
if (
|
s = Object.assign(s, middleManagerInfo);
|
||||||
e.response &&
|
}
|
||||||
typeof e.response.data === 'object' &&
|
return s;
|
||||||
e.response.data.error === 'Task Runner does not support worker listing'
|
});
|
||||||
) {
|
|
||||||
// Swallow this error because it simply a reflection of a local task runner.
|
|
||||||
middleManagers = [];
|
|
||||||
} else {
|
|
||||||
// Otherwise re-throw.
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const middleManagersLookup = lookupBy(middleManagers, m => m.worker.host);
|
return services;
|
||||||
|
|
||||||
return servers.map((s: any) => {
|
|
||||||
const middleManagerInfo = middleManagersLookup[s.server];
|
|
||||||
if (middleManagerInfo) {
|
|
||||||
s = Object.assign(s, middleManagerInfo);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
servers: result,
|
services: result,
|
||||||
serversLoading: loading,
|
servicesLoading: loading,
|
||||||
serversError: error,
|
servicesError: error,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -277,21 +280,21 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
const { capabilities } = this.props;
|
const { capabilities } = this.props;
|
||||||
this.serverQueryManager.runQuery(capabilities);
|
this.serviceQueryManager.runQuery(capabilities);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
this.serverQueryManager.terminate();
|
this.serviceQueryManager.terminate();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderServersTable() {
|
renderServicesTable() {
|
||||||
const { capabilities } = this.props;
|
const { capabilities } = this.props;
|
||||||
const {
|
const {
|
||||||
servers,
|
services,
|
||||||
serversLoading,
|
servicesLoading,
|
||||||
serversError,
|
servicesError,
|
||||||
serverFilter,
|
serviceFilter,
|
||||||
groupServersBy,
|
groupServicesBy,
|
||||||
hiddenColumns,
|
hiddenColumns,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
@ -308,36 +311,38 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactTable
|
<ReactTable
|
||||||
data={servers || []}
|
data={services || []}
|
||||||
loading={serversLoading}
|
loading={servicesLoading}
|
||||||
noDataText={
|
noDataText={
|
||||||
!serversLoading && servers && !servers.length ? 'No historicals' : serversError || ''
|
!servicesLoading && services && !services.length ? 'No historicals' : servicesError || ''
|
||||||
}
|
}
|
||||||
filterable
|
filterable
|
||||||
filtered={serverFilter}
|
filtered={serviceFilter}
|
||||||
onFilteredChange={filtered => {
|
onFilteredChange={filtered => {
|
||||||
this.setState({ serverFilter: filtered });
|
this.setState({ serviceFilter: filtered });
|
||||||
}}
|
}}
|
||||||
pivotBy={groupServersBy ? [groupServersBy] : []}
|
pivotBy={groupServicesBy ? [groupServicesBy] : []}
|
||||||
defaultPageSize={50}
|
defaultPageSize={50}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
Header: 'Server',
|
Header: 'Service',
|
||||||
accessor: 'server',
|
accessor: 'service',
|
||||||
width: 300,
|
width: 300,
|
||||||
Aggregated: () => '',
|
Aggregated: () => '',
|
||||||
show: hiddenColumns.exists('Server'),
|
show: hiddenColumns.exists('Service'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: 'Type',
|
Header: 'Type',
|
||||||
accessor: 'server_type',
|
accessor: 'service_type',
|
||||||
width: 150,
|
width: 150,
|
||||||
Cell: row => {
|
Cell: row => {
|
||||||
const value = row.value;
|
const value = row.value;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({ serverFilter: addFilter(serverFilter, 'server_type', value) });
|
this.setState({
|
||||||
|
serviceFilter: addFilter(serviceFilter, 'service_type', value),
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
|
@ -354,7 +359,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.setState({ serverFilter: addFilter(serverFilter, 'tier', value) });
|
this.setState({ serviceFilter: addFilter(serviceFilter, 'tier', value) });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
|
@ -398,7 +403,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
return formatBytes(totalCurr);
|
return formatBytes(totalCurr);
|
||||||
},
|
},
|
||||||
Cell: row => {
|
Cell: row => {
|
||||||
if (row.aggregated || row.original.server_type !== 'historical') return '';
|
if (row.aggregated || row.original.service_type !== 'historical') return '';
|
||||||
if (row.value === null) return '';
|
if (row.value === null) return '';
|
||||||
return formatBytes(row.value);
|
return formatBytes(row.value);
|
||||||
},
|
},
|
||||||
|
@ -417,7 +422,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
return formatBytes(totalMax);
|
return formatBytes(totalMax);
|
||||||
},
|
},
|
||||||
Cell: row => {
|
Cell: row => {
|
||||||
if (row.aggregated || row.original.server_type !== 'historical') return '';
|
if (row.aggregated || row.original.service_type !== 'historical') return '';
|
||||||
if (row.value === null) return '';
|
if (row.value === null) return '';
|
||||||
return formatBytes(row.value);
|
return formatBytes(row.value);
|
||||||
},
|
},
|
||||||
|
@ -429,7 +434,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
width: 100,
|
width: 100,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
accessor: row => {
|
accessor: row => {
|
||||||
if (row.server_type === 'middle_manager') {
|
if (row.service_type === 'middle_manager') {
|
||||||
return row.worker ? row.currCapacityUsed / row.worker.capacity : null;
|
return row.worker ? row.currCapacityUsed / row.worker.capacity : null;
|
||||||
} else {
|
} else {
|
||||||
return row.max_size ? row.curr_size / row.max_size : null;
|
return row.max_size ? row.curr_size / row.max_size : null;
|
||||||
|
@ -461,8 +466,8 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
},
|
},
|
||||||
Cell: row => {
|
Cell: row => {
|
||||||
if (row.aggregated) return '';
|
if (row.aggregated) return '';
|
||||||
const { server_type } = row.original;
|
const { service_type } = row.original;
|
||||||
switch (server_type) {
|
switch (service_type) {
|
||||||
case 'historical':
|
case 'historical':
|
||||||
return fillIndicator(row.value);
|
return fillIndicator(row.value);
|
||||||
|
|
||||||
|
@ -487,7 +492,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
width: 400,
|
width: 400,
|
||||||
filterable: false,
|
filterable: false,
|
||||||
accessor: row => {
|
accessor: row => {
|
||||||
if (row.server_type === 'middle_manager') {
|
if (row.service_type === 'middle_manager') {
|
||||||
if (deepGet(row, 'worker.version') === '') return 'Disabled';
|
if (deepGet(row, 'worker.version') === '') return 'Disabled';
|
||||||
|
|
||||||
const details: string[] = [];
|
const details: string[] = [];
|
||||||
|
@ -504,8 +509,8 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
},
|
},
|
||||||
Cell: row => {
|
Cell: row => {
|
||||||
if (row.aggregated) return '';
|
if (row.aggregated) return '';
|
||||||
const { server_type } = row.original;
|
const { service_type } = row.original;
|
||||||
switch (server_type) {
|
switch (service_type) {
|
||||||
case 'historical':
|
case 'historical':
|
||||||
const {
|
const {
|
||||||
segmentsToLoad,
|
segmentsToLoad,
|
||||||
|
@ -541,7 +546,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
segmentsToDropSize,
|
segmentsToDropSize,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
show: capabilities !== 'no-proxy' && hiddenColumns.exists('Detail'),
|
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Detail'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: ACTION_COLUMN_LABEL,
|
Header: ACTION_COLUMN_LABEL,
|
||||||
|
@ -555,7 +560,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
const workerActions = this.getWorkerActions(row.value.host, disabled);
|
const workerActions = this.getWorkerActions(row.value.host, disabled);
|
||||||
return <ActionCell actions={workerActions} />;
|
return <ActionCell actions={workerActions} />;
|
||||||
},
|
},
|
||||||
show: capabilities !== 'no-proxy' && hiddenColumns.exists(ACTION_COLUMN_LABEL),
|
show: capabilities.hasOverlordAccess() && hiddenColumns.exists(ACTION_COLUMN_LABEL),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -603,7 +608,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
this.setState({ middleManagerDisableWorkerHost: undefined });
|
this.setState({ middleManagerDisableWorkerHost: undefined });
|
||||||
}}
|
}}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
this.serverQueryManager.rerunLastQuery();
|
this.serviceQueryManager.rerunLastQuery();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p>{`Are you sure you want to disable worker '${middleManagerDisableWorkerHost}'?`}</p>
|
<p>{`Are you sure you want to disable worker '${middleManagerDisableWorkerHost}'?`}</p>
|
||||||
|
@ -632,7 +637,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
this.setState({ middleManagerEnableWorkerHost: undefined });
|
this.setState({ middleManagerEnableWorkerHost: undefined });
|
||||||
}}
|
}}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
this.serverQueryManager.rerunLastQuery();
|
this.serviceQueryManager.rerunLastQuery();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p>{`Are you sure you want to enable worker '${middleManagerEnableWorkerHost}'?`}</p>
|
<p>{`Are you sure you want to enable worker '${middleManagerEnableWorkerHost}'?`}</p>
|
||||||
|
@ -640,16 +645,16 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderBulkServersActions() {
|
renderBulkServicesActions() {
|
||||||
const { goToQuery, capabilities } = this.props;
|
const { goToQuery, capabilities } = this.props;
|
||||||
|
|
||||||
const bulkserversActionsMenu = (
|
const bulkservicesActionsMenu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
{capabilities !== 'no-sql' && (
|
{capabilities.hasSql() && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="View SQL query for table"
|
text="View SQL query for table"
|
||||||
onClick={() => goToQuery(ServersView.SERVER_SQL)}
|
onClick={() => goToQuery(ServicesView.SERVICE_SQL)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
|
@ -657,7 +662,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover content={bulkserversActionsMenu} position={Position.BOTTOM_LEFT}>
|
<Popover content={bulkservicesActionsMenu} position={Position.BOTTOM_LEFT}>
|
||||||
<Button icon={IconNames.MORE} />
|
<Button icon={IconNames.MORE} />
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
|
@ -666,39 +671,39 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const { capabilities } = this.props;
|
const { capabilities } = this.props;
|
||||||
const { groupServersBy, hiddenColumns } = this.state;
|
const { groupServicesBy, hiddenColumns } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="servers-view app-view">
|
<div className="services-view app-view">
|
||||||
<ViewControlBar label="Servers">
|
<ViewControlBar label="Services">
|
||||||
<Label>Group by</Label>
|
<Label>Group by</Label>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
active={!groupServersBy}
|
active={!groupServicesBy}
|
||||||
onClick={() => this.setState({ groupServersBy: undefined })}
|
onClick={() => this.setState({ groupServicesBy: undefined })}
|
||||||
>
|
>
|
||||||
None
|
None
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
active={groupServersBy === 'server_type'}
|
active={groupServicesBy === 'service_type'}
|
||||||
onClick={() => this.setState({ groupServersBy: 'server_type' })}
|
onClick={() => this.setState({ groupServicesBy: 'service_type' })}
|
||||||
>
|
>
|
||||||
Type
|
Type
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
active={groupServersBy === 'tier'}
|
active={groupServicesBy === 'tier'}
|
||||||
onClick={() => this.setState({ groupServersBy: 'tier' })}
|
onClick={() => this.setState({ groupServicesBy: 'tier' })}
|
||||||
>
|
>
|
||||||
Tier
|
Tier
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
onRefresh={auto => this.serverQueryManager.rerunLastQuery(auto)}
|
onRefresh={auto => this.serviceQueryManager.rerunLastQuery(auto)}
|
||||||
localStorageKey={LocalStorageKeys.SERVERS_REFRESH_RATE}
|
localStorageKey={LocalStorageKeys.SERVICES_REFRESH_RATE}
|
||||||
/>
|
/>
|
||||||
{this.renderBulkServersActions()}
|
{this.renderBulkServicesActions()}
|
||||||
<TableColumnSelector
|
<TableColumnSelector
|
||||||
columns={tableColumns[capabilities]}
|
columns={tableColumns[capabilities.getMode()]}
|
||||||
onChange={column =>
|
onChange={column =>
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
hiddenColumns: prevState.hiddenColumns.toggle(column),
|
hiddenColumns: prevState.hiddenColumns.toggle(column),
|
||||||
|
@ -707,7 +712,7 @@ ORDER BY "rank" DESC, "server" DESC`;
|
||||||
tableColumnsHidden={hiddenColumns.storedArray}
|
tableColumnsHidden={hiddenColumns.storedArray}
|
||||||
/>
|
/>
|
||||||
</ViewControlBar>
|
</ViewControlBar>
|
||||||
{this.renderServersTable()}
|
{this.renderServicesTable()}
|
||||||
{this.renderDisableWorkerAction()}
|
{this.renderDisableWorkerAction()}
|
||||||
{this.renderEnableWorkerAction()}
|
{this.renderEnableWorkerAction()}
|
||||||
</div>
|
</div>
|
|
@ -19,6 +19,8 @@
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Capabilities } from '../../utils/capabilities';
|
||||||
|
|
||||||
import { TasksView } from './tasks-view';
|
import { TasksView } from './tasks-view';
|
||||||
|
|
||||||
describe('tasks view', () => {
|
describe('tasks view', () => {
|
||||||
|
@ -32,7 +34,7 @@ describe('tasks view', () => {
|
||||||
goToQuery={() => {}}
|
goToQuery={() => {}}
|
||||||
goToMiddleManager={() => {}}
|
goToMiddleManager={() => {}}
|
||||||
goToLoadData={() => {}}
|
goToLoadData={() => {}}
|
||||||
capabilities="full"
|
capabilities={Capabilities.FULL}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(taskView).toMatchSnapshot();
|
expect(taskView).toMatchSnapshot();
|
||||||
|
|
|
@ -259,11 +259,11 @@ ORDER BY "rank" DESC, "created_time" DESC`;
|
||||||
|
|
||||||
this.supervisorQueryManager = new QueryManager({
|
this.supervisorQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
if (capabilities !== 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
return await queryDruidSql({
|
return await queryDruidSql({
|
||||||
query: TasksView.SUPERVISOR_SQL,
|
query: TasksView.SUPERVISOR_SQL,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (capabilities.hasOverlordAccess()) {
|
||||||
const supervisors = (await axios.get('/druid/indexer/v1/supervisor?full')).data;
|
const supervisors = (await axios.get('/druid/indexer/v1/supervisor?full')).data;
|
||||||
if (!Array.isArray(supervisors)) throw new Error(`Unexpected results`);
|
if (!Array.isArray(supervisors)) throw new Error(`Unexpected results`);
|
||||||
return supervisors.map((sup: any) => {
|
return supervisors.map((sup: any) => {
|
||||||
|
@ -279,6 +279,8 @@ ORDER BY "rank" DESC, "created_time" DESC`;
|
||||||
suspended: Number(deepGet(sup, 'suspended')),
|
suspended: Number(deepGet(sup, 'suspended')),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`must have SQL or overlord access`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
|
@ -292,11 +294,11 @@ ORDER BY "rank" DESC, "created_time" DESC`;
|
||||||
|
|
||||||
this.taskQueryManager = new QueryManager({
|
this.taskQueryManager = new QueryManager({
|
||||||
processQuery: async capabilities => {
|
processQuery: async capabilities => {
|
||||||
if (capabilities !== 'no-sql') {
|
if (capabilities.hasSql()) {
|
||||||
return await queryDruidSql({
|
return await queryDruidSql({
|
||||||
query: TasksView.TASK_SQL,
|
query: TasksView.TASK_SQL,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (capabilities.hasOverlordAccess()) {
|
||||||
const taskEndpoints: string[] = [
|
const taskEndpoints: string[] = [
|
||||||
'completeTasks',
|
'completeTasks',
|
||||||
'runningTasks',
|
'runningTasks',
|
||||||
|
@ -310,6 +312,8 @@ ORDER BY "rank" DESC, "created_time" DESC`;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return result.flat();
|
return result.flat();
|
||||||
|
} else {
|
||||||
|
throw new Error(`must have SQL or overlord access`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStateChange: ({ result, loading, error }) => {
|
onStateChange: ({ result, loading, error }) => {
|
||||||
|
@ -934,7 +938,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
|
||||||
|
|
||||||
const bulkSupervisorActionsMenu = (
|
const bulkSupervisorActionsMenu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
{capabilities !== 'no-sql' && (
|
{capabilities.hasSql() && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="View SQL query for table"
|
text="View SQL query for table"
|
||||||
|
@ -1060,7 +1064,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
|
||||||
|
|
||||||
const bulkTaskActionsMenu = (
|
const bulkTaskActionsMenu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
{capabilities !== 'no-sql' && (
|
{capabilities.hasSql() && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={IconNames.APPLICATION}
|
icon={IconNames.APPLICATION}
|
||||||
text="View SQL query for table"
|
text="View SQL query for table"
|
||||||
|
|
Loading…
Reference in New Issue