Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ exports[`HeaderBar matches snapshot 1`] = `
"queryType": "nativeAndSql",
}
}
onUnrestrict={[Function]}
/>
<Blueprint4.Popover2
boundary="clippingParents"
Expand Down
4 changes: 3 additions & 1 deletion web-console/src/components/header-bar/header-bar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
39 changes: 32 additions & 7 deletions web-console/src/components/header-bar/header-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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}
Expand All @@ -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] =
Expand Down Expand Up @@ -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>
Expand Down
14 changes: 11 additions & 3 deletions web-console/src/console-application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 }) => {
Expand All @@ -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;
Expand Down Expand Up @@ -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>
</>
);
Expand Down
48 changes: 37 additions & 11 deletions web-console/src/utils/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -55,7 +56,7 @@ export class Capabilities {
// Check SQL endpoint
try {
await Api.instance.post(
'/druid/v2/sql',
'/druid/v2/sql?capabilities',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the ?capabilities query parameter just for fun?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is mostly for being able to identify these bad boys in the Network tab:

image

{ query: 'SELECT 1337', context: { timeout: Capabilities.STATUS_TIMEOUT } },
{ timeout: Capabilities.STATUS_TIMEOUT },
);
Expand All @@ -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__',
Expand All @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down