Web console: added bulk supervisor actions (#8028)

* added bulk supervisor actions to the web console

* improve async action dialog

* fix tests
This commit is contained in:
Vadim Ogievetsky 2019-07-09 17:31:23 -07:00 committed by Fangjin Yang
parent 3353da2974
commit 8493bd5c1c
17 changed files with 448 additions and 399 deletions

View File

@ -33,7 +33,6 @@
"endOfLine": "lf"
},
"scripts": {
"watch": "./script/watch",
"compile": "./script/build",
"pretest": "./script/build",
"run": "./script/run",

View File

@ -62,7 +62,7 @@ export class ConsoleApplication extends React.PureComponent<
> {
static MESSAGE_KEY = 'druid-console-message';
static MESSAGE_DISMISSED = 'dismissed';
private capabilitiesQueryManager: QueryManager<string, Capabilities>;
private capabilitiesQueryManager: QueryManager<null, Capabilities>;
static async discoverCapabilities(): Promise<Capabilities> {
try {
@ -151,7 +151,7 @@ export class ConsoleApplication extends React.PureComponent<
}
componentDidMount(): void {
this.capabilitiesQueryManager.runQuery('dummy');
this.capabilitiesQueryManager.runQuery(null);
}
componentWillUnmount(): void {

View File

@ -16,7 +16,7 @@ exports[`async action dialog matches snapshot 1`] = `
tabindex="0"
>
<div
class="bp3-dialog bp3-alert async-alert-dialog"
class="bp3-dialog bp3-alert async-action-dialog"
>
<div
class="bp3-alert-body"

View File

@ -0,0 +1,23 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.async-action-dialog {
.progress-group {
width: 100%;
}
}

View File

@ -16,16 +16,28 @@
* limitations under the License.
*/
import { Button, Classes, Dialog, Icon, Intent, ProgressBar } from '@blueprintjs/core';
import { IconName } from '@blueprintjs/icons';
import {
Button,
Classes,
Dialog,
FormGroup,
Icon,
IconName,
Intent,
ProgressBar,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import React from 'react';
import { AppToaster } from '../../singletons/toaster';
export interface AsyncAlertDialogProps {
action: null | (() => Promise<void>);
onClose: (success: boolean) => void;
import './async-action-dialog.scss';
export interface AsyncActionDialogProps {
action: () => Promise<void>;
onClose: () => void;
onSuccess?: () => void;
confirmButtonText: string;
confirmButtonDisabled?: boolean;
cancelButtonText?: string;
@ -36,24 +48,33 @@ export interface AsyncAlertDialogProps {
failText: string;
}
export interface AsyncAlertDialogState {
export interface AsyncActionDialogState {
working: boolean;
}
export class AsyncActionDialog extends React.PureComponent<
AsyncAlertDialogProps,
AsyncAlertDialogState
AsyncActionDialogProps,
AsyncActionDialogState
> {
constructor(props: AsyncAlertDialogProps) {
private mounted = false;
constructor(props: AsyncActionDialogProps) {
super(props);
this.state = {
working: false,
};
}
componentDidMount(): void {
this.mounted = true;
}
componentWillUnmount(): void {
this.mounted = false;
}
private handleConfirm = async () => {
const { action, onClose, successText, failText } = this.props;
if (!action) throw new Error('should never get here');
const { action, onClose, onSuccess, successText, failText } = this.props;
this.setState({ working: true });
try {
@ -63,22 +84,30 @@ export class AsyncActionDialog extends React.PureComponent<
message: `${failText}: ${e.message}`,
intent: Intent.DANGER,
});
this.setState({ working: false });
onClose(false);
if (this.mounted) {
this.setState({ working: false });
onClose();
}
return;
}
AppToaster.show({
message: successText,
intent: Intent.SUCCESS,
});
this.setState({ working: false });
onClose(true);
if (this.mounted) {
this.setState({ working: false });
}
if (onSuccess) onSuccess();
onClose();
};
private handleClose = () => {
const { onClose } = this.props;
onClose();
};
render() {
const {
action,
onClose,
className,
icon,
intent,
@ -88,34 +117,41 @@ export class AsyncActionDialog extends React.PureComponent<
children,
} = this.props;
const { working } = this.state;
if (!action) return null;
const handleClose = () => onClose(false);
return (
<Dialog
isOpen
className={classNames(Classes.ALERT, 'async-alert-dialog', className)}
className={classNames(Classes.ALERT, 'async-action-dialog', className)}
canEscapeKeyClose={!working}
onClose={handleClose}
onClose={this.handleClose}
>
<div className={Classes.ALERT_BODY}>
{icon && <Icon icon={icon} />}
{!working && <div className={Classes.ALERT_CONTENTS}>{children}</div>}
{working ? (
<FormGroup className="progress-group" label="Processing action...">
<ProgressBar intent={intent || Intent.PRIMARY} />
</FormGroup>
) : (
<>
{icon && <Icon icon={icon} />}
<div className={Classes.ALERT_CONTENTS}>{children}</div>
</>
)}
</div>
<div className={Classes.ALERT_FOOTER}>
{working ? (
<Button icon={IconNames.EYE_OFF} text="Run in background" onClick={this.handleClose} />
) : (
<>
<Button
intent={intent}
text={confirmButtonText}
onClick={this.handleConfirm}
disabled={confirmButtonDisabled}
/>
<Button text={cancelButtonText || 'Cancel'} onClick={this.handleClose} />
</>
)}
</div>
{working ? (
<ProgressBar />
) : (
<div className={Classes.ALERT_FOOTER}>
<Button
intent={intent}
text={confirmButtonText}
onClick={this.handleConfirm}
disabled={confirmButtonDisabled}
/>
<Button text={cancelButtonText || 'Cancel'} onClick={handleClose} />
</div>
)}
</Dialog>
);
}

View File

@ -24,6 +24,12 @@ import numeral from 'numeral';
import React from 'react';
import { Filter, FilterRender } from 'react-table';
export function wait(ms: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
export function addFilter(filters: Filter[], id: string, value: string): Filter[] {
value = `"${value}"`;
const currentFilter = filters.find(f => f.id === id);

View File

@ -117,18 +117,15 @@ export class QueryManager<Q, R> {
}
public runQuery(query: Q): void {
if (this.terminated) return;
this.nextQuery = query;
this.trigger();
}
public rerunLastQuery(): void {
public rerunLastQuery(runInBackground = false): void {
if (this.terminated) return;
this.nextQuery = this.lastQuery;
this.trigger();
}
public rerunLastQueryInBackground(auto: boolean): void {
this.nextQuery = this.lastQuery;
if (auto) {
if (runInBackground) {
this.runWhenIdle();
} else {
this.trigger();

View File

@ -246,64 +246,5 @@ exports[`data source view matches snapshot 1`] = `
style={Object {}}
subRowsKey="_subRows"
/>
<AsyncActionDialog
action={null}
confirmButtonText="Drop data"
failText="Could not drop data"
intent="danger"
onClose={[Function]}
successText="Data drop request acknowledged, next time the coordinator runs data will be dropped"
>
<p>
Are you sure you want to drop all the data for datasource 'null'?
</p>
</AsyncActionDialog>
<AsyncActionDialog
action={null}
confirmButtonText="Enable datasource"
failText="Could not enable datasource"
intent="primary"
onClose={[Function]}
successText="Datasource has been enabled"
>
<p>
Are you sure you want to enable datasource 'null'?
</p>
</AsyncActionDialog>
<AsyncActionDialog
action={null}
confirmButtonDisabled={true}
confirmButtonText="Drop selected data"
failText="Could not drop data"
intent="primary"
onClose={[Function]}
successText="Drop request submitted"
>
<p>
Please select the interval that you want to drop?
</p>
<Blueprint3.FormGroup>
<Blueprint3.InputGroup
onChange={[Function]}
placeholder="2018-01-01T00:00:00/2018-01-03T00:00:00"
value=""
/>
</Blueprint3.FormGroup>
</AsyncActionDialog>
<AsyncActionDialog
action={null}
confirmButtonText="Permanently delete data"
failText="Could not submit kill task"
intent="danger"
onClose={[Function]}
successText="Kill task was issued. Datasource will be deleted"
>
<p>
Are you sure you want to permanently delete the data in datasource 'null'?
</p>
<p>
This action can not be undone.
</p>
</AsyncActionDialog>
</div>
`;

View File

@ -266,27 +266,26 @@ GROUP BY 1`;
renderDropDataAction() {
const { dropDataDatasource } = this.state;
if (!dropDataDatasource) return;
return (
<AsyncActionDialog
action={
dropDataDatasource
? async () => {
const resp = await axios.delete(
`/druid/coordinator/v1/datasources/${dropDataDatasource}`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.delete(
`/druid/coordinator/v1/datasources/${dropDataDatasource}`,
{},
);
return resp.data;
}}
confirmButtonText="Drop data"
successText="Data drop request acknowledged, next time the coordinator runs data will be dropped"
failText="Could not drop data"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ dropDataDatasource: null });
if (success) this.datasourceQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.datasourceQueryManager.rerunLastQuery();
}}
>
<p>
@ -298,27 +297,26 @@ GROUP BY 1`;
renderEnableAction() {
const { enableDatasource } = this.state;
if (!enableDatasource) return;
return (
<AsyncActionDialog
action={
enableDatasource
? async () => {
const resp = await axios.post(
`/druid/coordinator/v1/datasources/${enableDatasource}`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.post(
`/druid/coordinator/v1/datasources/${enableDatasource}`,
{},
);
return resp.data;
}}
confirmButtonText="Enable datasource"
successText="Datasource has been enabled"
failText="Could not enable datasource"
intent={Intent.PRIMARY}
onClose={success => {
onClose={() => {
this.setState({ enableDatasource: null });
if (success) this.datasourceQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.datasourceQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to enable datasource '${enableDatasource}'?`}</p>
@ -328,34 +326,33 @@ GROUP BY 1`;
renderDropReloadAction() {
const { dropReloadDatasource, dropReloadAction, dropReloadInterval } = this.state;
if (!dropReloadDatasource) return;
const isDrop = dropReloadAction === 'drop';
return (
<AsyncActionDialog
action={
dropReloadDatasource
? async () => {
if (!dropReloadInterval) return;
const resp = await axios.post(
`/druid/coordinator/v1/datasources/${dropReloadDatasource}/${
isDrop ? 'markUnused' : 'markUsed'
}`,
{
interval: dropReloadInterval,
},
);
return resp.data;
}
: null
}
action={async () => {
if (!dropReloadInterval) return;
const resp = await axios.post(
`/druid/coordinator/v1/datasources/${dropReloadDatasource}/${
isDrop ? 'markUnused' : 'markUsed'
}`,
{
interval: dropReloadInterval,
},
);
return resp.data;
}}
confirmButtonText={`${isDrop ? 'Drop' : 'Reload'} selected data`}
confirmButtonDisabled={!/.\/./.test(dropReloadInterval)}
successText={`${isDrop ? 'Drop' : 'Reload'} request submitted`}
failText={`Could not ${isDrop ? 'drop' : 'reload'} data`}
intent={Intent.PRIMARY}
onClose={success => {
onClose={() => {
this.setState({ dropReloadDatasource: null });
if (success) this.datasourceQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.datasourceQueryManager.rerunLastQuery();
}}
>
<p>{`Please select the interval that you want to ${isDrop ? 'drop' : 'reload'}?`}</p>
@ -375,27 +372,26 @@ GROUP BY 1`;
renderKillAction() {
const { killDatasource } = this.state;
if (!killDatasource) return;
return (
<AsyncActionDialog
action={
killDatasource
? async () => {
const resp = await axios.delete(
`/druid/coordinator/v1/datasources/${killDatasource}?kill=true&interval=1000/3000`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.delete(
`/druid/coordinator/v1/datasources/${killDatasource}?kill=true&interval=1000/3000`,
{},
);
return resp.data;
}}
confirmButtonText="Permanently delete data"
successText="Kill task was issued. Datasource will be deleted"
failText="Could not submit kill task"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ killDatasource: null });
if (success) this.datasourceQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.datasourceQueryManager.rerunLastQuery();
}}
>
<p>
@ -808,7 +804,7 @@ GROUP BY 1`;
<div className="data-sources-view app-view">
<ViewControlBar label="Datasources">
<RefreshButton
onRefresh={auto => this.datasourceQueryManager.rerunLastQueryInBackground(auto)}
onRefresh={auto => this.datasourceQueryManager.rerunLastQuery(auto)}
localStorageKey={LocalStorageKeys.DATASOURCES_REFRESH_RATE}
/>
{!noSqlMode && (

View File

@ -220,17 +220,5 @@ exports[`lookups view matches snapshot 1`] = `
onClose={[Function]}
onSubmit={[Function]}
/>
<AsyncActionDialog
action={null}
confirmButtonText="Delete lookup"
failText="Could not delete lookup"
intent="danger"
onClose={[Function]}
successText="Lookup was deleted"
>
<p>
Are you sure you want to delete the lookup 'null'?
</p>
</AsyncActionDialog>
</div>
`;

View File

@ -235,25 +235,24 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
renderDeleteLookupAction() {
const { deleteLookupTier, deleteLookupName } = this.state;
if (!deleteLookupTier) return;
return (
<AsyncActionDialog
action={
deleteLookupTier
? async () => {
await axios.delete(
`/druid/coordinator/v1/lookups/config/${deleteLookupTier}/${deleteLookupName}`,
);
}
: null
}
action={async () => {
await axios.delete(
`/druid/coordinator/v1/lookups/config/${deleteLookupTier}/${deleteLookupName}`,
);
}}
confirmButtonText="Delete lookup"
successText="Lookup was deleted"
failText="Could not delete lookup"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ deleteLookupTier: null, deleteLookupName: null });
if (success) this.lookupsQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.lookupsQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to delete the lookup '${deleteLookupName}'?`}</p>

