| /* |
| * 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) }}>● </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) }}>● </span> |
| {status} |
| {errorMsg && ( |
| <a |
| onClick={() => this.setState({ alertErrorMsg: errorMsg })} |
| title={errorMsg} |
| > |
| ? |
| </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 })} |
| /> |
| )} |
| </> |
| ); |
| } |
| } |