diff --git a/web-console/src/dialogs/index.ts b/web-console/src/dialogs/index.ts index 468ccf8d622..f7e239b53f4 100644 --- a/web-console/src/dialogs/index.ts +++ b/web-console/src/dialogs/index.ts @@ -34,6 +34,7 @@ export * from './retention-dialog/retention-dialog'; export * from './snitch-dialog/snitch-dialog'; export * from './spec-dialog/spec-dialog'; export * from './string-input-dialog/string-input-dialog'; +export * from './supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog'; export * from './supervisor-table-action-dialog/supervisor-table-action-dialog'; export * from './table-action-dialog/table-action-dialog'; export * from './task-table-action-dialog/task-table-action-dialog'; diff --git a/web-console/src/dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog.tsx b/web-console/src/dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog.tsx new file mode 100644 index 00000000000..8fd8fdba564 --- /dev/null +++ b/web-console/src/dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog.tsx @@ -0,0 +1,122 @@ +/* + * 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 { Button, Classes, Code, ControlGroup, Dialog, FormGroup, Intent } from '@blueprintjs/core'; +import React, { useState } from 'react'; + +import { FancyNumericInput } from '../../components/fancy-numeric-input/fancy-numeric-input'; +import { useQueryManager } from '../../hooks'; +import { Api, AppToaster } from '../../singletons'; +import { deepGet, getDruidErrorMessage } from '../../utils'; + +type OffsetMap = Record; + +interface SupervisorResetOffsetsDialogProps { + supervisorId: string; + supervisorType: string; + onClose: () => void; +} + +export const SupervisorResetOffsetsDialog = React.memo(function SupervisorResetOffsetsDialog( + props: SupervisorResetOffsetsDialogProps, +) { + const { supervisorId, supervisorType, onClose } = props; + const [offsetsToResetTo, setOffsetsToResetTo] = useState({}); + + const [statusResp] = useQueryManager({ + initQuery: supervisorId, + processQuery: async supervisorId => { + const statusResp = await Api.instance.get( + `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/status`, + ); + return statusResp.data; + }, + }); + + const stream = deepGet(statusResp.data || {}, 'payload.stream'); + const latestOffsets = deepGet(statusResp.data || {}, 'payload.latestOffsets'); + + async function onSave() { + if (!stream) return; + if (!Object.keys(offsetsToResetTo).length) return; + + try { + await Api.instance.post( + `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/resetOffsets`, + { + type: supervisorType, + partitions: { + type: 'end', + stream, + partitionOffsetMap: offsetsToResetTo, + }, + }, + ); + } catch (e) { + AppToaster.show({ + message: `Failed to set offsets: ${getDruidErrorMessage(e)}`, + intent: Intent.DANGER, + }); + return; + } + + AppToaster.show({ + message: `${supervisorId} offsets have been set`, + intent: Intent.SUCCESS, + }); + onClose(); + } + + return ( + +
+
+

+ Set {supervisorId} to specific offsets +

+ {latestOffsets && + Object.entries(latestOffsets).map(([key, latestOffset]) => ( + + +
+
+
+
+
+ ); +}); diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx b/web-console/src/views/supervisors-view/supervisors-view.tsx index c4bef912c3a..2959e06f62c 100644 --- a/web-console/src/views/supervisors-view/supervisors-view.tsx +++ b/web-console/src/views/supervisors-view/supervisors-view.tsx @@ -40,6 +40,7 @@ import { SpecDialog, SupervisorTableActionDialog, } from '../../dialogs'; +import { SupervisorResetOffsetsDialog } from '../../dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog'; import type { QueryWithContext } from '../../druid-models'; import type { Capabilities } from '../../helpers'; import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table'; @@ -108,6 +109,7 @@ export interface SupervisorsViewState { resumeSupervisorId?: string; suspendSupervisorId?: string; + resetOffsetsSupervisorInfo?: { id: string; type: string }; resetSupervisorId?: string; terminateSupervisorId?: string; @@ -339,6 +341,11 @@ GROUP BY 1, 2`; ? this.setState({ resumeSupervisorId: id }) : this.setState({ suspendSupervisorId: id }), }, + { + icon: IconNames.STEP_BACKWARD, + title: 'Set offsets', + onAction: () => this.setState({ resetOffsetsSupervisorInfo: { id, type } }), + }, { icon: IconNames.STEP_BACKWARD, title: 'Hard reset', @@ -417,6 +424,21 @@ GROUP BY 1, 2`; ); } + renderResetOffsetsSupervisorAction() { + const { resetOffsetsSupervisorInfo } = this.state; + if (!resetOffsetsSupervisorInfo) return; + + return ( + { + this.setState({ resetOffsetsSupervisorInfo: undefined }); + }} + /> + ); + } + renderResetSupervisorAction() { const { resetSupervisorId } = this.state; if (!resetSupervisorId) return; @@ -426,7 +448,7 @@ GROUP BY 1, 2`; action={async () => { const resp = await Api.instance.post( `/druid/indexer/v1/supervisor/${Api.encodePath(resetSupervisorId)}/reset`, - {}, + '', ); return resp.data; }} @@ -784,6 +806,7 @@ GROUP BY 1, 2`; {this.renderSupervisorTable()} {this.renderResumeSupervisorAction()} {this.renderSuspendSupervisorAction()} + {this.renderResetOffsetsSupervisorAction()} {this.renderResetSupervisorAction()} {this.renderTerminateSupervisorAction()} {supervisorSpecDialogOpen && (