View File

@ -305,20 +305,5 @@ exports[`segments-view matches snapshot 1`] = `
subRowsKey="_subRows"
/>
</div>
<AsyncActionDialog
action={null}
confirmButtonText="Drop Segment"
failText="Could not drop segment"
intent="danger"
onClose={[Function]}
successText="Segment drop request acknowledged, next time the coordinator runs segment will be dropped"
>
<p>
Are you sure you want to drop segment 'null'?
</p>
<p>
This action is not reversible.
</p>
</AsyncActionDialog>
</Fragment>
`;

View File

@ -479,30 +479,27 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
renderTerminateSegmentAction() {
const { terminateSegmentId, terminateDatasourceId } = this.state;
if (!terminateDatasourceId || !terminateSegmentId) return;
return (
<AsyncActionDialog
action={
terminateSegmentId
? async () => {
const resp = await axios.delete(
`/druid/coordinator/v1/datasources/${terminateDatasourceId}/segments/${terminateSegmentId}`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.delete(
`/druid/coordinator/v1/datasources/${terminateDatasourceId}/segments/${terminateSegmentId}`,
{},
);
return resp.data;
}}
confirmButtonText="Drop Segment"
successText="Segment drop request acknowledged, next time the coordinator runs segment will be dropped"
failText="Could not drop segment"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ terminateSegmentId: null });
if (success) {
this.segmentsNoSqlQueryManager.rerunLastQuery();
this.segmentsSqlQueryManager.rerunLastQuery();
}
}}
onSuccess={() => {
this.segmentsNoSqlQueryManager.rerunLastQuery();
this.segmentsSqlQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to drop segment '${terminateSegmentId}'?`}</p>
@ -528,8 +525,8 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
<RefreshButton
onRefresh={auto =>
noSqlMode
? this.segmentsNoSqlQueryManager.rerunLastQueryInBackground(auto)
: this.segmentsSqlQueryManager.rerunLastQueryInBackground(auto)
? this.segmentsNoSqlQueryManager.rerunLastQuery(auto)
: this.segmentsSqlQueryManager.rerunLastQuery(auto)
}
localStorageKey={LocalStorageKeys.SEGMENTS_REFRESH_RATE}
/>

View File

@ -284,29 +284,5 @@ exports[`servers view action servers view 1`] = `
style={Object {}}
subRowsKey="_subRows"
/>
<AsyncActionDialog
action={null}
confirmButtonText="Disable worker"
failText="Could not disable worker"
intent="danger"
onClose={[Function]}
successText="Worker has been disabled"
>
<p>
Are you sure you want to disable worker 'null'?
</p>
</AsyncActionDialog>
<AsyncActionDialog
action={null}
confirmButtonText="Enable worker"
failText="Could not enable worker"
intent="primary"
onClose={[Function]}
successText="Worker has been enabled"
>
<p>
Are you sure you want to enable worker 'null'?
</p>
</AsyncActionDialog>
</div>
`;

View File

@ -560,27 +560,26 @@ ORDER BY "rank" DESC, "server" DESC`;
renderDisableWorkerAction() {
const { middleManagerDisableWorkerHost } = this.state;
if (!middleManagerDisableWorkerHost) return;
return (
<AsyncActionDialog
action={
middleManagerDisableWorkerHost
? async () => {
const resp = await axios.post(
`/druid/indexer/v1/worker/${middleManagerDisableWorkerHost}/disable`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.post(
`/druid/indexer/v1/worker/${middleManagerDisableWorkerHost}/disable`,
{},
);
return resp.data;
}}
confirmButtonText="Disable worker"
successText="Worker has been disabled"
failText="Could not disable worker"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ middleManagerDisableWorkerHost: null });
if (success) this.serverQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.serverQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to disable worker '${middleManagerDisableWorkerHost}'?`}</p>
@ -590,27 +589,26 @@ ORDER BY "rank" DESC, "server" DESC`;
renderEnableWorkerAction() {
const { middleManagerEnableWorkerHost } = this.state;
if (!middleManagerEnableWorkerHost) return;
return (
<AsyncActionDialog
action={
middleManagerEnableWorkerHost
? async () => {
const resp = await axios.post(
`/druid/indexer/v1/worker/${middleManagerEnableWorkerHost}/enable`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.post(
`/druid/indexer/v1/worker/${middleManagerEnableWorkerHost}/enable`,
{},
);
return resp.data;
}}
confirmButtonText="Enable worker"
successText="Worker has been enabled"
failText="Could not enable worker"
intent={Intent.PRIMARY}
onClose={success => {
onClose={() => {
this.setState({ middleManagerEnableWorkerHost: null });
if (success) this.serverQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.serverQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to enable worker '${middleManagerEnableWorkerHost}'?`}</p>
@ -647,7 +645,7 @@ ORDER BY "rank" DESC, "server" DESC`;
</Button>
</ButtonGroup>
<RefreshButton
onRefresh={auto => this.serverQueryManager.rerunLastQueryInBackground(auto)}
onRefresh={auto => this.serverQueryManager.rerunLastQuery(auto)}
localStorageKey={LocalStorageKeys.SERVERS_REFRESH_RATE}
/>
{!noSqlMode && (

View File

@ -70,6 +70,61 @@ exports[`tasks view matches snapshot 1`] = `
text="Submit supervisor"
/>
</Blueprint3.Popover>
<Blueprint3.Popover
boundary="scrollParent"
captureDismiss={false}
content={
<Blueprint3.Menu>
<Blueprint3.MenuItem
disabled={false}
icon="play"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="Resume all supervisors"
/>
<Blueprint3.MenuItem
disabled={false}
icon="pause"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="Suspend all supervisors"
/>
<Blueprint3.MenuItem
disabled={false}
icon="cross"
intent="danger"
multiline={false}
onClick={[Function]}
popoverProps={Object {}}
shouldDismissPopover={true}
text="Terminate all supervisors"
/>
</Blueprint3.Menu>
}
defaultIsOpen={false}
disabled={false}
hasBackdrop={false}
hoverCloseDelay={300}
hoverOpenDelay={150}
inheritDarkTheme={true}
interactionKind="click"
minimal={false}
modifiers={Object {}}
openOnTargetFocus={true}
position="bottom-left"
targetTagName="span"
transitionDuration={300}
usePortal={true}
wrapperTagName="span"
>
<Blueprint3.Button
icon="more"
/>
</Blueprint3.Popover>
<TableColumnSelector
columns={
Array [
@ -261,57 +316,6 @@ exports[`tasks view matches snapshot 1`] = `
style={Object {}}
subRowsKey="_subRows"
/>
<AsyncActionDialog
action={null}
confirmButtonText="Resume supervisor"
failText="Could not resume supervisor"
intent="primary"
onClose={[Function]}
successText="Supervisor has been resumed"
>
<p>
Are you sure you want to resume supervisor 'null'?
</p>
</AsyncActionDialog>
<AsyncActionDialog
action={null}
confirmButtonText="Suspend supervisor"
failText="Could not suspend supervisor"
intent="danger"
onClose={[Function]}
successText="Supervisor has been suspended"
>
<p>
Are you sure you want to suspend supervisor 'null'?
</p>
</AsyncActionDialog>
<AsyncActionDialog
action={null}
confirmButtonText="Reset supervisor"
failText="Could not reset supervisor"
intent="danger"
onClose={[Function]}
successText="Supervisor has been reset"
>
<p>
Are you sure you want to reset supervisor 'null'?
</p>
</AsyncActionDialog>
<AsyncActionDialog
action={null}
confirmButtonText="Terminate supervisor"
failText="Could not terminate supervisor"
intent="danger"
onClose={[Function]}
successText="Supervisor has been terminated"
>
<p>
Are you sure you want to terminate supervisor 'null'?
</p>
<p>
This action is not reversible.
</p>
</AsyncActionDialog>
</div>
<div
className="bottom-pane"
@ -639,18 +643,6 @@ exports[`tasks view matches snapshot 1`] = `
style={Object {}}
subRowsKey="_subRows"
/>
<AsyncActionDialog
action={null}
confirmButtonText="Kill task"
failText="Could not kill task"
intent="danger"
onClose={[Function]}
successText="Task was killed"
>
<p>
Are you sure you want to kill task 'null'?
</p>
</AsyncActionDialog>
</div>
</t>
<Blueprint3.Alert

View File

@ -95,6 +95,10 @@ export interface TasksViewState {
resetSupervisorId: string | null;
terminateSupervisorId: string | null;
showResumeAllSupervisors: boolean;
showSuspendAllSupervisors: boolean;
showTerminateAllSupervisors: boolean;
tasksLoading: boolean;
tasks: any[] | null;
tasksError: string | null;
@ -206,6 +210,10 @@ ORDER BY "rank" DESC, "created_time" DESC`;
supervisorTableActionDialogId: null,
terminateSupervisorId: null,
showResumeAllSupervisors: false,
showSuspendAllSupervisors: false,
showTerminateAllSupervisors: false,
tasksLoading: true,
tasks: null,
tasksError: null,
@ -394,27 +402,26 @@ ORDER BY "rank" DESC, "created_time" DESC`;
renderResumeSupervisorAction() {
const { resumeSupervisorId } = this.state;
if (!resumeSupervisorId) return;
return (
<AsyncActionDialog
action={
resumeSupervisorId
? async () => {
const resp = await axios.post(
`/druid/indexer/v1/supervisor/${resumeSupervisorId}/resume`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.post(
`/druid/indexer/v1/supervisor/${resumeSupervisorId}/resume`,
{},
);
return resp.data;
}}
confirmButtonText="Resume supervisor"
successText="Supervisor has been resumed"
failText="Could not resume supervisor"
intent={Intent.PRIMARY}
onClose={success => {
onClose={() => {
this.setState({ resumeSupervisorId: null });
if (success) this.supervisorQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to resume supervisor '${resumeSupervisorId}'?`}</p>
@ -424,27 +431,26 @@ ORDER BY "rank" DESC, "created_time" DESC`;
renderSuspendSupervisorAction() {
const { suspendSupervisorId } = this.state;
if (!suspendSupervisorId) return;
return (
<AsyncActionDialog
action={
suspendSupervisorId
? async () => {
const resp = await axios.post(
`/druid/indexer/v1/supervisor/${suspendSupervisorId}/suspend`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.post(
`/druid/indexer/v1/supervisor/${suspendSupervisorId}/suspend`,
{},
);
return resp.data;
}}
confirmButtonText="Suspend supervisor"
successText="Supervisor has been suspended"
failText="Could not suspend supervisor"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ suspendSupervisorId: null });
if (success) this.supervisorQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to suspend supervisor '${suspendSupervisorId}'?`}</p>
@ -454,27 +460,26 @@ ORDER BY "rank" DESC, "created_time" DESC`;
renderResetSupervisorAction() {
const { resetSupervisorId } = this.state;
if (!resetSupervisorId) return;
return (
<AsyncActionDialog
action={
resetSupervisorId
? async () => {
const resp = await axios.post(
`/druid/indexer/v1/supervisor/${resetSupervisorId}/reset`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.post(
`/druid/indexer/v1/supervisor/${resetSupervisorId}/reset`,
{},
);
return resp.data;
}}
confirmButtonText="Reset supervisor"
successText="Supervisor has been reset"
failText="Could not reset supervisor"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ resetSupervisorId: null });
if (success) this.supervisorQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to reset supervisor '${resetSupervisorId}'?`}</p>
@ -484,27 +489,26 @@ ORDER BY "rank" DESC, "created_time" DESC`;
renderTerminateSupervisorAction() {
const { terminateSupervisorId } = this.state;
if (!terminateSupervisorId) return;
return (
<AsyncActionDialog
action={
terminateSupervisorId
? async () => {
const resp = await axios.post(
`/druid/indexer/v1/supervisor/${terminateSupervisorId}/terminate`,
{},
);
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.post(
`/druid/indexer/v1/supervisor/${terminateSupervisorId}/terminate`,
{},
);
return resp.data;
}}
confirmButtonText="Terminate supervisor"
successText="Supervisor has been terminated"
failText="Could not terminate supervisor"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ terminateSupervisorId: null });
if (success) this.supervisorQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to terminate supervisor '${terminateSupervisorId}'?`}</p>
@ -638,24 +642,23 @@ ORDER BY "rank" DESC, "created_time" DESC`;
renderKillTaskAction() {
const { killTaskId } = this.state;
if (!killTaskId) return;
return (
<AsyncActionDialog
action={
killTaskId
? async () => {
const resp = await axios.post(`/druid/indexer/v1/task/${killTaskId}/shutdown`, {});
return resp.data;
}
: null
}
action={async () => {
const resp = await axios.post(`/druid/indexer/v1/task/${killTaskId}/shutdown`, {});
return resp.data;
}}
confirmButtonText="Kill task"
successText="Task was killed"
failText="Could not kill task"
intent={Intent.DANGER}
onClose={success => {
onClose={() => {
this.setState({ killTaskId: null });
if (success) this.taskQueryManager.rerunLastQuery();
}}
onSuccess={() => {
this.taskQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to kill task '${killTaskId}'?`}</p>
@ -849,6 +852,118 @@ ORDER BY "rank" DESC, "created_time" DESC`;
);
}
renderBulkSupervisorActions() {
const bulkSupervisorActionsMenu = (
<Menu>
<MenuItem
icon={IconNames.PLAY}
text="Resume all supervisors"
onClick={() => this.setState({ showResumeAllSupervisors: true })}
/>
<MenuItem
icon={IconNames.PAUSE}
text="Suspend all supervisors"
onClick={() => this.setState({ showSuspendAllSupervisors: true })}
/>
<MenuItem
icon={IconNames.CROSS}
text="Terminate all supervisors"
intent={Intent.DANGER}
onClick={() => this.setState({ showTerminateAllSupervisors: true })}
/>
</Menu>
);
return (
<>
<Popover content={bulkSupervisorActionsMenu} position={Position.BOTTOM_LEFT}>
<Button icon={IconNames.MORE} />
</Popover>
{this.renderResumeAllSupervisorAction()}
{this.renderSuspendAllSupervisorAction()}
{this.renderTerminateAllSupervisorAction()}
</>
);
}
renderResumeAllSupervisorAction() {
const { showResumeAllSupervisors } = this.state;
if (!showResumeAllSupervisors) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await axios.post(`/druid/indexer/v1/supervisor/resumeAll`, {});
return resp.data;
}}
confirmButtonText="Resume all supervisors"
successText="All supervisors have been resumed"
failText="Could not resume all supervisors"
intent={Intent.PRIMARY}
onClose={() => {
this.setState({ showResumeAllSupervisors: false });
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>Are you sure you want to resume all the supervisors?</p>
</AsyncActionDialog>
);
}
renderSuspendAllSupervisorAction() {
const { showSuspendAllSupervisors } = this.state;
if (!showSuspendAllSupervisors) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await axios.post(`/druid/indexer/v1/supervisor/suspendAll`, {});
return resp.data;
}}
confirmButtonText="Suspend all supervisors"
successText="All supervisors have been suspended"
failText="Could not suspend all supervisors"
intent={Intent.DANGER}
onClose={() => {
this.setState({ showSuspendAllSupervisors: false });
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>Are you sure you want to suspend all the supervisors?</p>
</AsyncActionDialog>
);
}
renderTerminateAllSupervisorAction() {
const { showTerminateAllSupervisors } = this.state;
if (!showTerminateAllSupervisors) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await axios.post(`/druid/indexer/v1/supervisor/terminateAll`, {});
return resp.data;
}}
confirmButtonText="Terminate all supervisors"
successText="All supervisors have been terminated"
failText="Could not terminate all supervisors"
intent={Intent.DANGER}
onClose={() => {
this.setState({ showTerminateAllSupervisors: false });
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>Are you sure you want to terminate all the supervisors?</p>
</AsyncActionDialog>
);
}
render() {
const { goToQuery, goToLoadDataView, noSqlMode } = this.props;
const {
@ -912,11 +1027,12 @@ ORDER BY "rank" DESC, "created_time" DESC`;
<ViewControlBar label="Supervisors">
<RefreshButton
localStorageKey={LocalStorageKeys.SUPERVISORS_REFRESH_RATE}
onRefresh={auto => this.supervisorQueryManager.rerunLastQueryInBackground(auto)}
onRefresh={auto => this.supervisorQueryManager.rerunLastQuery(auto)}
/>
<Popover content={submitSupervisorMenu} position={Position.BOTTOM_LEFT}>
<Button icon={IconNames.PLUS} text="Submit supervisor" />
</Popover>
{this.renderBulkSupervisorActions()}
<TableColumnSelector
columns={supervisorTableColumns}
onChange={column =>
@ -958,7 +1074,7 @@ ORDER BY "rank" DESC, "created_time" DESC`;
</ButtonGroup>
<RefreshButton
localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE}
onRefresh={auto => this.taskQueryManager.rerunLastQueryInBackground(auto)}
onRefresh={auto => this.taskQueryManager.rerunLastQuery(auto)}
/>
{!noSqlMode && (
<Button