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
|
||||
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}
|
||||
/>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
.header-bar {
|
||||
overflow: hidden;
|
||||
|
||||
.logo {
|
||||
.druid-logo {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: $header-bar-height;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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} />
|
||||
|
||||
|
|
|
@ -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.
|
||||
</>
|
||||
|
|
|
@ -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,
|
||||
)}%)`,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
.home-view {
|
||||
display: grid;
|
||||
grid-gap: $standard-padding;
|
||||
gap: $standard-padding;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
|
||||
& > a {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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...
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
},
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue