diff --git a/kueue/locales/en/translation.json b/kueue/locales/en/translation.json
new file mode 100644
index 0000000000..c2166f7312
--- /dev/null
+++ b/kueue/locales/en/translation.json
@@ -0,0 +1,19 @@
+{
+ "Failed to update queue stop policy.": "Failed to update queue stop policy.",
+ "Queue Maintenance & Stop Policy": "Queue Maintenance & Stop Policy",
+ "Current State": "Current State",
+ "Clear Stop Policy (None)": "Clear Stop Policy (None)",
+ "Pause Admissions (Hold)": "Pause Admissions (Hold)",
+ "Drain Queue (HoldAndDrain)": "Drain Queue (HoldAndDrain)",
+ "Confirm Queue Drainage": "Confirm Queue Drainage",
+ "Are you sure you want to set {{queueType}} \"{{name}}\" to HoldAndDrain? This will block new admissions, evict admitted workloads, and cancel reservations for Reserving workloads.": "Are you sure you want to set {{queueType}} \"{{name}}\" to HoldAndDrain? This will block new admissions, evict admitted workloads, and cancel reservations for Reserving workloads.",
+ "Cancel": "Cancel",
+ "Confirm & Drain": "Confirm & Drain",
+ "Drain (HoldAndDrain)": "Drain (HoldAndDrain)",
+ "Paused (Hold)": "Paused (Hold)",
+ "No maintenance stop policy (None)": "No maintenance stop policy (None)",
+ "Queue is in Drain mode: new admissions are blocked, admitted workloads are evicted, and reservations for Reserving workloads are cancelled.": "Queue is in Drain mode: new admissions are blocked, admitted workloads are evicted, and reservations for Reserving workloads are cancelled.",
+ "Queue is in Pause mode: new admissions are blocked, reservations for Reserving workloads are cancelled, and in-flight workloads continue running.": "Queue is in Pause mode: new admissions are blocked, reservations for Reserving workloads are cancelled, and in-flight workloads continue running.",
+ "No maintenance stop policy is applied to this queue.": "No maintenance stop policy is applied to this queue.",
+ "Custom stop policy: {{policy}}": "Custom stop policy: {{policy}}"
+}
diff --git a/kueue/package.json b/kueue/package.json
index 341946f782..989865c91c 100644
--- a/kueue/package.json
+++ b/kueue/package.json
@@ -38,5 +38,10 @@
"overrides": {
"ws": "8.21.0",
"vitest": "^3.2.6"
+ },
+ "headlamp": {
+ "i18n": [
+ "en"
+ ]
}
}
diff --git a/kueue/src/components/clusterqueues/Detail.tsx b/kueue/src/components/clusterqueues/Detail.tsx
index 5b35b5dcaf..69f26d051d 100644
--- a/kueue/src/components/clusterqueues/Detail.tsx
+++ b/kueue/src/components/clusterqueues/Detail.tsx
@@ -13,25 +13,26 @@ import {
renderCohortLink,
renderResourceFlavorLink,
} from '../common/KueueResourceLinks';
+import QueueMaintenanceControl from '../common/QueueMaintenanceControl';
import { RelatedLocalQueuesSection, RelatedWorkloadsSection } from '../common/RelatedResources';
-/** Flattened row rendered for status flavor reservations or flavor usage. */
+/** Flattened row rendered in the ClusterQueue flavor reservations and usage tables. */
interface FlavorUsageRow {
- /** ResourceFlavor name reported by ClusterQueue status. */
+ /** ResourceFlavor name associated with this reservation or usage. */
flavor: string;
- /** Resource name reported under the flavor. */
+ /** Resource name within the flavor, for example cpu or memory. */
resource: string;
- /** Total reserved or used quantity for the resource. */
+ /** Total quota used or reserved by this resource and flavor pair. */
total?: string | number;
- /** Quantity currently borrowed from the cohort. */
+ /** Optional quota borrowed from the cohort for this resource. */
borrowed?: string | number;
}
-/** Admission check row from spec.admissionChecksStrategy. */
+/** Flattened row rendered in the ClusterQueue admission checks table. */
interface AdmissionCheckRow {
- /** AdmissionCheck resource name. */
+ /** AdmissionCheck name configured on the ClusterQueue. */
name: string;
- /** Flavor names this AdmissionCheck applies to; empty means all flavors. */
+ /** Specific ResourceFlavor names this check applies to, or all if empty. */
flavors: string[];
}
@@ -66,6 +67,14 @@ function getAdmissionCheckRows(clusterQueue: ClusterQueue): AdmissionCheckRow[]
);
}
+/** Build the queue maintenance control section. */
+function getMaintenanceControlSection(clusterQueue: ClusterQueue) {
+ return {
+ id: 'maintenance-controls',
+ section: ,
+ };
+}
+
/** Build the extra detail section that shows spec.resourceGroups as a table. */
function getResourceGroupsSection(clusterQueue: ClusterQueue) {
const rows = getResourceGroupRows(clusterQueue.resourceGroups);
@@ -285,6 +294,7 @@ export default function ClusterQueueDetail() {
extraSections={clusterQueue =>
clusterQueue
? [
+ getMaintenanceControlSection(clusterQueue),
getConditionsSection(clusterQueue),
getResourceGroupsSection(clusterQueue),
getAdmissionChecksSection(clusterQueue),
diff --git a/kueue/src/components/common/KueueResourceLinks.tsx b/kueue/src/components/common/KueueResourceLinks.tsx
index af095fb6b4..6e22bb4706 100644
--- a/kueue/src/components/common/KueueResourceLinks.tsx
+++ b/kueue/src/components/common/KueueResourceLinks.tsx
@@ -17,7 +17,7 @@ export function renderAdmissionCheckLink(admissionCheckName?: string) {
/** Render a Cohort reference as a detail-page link when present. */
export function renderCohortLink(cohortName?: string) {
- if (!cohortName) {
+ if (!cohortName || cohortName === '-') {
return '-';
}
diff --git a/kueue/src/components/common/QueueMaintenanceControl.tsx b/kueue/src/components/common/QueueMaintenanceControl.tsx
new file mode 100644
index 0000000000..9a62c96703
--- /dev/null
+++ b/kueue/src/components/common/QueueMaintenanceControl.tsx
@@ -0,0 +1,173 @@
+import { useTranslation } from '@kinvolk/headlamp-plugin/lib';
+import { AuthVisible, SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common';
+import type { KubeObject } from '@kinvolk/headlamp-plugin/lib/k8s/cluster';
+import {
+ Alert,
+ Box,
+ Button,
+ Chip,
+ CircularProgress,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ Paper,
+ Stack,
+ Typography,
+} from '@mui/material';
+import React, { useState } from 'react';
+import type { StopPolicy } from '../../resources/clusterQueue';
+import {
+ createStopPolicyPatch,
+ getStopPolicyColor,
+ getStopPolicyDescription,
+ getStopPolicyLabel,
+} from '../../resources/queueControlFormatters';
+
+export type QueueResource = KubeObject & {
+ stopPolicy?: string;
+};
+
+interface QueueMaintenanceControlProps {
+ queue: QueueResource;
+ queueType: 'ClusterQueue' | 'LocalQueue';
+}
+
+export default function QueueMaintenanceControl({
+ queue,
+ queueType,
+}: QueueMaintenanceControlProps) {
+ const { t } = useTranslation();
+ const rawPolicy = queue.stopPolicy;
+ const currentPolicy: StopPolicy =
+ !rawPolicy || rawPolicy === '-' || rawPolicy === 'None' ? 'None' : (rawPolicy as StopPolicy);
+ const [pendingPolicy, setPendingPolicy] = useState(null);
+ const [error, setError] = useState(null);
+ const [confirmDrainOpen, setConfirmDrainOpen] = useState(false);
+
+ const isLoading = pendingPolicy !== null;
+ const currentLabel = getStopPolicyLabel(currentPolicy, t);
+ const currentDescription = getStopPolicyDescription(currentPolicy, t);
+
+ const applyPolicy = async (targetPolicy: StopPolicy) => {
+ setPendingPolicy(targetPolicy);
+ setError(null);
+ try {
+ await queue.patch(createStopPolicyPatch(targetPolicy));
+ } catch (err: any) {
+ setError(err?.message || t('Failed to update queue stop policy.'));
+ } finally {
+ setPendingPolicy(null);
+ setConfirmDrainOpen(false);
+ }
+ };
+
+ return (
+
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+
+
+
+
+ {t('Current State')}:
+
+
+
+
+ {currentDescription}
+
+
+
+
+
+ {currentPolicy !== 'None' && (
+
+ )}
+
+ {currentPolicy !== 'Hold' && (
+
+ )}
+
+ {currentPolicy !== 'HoldAndDrain' && (
+
+ )}
+
+
+
+
+ {/* Confirmation modal for draining queue */}
+
+
+
+ );
+}
diff --git a/kueue/src/components/localqueues/Detail.tsx b/kueue/src/components/localqueues/Detail.tsx
index 7ac0ccda9b..3848b91ef0 100644
--- a/kueue/src/components/localqueues/Detail.tsx
+++ b/kueue/src/components/localqueues/Detail.tsx
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
import { LocalQueue } from '../../resources/localQueue';
import KueueAdminResourceAccess from '../common/KueueAdminResourceAccess';
import { renderClusterQueueLink } from '../common/KueueResourceLinks';
+import QueueMaintenanceControl from '../common/QueueMaintenanceControl';
import { RelatedWorkloadsSection } from '../common/RelatedResources';
/** Build the standard Headlamp conditions section for LocalQueue status. */
@@ -17,6 +18,14 @@ function getConditionsSection(localQueue: LocalQueue) {
};
}
+/** Build the queue maintenance control section for LocalQueue. */
+function getMaintenanceControlSection(localQueue: LocalQueue) {
+ return {
+ id: 'maintenance-controls',
+ section: ,
+ };
+}
+
export default function LocalQueueDetail() {
const { namespace, name } = useParams<{ namespace: string; name: string }>();
@@ -65,6 +74,7 @@ export default function LocalQueueDetail() {
extraSections={localQueue =>
localQueue
? [
+ getMaintenanceControlSection(localQueue),
getConditionsSection(localQueue),
{
id: 'related-workloads',
diff --git a/kueue/src/resources/queueControlFormatters.test.ts b/kueue/src/resources/queueControlFormatters.test.ts
new file mode 100644
index 0000000000..5bc4051f9f
--- /dev/null
+++ b/kueue/src/resources/queueControlFormatters.test.ts
@@ -0,0 +1,84 @@
+import { describe, expect, it } from 'vitest';
+import {
+ createStopPolicyPatch,
+ getStopPolicyColor,
+ getStopPolicyDescription,
+ getStopPolicyLabel,
+} from './queueControlFormatters';
+
+describe('queueControlFormatters', () => {
+ describe('getStopPolicyColor', () => {
+ it('returns error for HoldAndDrain', () => {
+ expect(getStopPolicyColor('HoldAndDrain')).toBe('error');
+ });
+
+ it('returns warning for Hold', () => {
+ expect(getStopPolicyColor('Hold')).toBe('warning');
+ });
+
+ it('returns success for None, empty, dash, or undefined', () => {
+ expect(getStopPolicyColor('None')).toBe('success');
+ expect(getStopPolicyColor('')).toBe('success');
+ expect(getStopPolicyColor('-')).toBe('success');
+ expect(getStopPolicyColor(undefined)).toBe('success');
+ });
+
+ it('returns default for unknown string', () => {
+ expect(getStopPolicyColor('CustomPolicy')).toBe('default');
+ });
+ });
+
+ describe('getStopPolicyLabel', () => {
+ it('returns correct label for standard policies', () => {
+ expect(getStopPolicyLabel('HoldAndDrain')).toBe('Drain (HoldAndDrain)');
+ expect(getStopPolicyLabel('Hold')).toBe('Paused (Hold)');
+ expect(getStopPolicyLabel('None')).toBe('No maintenance stop policy (None)');
+ expect(getStopPolicyLabel('-')).toBe('No maintenance stop policy (None)');
+ expect(getStopPolicyLabel(undefined)).toBe('No maintenance stop policy (None)');
+ });
+
+ it('supports localization function', () => {
+ const mockT = (key: string) => `[translated] ${key}`;
+ expect(getStopPolicyLabel('HoldAndDrain', mockT)).toBe('[translated] Drain (HoldAndDrain)');
+ expect(getStopPolicyLabel('Hold', mockT)).toBe('[translated] Paused (Hold)');
+ expect(getStopPolicyLabel('None', mockT)).toBe(
+ '[translated] No maintenance stop policy (None)'
+ );
+ });
+ });
+
+ describe('getStopPolicyDescription', () => {
+ it('returns explanatory description for each policy state', () => {
+ expect(getStopPolicyDescription('HoldAndDrain')).toContain('Drain mode');
+ expect(getStopPolicyDescription('HoldAndDrain')).toContain('admitted workloads are evicted');
+ expect(getStopPolicyDescription('HoldAndDrain')).toContain(
+ 'reservations for Reserving workloads are cancelled'
+ );
+ expect(getStopPolicyDescription('Hold')).toContain('Pause mode');
+ expect(getStopPolicyDescription('Hold')).toContain(
+ 'reservations for Reserving workloads are cancelled'
+ );
+ expect(getStopPolicyDescription('None')).toContain('No maintenance stop policy');
+ expect(getStopPolicyDescription('-')).toContain('No maintenance stop policy');
+ expect(getStopPolicyDescription(undefined)).toContain('No maintenance stop policy');
+ });
+
+ it('supports localization function', () => {
+ const mockT = (key: string) => `[translated] ${key}`;
+ expect(getStopPolicyDescription('HoldAndDrain', mockT)).toContain('[translated]');
+ expect(getStopPolicyDescription('Hold', mockT)).toContain('[translated]');
+ expect(getStopPolicyDescription('None', mockT)).toContain('[translated]');
+ });
+ });
+
+ describe('createStopPolicyPatch', () => {
+ it('creates standard spec patch', () => {
+ expect(createStopPolicyPatch('Hold')).toEqual({
+ spec: { stopPolicy: 'Hold' },
+ });
+ expect(createStopPolicyPatch('None')).toEqual({
+ spec: { stopPolicy: 'None' },
+ });
+ });
+ });
+});
diff --git a/kueue/src/resources/queueControlFormatters.ts b/kueue/src/resources/queueControlFormatters.ts
new file mode 100644
index 0000000000..cc2f283473
--- /dev/null
+++ b/kueue/src/resources/queueControlFormatters.ts
@@ -0,0 +1,89 @@
+import type { StopPolicy } from './clusterQueue';
+
+export type StopPolicyColor = 'success' | 'warning' | 'error' | 'default';
+
+/**
+ * Returns a theme-friendly color representing the queue's maintenance state.
+ */
+export function getStopPolicyColor(stopPolicy?: StopPolicy | string): StopPolicyColor {
+ switch (stopPolicy) {
+ case 'HoldAndDrain':
+ return 'error';
+ case 'Hold':
+ return 'warning';
+ case 'None':
+ case undefined:
+ case '':
+ case '-':
+ return 'success';
+ default:
+ return 'default';
+ }
+}
+
+/**
+ * Returns a human-readable title for the StopPolicy state, optionally localized.
+ */
+export function getStopPolicyLabel(
+ stopPolicy?: StopPolicy | string,
+ t?: (key: string) => string
+): string {
+ switch (stopPolicy) {
+ case 'HoldAndDrain':
+ return t ? t('Drain (HoldAndDrain)') : 'Drain (HoldAndDrain)';
+ case 'Hold':
+ return t ? t('Paused (Hold)') : 'Paused (Hold)';
+ case 'None':
+ case undefined:
+ case '':
+ case '-':
+ return t ? t('No maintenance stop policy (None)') : 'No maintenance stop policy (None)';
+ default:
+ return stopPolicy;
+ }
+}
+
+/**
+ * Returns a detailed operational description explaining the impact of the policy, optionally localized.
+ */
+export function getStopPolicyDescription(
+ stopPolicy?: StopPolicy | string,
+ t?: (key: string, options?: Record) => string
+): string {
+ switch (stopPolicy) {
+ case 'HoldAndDrain':
+ return t
+ ? t(
+ 'Queue is in Drain mode: new admissions are blocked, admitted workloads are evicted, and reservations for Reserving workloads are cancelled.'
+ )
+ : 'Queue is in Drain mode: new admissions are blocked, admitted workloads are evicted, and reservations for Reserving workloads are cancelled.';
+ case 'Hold':
+ return t
+ ? t(
+ 'Queue is in Pause mode: new admissions are blocked, reservations for Reserving workloads are cancelled, and in-flight workloads continue running.'
+ )
+ : 'Queue is in Pause mode: new admissions are blocked, reservations for Reserving workloads are cancelled, and in-flight workloads continue running.';
+ case 'None':
+ case undefined:
+ case '':
+ case '-':
+ return t
+ ? t('No maintenance stop policy is applied to this queue.')
+ : 'No maintenance stop policy is applied to this queue.';
+ default:
+ return t
+ ? t('Custom stop policy: {{policy}}', { policy: stopPolicy })
+ : `Custom stop policy: ${stopPolicy}`;
+ }
+}
+
+/**
+ * Returns the target patch object for updating the queue's stop policy.
+ */
+export function createStopPolicyPatch(stopPolicy: StopPolicy) {
+ return {
+ spec: {
+ stopPolicy,
+ },
+ };
+}