blob: e28c4491ad8443f17c599b05b6f00a88152ef1bd [file] [log] [blame]
/*
* 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.
*/
import { Alert, Button, ButtonGroup, Intent, Label, MenuItem } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import React from 'react';
import SplitterLayout from 'react-splitter-layout';
import ReactTable, { Filter } from 'react-table';
import {
ACTION_COLUMN_ID,
ACTION_COLUMN_LABEL,
ACTION_COLUMN_WIDTH,
ActionCell,
MoreButton,
RefreshButton,
TableColumnSelector,
ViewControlBar,
} from '../../components';
import {
AsyncActionDialog,
SpecDialog,
SupervisorTableActionDialog,
TaskTableActionDialog,
} from '../../dialogs';
import { Api, AppToaster } from '../../singletons';
import {
addFilter,
addFilterRaw,
booleanCustomTableFilter,
Capabilities,
deepGet,
formatDuration,
getDruidErrorMessage,
localStorageGet,
LocalStorageKeys,
localStorageSet,
oneOf,
queryDruidSql,
QueryManager,
QueryState,
} from '../../utils';
import { BasicAction } from '../../utils/basic-action';
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
import './ingestion-view.scss';
const supervisorTableColumns: string[] = [
'Datasource',
'Type',
'Topic/Stream',
'Status',
ACTION_COLUMN_LABEL,
];
const taskTableColumns: string[] = [
'Task ID',
'Group ID',
'Type',
'Datasource',
'Location',
'Created time',
'Status',
'Duration',
ACTION_COLUMN_LABEL,
];
interface SupervisorQueryResultRow {
supervisor_id: string;
type: string;
source: string;
state: string;
detailed_state: string;
suspended: number;
}
interface TaskQueryResultRow {
task_id: string;
group_id: string;
type: string;
created_time: string;
datasource: string;
duration: number;
error_msg: string | null;
location: string | null;
status: string;
rank: number;
}
export interface IngestionViewProps {
taskGroupId: string | undefined;
datasourceId: string | undefined;
openDialog: string | undefined;
goToDatasource: (datasource: string) => void;
goToQuery: (initSql: string) => void;
goToLoadData: (supervisorId?: string, taskId?: string) => void;
capabilities: Capabilities;
}
export interface IngestionViewState {
supervisorsState: QueryState<SupervisorQueryResultRow[]>;
resumeSupervisorId?: string;
suspendSupervisorId?: string;
resetSupervisorId?: string;
terminateSupervisorId?: string;
showResumeAllSupervisors: boolean;
showSuspendAllSupervisors: boolean;
showTerminateAllSupervisors: boolean;
tasksState: QueryState<TaskQueryResultRow[]>;
taskFilter: Filter[];
supervisorFilter: Filter[];
groupTasksBy?: 'group_id' | 'type' | 'datasource' | 'status';
killTaskId?: string;
supervisorSpecDialogOpen: boolean;
taskSpecDialogOpen: boolean;
alertErrorMsg?: string;
taskTableActionDialogId?: string;
taskTableActionDialogStatus?: string;
taskTableActionDialogActions: BasicAction[];
supervisorTableActionDialogId?: string;
supervisorTableActionDialogActions: BasicAction[];
hiddenTaskColumns: LocalStorageBackedArray<string>;
hiddenSupervisorColumns: LocalStorageBackedArray<string>;
}
function statusToColor(status: string): string {
switch (status) {
case 'RUNNING':
return '#2167d5';
case 'WAITING':
return '#d5631a';
case 'PENDING':
return '#ffbf00';
case 'SUCCESS':
return '#57d500';
case 'FAILED':
return '#d5100a';
default:
return '#0a1500';
}
}
function stateToColor(status: string): string {
switch (status) {
case 'UNHEALTHY_SUPERVISOR':
return '#d5100a';
case 'UNHEALTHY_TASKS':
return '#d5100a';
case 'PENDING':
return '#ffbf00';
case `SUSPENDED`:
return '#ffbf00';
case 'STOPPING':
return '#d5100a';
case 'RUNNING':
return '#2167d5';
default:
return '#0a1500';
}
}
export class IngestionView extends React.PureComponent<IngestionViewProps, IngestionViewState> {
private readonly supervisorQueryManager: QueryManager<Capabilities, SupervisorQueryResultRow[]>;
private readonly taskQueryManager: QueryManager<Capabilities, TaskQueryResultRow[]>;
static statusRanking: Record<string, number> = {
RUNNING: 4,
PENDING: 3,
WAITING: 2,
SUCCESS: 1,
FAILED: 1,
};
static SUPERVISOR_SQL = `SELECT
"supervisor_id", "type", "source", "state", "detailed_state", "suspended"
FROM sys.supervisors
ORDER BY "supervisor_id"`;
static TASK_SQL = `SELECT
"task_id", "group_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
(
CASE WHEN "status" = 'RUNNING' THEN
(CASE "runner_status" WHEN 'RUNNING' THEN 4 WHEN 'PENDING' THEN 3 ELSE 2 END)
ELSE 1
END
) AS "rank"
FROM sys.tasks
ORDER BY "rank" DESC, "created_time" DESC`;
constructor(props: IngestionViewProps, context: any) {
super(props, context);
const taskFilter: Filter[] = [];
if (props.taskGroupId) taskFilter.push({ id: 'group_id', value: props.taskGroupId });
if (props.datasourceId) taskFilter.push({ id: 'datasource', value: props.datasourceId });
const supervisorFilter: Filter[] = [];
if (props.datasourceId) supervisorFilter.push({ id: 'datasource', value: props.datasourceId });
this.state = {
supervisorsState: QueryState.INIT,
showResumeAllSupervisors: false,
showSuspendAllSupervisors: false,
showTerminateAllSupervisors: false,
tasksState: QueryState.INIT,
taskFilter: taskFilter,
supervisorFilter: supervisorFilter,
supervisorSpecDialogOpen: props.openDialog === 'supervisor',
taskSpecDialogOpen: props.openDialog === 'task',
taskTableActionDialogActions: [],
supervisorTableActionDialogActions: [],
hiddenTaskColumns: new LocalStorageBackedArray<string>(
LocalStorageKeys.TASK_TABLE_COLUMN_SELECTION,
),
hiddenSupervisorColumns: new LocalStorageBackedArray<string>(
LocalStorageKeys.SUPERVISOR_TABLE_COLUMN_SELECTION,
),
};
this.supervisorQueryManager = new QueryManager({
processQuery: async capabilities => {
if (capabilities.hasSql()) {
return await queryDruidSql({
query: IngestionView.SUPERVISOR_SQL,
});
} else if (capabilities.hasOverlordAccess()) {
const supervisors = (await Api.instance.get('/druid/indexer/v1/supervisor?full')).data;
if (!Array.isArray(supervisors)) throw new Error(`Unexpected results`);
return supervisors.map((sup: any) => {
return {
supervisor_id: deepGet(sup, 'id'),
type: deepGet(sup, 'spec.tuningConfig.type'),
source:
deepGet(sup, 'spec.ioConfig.topic') ||
deepGet(sup, 'spec.ioConfig.stream') ||
'n/a',
state: deepGet(sup, 'state'),
detailed_state: deepGet(sup, 'detailedState'),
suspended: Number(deepGet(sup, 'suspended')),
};
});
} else {
throw new Error(`must have SQL or overlord access`);
}
},
onStateChange: supervisorsState => {
this.setState({
supervisorsState,
});
},
});
this.taskQueryManager = new QueryManager({
processQuery: async capabilities => {
if (capabilities.hasSql()) {
return await queryDruidSql({
query: IngestionView.TASK_SQL,
});
} else if (capabilities.hasOverlordAccess()) {
const resp = await Api.instance.get(`/druid/indexer/v1/tasks`);
return IngestionView.parseTasks(resp.data);
} else {
throw new Error(`must have SQL or overlord access`);
}
},
onStateChange: tasksState => {
this.setState({
tasksState,
});
},
});
}
static parseTasks = (data: any[]): TaskQueryResultRow[] => {
return data.map((d: any) => {
return {
task_id: d.id,
group_id: d.groupId,
type: d.type,
created_time: d.createdTime,
datasource: d.dataSource,
duration: d.duration ? d.duration : 0,
error_msg: d.errorMsg,
location: d.location.host ? `${d.location.host}:${d.location.port}` : null,
status: d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode,
rank:
IngestionView.statusRanking[
d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode
],
};
});
};
private static onSecondaryPaneSizeChange(secondaryPaneSize: number) {
localStorageSet(LocalStorageKeys.INGESTION_VIEW_PANE_SIZE, String(secondaryPaneSize));
}
componentDidMount(): void {
const { capabilities } = this.props;
this.supervisorQueryManager.runQuery(capabilities);
this.taskQueryManager.runQuery(capabilities);
}
componentWillUnmount(): void {
this.supervisorQueryManager.terminate();
this.taskQueryManager.terminate();
}
private readonly closeSpecDialogs = () => {
this.setState({
supervisorSpecDialogOpen: false,
taskSpecDialogOpen: false,
});
};
private readonly submitSupervisor = async (spec: JSON) => {
try {
await Api.instance.post('/druid/indexer/v1/supervisor', spec);
} catch (e) {
AppToaster.show({
message: `Failed to submit supervisor: ${getDruidErrorMessage(e)}`,
intent: Intent.DANGER,
});
return;
}
AppToaster.show({
message: 'Supervisor submitted successfully',
intent: Intent.SUCCESS,
});
this.supervisorQueryManager.rerunLastQuery();
};
private readonly submitTask = async (spec: JSON) => {
try {
await Api.instance.post('/druid/indexer/v1/task', spec);
} catch (e) {
AppToaster.show({
message: `Failed to submit task: ${getDruidErrorMessage(e)}`,
intent: Intent.DANGER,
});
return;
}
AppToaster.show({
message: 'Task submitted successfully',
intent: Intent.SUCCESS,
});
this.taskQueryManager.rerunLastQuery();
};
private getSupervisorActions(
id: string,
supervisorSuspended: boolean,
type: string,
): BasicAction[] {
const { goToDatasource, goToLoadData } = this.props;
const actions: BasicAction[] = [];
if (oneOf(type, 'kafka', 'kinesis')) {
actions.push(
{
icon: IconNames.MULTI_SELECT,
title: 'Go to datasource',
onAction: () => goToDatasource(id),
},
{
icon: IconNames.CLOUD_UPLOAD,
title: 'Open in data loader',
onAction: () => goToLoadData(id),
},
);
}
actions.push(
{
icon: supervisorSuspended ? IconNames.PLAY : IconNames.PAUSE,
title: supervisorSuspended ? 'Resume' : 'Suspend',
onAction: () =>
supervisorSuspended
? this.setState({ resumeSupervisorId: id })
: this.setState({ suspendSupervisorId: id }),
},
{
icon: IconNames.STEP_BACKWARD,
title: 'Hard reset',
intent: Intent.DANGER,
onAction: () => this.setState({ resetSupervisorId: id }),
},
{
icon: IconNames.CROSS,
title: 'Terminate',
intent: Intent.DANGER,
onAction: () => this.setState({ terminateSupervisorId: id }),
},
);
return actions;
}
renderResumeSupervisorAction() {
const { resumeSupervisorId } = this.state;
if (!resumeSupervisorId) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/supervisor/${Api.encodePath(resumeSupervisorId)}/resume`,
{},
);
return resp.data;
}}
confirmButtonText="Resume supervisor"
successText="Supervisor has been resumed"
failText="Could not resume supervisor"
intent={Intent.PRIMARY}
onClose={() => {
this.setState({ resumeSupervisorId: undefined });
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to resume supervisor '${resumeSupervisorId}'?`}</p>
</AsyncActionDialog>
);
}
renderSuspendSupervisorAction() {
const { suspendSupervisorId } = this.state;
if (!suspendSupervisorId) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/supervisor/${Api.encodePath(suspendSupervisorId)}/suspend`,
{},
);
return resp.data;
}}
confirmButtonText="Suspend supervisor"
successText="Supervisor has been suspended"
failText="Could not suspend supervisor"
intent={Intent.DANGER}
onClose={() => {
this.setState({ suspendSupervisorId: undefined });
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to suspend supervisor '${suspendSupervisorId}'?`}</p>
</AsyncActionDialog>
);
}
renderResetSupervisorAction() {
const { resetSupervisorId } = this.state;
if (!resetSupervisorId) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/supervisor/${Api.encodePath(resetSupervisorId)}/reset`,
{},
);
return resp.data;
}}
confirmButtonText="Hard reset supervisor"
successText="Supervisor has been hard reset"
failText="Could not hard reset supervisor"
intent={Intent.DANGER}
onClose={() => {
this.setState({ resetSupervisorId: undefined });
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
warningChecks={[
`I understand that resetting ${resetSupervisorId} will clear checkpoints and therefore lead to data loss or duplication.`,
'I understand that this operation cannot be undone.',
]}
>
<p>{`Are you sure you want to hard reset supervisor '${resetSupervisorId}'?`}</p>
<p>Hard resetting a supervisor will lead to data loss or data duplication.</p>
<p>
The reason for using this operation is to recover from a state in which the supervisor
ceases operating due to missing offsets.
</p>
</AsyncActionDialog>
);
}
renderTerminateSupervisorAction() {
const { terminateSupervisorId } = this.state;
if (!terminateSupervisorId) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/supervisor/${Api.encodePath(terminateSupervisorId)}/terminate`,
{},
);
return resp.data;
}}
confirmButtonText="Terminate supervisor"
successText="Supervisor has been terminated"
failText="Could not terminate supervisor"
intent={Intent.DANGER}
onClose={() => {
this.setState({ terminateSupervisorId: undefined });
}}
onSuccess={() => {
this.supervisorQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to terminate supervisor '${terminateSupervisorId}'?`}</p>
<p>This action is not reversible.</p>
</AsyncActionDialog>
);
}
renderSupervisorTable() {
const { supervisorsState, hiddenSupervisorColumns, taskFilter, supervisorFilter } = this.state;
return (
<>
<ReactTable
data={supervisorsState.data || []}
loading={supervisorsState.loading}
noDataText={
supervisorsState.isEmpty() ? 'No supervisors' : supervisorsState.getErrorMessage() || ''
}
filtered={supervisorFilter}
onFilteredChange={filtered => {
const datasourceFilter = filtered.find(filter => filter.id === 'datasource');
let newTaskFilter = taskFilter.filter(filter => filter.id !== 'datasource');
if (datasourceFilter) {
newTaskFilter = addFilterRaw(
newTaskFilter,
datasourceFilter.id,
datasourceFilter.value,
);
}
this.setState({ supervisorFilter: filtered, taskFilter: newTaskFilter });
}}
filterable
columns={[
{
Header: 'Datasource',
id: 'datasource',
accessor: 'supervisor_id',
width: 300,
show: hiddenSupervisorColumns.exists('Datasource'),
},
{
Header: 'Type',
id: 'type',
accessor: row => row.type,
show: hiddenSupervisorColumns.exists('Type'),
},
{
Header: 'Topic/Stream',
id: 'source',
accessor: row => row.source,
show: hiddenSupervisorColumns.exists('Topic/Stream'),
},
{
Header: 'Status',
id: 'status',
width: 300,
accessor: row => row.detailed_state,
Cell: row => (
<span>
<span style={{ color: stateToColor(row.original.state) }}>&#x25cf;&nbsp;</span>
{row.value}
</span>
),
show: hiddenSupervisorColumns.exists('Status'),
},
{
Header: ACTION_COLUMN_LABEL,
id: ACTION_COLUMN_ID,
accessor: 'supervisor_id',
width: ACTION_COLUMN_WIDTH,
filterable: false,
Cell: row => {
const id = row.value;
const type = row.original.type;
const supervisorSuspended = row.original.suspended;
const supervisorActions = this.getSupervisorActions(id, supervisorSuspended, type);
return (
<ActionCell
onDetail={() =>
this.setState({
supervisorTableActionDialogId: id,
supervisorTableActionDialogActions: supervisorActions,
})
}
actions={supervisorActions}
/>
);
},
show: hiddenSupervisorColumns.exists(ACTION_COLUMN_LABEL),
},
]}
/>
{this.renderResumeSupervisorAction()}
{this.renderSuspendSupervisorAction()}
{this.renderResetSupervisorAction()}
{this.renderTerminateSupervisorAction()}
</>
);
}
private getTaskActions(
id: string,
datasource: string,
status: string,
type: string,
): BasicAction[] {
const { goToDatasource, goToLoadData } = this.props;
const actions: BasicAction[] = [];
if (datasource && status === 'SUCCESS') {
actions.push({
icon: IconNames.MULTI_SELECT,
title: 'Go to datasource',
onAction: () => goToDatasource(datasource),
});
}
if (oneOf(type, 'index', 'index_parallel')) {
actions.push({
icon: IconNames.CLOUD_UPLOAD,
title: 'Open in data loader',
onAction: () => goToLoadData(undefined, id),
});
}
if (oneOf(status, 'RUNNING', 'WAITING', 'PENDING')) {
actions.push({
icon: IconNames.CROSS,
title: 'Kill',
intent: Intent.DANGER,
onAction: () => this.setState({ killTaskId: id }),
});
}
return actions;
}
renderKillTaskAction() {
const { killTaskId } = this.state;
if (!killTaskId) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.post(
`/druid/indexer/v1/task/${Api.encodePath(killTaskId)}/shutdown`,
{},
);
return resp.data;
}}
confirmButtonText="Kill task"
successText="Task was killed"
failText="Could not kill task"
intent={Intent.DANGER}
onClose={() => {
this.setState({ killTaskId: undefined });
}}
onSuccess={() => {
this.taskQueryManager.rerunLastQuery();
}}
>
<p>{`Are you sure you want to kill task '${killTaskId}'?`}</p>
</AsyncActionDialog>
);
}
renderTaskTable() {
const {
tasksState,
taskFilter,
groupTasksBy,
hiddenTaskColumns,
supervisorFilter,
} = this.state;
return (
<>
<ReactTable
data={tasksState.data || []}
loading={tasksState.loading}
noDataText={tasksState.isEmpty() ? 'No tasks' : tasksState.getErrorMessage() || ''}
filterable
filtered={taskFilter}
onFilteredChange={filtered => {
const datasourceFilter = filtered.find(filter => filter.id === 'datasource');
let newSupervisorFilter = supervisorFilter.filter(filter => filter.id !== 'datasource');
if (datasourceFilter) {
newSupervisorFilter = addFilterRaw(
newSupervisorFilter,
datasourceFilter.id,
datasourceFilter.value,
);
}
this.setState({ supervisorFilter: newSupervisorFilter, taskFilter: filtered });
}}
defaultSorted={[{ id: 'status', desc: true }]}
pivotBy={groupTasksBy ? [groupTasksBy] : []}
columns={[
{
Header: 'Task ID',
accessor: 'task_id',
width: 500,
Aggregated: () => '',
show: hiddenTaskColumns.exists('Task ID'),
},
{
Header: 'Group ID',
accessor: 'group_id',
width: 300,
Aggregated: () => '',
Cell: row => {
const value = row.value;
return (
<a
onClick={() => {
this.setState({ taskFilter: addFilter(taskFilter, 'group_id', value) });
}}
>
{value}
</a>
);
},
show: hiddenTaskColumns.exists('Group ID'),
},
{
Header: 'Type',
accessor: 'type',
width: 140,
Cell: row => {
const value = row.value;
return (
<a
onClick={() => {
this.setState({ taskFilter: addFilter(taskFilter, 'type', value) });
}}
>
{value}
</a>
);
},
show: hiddenTaskColumns.exists('Type'),
},
{
Header: 'Datasource',
accessor: 'datasource',
Cell: row => {
const value = row.value;
return (
<a
onClick={() => {
this.setState({ taskFilter: addFilter(taskFilter, 'datasource', value) });
}}
>
{value}
</a>
);
},
show: hiddenTaskColumns.exists('Datasource'),
},
{
Header: 'Location',
accessor: 'location',
Aggregated: () => '',
filterMethod: (filter: Filter, row: any) => {
return booleanCustomTableFilter(filter, row.location);
},
show: hiddenTaskColumns.exists('Location'),
},
{
Header: 'Created time',
accessor: 'created_time',
width: 190,
Aggregated: () => '',
show: hiddenTaskColumns.exists('Created time'),
},
{
Header: 'Status',
id: 'status',
width: 110,
accessor: row => ({
status: row.status,
created_time: row.created_time,
toString: () => row.status,
}),
Cell: row => {
if (row.aggregated) return '';
const { status } = row.original;
const errorMsg = row.original.error_msg;
return (
<span>
<span style={{ color: statusToColor(status) }}>&#x25cf;&nbsp;</span>
{status}
{errorMsg && (
<a
onClick={() => this.setState({ alertErrorMsg: errorMsg })}
title={errorMsg}
>
&nbsp;?
</a>
)}
</span>
);
},
sortMethod: (d1, d2) => {
const typeofD1 = typeof d1;
const typeofD2 = typeof d2;
if (typeofD1 !== typeofD2) return 0;
switch (typeofD1) {
case 'string':
return IngestionView.statusRanking[d1] - IngestionView.statusRanking[d2];
case 'object':
return (
IngestionView.statusRanking[d1.status] -
IngestionView.statusRanking[d2.status] ||
d1.created_time.localeCompare(d2.created_time)
);
default:
return 0;
}
},
filterMethod: (filter: Filter, row: any) => {
return booleanCustomTableFilter(filter, row.status.status);
},
show: hiddenTaskColumns.exists('Status'),
},
{
Header: 'Duration',
accessor: 'duration',
width: 70,
filterable: false,
Cell: row => (row.value > 0 ? formatDuration(row.value) : ''),
Aggregated: () => '',
show: hiddenTaskColumns.exists('Duration'),
},
{
Header: ACTION_COLUMN_LABEL,
id: ACTION_COLUMN_ID,
accessor: 'task_id',
width: ACTION_COLUMN_WIDTH,
filterable: false,
Cell: row => {
if (row.aggregated) return '';
const id = row.value;
const type = row.row.type;
const { datasource, status } = row.original;
const taskActions = this.getTaskActions(id, datasource, status, type);
return (
<ActionCell
onDetail={() =>
this.setState({
taskTableActionDialogId: id,
taskTableActionDialogStatus: status,
taskTableActionDialogActions: taskActions,
})
}
actions={taskActions}
/>
);
},
Aggregated: () => '',
show: hiddenTaskColumns.exists(ACTION_COLUMN_LABEL),
},
]}
/>
{this.renderKillTaskAction()}
</>
);
}
renderBulkSupervisorActions() {
const { capabilities, goToQuery } = this.props;
return (
<>
<MoreButton>
{capabilities.hasSql() && (
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"
onClick={() => goToQuery(IngestionView.SUPERVISOR_SQL)}
/>
)}
<MenuItem
icon={IconNames.MANUALLY_ENTERED_DATA}
text="Submit JSON supervisor"
onClick={() => this.setState({ supervisorSpecDialogOpen: true })}
/>
<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 })}
/>
</MoreButton>
{this.renderResumeAllSupervisorAction()}
{this.renderSuspendAllSupervisorAction()}
{this.renderTerminateAllSupervisorAction()}
</>
);
}
renderResumeAllSupervisorAction() {
const { showResumeAllSupervisors } = this.state;
if (!showResumeAllSupervisors) return;
return (
<AsyncActionDialog
action={async () => {
const resp = await Api.instance.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 Api.instance.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 Api.instance.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>
);
}
renderBulkTasksActions() {
const { goToQuery, capabilities } = this.props;
return (
<MoreButton>
{capabilities.hasSql() && (
<MenuItem
icon={IconNames.APPLICATION}
text="View SQL query for table"
onClick={() => goToQuery(IngestionView.TASK_SQL)}
/>
)}
<MenuItem
icon={IconNames.MANUALLY_ENTERED_DATA}
text="Submit JSON task"
onClick={() => this.setState({ taskSpecDialogOpen: true })}
/>
</MoreButton>
);
}
render(): JSX.Element {
const {
groupTasksBy,
supervisorSpecDialogOpen,
taskSpecDialogOpen,
alertErrorMsg,
taskTableActionDialogId,
taskTableActionDialogActions,
supervisorTableActionDialogId,
supervisorTableActionDialogActions,
taskTableActionDialogStatus,
hiddenSupervisorColumns,
hiddenTaskColumns,
} = this.state;
return (
<>
<SplitterLayout
customClassName="ingestion-view app-view"
vertical
percentage
secondaryInitialSize={
Number(localStorageGet(LocalStorageKeys.INGESTION_VIEW_PANE_SIZE)!) || 60
}
primaryMinSize={30}
secondaryMinSize={30}
onSecondaryPaneSizeChange={IngestionView.onSecondaryPaneSizeChange}
>
<div className="top-pane">
<ViewControlBar label="Supervisors">
<RefreshButton
localStorageKey={LocalStorageKeys.SUPERVISORS_REFRESH_RATE}
onRefresh={auto => this.supervisorQueryManager.rerunLastQuery(auto)}
/>
{this.renderBulkSupervisorActions()}
<TableColumnSelector
columns={supervisorTableColumns}
onChange={column =>
this.setState(prevState => ({
hiddenSupervisorColumns: prevState.hiddenSupervisorColumns.toggle(column),
}))
}
tableColumnsHidden={hiddenSupervisorColumns.storedArray}
/>
</ViewControlBar>
{this.renderSupervisorTable()}
</div>
<div className="bottom-pane">
<ViewControlBar label="Tasks">
<Label>Group by</Label>
<ButtonGroup>
<Button
active={!groupTasksBy}
onClick={() => this.setState({ groupTasksBy: undefined })}
>
None
</Button>
<Button
active={groupTasksBy === 'group_id'}
onClick={() => this.setState({ groupTasksBy: 'group_id' })}
>
Group ID
</Button>
<Button
active={groupTasksBy === 'type'}
onClick={() => this.setState({ groupTasksBy: 'type' })}
>
Type
</Button>
<Button
active={groupTasksBy === 'datasource'}
onClick={() => this.setState({ groupTasksBy: 'datasource' })}
>
Datasource
</Button>
<Button
active={groupTasksBy === 'status'}
onClick={() => this.setState({ groupTasksBy: 'status' })}
>
Status
</Button>
</ButtonGroup>
<RefreshButton
localStorageKey={LocalStorageKeys.TASKS_REFRESH_RATE}
onRefresh={auto => this.taskQueryManager.rerunLastQuery(auto)}
/>
{this.renderBulkTasksActions()}
<TableColumnSelector
columns={taskTableColumns}
onChange={column =>
this.setState(prevState => ({
hiddenTaskColumns: prevState.hiddenTaskColumns.toggle(column),
}))
}
tableColumnsHidden={hiddenTaskColumns.storedArray}
/>
</ViewControlBar>
{this.renderTaskTable()}
</div>
</SplitterLayout>
{supervisorSpecDialogOpen && (
<SpecDialog
onClose={this.closeSpecDialogs}
onSubmit={this.submitSupervisor}
title="Submit supervisor"
/>
)}
{taskSpecDialogOpen && (
<SpecDialog
onClose={this.closeSpecDialogs}
onSubmit={this.submitTask}
title="Submit task"
/>
)}
<Alert
icon={IconNames.ERROR}
intent={Intent.PRIMARY}
isOpen={Boolean(alertErrorMsg)}
confirmButtonText="OK"
onConfirm={() => this.setState({ alertErrorMsg: undefined })}
>
<p>{alertErrorMsg}</p>
</Alert>
{supervisorTableActionDialogId && (
<SupervisorTableActionDialog
supervisorId={supervisorTableActionDialogId}
actions={supervisorTableActionDialogActions}
onClose={() => this.setState({ supervisorTableActionDialogId: undefined })}
/>
)}
{taskTableActionDialogId && taskTableActionDialogStatus && (
<TaskTableActionDialog
status={taskTableActionDialogStatus}
taskId={taskTableActionDialogId}
actions={taskTableActionDialogActions}
onClose={() => this.setState({ taskTableActionDialogId: undefined })}
/>
)}
</>
);
}
}