Skip to content

Commit

Permalink
Merge pull request #535 from devtron-labs/feat/extend-node-bulk-actions
Browse files Browse the repository at this point in the history
feat: extend node bulk actions (cordon, un-cordon & drain) & other fixes
  • Loading branch information
Elessar1802 authored Jan 27, 2025
2 parents 95c18e1 + 3596708 commit b210aac
Show file tree
Hide file tree
Showing 16 changed files with 262 additions and 53 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devtron-labs/devtron-fe-common-lib",
"version": "1.5.9",
"version": "1.5.10",
"description": "Supporting common component library",
"type": "module",
"main": "dist/index.js",
Expand Down
8 changes: 8 additions & 0 deletions src/Assets/Icon/ic-medium-clean-brush.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-medium-pause.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/Assets/Icon/ic-medium-play.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
87 changes: 87 additions & 0 deletions src/Pages/ResourceBrowser/NodeDrainOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Checkbox } from '@Common/Checkbox'
import { Tooltip } from '@Common/Tooltip'
import { CHECKBOX_VALUE } from '@Common/Types'
import { ReactComponent as ICTimer } from '@Icons/ic-timer.svg'
import { ChangeEvent, FocusEvent } from 'react'
import { DRAIN_NODE_MODAL_MESSAGING, NODE_DRAIN_OPTIONS_CHECKBOX_CONFIG } from './constants'
import { AdditionalConfirmationModalOptionsProps, NodeDrainRequest } from './types'

const NodeDrainOptions = ({
optionsData,
setOptionsData: setNodeDrainOptions,
children,
}: AdditionalConfirmationModalOptionsProps<NodeDrainRequest['nodeDrainOptions']>) => {
const nodeDrainOptions: NodeDrainRequest['nodeDrainOptions'] = optionsData ?? {
gracePeriodSeconds: -1,
deleteEmptyDirData: false,
disableEviction: false,
force: false,
ignoreAllDaemonSets: false,
}

const handleGracePeriodOnChange = (e: ChangeEvent<HTMLInputElement>) => {
setNodeDrainOptions({
...nodeDrainOptions,
gracePeriodSeconds: e.target.value ? Number(e.target.value) : -1,
})
}

const handleGracePeriodOnBlur = (e: FocusEvent<HTMLInputElement>) => {
if (!e.target.value || Number(e.target.value) < -1) {
e.target.value = '-1'
}
}

const getCheckboxOnChangeHandler =
(key: keyof NodeDrainRequest['nodeDrainOptions']) => (e: ChangeEvent<HTMLInputElement>) => {
setNodeDrainOptions({
...nodeDrainOptions,
[key]: e.target.checked,
})
}

return (
<div className="flexbox-col dc__gap-12 w-100">
<div>
<div className="flexbox dc__gap-8 dc__align-items-center px-8 py-2">
<ICTimer className="icon-dim-20 dc__no-shrink scn-7" />
<Tooltip content={DRAIN_NODE_MODAL_MESSAGING.GracePeriod.infoText} alwaysShowTippyOnHover>
<span className="fs-13 cn-9 lh-20 dc__underline-dotted">Grace period</span>
</Tooltip>
<span className="flex left dc__border br-4 cn-9 fw-4 fs-13 lh-20 dc__overflow-hidden">
<input
name="grace-period"
type="number"
autoComplete="off"
min={-1}
defaultValue={nodeDrainOptions.gracePeriodSeconds}
className="px-8 py-4 lh-20 w-60 dc__no-border"
onChange={handleGracePeriodOnChange}
onBlur={handleGracePeriodOnBlur}
/>
<span className="flex px-8 py-4 dc__border--left">sec</span>
</span>
</div>

{NODE_DRAIN_OPTIONS_CHECKBOX_CONFIG.map(({ key, infoText, label }) => (
<Checkbox
key={key}
value={CHECKBOX_VALUE.CHECKED}
isChecked={nodeDrainOptions[key]}
dataTestId="disable-eviction"
rootClassName="mt-0 mb-0 ml-8 mr-8 cn-9 fs-13 py-6 px-8 form__checkbox__root--gap-8"
onChange={getCheckboxOnChangeHandler(key)}
>
<Tooltip content={infoText} alwaysShowTippyOnHover>
<span className="dc__underline-dotted">{label}</span>
</Tooltip>
</Checkbox>
))}
</div>

{children}
</div>
)
}

