Skip to content
Permalink
Browse files
Web console: add a button to get out of restricted mode, make capabil…
…ity detection more robust (#12503)

* allow unrestrict

* update tests
  • Loading branch information
vogievetsky committed May 5, 2022
1 parent 0206a2d commit 2d8eb117c0bc59aa79016ed0e28519f6b7aa7afc
Showing 5 changed files with 84 additions and 22 deletions.
@@ -76,6 +76,7 @@ exports[`HeaderBar matches snapshot 1`] = `
"queryType": "nativeAndSql",
}
}
onUnrestrict={[Function]}
/>
<Blueprint4.Popover2
boundary="clippingParents"
@@ -25,7 +25,9 @@ import { HeaderBar } from './header-bar';

describe('HeaderBar', () => {
it('matches snapshot', () => {
const headerBar = shallow(<HeaderBar active="load-data" capabilities={Capabilities.FULL} />);
const headerBar = shallow(
<HeaderBar active="load-data" capabilities={Capabilities.FULL} onUnrestrict={() => {}} />,
);
expect(headerBar).toMatchSnapshot();
});
});
@@ -85,10 +85,11 @@ const DruidLogo = React.memo(function DruidLogo() {

interface RestrictedModeProps {
capabilities: Capabilities;
onUnrestrict(capabilities: Capabilities): void;
}

const RestrictedMode = React.memo(function RestrictedMode(props: RestrictedModeProps) {
const { capabilities } = props;
const { capabilities, onUnrestrict } = props;
const mode = capabilities.getModeExtended();

let label: string;
@@ -136,7 +137,8 @@ const RestrictedMode = React.memo(function RestrictedMode(props: RestrictedModeP
<p>
It appears that you are accessing the console on the Coordinator/Overlord shared service.
Due to the lack of access to some APIs on this service the console will operate in a
limited mode. The full version of the console can be accessed on the Router service.
limited mode. The unrestricted version of the console can be accessed on the Router
service.
</p>
);
break;
@@ -157,8 +159,8 @@ const RestrictedMode = React.memo(function RestrictedMode(props: RestrictedModeP
message = (
<p>
It appears that you are accessing the console on the Overlord service. Due to the lack of
access to some APIs on this service the console will operate in a limited mode. The full
version of the console can be accessed on the Router service.
access to some APIs on this service the console will operate in a limited mode. The
unrestricted version of the console can be accessed on the Router service.
</p>
);
break;
@@ -168,7 +170,8 @@ const RestrictedMode = React.memo(function RestrictedMode(props: RestrictedModeP
message = (
<p>
Due to the lack of access to some APIs on this service the console will operate in a
limited mode. The full version of the console can be accessed on the Router service.
limited mode. The unrestricted version of the console can be accessed on the Router
service.
</p>
);
break;
@@ -187,6 +190,27 @@ const RestrictedMode = React.memo(function RestrictedMode(props: RestrictedModeP
</ExternalLink>
.
</p>
<p>
It is possible that there is an issue with the capability detection. You can enable the
unrestricted console but certain features might not work if the underlying APIs are not
available.
</p>
<p>
<Button
icon={IconNames.WARNING_SIGN}
text={`Temporarily unrestrict console${capabilities.hasSql() ? '' : ' (with SQL)'}`}
onClick={() => onUnrestrict(Capabilities.FULL)}
/>
</p>
{!capabilities.hasSql() && (
<p>
<Button
icon={IconNames.WARNING_SIGN}
text="Temporarily unrestrict console (without SQL)"
onClick={() => onUnrestrict(Capabilities.NO_SQL)}
/>
</p>
)}
</PopoverText>
}
position={Position.BOTTOM_RIGHT}
@@ -199,10 +223,11 @@ const RestrictedMode = React.memo(function RestrictedMode(props: RestrictedModeP
export interface HeaderBarProps {
active: HeaderActiveTab;
capabilities: Capabilities;
onUnrestrict(capabilities: Capabilities): void;
}

export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
const { active, capabilities } = props;
const { active, capabilities, onUnrestrict } = props;
const [aboutDialogOpen, setAboutDialogOpen] = useState(false);
const [doctorDialogOpen, setDoctorDialogOpen] = useState(false);
const [coordinatorDynamicConfigDialogOpen, setCoordinatorDynamicConfigDialogOpen] =
@@ -371,7 +396,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: HeaderBarProps) {
/>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<RestrictedMode capabilities={capabilities} />
<RestrictedMode capabilities={capabilities} onUnrestrict={onUnrestrict} />
<Popover2 content={configMenu} position={Position.BOTTOM_RIGHT}>
<Button minimal icon={IconNames.COG} />
</Popover2>
@@ -55,7 +55,7 @@ export class ConsoleApplication extends React.PureComponent<
> {
private readonly capabilitiesQueryManager: QueryManager<null, Capabilities>;

static shownNotifications() {
static shownServiceNotification() {
AppToaster.show({
icon: IconNames.ERROR,
intent: Intent.DANGER,
@@ -87,7 +87,7 @@ export class ConsoleApplication extends React.PureComponent<
this.capabilitiesQueryManager = new QueryManager({
processQuery: async () => {
const capabilities = await Capabilities.detectCapabilities();
if (!capabilities) ConsoleApplication.shownNotifications();
if (!capabilities) ConsoleApplication.shownServiceNotification();
return capabilities || Capabilities.FULL;
},
onStateChange: ({ data, loading }) => {
@@ -107,6 +107,10 @@ export class ConsoleApplication extends React.PureComponent<
this.capabilitiesQueryManager.terminate();
}

private readonly handleUnrestrict = (capabilities: Capabilities) => {
this.setState({ capabilities });
};

private resetInitialsWithDelay() {
setTimeout(() => {
this.taskId = undefined;
@@ -168,7 +172,11 @@ export class ConsoleApplication extends React.PureComponent<

return (
<>
<HeaderBar active={active} capabilities={capabilities} />
<HeaderBar
active={active}
capabilities={capabilities}
onUnrestrict={this.handleUnrestrict}
/>
<div className={classNames('view-container', classType)}>{el}</div>
</>
);
@@ -40,8 +40,9 @@ export interface CapabilitiesOptions {
}

export class Capabilities {
static STATUS_TIMEOUT = 2000;
static STATUS_TIMEOUT = 15000;
static FULL: Capabilities;
static NO_SQL: Capabilities;
static COORDINATOR_OVERLORD: Capabilities;
static COORDINATOR: Capabilities;
static OVERLORD: Capabilities;
@@ -55,7 +56,7 @@ export class Capabilities {
// Check SQL endpoint
try {
await Api.instance.post(
'/druid/v2/sql',
'/druid/v2/sql?capabilities',
{ query: 'SELECT 1337', context: { timeout: Capabilities.STATUS_TIMEOUT } },
{ timeout: Capabilities.STATUS_TIMEOUT },
);
@@ -65,15 +66,15 @@ export class Capabilities {
return; // other failure
}
try {
await Api.instance.get('/status', { timeout: Capabilities.STATUS_TIMEOUT });
await Api.instance.get('/status?capabilities', { timeout: Capabilities.STATUS_TIMEOUT });
} catch (e) {
return; // total failure
}
// Status works but SQL 405s => the SQL endpoint is disabled

try {
await Api.instance.post(
'/druid/v2',
'/druid/v2?capabilities',
{
queryType: 'dataSourceMetadata',
dataSource: '__web_console_probe__',
@@ -95,9 +96,9 @@ export class Capabilities {
return 'nativeAndSql';
}

static async detectNode(node: 'coordinator' | 'overlord'): Promise<boolean | undefined> {
static async detectManagementProxy(): Promise<boolean> {
try {
await Api.instance.get(`/proxy/${node}/status`, {
await Api.instance.get(`/proxy/coordinator/status?capabilities`, {
timeout: Capabilities.STATUS_TIMEOUT,
});
} catch (e) {
@@ -107,18 +108,38 @@ export class Capabilities {
return true;
}

static async detectNode(node: 'coordinator' | 'overlord'): Promise<boolean> {
try {
await Api.instance.get(
`/druid/${node === 'overlord' ? 'indexer' : node}/v1/isLeader?capabilities`,
{
timeout: Capabilities.STATUS_TIMEOUT,
},
);
} catch (e) {
return false;
}

return true;
}

static async detectCapabilities(): Promise<Capabilities | undefined> {
const capabilitiesOverride = localStorageGetJson(LocalStorageKeys.CAPABILITIES_OVERRIDE);
if (capabilitiesOverride) return new Capabilities(capabilitiesOverride);

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;
let coordinator: boolean;
let overlord: boolean;
if (queryType === 'none') {
// must not be running on the router, figure out what node the console is on (or both?)
coordinator = await Capabilities.detectNode('coordinator');
overlord = await Capabilities.detectNode('overlord');
} else {
// must be running on the router, figure out if the management proxy is working
coordinator = overlord = await Capabilities.detectManagementProxy();
}

return new Capabilities({
queryType,
@@ -204,6 +225,11 @@ Capabilities.FULL = new Capabilities({
coordinator: true,
overlord: true,
});
Capabilities.NO_SQL = new Capabilities({
queryType: 'nativeOnly',
coordinator: true,
overlord: true,
});
Capabilities.COORDINATOR_OVERLORD = new Capabilities({
queryType: 'none',
coordinator: true,

0 comments on commit 2d8eb11

Please sign in to comment.