diff --git a/src/assets/icons/decommission.svg b/src/assets/icons/decommission.svg new file mode 100644 index 000000000..20dccbdea --- /dev/null +++ b/src/assets/icons/decommission.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx b/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx index 55a75a4e1..9806d84c1 100644 --- a/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx +++ b/src/components/ButtonWithConfirmDialog/ButtonWithConfirmDialog.tsx @@ -3,13 +3,15 @@ import React from 'react'; import {Button, Popover} from '@gravity-ui/uikit'; import type {ButtonProps, PopoverProps} from '@gravity-ui/uikit'; -import {CriticalActionDialog} from '../CriticalActionDialog'; +import {CriticalActionDialog} from '../CriticalActionDialog/CriticalActionDialog'; +import {isErrorWithRetry} from '../CriticalActionDialog/utils'; interface ButtonWithConfirmDialogProps { children: React.ReactNode; onConfirmAction: (isRetry?: boolean) => Promise; onConfirmActionSuccess?: (() => Promise) | VoidFunction; - dialogContent: string; + dialogHeader: string; + dialogText: string; retryButtonText?: string; buttonDisabled?: ButtonProps['disabled']; buttonView?: ButtonProps['view']; @@ -24,7 +26,8 @@ export function ButtonWithConfirmDialog({ children, onConfirmAction, onConfirmActionSuccess, - dialogContent, + dialogHeader, + dialogText, retryButtonText, buttonDisabled = false, buttonView = 'action', @@ -41,29 +44,20 @@ export function ButtonWithConfirmDialog({ const handleConfirmAction = async (isRetry?: boolean) => { setButtonLoading(true); await onConfirmAction(isRetry); - setButtonLoading(false); }; const handleConfirmActionSuccess = async () => { setWithRetry(false); - if (onConfirmActionSuccess) { - setButtonLoading(true); - - try { - await onConfirmActionSuccess(); - } catch { - } finally { - setButtonLoading(false); - } + try { + await onConfirmActionSuccess?.(); + } finally { + setButtonLoading(false); } }; const handleConfirmActionError = (error: unknown) => { - const isWithRetry = Boolean( - error && typeof error === 'object' && 'retryPossible' in error && error.retryPossible, - ); - setWithRetry(isWithRetry); + setWithRetry(isErrorWithRetry(error)); setButtonLoading(false); }; @@ -101,7 +95,8 @@ export function ButtonWithConfirmDialog({ { interface CriticalActionDialogProps { visible: boolean; - text: string; + header?: React.ReactNode; + text?: string; withRetry?: boolean; retryButtonText?: string; onClose: VoidFunction; @@ -36,6 +37,7 @@ interface CriticalActionDialogProps { export function CriticalActionDialog({ visible, + header, text, withRetry, retryButtonText, @@ -64,15 +66,22 @@ export function CriticalActionDialog({ }); }; + const handleTransitionExited = () => { + setError(undefined); + }; + const renderDialogContent = () => { if (error) { return ( + - - - - {parseError(error)} +
+ + + + {parseError(error)} +
({ return ( + + - - - - {text} +
+ + + + {text} +
({ className={b()} size="s" onClose={onClose} - onTransitionExited={() => setError(undefined)} + onTransitionExited={handleTransitionExited} > {renderDialogContent()} diff --git a/src/components/CriticalActionDialog/index.ts b/src/components/CriticalActionDialog/index.ts deleted file mode 100644 index a9effc020..000000000 --- a/src/components/CriticalActionDialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CriticalActionDialog'; diff --git a/src/components/CriticalActionDialog/utils.ts b/src/components/CriticalActionDialog/utils.ts new file mode 100644 index 000000000..c10dbee3d --- /dev/null +++ b/src/components/CriticalActionDialog/utils.ts @@ -0,0 +1,9 @@ +interface ErrorWithRetry { + retryPossible: boolean; +} + +export const isErrorWithRetry = (error: unknown): error is ErrorWithRetry => { + return Boolean( + error && typeof error === 'object' && 'retryPossible' in error && error.retryPossible, + ); +}; diff --git a/src/components/EntityPageTitle/EntityPageTitle.tsx b/src/components/EntityPageTitle/EntityPageTitle.tsx index d94956941..29535e8dc 100644 --- a/src/components/EntityPageTitle/EntityPageTitle.tsx +++ b/src/components/EntityPageTitle/EntityPageTitle.tsx @@ -11,22 +11,14 @@ interface EntityPageTitleProps { status: EFlag; id: React.ReactNode; className?: string; - children?: React.ReactNode; } -export function EntityPageTitle({ - entityName, - status, - id, - className, - children, -}: EntityPageTitleProps) { +export function EntityPageTitle({entityName, status, id, className}: EntityPageTitleProps) { return (
{entityName} {id} - {children}
); } diff --git a/src/components/PDiskInfo/PDiskInfo.tsx b/src/components/PDiskInfo/PDiskInfo.tsx index 65b1ccc21..85aa88fb9 100644 --- a/src/components/PDiskInfo/PDiskInfo.tsx +++ b/src/components/PDiskInfo/PDiskInfo.tsx @@ -46,7 +46,6 @@ function getPDiskInfo({ SerialNumber, TotalSize, AllocatedSize, - DecommitStatus, StatusV2, NumActiveSlots, ExpectedSlotCount, @@ -58,12 +57,6 @@ function getPDiskInfo({ const generalInfo: InfoViewerItem[] = []; - if (valueIsDefined(DecommitStatus)) { - generalInfo.push({ - label: pDiskInfoKeyset('decomission-status'), - value: DecommitStatus.replace('DECOMMIT_', ''), - }); - } if (valueIsDefined(Category)) { generalInfo.push({label: pDiskInfoKeyset('type'), value: Type}); } diff --git a/src/components/PDiskInfo/i18n/en.json b/src/components/PDiskInfo/i18n/en.json index d2e02a198..ac3f95839 100644 --- a/src/components/PDiskInfo/i18n/en.json +++ b/src/components/PDiskInfo/i18n/en.json @@ -1,5 +1,4 @@ { - "decomission-status": "Decomission Status", "type": "Type", "path": "Path", "guid": "GUID", diff --git a/src/containers/PDiskPage/DecommissionButton/DecommissionButton.scss b/src/containers/PDiskPage/DecommissionButton/DecommissionButton.scss new file mode 100644 index 000000000..32c973f1b --- /dev/null +++ b/src/containers/PDiskPage/DecommissionButton/DecommissionButton.scss @@ -0,0 +1,6 @@ +.ydb-pdisk-decommission-button { + &__button, + &__popup { + width: 160px; + } +} diff --git a/src/containers/PDiskPage/DecommissionButton/DecommissionButton.tsx b/src/containers/PDiskPage/DecommissionButton/DecommissionButton.tsx new file mode 100644 index 000000000..86b25e7a9 --- /dev/null +++ b/src/containers/PDiskPage/DecommissionButton/DecommissionButton.tsx @@ -0,0 +1,168 @@ +import React from 'react'; + +import {ChevronDown} from '@gravity-ui/icons'; +import type {ButtonProps, DropdownMenuItem, DropdownMenuProps} from '@gravity-ui/uikit'; +import {Button, DropdownMenu, Icon, Popover} from '@gravity-ui/uikit'; + +import {CriticalActionDialog} from '../../../components/CriticalActionDialog/CriticalActionDialog'; +import {isErrorWithRetry} from '../../../components/CriticalActionDialog/utils'; +import type {EDecommitStatus} from '../../../types/api/pdisk'; +import {wait} from '../../../utils'; +import {cn} from '../../../utils/cn'; +import {pDiskPageKeyset} from '../i18n'; + +import decommissionIcon from '../../../assets/icons/decommission.svg'; + +import './DecommissionButton.scss'; + +const b = cn('ydb-pdisk-decommission-button'); + +function getDecommissionWarningText(decommission?: EDecommitStatus) { + if (decommission === 'DECOMMIT_IMMINENT') { + return pDiskPageKeyset('decommission-dialog-imminent-warning'); + } + if (decommission === 'DECOMMIT_PENDING') { + return pDiskPageKeyset('decommission-dialog-pending-warning'); + } + if (decommission === 'DECOMMIT_REJECTED') { + return pDiskPageKeyset('decommission-dialog-rejected-warning'); + } + if (decommission === 'DECOMMIT_NONE') { + return pDiskPageKeyset('decommission-dialog-none-warning'); + } + + return undefined; +} + +function getDecommissionButtonItems( + decommission: EDecommitStatus | undefined, + setNewDecommission: (newDecommission: EDecommitStatus | undefined) => void, +): DropdownMenuItem[] { + return [ + { + text: pDiskPageKeyset('decommission-none'), + action: () => setNewDecommission('DECOMMIT_NONE'), + hidden: + !decommission || + decommission === 'DECOMMIT_NONE' || + decommission === 'DECOMMIT_UNSET', + }, + { + text: pDiskPageKeyset('decommission-pending'), + action: () => setNewDecommission('DECOMMIT_PENDING'), + hidden: decommission === 'DECOMMIT_PENDING', + }, + { + text: pDiskPageKeyset('decommission-rejected'), + action: () => setNewDecommission('DECOMMIT_REJECTED'), + hidden: decommission === 'DECOMMIT_REJECTED', + }, + { + text: pDiskPageKeyset('decommission-imminent'), + theme: 'danger', + action: () => setNewDecommission('DECOMMIT_IMMINENT'), + hidden: decommission === 'DECOMMIT_IMMINENT', + }, + ]; +} + +interface DecommissionButtonProps { + decommission?: EDecommitStatus; + onConfirmAction: (newDecommissionStatus?: EDecommitStatus, isRetry?: boolean) => Promise; + onConfirmActionSuccess: (() => Promise) | VoidFunction; + buttonDisabled?: boolean; + popoverDisabled?: boolean; +} + +export function DecommissionButton({ + decommission, + onConfirmAction, + onConfirmActionSuccess, + buttonDisabled, + popoverDisabled, +}: DecommissionButtonProps) { + const [newDecommission, setNewDecommission] = React.useState(); + const [buttonLoading, setButtonLoading] = React.useState(false); + const [withRetry, setWithRetry] = React.useState(false); + + const handleConfirmAction = async (isRetry?: boolean) => { + setButtonLoading(true); + await onConfirmAction(newDecommission, isRetry); + }; + + const handleConfirmActionSuccess = async () => { + setWithRetry(false); + + // Decommission needs some time to change + // Wait for some time to send request for updated data + await wait(5000); + + try { + await onConfirmActionSuccess(); + } finally { + setButtonLoading(false); + } + }; + + const handleConfirmActionError = (error: unknown) => { + setWithRetry(isErrorWithRetry(error)); + setButtonLoading(false); + }; + + const handleClose = () => { + setNewDecommission(undefined); + }; + + const items = getDecommissionButtonItems(decommission, setNewDecommission); + + const renderSwitcher: DropdownMenuProps['renderSwitcher'] = (props) => { + return ( + + ); + }; + + return ( + + + + + ); +} + +function DecommissionButtonSwitcher({ + popoverDisabled, + ...restProps +}: ButtonProps & {popoverDisabled?: boolean}) { + return ( + + + + ); +} diff --git a/src/containers/PDiskPage/DecommissionLabel/DecommissionLabel.tsx b/src/containers/PDiskPage/DecommissionLabel/DecommissionLabel.tsx new file mode 100644 index 000000000..2cf6d635d --- /dev/null +++ b/src/containers/PDiskPage/DecommissionLabel/DecommissionLabel.tsx @@ -0,0 +1,39 @@ +import {Label} from '@gravity-ui/uikit'; + +import type {EDecommitStatus} from '../../../types/api/pdisk'; +import {pDiskPageKeyset} from '../i18n'; + +interface DecommissionLabelProps { + decommission?: EDecommitStatus; +} + +function getDecommissionLabelText(decommission: string) { + return pDiskPageKeyset('decommission-label', {decommission}); +} + +export function DecommissionLabel({decommission}: DecommissionLabelProps) { + if (decommission === 'DECOMMIT_IMMINENT') { + return ( + + ); + } + if (decommission === 'DECOMMIT_PENDING') { + return ( + + ); + } + if (decommission === 'DECOMMIT_REJECTED') { + return ( + + ); + } + + // Don't display status for undefined, NONE or UNSET decommission + return null; +} diff --git a/src/containers/PDiskPage/PDiskPage.scss b/src/containers/PDiskPage/PDiskPage.scss index 9c4b21895..fa3071ea1 100644 --- a/src/containers/PDiskPage/PDiskPage.scss +++ b/src/containers/PDiskPage/PDiskPage.scss @@ -20,6 +20,13 @@ left: 0; } + &__title { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--g-spacing-2); + } + &__controls { display: flex; align-items: center; diff --git a/src/containers/PDiskPage/PDiskPage.tsx b/src/containers/PDiskPage/PDiskPage.tsx index 8a20f8a03..cd5ea1a21 100644 --- a/src/containers/PDiskPage/PDiskPage.tsx +++ b/src/containers/PDiskPage/PDiskPage.tsx @@ -19,17 +19,22 @@ import {api} from '../../store/reducers/api'; import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {pDiskApi} from '../../store/reducers/pdisk/pdisk'; +import type {EDecommitStatus} from '../../types/api/pdisk'; import {valueIsDefined} from '../../utils'; +import {cn} from '../../utils/cn'; import {getPDiskId, getSeverityColor} from '../../utils/disks/helpers'; import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; +import {DecommissionButton} from './DecommissionButton/DecommissionButton'; +import {DecommissionLabel} from './DecommissionLabel/DecommissionLabel'; import {PDiskGroups} from './PDiskGroups/PDiskGroups'; import {PDiskSpaceDistribution} from './PDiskSpaceDistribution/PDiskSpaceDistribution'; import {pDiskPageKeyset} from './i18n'; -import {pdiskPageCn} from './shared'; import './PDiskPage.scss'; +const pdiskPageCn = cn('ydb-pdisk-page'); + const PDISK_TABS_IDS = { diskDistribution: 'diskDistribution', groups: 'groups', @@ -78,25 +83,44 @@ export function PDiskPage() { }); const pDiskLoading = pdiskDataQuery.isFetching && pdiskDataQuery.currentData === undefined; const pDiskData = pdiskDataQuery.currentData; - const {NodeHost, NodeId, NodeType, NodeDC, Severity} = pDiskData || {}; + const {NodeHost, NodeId, NodeType, NodeDC, Severity, DecommitStatus} = pDiskData || {}; const handleRestart = async (isRetry?: boolean) => { if (pDiskParamsDefined) { - return window.api.restartPDisk({nodeId, pDiskId, force: isRetry}).then((response) => { - if (response?.result === false) { - const err = { - statusText: response.error, - retryPossible: response.forceRetryPossible, - }; - throw err; - } - }); + const response = await window.api.restartPDisk({nodeId, pDiskId, force: isRetry}); + + if (response?.result === false) { + const err = { + statusText: response.error, + retryPossible: response.forceRetryPossible, + }; + throw err; + } } + }; - return undefined; + const handleDecommissionChange = async ( + newDecommissionStatus?: EDecommitStatus, + isRetry?: boolean, + ) => { + if (pDiskParamsDefined) { + const response = await window.api.changePDiskStatus({ + nodeId, + pDiskId, + force: isRetry, + decommissionStatus: newDecommissionStatus, + }); + if (response?.result === false) { + const err = { + statusText: response.error, + retryPossible: response.forceRetryPossible, + }; + throw err; + } + } }; - const handleAfterRestart = () => { + const handleAfterAction = () => { if (pDiskParamsDefined) { dispatch( api.util.invalidateTags([{type: 'PDiskData', id: getPDiskId(nodeId, pDiskId)}]), @@ -134,12 +158,14 @@ export function PDiskPage() { const renderPageTitle = () => { return ( - +
+ + +
); }; @@ -148,10 +174,11 @@ export function PDiskPage() {
{pDiskPageKeyset('restart-pdisk-button')} +
); }; diff --git a/src/containers/PDiskPage/i18n/en.json b/src/containers/PDiskPage/i18n/en.json index 05f5c2415..336060762 100644 --- a/src/containers/PDiskPage/i18n/en.json +++ b/src/containers/PDiskPage/i18n/en.json @@ -17,6 +17,25 @@ "restart-pdisk-button": "Restart PDisk", "force-restart-pdisk-button": "Restart anyway", - "restart-pdisk-dialog": "PDisk will be restarted. Do you want to proceed?", - "restart-pdisk-not-allowed": "You don't have enough rights to restart PDisk" + "restart-pdisk-not-allowed": "You don't have enough rights to restart PDisk", + + "restart-pdisk-dialog-header": "Restart PDisk", + "restart-pdisk-dialog-text": "PDisk will be restarted. Do you want to proceed?", + + "decommission-none": "None", + "decommission-imminent": "Imminent", + "decommission-pending": "Pending", + "decommission-rejected": "Rejected", + + "decommission-label": "{{decommission}} decommission", + + "decommission-button": "Decommission", + "decommission-change-not-allowed": "You don't have enough rights to change PDisk decommission", + "decommission-dialog-title": "Change decommission status", + "decommission-dialog-force-change": "Change anyway", + + "decommission-dialog-imminent-warning": "This will start imminent decommission. Existing slots will be moved from the disk", + "decommission-dialog-pending-warning": "This will start pending decommission. Decommission will be planned for this disk, but will not start immediatelly. Existing slots will not be moved from the disk, but no new slots will be allocated on it", + "decommission-dialog-rejected-warning": "This will start rejected decommission. No slots from other disks are placed on this disk in the process of decommission", + "decommission-dialog-none-warning": "This will reset decommission mode, allowing the disk to be used by the storage" } diff --git a/src/containers/Tablet/components/TabletControls/TabletControls.tsx b/src/containers/Tablet/components/TabletControls/TabletControls.tsx index f975148f8..31aeea630 100644 --- a/src/containers/Tablet/components/TabletControls/TabletControls.tsx +++ b/src/containers/Tablet/components/TabletControls/TabletControls.tsx @@ -42,7 +42,8 @@ export const TabletControls = ({tablet}: TabletControlsProps) => { return ( killTablet({id: TabletId}).unwrap()} buttonDisabled={isDisabledRestart || !isUserAllowedToMakeChanges} withPopover @@ -57,7 +58,8 @@ export const TabletControls = ({tablet}: TabletControlsProps) => { {hasHiveId && ( stopTablet({id: TabletId, hiveId: HiveId}).unwrap()} buttonDisabled={isDisabledStop || !isUserAllowedToMakeChanges} withPopover @@ -70,7 +72,8 @@ export const TabletControls = ({tablet}: TabletControlsProps) => { {i18n('controls.stop')} resumeTablet({id: TabletId, hiveId: HiveId}).unwrap() } diff --git a/src/containers/Tablet/i18n/en.json b/src/containers/Tablet/i18n/en.json index d24385939..c1d530f72 100644 --- a/src/containers/Tablet/i18n/en.json +++ b/src/containers/Tablet/i18n/en.json @@ -12,9 +12,13 @@ "controls.stop-not-allowed": "You don't have enough rights to stop tablet", "controls.resume-not-allowed": "You don't have enough rights to resume tablet", - "dialog.kill": "The tablet will be restarted. Do you want to proceed?", - "dialog.stop": "The tablet will be stopped. Do you want to proceed?", - "dialog.resume": "The tablet will be resumed. Do you want to proceed?", + "dialog.kill-header": "Restart tablet", + "dialog.stop-header": "Stop tablet", + "dialog.resume-header": "Resume tablet", + + "dialog.kill-text": "The tablet will be restarted. Do you want to proceed?", + "dialog.stop-text": "The tablet will be stopped. Do you want to proceed?", + "dialog.resume-text": "The tablet will be resumed. Do you want to proceed?", "emptyState": "The tablet was not found", diff --git a/src/containers/Tablets/Tablets.tsx b/src/containers/Tablets/Tablets.tsx index 337eed190..7c5ba2aca 100644 --- a/src/containers/Tablets/Tablets.tsx +++ b/src/containers/Tablets/Tablets.tsx @@ -154,7 +154,8 @@ function TabletActions(tablet: TTabletStateInfo) { return ( { return killTablet({id}).unwrap(); }} diff --git a/src/containers/Tablets/i18n/en.json b/src/containers/Tablets/i18n/en.json index 5065f7357..3af77871a 100644 --- a/src/containers/Tablets/i18n/en.json +++ b/src/containers/Tablets/i18n/en.json @@ -7,6 +7,7 @@ "Node FQDN": "Node FQDN", "Generation": "Generation", "Uptime": "Uptime", - "dialog.kill": "The tablet will be restarted. Do you want to proceed?", + "dialog.kill-header": "Restart tablet", + "dialog.kill-text": "The tablet will be restarted. Do you want to proceed?", "controls.kill-not-allowed": "You don't have enough rights to restart tablet" } diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx index 833e96509..61d71dfd8 100644 --- a/src/containers/VDiskPage/VDiskPage.tsx +++ b/src/containers/VDiskPage/VDiskPage.tsx @@ -147,7 +147,8 @@ export function VDiskPage() { onConfirmActionSuccess={handleAfterEvictVDisk} buttonDisabled={!vDiskIdParamsDefined || !isUserAllowedToMakeChanges} buttonView="normal" - dialogContent={vDiskPageKeyset('evict-vdisk-dialog')} + dialogHeader={vDiskPageKeyset('evict-vdisk-dialog-header')} + dialogText={vDiskPageKeyset('evict-vdisk-dialog-text')} retryButtonText={vDiskPageKeyset('force-evict-vdisk-button')} withPopover popoverContent={vDiskPageKeyset('evict-vdisk-not-allowed')} diff --git a/src/containers/VDiskPage/i18n/en.json b/src/containers/VDiskPage/i18n/en.json index d5201532f..b88fbba32 100644 --- a/src/containers/VDiskPage/i18n/en.json +++ b/src/containers/VDiskPage/i18n/en.json @@ -7,6 +7,7 @@ "evict-vdisk-button": "Evict VDisk", "force-evict-vdisk-button": "Evict anyway", - "evict-vdisk-dialog": "VDisk will be evicted. Do you want to proceed?", + "evict-vdisk-dialog-header": "Evict VDisk", + "evict-vdisk-dialog-text": "VDisk will be evicted. Do you want to proceed?", "evict-vdisk-not-allowed": "You don't have enough rights to evict VDisk" } diff --git a/src/services/api.ts b/src/services/api.ts index 867c7a574..ef0e45dae 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -2,7 +2,6 @@ import AxiosWrapper from '@gravity-ui/axios-wrapper'; import type {AxiosWrapperOptions} from '@gravity-ui/axios-wrapper'; import type {AxiosRequestConfig} from 'axios'; import axiosRetry from 'axios-retry'; -import qs from 'qs'; import {backend as BACKEND, metaBackend as META_BACKEND} from '../store'; import type {ComputeApiRequestParams, NodesApiRequestParams} from '../store/reducers/nodes/types'; @@ -20,7 +19,7 @@ import type {ModifyDiskResponse} from '../types/api/modifyDisk'; import type {TNetInfo} from '../types/api/netInfo'; import type {TNodesInfo} from '../types/api/nodes'; import type {TEvNodesInfo} from '../types/api/nodesList'; -import type {TEvPDiskStateResponse, TPDiskInfoResponse} from '../types/api/pdisk'; +import type {EDecommitStatus, TEvPDiskStateResponse, TPDiskInfoResponse} from '../types/api/pdisk'; import type { Actions, ErrorResponse, @@ -599,24 +598,18 @@ export class YdbEmbeddedAPI extends AxiosWrapper { vDiskIdx: string | number; force?: boolean; }) { - const params = { - group_id: groupId, - group_generation_id: groupGeneration, - fail_realm_idx: failRealmIdx, - fail_domain_idx: failDomainIdx, - vdisk_idx: vDiskIdx, - - force, - }; - - const paramsString = qs.stringify(params); - - const path = this.getPath(`/vdisk/evict?${paramsString}`); - return this.post( - path, - {}, + this.getPath('/vdisk/evict'), {}, + { + group_id: groupId, + group_generation_id: groupGeneration, + fail_realm_idx: failRealmIdx, + fail_domain_idx: failDomainIdx, + vdisk_idx: vDiskIdx, + + force, + }, { requestConfig: {'axios-retry': {retries: 0}}, }, @@ -632,9 +625,39 @@ export class YdbEmbeddedAPI extends AxiosWrapper { force?: boolean; }) { return this.post( - this.getPath(`/pdisk/restart?node_id=${nodeId}&pdisk_id=${pDiskId}&force=${force}`), - {}, + this.getPath('/pdisk/restart'), {}, + { + node_id: nodeId, + pdisk_id: pDiskId, + force, + }, + { + requestConfig: {'axios-retry': {retries: 0}}, + }, + ); + } + changePDiskStatus({ + nodeId, + pDiskId, + force, + decommissionStatus, + }: { + nodeId: number | string; + pDiskId: number | string; + force?: boolean; + decommissionStatus?: EDecommitStatus; + }) { + return this.post( + this.getPath('/pdisk/status'), + { + decommit_status: decommissionStatus, + }, + { + node_id: nodeId, + pdisk_id: pDiskId, + force, + }, { requestConfig: {'axios-retry': {retries: 0}}, }, diff --git a/src/types/api/modifyDisk.ts b/src/types/api/modifyDisk.ts index e24e78f71..21ad58e0c 100644 --- a/src/types/api/modifyDisk.ts +++ b/src/types/api/modifyDisk.ts @@ -2,6 +2,7 @@ * endpoints: pdisk/restart and vdiks/evict */ export interface ModifyDiskResponse { + debugMessage?: string; // true if successful, false if not result?: boolean; // Error message diff --git a/src/types/api/pdisk.ts b/src/types/api/pdisk.ts index 18aadd534..99c639af2 100644 --- a/src/types/api/pdisk.ts +++ b/src/types/api/pdisk.ts @@ -131,9 +131,9 @@ type EDriveStatus = | 'FAULTY' // drive is expected to become BROKEN soon, new groups are not created, old groups are asynchronously moved out from this drive | 'TO_BE_REMOVED'; // same as INACTIVE, but drive is counted in fault model as not working -type EDecommitStatus = +export type EDecommitStatus = | 'DECOMMIT_UNSET' - | 'DECOMMIT_NONE' // no decomission + | 'DECOMMIT_NONE' // no decommission | 'DECOMMIT_PENDING' // drive is going to be removed soon, but SelfHeal logic would not remove it automatically | 'DECOMMIT_IMMINENT' // drive is going to be settled automatically | 'DECOMMIT_REJECTED'; // drive is working as usual, but decommitted slots are not placed here diff --git a/src/utils/index.ts b/src/utils/index.ts index 4d231399f..308dce79e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,3 +5,9 @@ export const getArray = (arrayLength: number) => { export function valueIsDefined(value: T | null | undefined): value is T { return value !== null && value !== undefined; } + +export async function wait(time: number) { + return new Promise((resolve) => { + setTimeout(resolve, time); + }); +}