Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions kueue/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -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}}"
}
5 changes: 5 additions & 0 deletions kueue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,10 @@
"overrides": {
"ws": "8.21.0",
"vitest": "^3.2.6"
},
"headlamp": {
"i18n": [
"en"
]
}
}
26 changes: 18 additions & 8 deletions kueue/src/components/clusterqueues/Detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

Expand Down Expand Up @@ -66,6 +67,14 @@ function getAdmissionCheckRows(clusterQueue: ClusterQueue): AdmissionCheckRow[]
);
}

/** Build the queue maintenance control section. */
function getMaintenanceControlSection(clusterQueue: ClusterQueue) {
return {
id: 'maintenance-controls',
section: <QueueMaintenanceControl queue={clusterQueue} queueType="ClusterQueue" />,
};
}

/** Build the extra detail section that shows spec.resourceGroups as a table. */
function getResourceGroupsSection(clusterQueue: ClusterQueue) {
const rows = getResourceGroupRows(clusterQueue.resourceGroups);
Expand Down Expand Up @@ -285,6 +294,7 @@ export default function ClusterQueueDetail() {
extraSections={clusterQueue =>
clusterQueue
? [
getMaintenanceControlSection(clusterQueue),
getConditionsSection(clusterQueue),
getResourceGroupsSection(clusterQueue),
getAdmissionChecksSection(clusterQueue),
Expand Down
2 changes: 1 addition & 1 deletion kueue/src/components/common/KueueResourceLinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 '-';
}

Expand Down
173 changes: 173 additions & 0 deletions kueue/src/components/common/QueueMaintenanceControl.tsx
Original file line number Diff line number Diff line change
@@ -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<StopPolicy | null>(null);
const [error, setError] = useState<string | null>(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 (
<SectionBox title={t('Queue Maintenance & Stop Policy')}>
<Paper variant="outlined" sx={{ p: 2.5 }}>
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
{error}
</Alert>
)}

<Box
display="flex"
justifyContent="space-between"
alignItems="center"
flexWrap="wrap"
gap={2}
>
<Box>
<Box display="flex" alignItems="center" gap={1.5} mb={1}>
<Typography variant="subtitle1" fontWeight="bold">
{t('Current State')}:
</Typography>
<Chip
label={currentLabel}
color={getStopPolicyColor(currentPolicy)}
size="small"
variant="filled"
/>
</Box>
<Typography variant="body2" color="text.secondary">
{currentDescription}
</Typography>
</Box>

<AuthVisible item={queue} authVerb="patch" namespace={queue.metadata?.namespace}>
<Stack direction="row" spacing={1.5} alignItems="center">
{currentPolicy !== 'None' && (
<Button
variant="outlined"
color="success"
size="small"
disabled={isLoading}
onClick={() => applyPolicy('None')}
>
{pendingPolicy === 'None' ? <CircularProgress size={16} sx={{ mr: 1 }} /> : null}
{t('Clear Stop Policy (None)')}
</Button>
)}

{currentPolicy !== 'Hold' && (
<Button
variant="outlined"
color="warning"
size="small"
disabled={isLoading}
onClick={() => applyPolicy('Hold')}
>
{pendingPolicy === 'Hold' ? <CircularProgress size={16} sx={{ mr: 1 }} /> : null}
{t('Pause Admissions (Hold)')}
</Button>
)}

{currentPolicy !== 'HoldAndDrain' && (
<Button
variant="outlined"
color="error"
size="small"
disabled={isLoading}
onClick={() => setConfirmDrainOpen(true)}
>
{t('Drain Queue (HoldAndDrain)')}
</Button>
)}
</Stack>
</AuthVisible>
</Box>

{/* Confirmation modal for draining queue */}
<Dialog open={confirmDrainOpen} onClose={() => setConfirmDrainOpen(false)}>
<DialogTitle>{t('Confirm Queue Drainage')}</DialogTitle>
<DialogContent>
<DialogContentText>
{t(
'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.',
{ queueType, name: queue.metadata?.name || '' }
)}
Comment thread
abhayrajjais01 marked this conversation as resolved.
Resolved
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfirmDrainOpen(false)} disabled={isLoading}>
{t('Cancel')}
</Button>
<Button
onClick={() => applyPolicy('HoldAndDrain')}
color="error"
variant="contained"
disabled={isLoading}
>
{pendingPolicy === 'HoldAndDrain' ? (
<CircularProgress size={16} sx={{ mr: 1, color: 'inherit' }} />
) : null}
{t('Confirm & Drain')}
</Button>
</DialogActions>
</Dialog>
</Paper>
</SectionBox>
);
}
10 changes: 10 additions & 0 deletions kueue/src/components/localqueues/Detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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: <QueueMaintenanceControl queue={localQueue} queueType="LocalQueue" />,
};
}

export default function LocalQueueDetail() {
const { namespace, name } = useParams<{ namespace: string; name: string }>();

Expand Down Expand Up @@ -65,6 +74,7 @@ export default function LocalQueueDetail() {
extraSections={localQueue =>
localQueue
? [
getMaintenanceControlSection(localQueue),
getConditionsSection(localQueue),
{
id: 'related-workloads',
Expand Down
84 changes: 84 additions & 0 deletions kueue/src/resources/queueControlFormatters.test.ts
Original file line number Diff line number Diff line change
@@ -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' },
});
});
});
});
Loading
Loading