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:
Vadim Ogievetsky 2019-11-05 23:39:14 -08:00 committed by GitHub
parent 6f7fbeb63a
commit 7addfc27da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 623 additions and 440 deletions

View File

@ -10,7 +10,7 @@ exports[`header bar matches snapshot 1`] = `
<a
href="#"
>
<Logo />
<Memo(DruidLogo) />
</a>
<Blueprint3.NavbarDivider />
<Blueprint3.AnchorButton
@ -46,14 +46,15 @@ exports[`header bar matches snapshot 1`] = `
/>
<Blueprint3.AnchorButton
active={false}
href="#servers"
href="#services"
icon="database"
minimal={true}
text="Servers"
text="Services"
/>
<Blueprint3.NavbarDivider />
<Blueprint3.AnchorButton
active={false}
disabled={false}
href="#query"
icon="application"
minimal={true}
@ -66,7 +67,17 @@ exports[`header bar matches snapshot 1`] = `
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
content={<LegacyMenu />}
content={
<Memo(LegacyMenu)
capabilities={
Capabilities {
"coordinator": true,
"overlord": true,
"queryType": "nativeAndSql",
}
}
/>
}
defaultIsOpen={false}
disabled={false}
fill={false}
@ -85,7 +96,6 @@ exports[`header bar matches snapshot 1`] = `
wrapperTagName="span"
>
<Blueprint3.Button
disabled={false}
icon="share"
minimal={true}
text="Legacy"
@ -153,7 +163,6 @@ exports[`header bar matches snapshot 1`] = `
wrapperTagName="span"
>
<Blueprint3.Button
disabled={false}
icon="cog"
minimal={true}
/>

View File

@ -21,7 +21,7 @@
.header-bar {
overflow: hidden;
.logo {
.druid-logo {
position: relative;
width: 100px;
height: $header-bar-height;

View File

@ -19,12 +19,14 @@
import { shallow } from 'enzyme';
import React from 'react';
import { Capabilities } from '../../utils/capabilities';
import { HeaderBar } from './header-bar';
describe('header bar', () => {
it('matches snapshot', () => {
const headerBar = shallow(
<HeaderBar active={'load-data'} hideLegacy={false} capabilities="full" />,
<HeaderBar active={'load-data'} hideLegacy={false} capabilities={Capabilities.FULL} />,
);
expect(headerBar).toMatchSnapshot();
});

View File

@ -55,12 +55,12 @@ export type HeaderActiveTab =
| 'datasources'
| 'segments'
| 'tasks'
| 'servers'
| 'services'
| 'lookups';
function Logo() {
const DruidLogo = React.memo(function DruidLogo() {
return (
<div className="logo">
<div className="druid-logo">
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
@ -113,9 +113,15 @@ function Logo() {
</svg>
</div>
);
});
interface LegacyMenuProps {
capabilities: Capabilities;
}
function LegacyMenu() {
const LegacyMenu = React.memo(function LegacyMenu(props: LegacyMenuProps) {
const { capabilities } = props;
return (
<Menu>
<MenuItem
@ -123,16 +129,18 @@ function LegacyMenu() {
text="Legacy coordinator console"
href={LEGACY_COORDINATOR_CONSOLE}
target="_blank"
disabled={!capabilities.hasCoordinatorAccess()}
/>
<MenuItem
icon={IconNames.MAP}
text="Legacy overlord console"
href={LEGACY_OVERLORD_CONSOLE}
target="_blank"
disabled={!capabilities.hasOverlordAccess()}
/>
</Menu>
);
}
});
export interface HeaderBarProps {
active: HeaderActiveTab;
@ -171,22 +179,26 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
icon={IconNames.PULSE}
text="Druid Doctor"
onClick={() => setDoctorDialogOpen(true)}
disabled={!capabilities.hasEverything()}
/>
<MenuItem
icon={IconNames.SETTINGS}
text="Coordinator dynamic config"
onClick={() => setCoordinatorDynamicConfigDialogOpen(true)}
disabled={!capabilities.hasCoordinatorAccess()}
/>
<MenuItem
icon={IconNames.WRENCH}
text="Overlord dynamic config"
onClick={() => setOverlordDynamicConfigDialogOpen(true)}
disabled={!capabilities.hasOverlordAccess()}
/>
<MenuItem
icon={IconNames.PROPERTIES}
active={active === 'lookups'}
text="Lookups"
href="#lookups"
disabled={!capabilities.hasCoordinatorAccess()}
/>
</Menu>
);
@ -195,7 +207,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
<Navbar className="header-bar">
<NavbarGroup align={Alignment.LEFT}>
<a href="#">
<Logo />
<DruidLogo />
</a>
<NavbarDivider />
@ -206,7 +218,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
href="#load-data"
minimal={!loadDataPrimary}
intent={loadDataPrimary ? Intent.PRIMARY : Intent.NONE}
disabled={capabilities === 'no-proxy'}
disabled={!capabilities.hasEverything()}
/>
<NavbarDivider />
@ -233,10 +245,10 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
/>
<AnchorButton
minimal
active={active === 'servers'}
active={active === 'services'}
icon={IconNames.DATABASE}
text="Servers"
href="#servers"
text="Services"
href="#services"
/>
<NavbarDivider />
@ -246,29 +258,20 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
icon={IconNames.APPLICATION}
text="Query"
href="#query"
disabled={!capabilities.hasQuerying()}
/>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
{!hideLegacy && (
<Popover
content={<LegacyMenu />}
content={<LegacyMenu capabilities={capabilities} />}
position={Position.BOTTOM_RIGHT}
disabled={capabilities === 'no-proxy'}
>
<Button
minimal
icon={IconNames.SHARE}
text="Legacy"
disabled={capabilities === 'no-proxy'}
/>
<Button minimal icon={IconNames.SHARE} text="Legacy" />
</Popover>
)}
<Popover
content={configMenu}
position={Position.BOTTOM_RIGHT}
disabled={capabilities === 'no-proxy'}
>
<Button minimal icon={IconNames.COG} disabled={capabilities === 'no-proxy'} />
<Popover content={configMenu} position={Position.BOTTOM_RIGHT}>
<Button minimal icon={IconNames.COG} />
</Popover>
<Popover content={helpMenu} position={Position.BOTTOM_RIGHT}>
<Button minimal icon={IconNames.HELP} />

View File

@ -18,14 +18,13 @@
import { Intent } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import classNames from 'classnames';
import React from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import { ExternalLink, HeaderActiveTab, HeaderBar, Loader } from './components';
import { AppToaster } from './singletons/toaster';
import { localStorageGet, LocalStorageKeys, QueryManager } from './utils';
import { QueryManager } from './utils';
import { Capabilities } from './utils/capabilities';
import { DRUID_DOCS_API, DRUID_DOCS_SQL, DRUID_DOCS_VERSION } from './variables';
import {
@ -35,7 +34,7 @@ import {
LookupsView,
QueryView,
SegmentsView,
ServersView,
ServicesView,
TasksView,
} from './views';
@ -55,91 +54,49 @@ export class ConsoleApplication extends React.PureComponent<
ConsoleApplicationProps,
ConsoleApplicationState
> {
static STATUS_TIMEOUT = 2000;
private capabilitiesQueryManager: QueryManager<null, Capabilities>;
static async discoverCapabilities(): Promise<Capabilities> {
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) {
static shownNotifications(capabilities: Capabilities | undefined) {
let message: JSX.Element;
switch (capabilities) {
case 'no-sql':
message = (
<>
It appears that the SQL endpoint is disabled. The console will fall back to{' '}
<ExternalLink href={DRUID_DOCS_API}>native Druid APIs</ExternalLink> and will be limited
in functionality. Look at{' '}
<ExternalLink href={DRUID_DOCS_SQL}>the SQL docs</ExternalLink> to enable the SQL
endpoint.
</>
);
break;
if (!capabilities) {
message = (
<>
It appears that the the service serving this console is not responding. The console will
not function at the moment
</>
);
} else {
switch (capabilities.getMode()) {
case 'no-sql':
message = (
<>
It appears that the SQL endpoint is disabled. The console will fall back to{' '}
<ExternalLink href={DRUID_DOCS_API}>native Druid APIs</ExternalLink> and will be
limited in functionality. Look at{' '}
<ExternalLink href={DRUID_DOCS_SQL}>the SQL docs</ExternalLink> to enable the SQL
endpoint.
</>
);
break;
case 'no-proxy':
message = (
<>
It appears that the management proxy is not enabled, the console will operate with
limited functionality. Look at{' '}
<ExternalLink
href={`https://druid.apache.org/docs/${DRUID_DOCS_VERSION}/operations/management-uis.html#druid-console`}
>
the console docs
</ExternalLink>{' '}
for more info on how to enable the management proxy.
</>
);
break;
case 'no-proxy':
message = (
<>
It appears that the management proxy is not enabled, the console will operate with
limited functionality. Look at{' '}
<ExternalLink
href={`https://druid.apache.org/docs/${DRUID_DOCS_VERSION}/operations/management-uis.html#druid-console`}
>
the console docs
</ExternalLink>{' '}
for more info on how to enable the management proxy.
</>
);
break;
case 'broken':
message = (
<>
It appears that the the Router node is not responding. The console will not function at
the moment
</>
);
break;
default:
return;
default:
return;
}
}
AppToaster.show({
@ -161,21 +118,19 @@ export class ConsoleApplication extends React.PureComponent<
constructor(props: ConsoleApplicationProps, context: any) {
super(props, context);
this.state = {
capabilities: 'full',
capabilities: Capabilities.FULL,
capabilitiesLoading: true,
};
this.capabilitiesQueryManager = new QueryManager({
processQuery: async () => {
const capabilities = await ConsoleApplication.discoverCapabilities();
if (capabilities !== 'full') {
ConsoleApplication.shownNotifications(capabilities);
}
return capabilities;
const capabilities = await Capabilities.detectCapabilities();
ConsoleApplication.shownNotifications(capabilities);
return capabilities || Capabilities.FULL;
},
onStateChange: ({ result, loading }) => {
this.setState({
capabilities: result || 'full',
capabilities: result || Capabilities.FULL,
capabilitiesLoading: loading,
});
},
@ -238,7 +193,7 @@ export class ConsoleApplication extends React.PureComponent<
private goToMiddleManager = (middleManager: string) => {
this.middleManager = middleManager;
window.location.hash = 'servers';
window.location.hash = 'services';
this.resetInitialsWithDelay();
};
@ -332,11 +287,11 @@ export class ConsoleApplication extends React.PureComponent<
);
};
private wrappedServersView = () => {
private wrappedServicesView = () => {
const { capabilities } = this.state;
return this.wrapInViewContainer(
'servers',
<ServersView
'services',
<ServicesView
middleManager={this.middleManager}
goToQuery={this.goToQuery}
goToTask={this.goToTaskWithTaskId}
@ -369,7 +324,7 @@ export class ConsoleApplication extends React.PureComponent<
<Route path="/datasources" component={this.wrappedDatasourcesView} />
<Route path="/segments" component={this.wrappedSegmentsView} />
<Route path="/tasks" component={this.wrappedTasksView} />
<Route path="/servers" component={this.wrappedServersView} />
<Route path="/services" component={this.wrappedServicesView} />
<Route path="/query" component={this.wrappedQueryView} />

View File

@ -261,9 +261,9 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
type: 'string-array',
info: (
<>
List of historical servers to 'decommission'. Coordinator will not assign new
segments to 'decommissioning' servers, and segments will be moved away from them
to be placed on non-decommissioning servers at the maximum rate specified by{' '}
List of historical services to 'decommission'. Coordinator will not assign new
segments to 'decommissioning' services, and segments will be moved away from them
to be placed on non-decommissioning services at the maximum rate specified by{' '}
<Code>decommissioningMaxPercentOfMaxSegmentsToMove</Code>.
</>
),
@ -275,15 +275,15 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
info: (
<>
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
one run which is determined by <Code>maxSegmentsToMove</Code>. If
<Code>decommissioningMaxPercentOfMaxSegmentsToMove</Code> is 0, segments will
neither be moved from or to 'decommissioning' servers, effectively putting them in
a sort of "maintenance" mode that will not participate in balancing or assignment
by load rules. Decommissioning can also become stalled if there are no available
active servers to place the segments. By leveraging the maximum percent of
decommissioning segment movements, an operator can prevent active servers from
neither be moved from or to 'decommissioning' services, effectively putting them
in a sort of "maintenance" mode that will not participate in balancing or
assignment by load rules. Decommissioning can also become stalled if there are no
available active services to place the segments. By leveraging the maximum percent
of decommissioning segment movements, an operator can prevent active services from
overload by prioritizing balancing, or decrease decommissioning time instead. The
value should be between 0 and 100.
</>

View File

@ -38,7 +38,7 @@ const RUNTIME_PROPERTIES_ALL_NODES_MUST_AGREE_ON: string[] = [
'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.indexer.logs.type' <=> overlord, mm, + peons
@ -60,7 +60,7 @@ export const DOCTOR_CHECKS: DoctorCheck[] = [
status = (await axios.get(`/status`)).data;
} catch (e) {
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();
return;
@ -137,7 +137,7 @@ export const DOCTOR_CHECKS: DoctorCheck[] = [
coordinatorStatus = (await axios.get(`/proxy/coordinator/status`)).data;
} catch (e) {
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;
}
@ -147,20 +147,20 @@ export const DOCTOR_CHECKS: DoctorCheck[] = [
overlordStatus = (await axios.get(`/proxy/overlord/status`)).data;
} catch (e) {
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;
}
if (myStatus.version !== coordinatorStatus.version) {
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) {
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"` });
} catch (e) {
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();
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 => {
// 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[];
try {
sqlResult = await queryDruidSql({
@ -311,19 +311,19 @@ WHERE "server_type" = 'historical'`,
}
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',
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[];
try {
sqlResult = await queryDruidSql({
query: `SELECT
"server",
"server" AS "service",
"curr_size" * 1.0 / "max_size" AS "fill"
FROM sys.servers
WHERE "server_type" = 'historical' AND "curr_size" * 1.0 / "max_size" > 0.9
@ -334,21 +334,21 @@ ORDER BY "server" DESC`,
return;
}
function formatPercent(server: any): string {
return (server['fill'] * 100).toFixed(2);
function formatPercent(service: any): string {
return (service['fill'] * 100).toFixed(2);
}
for (const server of sqlResult) {
if (server['fill'] > 0.95) {
for (const service of sqlResult) {
if (service['fill'] > 0.95) {
controls.addIssue(
`Server "${server['server']}" appears to be over 95% full (is ${formatPercent(
server,
`Historical "${service['service']}" appears to be over 95% full (is ${formatPercent(
service,
)}%). Increase capacity.`,
);
} else {
controls.addSuggestion(
`Server "${server['server']}" appears to be over 90% full (is ${formatPercent(
server,
`Historical "${service['service']}" appears to be over 90% full (is ${formatPercent(
service,
)}%)`,
);
}

View File

@ -16,4 +16,142 @@
* 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,
});

View File

@ -23,7 +23,7 @@ export const LocalStorageKeys = {
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',
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',
QUERY_KEY: 'druid-console-query' as 'druid-console-query',
QUERY_CONTEXT: 'query-context' as 'query-context',
@ -32,7 +32,7 @@ export const LocalStorageKeys = {
TASKS_REFRESH_RATE: 'task-refresh-rate' as 'task-refresh-rate',
DATASOURCES_REFRESH_RATE: 'datasources-refresh-rate' as 'datasources-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',
LOOKUPS_REFRESH_RATE: 'lookups-refresh-rate' as 'lookups-refresh-rate',
QUERY_HISTORY: 'query-history' as 'query-history',

View File

@ -19,6 +19,8 @@
import { shallow } from 'enzyme';
import React from 'react';
import { Capabilities } from '../../utils/capabilities';
import { DatasourcesView } from './datasource-view';
describe('data source view', () => {
@ -28,7 +30,7 @@ describe('data source view', () => {
goToQuery={() => {}}
goToTask={() => null}
goToSegments={() => {}}
capabilities="full"
capabilities={Capabilities.FULL}
/>,
);
expect(dataSourceView).toMatchSnapshot();

View File

@ -61,14 +61,14 @@ import {
QueryManager,
} from '../../utils';
import { BasicAction } from '../../utils/basic-action';
import { Capabilities } from '../../utils/capabilities';
import { Capabilities, CapabilitiesMode } from '../../utils/capabilities';
import { RuleUtil } from '../../utils/load-rule';
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
import { deepGet } from '../../utils/object-change';
import './datasource-view.scss';
const tableColumns: Record<Capabilities, string[]> = {
const tableColumns: Record<CapabilitiesMode, string[]> = {
full: [
'Datasource',
'Availability',
@ -101,7 +101,6 @@ const tableColumns: Record<Capabilities, string[]> = {
'Num rows',
ACTION_COLUMN_LABEL,
],
broken: ['Datasource'],
};
function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string {
@ -247,9 +246,9 @@ GROUP BY 1`;
this.datasourceQueryManager = new QueryManager({
processQuery: async capabilities => {
let datasources: DatasourceQueryResultRow[];
if (capabilities !== 'no-sql') {
if (capabilities.hasSql()) {
datasources = await queryDruidSql({ query: DatasourcesView.DATASOURCE_SQL });
} else {
} else if (capabilities.hasCoordinatorAccess()) {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple');
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
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) => {
ds.rules = [];
});
@ -496,7 +497,7 @@ GROUP BY 1`;
const { goToQuery, capabilities } = this.props;
const bulkDatasourceActionsMenu = (
<Menu>
{capabilities !== 'no-sql' && (
{capabilities.hasSql() && (
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"
@ -621,7 +622,7 @@ GROUP BY 1`;
},
];
if (capabilities === 'no-proxy') {
if (!capabilities.hasCoordinatorAccess()) {
return goToActions;
}
@ -887,7 +888,7 @@ GROUP BY 1`;
</span>
);
},
show: capabilities !== 'no-proxy' && hiddenColumns.exists('Retention'),
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Retention'),
},
{
Header: 'Replicated size',
@ -941,7 +942,7 @@ GROUP BY 1`;
</span>
);
},
show: capabilities !== 'no-proxy' && hiddenColumns.exists('Compaction'),
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Compaction'),
},
{
Header: 'Avg. segment size',
@ -957,7 +958,7 @@ GROUP BY 1`;
filterable: false,
width: 100,
Cell: row => formatNumber(row.value),
show: capabilities !== 'no-sql' && hiddenColumns.exists('Num rows'),
show: capabilities.hasSql() && hiddenColumns.exists('Num rows'),
},
{
Header: ACTION_COLUMN_LABEL,
@ -1030,7 +1031,7 @@ GROUP BY 1`;
label="Show segment timeline"
onChange={() => this.setState({ showChart: !showChart })}
/>
{capabilities !== 'no-proxy' && (
{capabilities.hasCoordinatorAccess() && (
<Switch
checked={showDisabled}
label="Show disabled"
@ -1038,7 +1039,7 @@ GROUP BY 1`;
/>
)}
<TableColumnSelector
columns={tableColumns[capabilities]}
columns={tableColumns[capabilities.getMode()]}
onChange={column =>
this.setState(prevState => ({
hiddenColumns: prevState.hiddenColumns.toggle(column),

View File

@ -6,19 +6,49 @@ exports[`home view matches snapshot 1`] = `
>
<StatusCard />
<DatasourcesCard
capabilities="full"
capabilities={
Capabilities {
"coordinator": true,
"overlord": true,
"queryType": "nativeAndSql",
}
}
/>
<SegmentsCard
capabilities="full"
capabilities={
Capabilities {
"coordinator": true,
"overlord": true,
"queryType": "nativeAndSql",
}
}
/>
<SupervisorsCard
capabilities="full"
capabilities={
Capabilities {
"coordinator": true,
"overlord": true,
"queryType": "nativeAndSql",
}
}
/>
<TasksCard
capabilities="full"
capabilities={
Capabilities {
"coordinator": true,
"overlord": true,
"queryType": "nativeAndSql",
}
}
/>
<ServersCard
capabilities="full"
<ServicesCard
capabilities={
Capabilities {
"coordinator": true,
"overlord": true,
"queryType": "nativeAndSql",
}
}
/>
<LookupsCard />
</div>

View File

@ -19,11 +19,13 @@
import { render } from '@testing-library/react';
import React from 'react';
import { Capabilities } from '../../../utils/capabilities';
import { DatasourcesCard } from './datasources-card';
describe('datasources card', () => {
it('matches snapshot', () => {
const datasourcesCard = <DatasourcesCard capabilities="full" />;
const datasourcesCard = <DatasourcesCard capabilities={Capabilities.FULL} />;
const { container } = render(datasourcesCard);
expect(container.firstChild).toMatchSnapshot();

View File

@ -50,14 +50,17 @@ export class DatasourcesCard extends React.PureComponent<
this.datasourceQueryManager = new QueryManager({
processQuery: async capabilities => {
let datasources: string[];
if (capabilities !== 'no-sql') {
if (capabilities.hasSql()) {
datasources = await queryDruidSql({
query: `SELECT datasource FROM sys.segments GROUP BY 1`,
});
} else {
} else if (capabilities.hasCoordinatorAccess()) {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
datasources = datasourcesResp.data;
} else {
throw new Error(`must have SQL or coordinator access`);
}
return datasources.length;
},
onStateChange: ({ result, loading, error }) => {

View File

@ -20,7 +20,7 @@
.home-view {
display: grid;
grid-gap: $standard-padding;
gap: $standard-padding;
grid-template-columns: 1fr 1fr 1fr 1fr;
& > a {

View File

@ -19,11 +19,13 @@
import { shallow } from 'enzyme';
import React from 'react';
import { Capabilities } from '../../utils/capabilities';
import { HomeView } from './home-view';
describe('home view', () => {
it('matches snapshot', () => {
const homeView = shallow(<HomeView capabilities="full" />);
const homeView = shallow(<HomeView capabilities={Capabilities.FULL} />);
expect(homeView).toMatchSnapshot();
});
});

View File

@ -23,7 +23,7 @@ import { Capabilities } from '../../utils/capabilities';
import { DatasourcesCard } from './datasources-card/datasources-card';
import { LookupsCard } from './lookups-card/lookups-card';
import { SegmentsCard } from './segments-card/segments-card';
import { ServersCard } from './servers-card/servers-card';
import { ServicesCard } from './services-card/services-card';
import { StatusCard } from './status-card/status-card';
import { SupervisorsCard } from './supervisors-card/supervisors-card';
import { TasksCard } from './tasks-card/tasks-card';
@ -44,7 +44,7 @@ export const HomeView = React.memo(function HomeView(props: HomeViewProps) {
<SegmentsCard capabilities={capabilities} />
<SupervisorsCard capabilities={capabilities} />
<TasksCard capabilities={capabilities} />
<ServersCard capabilities={capabilities} />
<ServicesCard capabilities={capabilities} />
<LookupsCard />
</div>
);

View File

@ -19,11 +19,13 @@
import { render } from '@testing-library/react';
import React from 'react';
import { Capabilities } from '../../../utils/capabilities';
import { SegmentsCard } from './segments-card';
describe('segments card', () => {
it('matches snapshot', () => {
const segmentsCard = <SegmentsCard capabilities="full" />;
const segmentsCard = <SegmentsCard capabilities={Capabilities.FULL} />;
const { container } = render(segmentsCard);
expect(container.firstChild).toMatchSnapshot();

View File

@ -50,7 +50,15 @@ export class SegmentsCard extends React.PureComponent<SegmentsCardProps, Segment
this.segmentQueryManager = new QueryManager({
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 loadstatus = loadstatusResp.data;
const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
@ -66,13 +74,7 @@ export class SegmentsCard extends React.PureComponent<SegmentsCardProps, Segment
unavailable: unavailableSegmentNum,
};
} else {
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;
throw new Error(`must have SQL or coordinator access`);
}
},
onStateChange: ({ result, loading, error }) => {

View File

@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`servers card matches snapshot 1`] = `
exports[`services card matches snapshot 1`] = `
<a
class="home-view-card servers-card"
href="#servers"
class="home-view-card services-card"
href="#services"
>
<div
class="bp3-card bp3-interactive bp3-elevation-0"
@ -32,7 +32,7 @@ exports[`servers card matches snapshot 1`] = `
</svg>
</span>
 
Servers
Services
</h5>
<p>
Loading...

View File

@ -19,13 +19,15 @@
import { render } from '@testing-library/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', () => {
const serversCard = <ServersCard capabilities="full" />;
const servicesCard = <ServicesCard capabilities={Capabilities.FULL} />;
const { container } = render(serversCard);
const { container } = render(servicesCard);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -24,12 +24,12 @@ import { compact, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '
import { Capabilities } from '../../../utils/capabilities';
import { HomeViewCard } from '../home-view-card/home-view-card';
export interface ServersCardProps {
export interface ServicesCardProps {
capabilities: Capabilities;
}
export interface ServersCardState {
serverCountLoading: boolean;
export interface ServicesCardState {
serviceCountLoading: boolean;
coordinatorCount: number;
overlordCount: number;
routerCount: number;
@ -38,10 +38,10 @@ export interface ServersCardState {
middleManagerCount: number;
peonCount: 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(
count1: number,
singular1: string,
@ -56,12 +56,12 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
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);
this.state = {
serverCountLoading: false,
serviceCountLoading: false,
coordinatorCount: 0,
overlordCount: 0,
routerCount: 0,
@ -72,29 +72,37 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
indexerCount: 0,
};
this.serverQueryManager = new QueryManager({
this.serviceQueryManager = new QueryManager({
processQuery: async capabilities => {
if (capabilities === 'no-sql') {
const serversResp = await axios.get('/druid/coordinator/v1/servers?simple');
const middleManagerResp = await axios.get('/druid/indexer/v1/workers');
return {
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;
if (capabilities.hasSql()) {
const serviceCountsFromQuery: {
service_type: string;
count: number;
}[] = 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 }) => {
this.setState({
serverCountLoading: loading,
serviceCountLoading: loading,
coordinatorCount: result ? result.coordinator : 0,
overlordCount: result ? result.overlord : 0,
routerCount: result ? result.router : 0,
@ -103,7 +111,7 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
middleManagerCount: result ? result.middle_manager : 0,
peonCount: result ? result.peon : 0,
indexerCount: result ? result.indexer : 0,
serverCountError: error,
serviceCountError: error,
});
},
});
@ -112,16 +120,16 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
componentDidMount(): void {
const { capabilities } = this.props;
this.serverQueryManager.runQuery(capabilities);
this.serviceQueryManager.runQuery(capabilities);
}
componentWillUnmount(): void {
this.serverQueryManager.terminate();
this.serviceQueryManager.terminate();
}
render(): JSX.Element {
const {
serverCountLoading,
serviceCountLoading,
coordinatorCount,
overlordCount,
routerCount,
@ -130,31 +138,31 @@ export class ServersCard extends React.PureComponent<ServersCardProps, ServersCa
middleManagerCount,
peonCount,
indexerCount,
serverCountError,
serviceCountError,
} = this.state;
return (
<HomeViewCard
className="servers-card"
href={'#servers'}
className="services-card"
href={'#services'}
icon={IconNames.DATABASE}
title={'Servers'}
loading={serverCountLoading}
error={serverCountError}
title={'Services'}
loading={serviceCountLoading}
error={serviceCountError}
>
{ServersCard.renderPluralIfNeededPair(
{ServicesCard.renderPluralIfNeededPair(
overlordCount,
'overlord',
coordinatorCount,
'coordinator',
)}
{ServersCard.renderPluralIfNeededPair(routerCount, 'router', brokerCount, 'broker')}
{ServersCard.renderPluralIfNeededPair(
{ServicesCard.renderPluralIfNeededPair(routerCount, 'router', brokerCount, 'broker')}
{ServicesCard.renderPluralIfNeededPair(
historicalCount,
'historical',
middleManagerCount,
'middle manager',
)}
{ServersCard.renderPluralIfNeededPair(peonCount, 'peon', indexerCount, 'indexer')}
{ServicesCard.renderPluralIfNeededPair(peonCount, 'peon', indexerCount, 'indexer')}
</HomeViewCard>
);
}

View File

@ -19,11 +19,13 @@
import { render } from '@testing-library/react';
import React from 'react';
import { Capabilities } from '../../../utils/capabilities';
import { SupervisorsCard } from './supervisors-card';
describe('supervisors card', () => {
it('matches snapshot', () => {
const supervisorsCard = <SupervisorsCard capabilities="full" />;
const supervisorsCard = <SupervisorsCard capabilities={Capabilities.FULL} />;
const { container } = render(supervisorsCard);
expect(container.firstChild).toMatchSnapshot();

View File

@ -51,14 +51,14 @@ export class SupervisorsCard extends React.PureComponent<
this.supervisorQueryManager = new QueryManager({
processQuery: async capabilities => {
if (capabilities !== 'no-sql') {
if (capabilities.hasSql()) {
return (await queryDruidSql({
query: `SELECT
COUNT(*) FILTER (WHERE "suspended" = 0) AS "runningSupervisorCount",
COUNT(*) FILTER (WHERE "suspended" = 1) AS "suspendedSupervisorCount"
FROM sys.supervisors`,
}))[0];
} else {
} else if (capabilities.hasOverlordAccess()) {
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
const data = resp.data;
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
@ -68,6 +68,8 @@ FROM sys.supervisors`,
runningSupervisorCount,
suspendedSupervisorCount,
};
} else {
throw new Error(`must have SQL or overlord access`);
}
},
onStateChange: ({ result, loading, error }) => {

View File

@ -19,11 +19,13 @@
import { render } from '@testing-library/react';
import React from 'react';
import { Capabilities } from '../../../utils/capabilities';
import { TasksCard } from './tasks-card';
describe('tasks card', () => {
it('matches snapshot', () => {
const tasksCard = <TasksCard capabilities="full" />;
const tasksCard = <TasksCard capabilities={Capabilities.FULL} />;
const { container } = render(tasksCard);
expect(container.firstChild).toMatchSnapshot();

View File

@ -54,7 +54,16 @@ export class TasksCard extends React.PureComponent<TasksCardProps, TasksCardStat
this.taskQueryManager = new QueryManager({
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 runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
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,
};
} else {
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);
throw new Error(`must have SQL or overlord access`);
}
},
onStateChange: ({ result, loading, error }) => {

View File

@ -21,6 +21,6 @@ export * from './home-view/home-view';
export * from './load-data-view/load-data-view';
export * from './lookups-view/lookups-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 './task-view/tasks-view';

View File

@ -326,7 +326,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
let spec = parseJson(String(localStorageGet(LocalStorageKeys.INGESTION_SPEC)));
if (!spec || typeof spec !== 'object') spec = {};
this.state = {
step: 'welcome',
step: 'loading',
spec,
specPreview: spec,
@ -534,6 +534,8 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
return (
<div className={classNames('load-data-view', 'app-view', step)}>
{this.renderStepNav()}
{step === 'loading' && <Loader loading />}
{step === 'welcome' && this.renderWelcomeStep()}
{step === 'connect' && this.renderConnectStep()}
{step === 'parser' && this.renderParserStep()}
@ -548,7 +550,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
{step === 'publish' && this.renderPublishStep()}
{step === 'spec' && this.renderSpecStep()}
{step === 'loading' && this.renderLoading()}
{this.renderResetConfirm()}
</div>
@ -1071,7 +1072,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
{deepGet(spec, 'ioConfig.firehose.type') === 'local' && (
<FormGroup>
<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>
</FormGroup>
)}
@ -3000,10 +3001,6 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
}
};
renderLoading() {
return <Loader loading />;
}
renderSpecStep() {
const { spec, submitting } = this.state;

View File

@ -19,6 +19,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import { Capabilities } from '../../utils/capabilities';
import { SegmentsView } from '../segments-view/segments-view';
describe('segments-view', () => {
@ -28,7 +29,7 @@ describe('segments-view', () => {
datasource={'test'}
onlyUnavailable={false}
goToQuery={() => {}}
capabilities="full"
capabilities={Capabilities.FULL}
/>,
);
expect(segmentsView).toMatchSnapshot();

View File

@ -56,12 +56,12 @@ import {
sqlQueryCustomTableFilter,
} from '../../utils';
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 './segments-view.scss';
const tableColumns: Record<Capabilities, string[]> = {
const tableColumns: Record<CapabilitiesMode, string[]> = {
full: [
'Segment ID',
'Datasource',
@ -103,7 +103,6 @@ const tableColumns: Record<Capabilities, string[]> = {
'Is available',
'Is overshadowed',
],
broken: ['Segment ID'],
};
export interface SegmentsViewProps {
@ -339,7 +338,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
componentDidMount(): void {
const { capabilities } = this.props;
if (capabilities === 'no-sql') {
if (!capabilities.hasSql() && capabilities.hasCoordinatorAccess()) {
this.segmentsNoSqlQueryManager.runQuery(null);
}
}
@ -426,21 +425,21 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
onFilteredChange={filtered => {
this.setState({ segmentFilter: filtered });
}}
onFetchData={
capabilities === 'no-sql'
? this.fetchClientSideData
: state => {
this.setState({
page: state.page,
pageSize: state.pageSize,
filtered: state.filtered,
sorted: state.sorted,
});
if (this.segmentsSqlQueryManager.getLastQuery) {
this.fetchData(groupByInterval, state);
}
}
}
onFetchData={state => {
if (capabilities.hasSql()) {
this.setState({
page: state.page,
pageSize: state.pageSize,
filtered: state.filtered,
sorted: state.sorted,
});
if (this.segmentsSqlQueryManager.getLastQuery) {
this.fetchData(groupByInterval, state);
}
} else if (capabilities.hasCoordinatorAccess()) {
this.fetchClientSideData(state);
}
}}
showPageJump={false}
ofText=""
pivotBy={groupByInterval ? ['interval'] : []}
@ -556,7 +555,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
filterable: false,
defaultSortDesc: true,
Cell: row => (row.original.is_available ? formatNumber(row.value) : <em>(unknown)</em>),
show: capabilities !== 'no-sql' && hiddenColumns.exists('Num rows'),
show: capabilities.hasSql() && hiddenColumns.exists('Num rows'),
},
{
Header: 'Replicas',
@ -564,35 +563,35 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
width: 60,
filterable: false,
defaultSortDesc: true,
show: capabilities !== 'no-sql' && hiddenColumns.exists('Replicas'),
show: capabilities.hasSql() && hiddenColumns.exists('Replicas'),
},
{
Header: 'Is published',
id: 'is_published',
accessor: row => String(Boolean(row.is_published)),
Filter: makeBooleanFilter(),
show: capabilities !== 'no-sql' && hiddenColumns.exists('Is published'),
show: capabilities.hasSql() && hiddenColumns.exists('Is published'),
},
{
Header: 'Is realtime',
id: 'is_realtime',
accessor: row => String(Boolean(row.is_realtime)),
Filter: makeBooleanFilter(),
show: capabilities !== 'no-sql' && hiddenColumns.exists('Is realtime'),
show: capabilities.hasSql() && hiddenColumns.exists('Is realtime'),
},
{
Header: 'Is available',
id: 'is_available',
accessor: row => String(Boolean(row.is_available)),
Filter: makeBooleanFilter(),
show: capabilities !== 'no-sql' && hiddenColumns.exists('Is available'),
show: capabilities.hasSql() && hiddenColumns.exists('Is available'),
},
{
Header: 'Is overshadowed',
id: 'is_overshadowed',
accessor: row => String(Boolean(row.is_overshadowed)),
Filter: makeBooleanFilter(),
show: capabilities !== 'no-sql' && hiddenColumns.exists('Is overshadowed'),
show: capabilities.hasSql() && hiddenColumns.exists('Is overshadowed'),
},
{
Header: ACTION_COLUMN_LABEL,
@ -618,7 +617,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
);
},
Aggregated: () => '',
show: capabilities !== 'no-proxy' && hiddenColumns.exists(ACTION_COLUMN_LABEL),
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists(ACTION_COLUMN_LABEL),
},
]}
defaultPageSize={SegmentsView.PAGE_SIZE}
@ -663,7 +662,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
const bulkSegmentsActionsMenu = (
<Menu>
{capabilities !== 'no-sql' && (
{capabilities.hasSql() && (
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"
@ -714,7 +713,11 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
active={!groupByInterval}
onClick={() => {
this.setState({ groupByInterval: false });
capabilities === 'no-sql' ? this.fetchClientSideData() : this.fetchData(false);
if (capabilities.hasSql()) {
this.fetchData(false);
} else {
this.fetchClientSideData();
}
}}
>
None
@ -731,7 +734,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
</ButtonGroup>
{this.renderBulkSegmentsActions()}
<TableColumnSelector
columns={tableColumns[capabilities]}
columns={tableColumns[capabilities.getMode()]}
onChange={column =>
this.setState(prevState => ({
hiddenColumns: prevState.hiddenColumns.toggle(column),

View File

@ -1,11 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`servers view action servers view 1`] = `
exports[`services view action services view 1`] = `
<div
className="servers-view app-view"
className="services-view app-view"
>
<Memo(ViewControlBar)
label="Servers"
label="Services"
>
<Component>
Group by
@ -31,7 +31,7 @@ exports[`servers view action servers view 1`] = `
</Blueprint3.Button>
</Blueprint3.ButtonGroup>
<Memo(RefreshButton)
localStorageKey="servers-refresh-rate"
localStorageKey="services-refresh-rate"
onRefresh={[Function]}
/>
<Blueprint3.Popover
@ -74,7 +74,7 @@ exports[`servers view action servers view 1`] = `
<Memo(TableColumnSelector)
columns={
Array [
"Server",
"Service",
"Type",
"Tier",
"Host",
@ -149,15 +149,15 @@ exports[`servers view action servers view 1`] = `
Array [
Object {
"Aggregated": [Function],
"Header": "Server",
"accessor": "server",
"Header": "Service",
"accessor": "service",
"show": true,
"width": 300,
},
Object {
"Cell": [Function],
"Header": "Type",
"accessor": "server_type",
"accessor": "service_type",
"show": true,
"width": 150,
},

View File

@ -19,18 +19,20 @@
import { shallow } from 'enzyme';
import React from 'react';
import { ServersView } from './servers-view';
import { Capabilities } from '../../utils/capabilities';
describe('servers view', () => {
it('action servers view', () => {
const serversView = shallow(
<ServersView
import { ServicesView } from './services-view';
describe('services view', () => {
it('action services view', () => {
const servicesView = shallow(
<ServicesView
middleManager={'test'}
goToQuery={() => {}}
goToTask={() => {}}
capabilities="full"
capabilities={Capabilities.FULL}
/>,
);
expect(serversView).toMatchSnapshot();
expect(servicesView).toMatchSnapshot();
});
});

View File

@ -53,14 +53,14 @@ import {
QueryManager,
} from '../../utils';
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 { deepGet } from '../../utils/object-change';
import './servers-view.scss';
import './services-view.scss';
const allColumns: string[] = [
'Server',
'Service',
'Type',
'Tier',
'Host',
@ -72,11 +72,10 @@ const allColumns: string[] = [
ACTION_COLUMN_LABEL,
];
const tableColumns: Record<Capabilities, string[]> = {
const tableColumns: Record<CapabilitiesMode, string[]> = {
full: allColumns,
'no-sql': allColumns,
'no-proxy': ['Server', 'Type', 'Tier', 'Host', 'Port', 'Curr size', 'Max size', 'Usage'],
broken: ['Server'],
'no-proxy': ['Service', 'Type', 'Tier', 'Host', 'Port', 'Curr size', 'Max size', 'Usage'],
};
function formatQueues(
@ -99,19 +98,19 @@ function formatQueues(
return queueParts.join(', ') || 'Empty load/drop queues';
}
export interface ServersViewProps {
export interface ServicesViewProps {
middleManager: string | undefined;
goToQuery: (initSql: string) => void;
goToTask: (taskId: string) => void;
capabilities: Capabilities;
}
export interface ServersViewState {
serversLoading: boolean;
servers?: any[];
serversError?: string;
serverFilter: Filter[];
groupServersBy?: 'server_type' | 'tier';
export interface ServicesViewState {
servicesLoading: boolean;
services?: any[];
servicesError?: string;
serviceFilter: Filter[];
groupServicesBy?: 'service_type' | 'tier';
middleManagerDisableWorkerHost?: string;
middleManagerEnableWorkerHost?: string;
@ -119,9 +118,9 @@ export interface ServersViewState {
hiddenColumns: LocalStorageBackedArray<string>;
}
interface ServerQueryResultRow {
server: string;
server_type: string;
interface ServiceQueryResultRow {
service: string;
service_type: string;
tier: string;
curr_size: number;
host: string;
@ -152,13 +151,13 @@ interface MiddleManagerQueryResultRow {
};
}
interface ServerResultRow
extends ServerQueryResultRow,
interface ServiceResultRow
extends ServiceQueryResultRow,
Partial<LoadQueueStatus>,
Partial<MiddleManagerQueryResultRow> {}
export class ServersView extends React.PureComponent<ServersViewProps, ServersViewState> {
private serverQueryManager: QueryManager<Capabilities, ServerResultRow[]>;
export class ServicesView extends React.PureComponent<ServicesViewProps, ServicesViewState> {
private serviceQueryManager: QueryManager<Capabilities, ServiceResultRow[]>;
// Ranking
// coordinator => 7
@ -169,8 +168,8 @@ export class ServersView extends React.PureComponent<ServersViewProps, ServersVi
// middle_manager => 2
// peon => 1
static SERVER_SQL = `SELECT
"server", "server_type", "tier", "host", "plaintext_port", "tls_port", "curr_size", "max_size",
static SERVICE_SQL = `SELECT
"server" AS "service", "server_type" AS "service_type", "tier", "host", "plaintext_port", "tls_port", "curr_size", "max_size",
(
CASE "server_type"
WHEN 'coordinator' THEN 7
@ -184,15 +183,15 @@ export class ServersView extends React.PureComponent<ServersViewProps, ServersVi
END
) AS "rank"
FROM sys.servers
ORDER BY "rank" DESC, "server" DESC`;
ORDER BY "rank" DESC, "service" DESC`;
static async getServers(): Promise<ServerQueryResultRow[]> {
const allServerResp = await axios.get('/druid/coordinator/v1/servers?simple');
const allServers = allServerResp.data;
return allServers.map((s: any) => {
static async getServices(): Promise<ServiceQueryResultRow[]> {
const allServiceResp = await axios.get('/druid/coordinator/v1/servers?simple');
const allServices = allServiceResp.data;
return allServices.map((s: any) => {
return {
server: s.host,
server_type: s.type === 'indexer-executor' ? 'peon' : s.type,
service: s.host,
service_type: s.type === 'indexer-executor' ? 'peon' : s.type,
tier: s.tier,
host: s.host.split(':')[0],
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);
this.state = {
serversLoading: true,
serverFilter: [],
servicesLoading: true,
serviceFilter: [],
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 => {
let servers: ServerQueryResultRow[];
if (capabilities !== 'no-sql') {
servers = await queryDruidSql({ query: ServersView.SERVER_SQL });
let services: ServiceQueryResultRow[];
if (capabilities.hasSql()) {
services = await queryDruidSql({ query: ServicesView.SERVICE_SQL });
} else if (capabilities.hasCoordinatorAccess()) {
services = await ServicesView.getServices();
} else {
servers = await ServersView.getServers();
throw new Error(`must have SQL or coordinator access`);
}
if (capabilities === 'no-proxy') {
return servers;
if (capabilities.hasCoordinatorAccess()) {
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');
const loadQueues: Record<string, LoadQueueStatus> = loadQueueResponse.data;
servers = servers.map((s: any) => {
const loadQueueInfo = loadQueues[s.server];
if (loadQueueInfo) {
s = Object.assign(s, loadQueueInfo);
if (capabilities.hasOverlordAccess()) {
let middleManagers: MiddleManagerQueryResultRow[];
try {
const middleManagerResponse = await axios.get('/druid/indexer/v1/workers');
middleManagers = middleManagerResponse.data;
} 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[];
try {
const middleManagerResponse = await axios.get('/druid/indexer/v1/workers');
middleManagers = middleManagerResponse.data;
} 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;
}
const middleManagersLookup = lookupBy(middleManagers, m => m.worker.host);
services = services.map(s => {
const middleManagerInfo = middleManagersLookup[s.service];
if (middleManagerInfo) {
s = Object.assign(s, middleManagerInfo);
}
return s;
});
}
const middleManagersLookup = lookupBy(middleManagers, m => m.worker.host);
return servers.map((s: any) => {
const middleManagerInfo = middleManagersLookup[s.server];
if (middleManagerInfo) {
s = Object.assign(s, middleManagerInfo);
}
return s;
});
return services;
},
onStateChange: ({ result, loading, error }) => {
this.setState({
servers: result,
serversLoading: loading,
serversError: error,
services: result,
servicesLoading: loading,
servicesError: error,
});
},
});
@ -277,21 +280,21 @@ ORDER BY "rank" DESC, "server" DESC`;
componentDidMount(): void {
const { capabilities } = this.props;
this.serverQueryManager.runQuery(capabilities);
this.serviceQueryManager.runQuery(capabilities);
}
componentWillUnmount(): void {
this.serverQueryManager.terminate();
this.serviceQueryManager.terminate();
}
renderServersTable() {
renderServicesTable() {
const { capabilities } = this.props;
const {
servers,
serversLoading,
serversError,
serverFilter,
groupServersBy,
services,
servicesLoading,
servicesError,
serviceFilter,
groupServicesBy,
hiddenColumns,
} = this.state;
@ -308,36 +311,38 @@ ORDER BY "rank" DESC, "server" DESC`;
return (
<ReactTable
data={servers || []}
loading={serversLoading}
data={services || []}
loading={servicesLoading}
noDataText={
!serversLoading && servers && !servers.length ? 'No historicals' : serversError || ''
!servicesLoading && services && !services.length ? 'No historicals' : servicesError || ''
}
filterable
filtered={serverFilter}
filtered={serviceFilter}
onFilteredChange={filtered => {
this.setState({ serverFilter: filtered });
this.setState({ serviceFilter: filtered });
}}
pivotBy={groupServersBy ? [groupServersBy] : []}
pivotBy={groupServicesBy ? [groupServicesBy] : []}
defaultPageSize={50}
columns={[
{
Header: 'Server',
accessor: 'server',
Header: 'Service',
accessor: 'service',
width: 300,
Aggregated: () => '',
show: hiddenColumns.exists('Server'),
show: hiddenColumns.exists('Service'),
},
{
Header: 'Type',
accessor: 'server_type',
accessor: 'service_type',
width: 150,
Cell: row => {
const value = row.value;
return (
<a
onClick={() => {
this.setState({ serverFilter: addFilter(serverFilter, 'server_type', value) });
this.setState({
serviceFilter: addFilter(serviceFilter, 'service_type', value),
});
}}
>
{value}
@ -354,7 +359,7 @@ ORDER BY "rank" DESC, "server" DESC`;
return (
<a
onClick={() => {
this.setState({ serverFilter: addFilter(serverFilter, 'tier', value) });
this.setState({ serviceFilter: addFilter(serviceFilter, 'tier', value) });
}}
>
{value}
@ -398,7 +403,7 @@ ORDER BY "rank" DESC, "server" DESC`;
return formatBytes(totalCurr);
},
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 '';
return formatBytes(row.value);
},
@ -417,7 +422,7 @@ ORDER BY "rank" DESC, "server" DESC`;
return formatBytes(totalMax);
},
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 '';
return formatBytes(row.value);
},
@ -429,7 +434,7 @@ ORDER BY "rank" DESC, "server" DESC`;
width: 100,
filterable: false,
accessor: row => {
if (row.server_type === 'middle_manager') {
if (row.service_type === 'middle_manager') {
return row.worker ? row.currCapacityUsed / row.worker.capacity : null;
} else {
return row.max_size ? row.curr_size / row.max_size : null;
@ -461,8 +466,8 @@ ORDER BY "rank" DESC, "server" DESC`;
},
Cell: row => {
if (row.aggregated) return '';
const { server_type } = row.original;
switch (server_type) {
const { service_type } = row.original;
switch (service_type) {
case 'historical':
return fillIndicator(row.value);
@ -487,7 +492,7 @@ ORDER BY "rank" DESC, "server" DESC`;
width: 400,
filterable: false,
accessor: row => {
if (row.server_type === 'middle_manager') {
if (row.service_type === 'middle_manager') {
if (deepGet(row, 'worker.version') === '') return 'Disabled';
const details: string[] = [];
@ -504,8 +509,8 @@ ORDER BY "rank" DESC, "server" DESC`;
},
Cell: row => {
if (row.aggregated) return '';
const { server_type } = row.original;
switch (server_type) {
const { service_type } = row.original;
switch (service_type) {
case 'historical':
const {
segmentsToLoad,
@ -541,7 +546,7 @@ ORDER BY "rank" DESC, "server" DESC`;
segmentsToDropSize,
);
},
show: capabilities !== 'no-proxy' && hiddenColumns.exists('Detail'),
show: capabilities.hasCoordinatorAccess() && hiddenColumns.exists('Detail'),
},
{
Header: ACTION_COLUMN_LABEL,
@ -555,7 +560,7 @@ ORDER BY "rank" DESC, "server" DESC`;
const workerActions = this.getWorkerActions(row.value.host, disabled);
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 });
}}
onSuccess={() => {
this.serverQueryManager.rerunLastQuery();
this.serviceQueryManager.rerunLastQuery();
}}
>
<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 });
}}
onSuccess={() => {
this.serverQueryManager.rerunLastQuery();
this.serviceQueryManager.rerunLastQuery();
}}
>
<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 bulkserversActionsMenu = (
const bulkservicesActionsMenu = (
<Menu>
{capabilities !== 'no-sql' && (
{capabilities.hasSql() && (
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"
onClick={() => goToQuery(ServersView.SERVER_SQL)}
onClick={() => goToQuery(ServicesView.SERVICE_SQL)}
/>
)}
</Menu>
@ -657,7 +662,7 @@ ORDER BY "rank" DESC, "server" DESC`;
return (
<>
<Popover content={bulkserversActionsMenu} position={Position.BOTTOM_LEFT}>
<Popover content={bulkservicesActionsMenu} position={Position.BOTTOM_LEFT}>
<Button icon={IconNames.MORE} />
</Popover>
</>
@ -666,39 +671,39 @@ ORDER BY "rank" DESC, "server" DESC`;
render(): JSX.Element {
const { capabilities } = this.props;
const { groupServersBy, hiddenColumns } = this.state;
const { groupServicesBy, hiddenColumns } = this.state;
return (
<div className="servers-view app-view">
<ViewControlBar label="Servers">
<div className="services-view app-view">
<ViewControlBar label="Services">
<Label>Group by</Label>
<ButtonGroup>
<Button
active={!groupServersBy}
onClick={() => this.setState({ groupServersBy: undefined })}
active={!groupServicesBy}
onClick={() => this.setState({ groupServicesBy: undefined })}
>
None
</Button>
<Button
active={groupServersBy === 'server_type'}
onClick={() => this.setState({ groupServersBy: 'server_type' })}
active={groupServicesBy === 'service_type'}
onClick={() => this.setState({ groupServicesBy: 'service_type' })}
>
Type
</Button>
<Button
active={groupServersBy === 'tier'}
onClick={() => this.setState({ groupServersBy: 'tier' })}
active={groupServicesBy === 'tier'}
onClick={() => this.setState({ groupServicesBy: 'tier' })}
>
Tier
</Button>
</ButtonGroup>
<RefreshButton
onRefresh={auto => this.serverQueryManager.rerunLastQuery(auto)}
localStorageKey={LocalStorageKeys.SERVERS_REFRESH_RATE}
onRefresh={auto => this.serviceQueryManager.rerunLastQuery(auto)}
localStorageKey={LocalStorageKeys.SERVICES_REFRESH_RATE}
/>
{this.renderBulkServersActions()}
{this.renderBulkServicesActions()}
<TableColumnSelector
columns={tableColumns[capabilities]}
columns={tableColumns[capabilities.getMode()]}
onChange={column =>
this.setState(prevState => ({
hiddenColumns: prevState.hiddenColumns.toggle(column),
@ -707,7 +712,7 @@ ORDER BY "rank" DESC, "server" DESC`;
tableColumnsHidden={hiddenColumns.storedArray}
/>
</ViewControlBar>
{this.renderServersTable()}
{this.renderServicesTable()}
{this.renderDisableWorkerAction()}
{this.renderEnableWorkerAction()}
</div>

View File

@ -19,6 +19,8 @@
import { shallow } from 'enzyme';
import React from 'react';
import { Capabilities } from '../../utils/capabilities';
import { TasksView } from './tasks-view';
describe('tasks view', () => {
@ -32,7 +34,7 @@ describe('tasks view', () => {
goToQuery={() => {}}
goToMiddleManager={() => {}}
goToLoadData={() => {}}
capabilities="full"
capabilities={Capabilities.FULL}
/>,
);
expect(taskView).toMatchSnapshot();

View File

@ -259,11 +259,11 @@ ORDER BY "rank" DESC, "created_time" DESC`;
this.supervisorQueryManager = new QueryManager({
processQuery: async capabilities => {
if (capabilities !== 'no-sql') {
if (capabilities.hasSql()) {
return await queryDruidSql({
query: TasksView.SUPERVISOR_SQL,
});
} else {
} else if (capabilities.hasOverlordAccess()) {
const supervisors = (await axios.get('/druid/indexer/v1/supervisor?full')).data;
if (!Array.isArray(supervisors)) throw new Error(`Unexpected results`);
return supervisors.map((sup: any) => {
@ -279,6 +279,8 @@ ORDER BY "rank" DESC, "created_time" DESC`;
suspended: Number(deepGet(sup, 'suspended')),
};
});
} else {
throw new Error(`must have SQL or overlord access`);
}
},
onStateChange: ({ result, loading, error }) => {
@ -292,11 +294,11 @@ ORDER BY "rank" DESC, "created_time" DESC`;
this.taskQueryManager = new QueryManager({
processQuery: async capabilities => {
if (capabilities !== 'no-sql') {
if (capabilities.hasSql()) {
return await queryDruidSql({
query: TasksView.TASK_SQL,
});
} else {
} else if (capabilities.hasOverlordAccess()) {
const taskEndpoints: string[] = [
'completeTasks',
'runningTasks',
@ -310,6 +312,8 @@ ORDER BY "rank" DESC, "created_time" DESC`;
}),
);
return result.flat();
} else {
throw new Error(`must have SQL or overlord access`);
}
},
onStateChange: ({ result, loading, error }) => {
@ -934,7 +938,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
const bulkSupervisorActionsMenu = (
<Menu>
{capabilities !== 'no-sql' && (
{capabilities.hasSql() && (
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"
@ -1060,7 +1064,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
const bulkTaskActionsMenu = (
<Menu>
{capabilities !== 'no-sql' && (
{capabilities.hasSql() && (
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"