export default NodeDrainOptions
8 changes: 7 additions & 1 deletion src/Pages/ResourceBrowser/ResourceBrowser.Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface K8sResourceListPayloadType {
}

export type K8sResourceDetailDataType = {
[key: string]: string | number | object
[key: string]: string | number | object | boolean
}

export interface K8sResourceDetailType {
Expand All @@ -71,11 +71,17 @@ export interface BulkSelectionActionWidgetProps {
count: number
handleOpenBulkDeleteModal: () => void
handleClearBulkSelection: () => void
handleOpenCordonNodeModal: () => void
handleOpenUncordonNodeModal: () => void
handleOpenDrainNodeModal: () => void
handleOpenRestartWorkloadModal: () => void
parentRef: RefObject<HTMLDivElement>
showBulkRestartOption: boolean
showNodeListingOptions: boolean
}

export type RBBulkOperationType = 'restart' | 'delete' | 'cordon' | 'uncordon' | 'drain'

export interface CreateResourceRequestBodyType {
appId: string
clusterId: number
Expand Down
78 changes: 78 additions & 0 deletions src/Pages/ResourceBrowser/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,84 @@
import { SelectPickerOptionType } from '@Shared/Components'
import { ReactComponent as ICMediumPlay } from '@Icons/ic-medium-play.svg'
import { ReactComponent as ICMediumPause } from '@Icons/ic-medium-pause.svg'
import { ReactComponent as ICCleanBrush } from '@Icons/ic-medium-clean-brush.svg'
import { NodeDrainRequest } from './types'

export const ALL_NAMESPACE_OPTION: Readonly<Pick<SelectPickerOptionType<string>, 'value' | 'label'>> = {
value: 'all',
label: 'All namespaces',
}

export const DRAIN_NODE_MODAL_MESSAGING = {
DrainIcon: ICCleanBrush,
GracePeriod: {
heading: 'Grace period',
infoText:
'Period of time in seconds given to each pod to terminate gracefully. If negative, the default value specified in the pod will be used.',
},
DeleteEmptyDirectoryData: {
heading: 'Delete empty directory data',
infoText: 'Enabling this field will delete the pods using empty directory data when the node is drained.',
},
DisableEviction: {
heading: 'Disable eviction (use with caution)',
infoText: `Enabling this field will force drain to use delete, even if eviction is supported. This will bypass checking PodDisruptionBudgets.
Note: Make sure to use with caution.`,
},
ForceDrain: {
heading: 'Force drain',
infoText:
'Enabling this field will force drain a node even if there are pods that do not declare a controller.',
},
IgnoreDaemonSets: {
heading: 'Ignore DaemonSets',
infoText: 'Enabling this field will ignore DaemonSet-managed pods.',
},
Actions: {
infoText: 'Drain will cordon off the node and evict all pods of the node.',
drain: 'Drain',
draining: 'Draining node',
cancel: 'Cancel',
},
}

export const CORDON_NODE_MODAL_MESSAGING = {
UncordonIcon: ICMediumPlay,
CordonIcon: ICMediumPause,
cordonInfoText:
'Cordoning a node will mark it as unschedulable. By cordoning a node, you can be sure that no new pods will be scheduled on the node.',
uncordonInfoText:
'Uncordoning this node will mark this node as schedulable. By uncordoning a node, you will allow pods to be scheduled on this node.',
cordon: 'Cordon',
uncordon: 'Uncordon',
cordoning: 'Cordoning node',
uncordoning: 'Uncordoning node',
cancel: 'Cancel',
}

export const NODE_DRAIN_OPTIONS_CHECKBOX_CONFIG: {
key: Exclude<keyof NodeDrainRequest['nodeDrainOptions'], 'gracePeriodSeconds'>
infoText: string
label: string
}[] = [
{
key: 'deleteEmptyDirData',
infoText: DRAIN_NODE_MODAL_MESSAGING.DeleteEmptyDirectoryData.infoText,
label: DRAIN_NODE_MODAL_MESSAGING.DeleteEmptyDirectoryData.heading,
},
{
key: 'disableEviction',
infoText: DRAIN_NODE_MODAL_MESSAGING.DisableEviction.infoText,
label: DRAIN_NODE_MODAL_MESSAGING.DisableEviction.heading,
},
{
key: 'force',
infoText: DRAIN_NODE_MODAL_MESSAGING.ForceDrain.infoText,
label: DRAIN_NODE_MODAL_MESSAGING.ForceDrain.heading,
},
{
key: 'ignoreAllDaemonSets',
infoText: DRAIN_NODE_MODAL_MESSAGING.IgnoreDaemonSets.infoText,
label: DRAIN_NODE_MODAL_MESSAGING.IgnoreDaemonSets.heading,
},
] as const
1 change: 1 addition & 0 deletions src/Pages/ResourceBrowser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from './Helper'
export * from './constants'
export * from './types'
export * from './service'
export { default as NodeDrainOptions } from './NodeDrainOptions'
14 changes: 12 additions & 2 deletions src/Pages/ResourceBrowser/service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { get, post, trash } from '@Common/Api'
import { get, post, put, trash } from '@Common/Api'
import { ROUTES } from '@Common/Constants'
import { APIOptions, ResponseType } from '@Common/Types'
import {
Expand All @@ -10,7 +10,7 @@ import {
ResourceListPayloadType,
ResourceType,
} from './ResourceBrowser.Types'
import { ClusterDetail } from './types'
import { ClusterDetail, NodeCordonRequest } from './types'

export const getK8sResourceList = (
resourceListPayload: K8sResourceListPayloadType,
Expand All @@ -34,4 +34,14 @@ export const deleteNodeCapacity = (
abortControllerRef?: APIOptions['abortControllerRef'],
): Promise<ResponseType> => trash(ROUTES.NODE_CAPACITY, requestPayload, { abortControllerRef })

export const cordonNodeCapacity = (
requestPayload: NodeCordonRequest,
abortControllerRef?: APIOptions['abortControllerRef'],
): Promise<ResponseType> => put(`${ROUTES.NODE_CAPACITY}/cordon`, requestPayload, { abortControllerRef })

export const drainNodeCapacity = (
requestPayload: NodeActionRequest,
abortControllerRef?: APIOptions['abortControllerRef'],
): Promise<ResponseType> => put(`${ROUTES.NODE_CAPACITY}/drain`, requestPayload, { abortControllerRef })

export const getClusterListRaw = () => get<ClusterDetail[]>(ROUTES.CLUSTER_LIST_RAW)
29 changes: 29 additions & 0 deletions src/Pages/ResourceBrowser/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Dispatch, SetStateAction, ReactElement } from 'react'
import { NodeActionRequest } from './ResourceBrowser.Types'

export enum ClusterFiltersType {
ALL_CLUSTERS = 'all',
HEALTHY = 'healthy',
Expand Down Expand Up @@ -53,3 +56,29 @@ export interface ClusterDetail extends ClusterCapacityType {
nodeNames?: string[]
isVirtualCluster?: boolean
}

interface NodeCordonOptions {
unschedulableDesired: boolean
}

export interface NodeCordonRequest extends NodeActionRequest {
nodeCordonOptions: NodeCordonOptions
}

interface NodeDrainOptions {
gracePeriodSeconds: number
deleteEmptyDirData: boolean
disableEviction: boolean
force: boolean
ignoreAllDaemonSets: boolean
}

export interface NodeDrainRequest extends NodeActionRequest {
nodeDrainOptions: NodeDrainOptions
}

export interface AdditionalConfirmationModalOptionsProps<T = unknown> {
optionsData: T
setOptionsData: Dispatch<SetStateAction<T>>
children?: ReactElement
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,13 @@ const BulkOperations = ({
primaryButtonConfig: {
...confirmationModalConfig.buttonConfig.primaryButtonConfig,
onClick: handleBulkOperations,
disabled: apiCallInProgress,
disabled: apiCallInProgress || confirmationModalConfig.buttonConfig.primaryButtonConfig.disabled,
isLoading: apiCallInProgress,
},
secondaryButtonConfig: {
...confirmationModalConfig.buttonConfig.secondaryButtonConfig,
onClick: handleModalClose,
disabled: apiCallInProgress,
disabled: apiCallInProgress || confirmationModalConfig.buttonConfig.secondaryButtonConfig.disabled,
},
}}
/>
Expand Down
Loading

0 comments on commit b210aac

Please sign in to comment.