Skip to content

Commit

Permalink
frontend CronJobDetails: Refactor and use patch for suspend and resume
Browse files Browse the repository at this point in the history
Signed-off-by: Oleksandr Dubenko <[email protected]>
  • Loading branch information
sniok committed Jan 14, 2025
1 parent 3fa16d5 commit 2c51759
Showing 1 changed file with 124 additions and 194 deletions.
318 changes: 124 additions & 194 deletions frontend/src/components/cronjob/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import {
InputLabel,
} from '@mui/material';
import _ from 'lodash';
import { useEffect, useState } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import { apply } from '../../lib/k8s/apiProxy';
import CronJob from '../../lib/k8s/cronJob';
import Job from '../../lib/k8s/job';
import { KubeObjectInterface } from '../../lib/k8s/KubeObject';
import { clusterAction } from '../../redux/clusterActionSlice';
import { AppDispatch } from '../../redux/stores/store';
import { ActionButton } from '../common';
Expand All @@ -26,68 +25,70 @@ import AuthVisible from '../common/Resource/AuthVisible';
import { JobsListRenderer } from '../job/List';
import { getLastScheduleTime, getSchedule } from './List';

function SpawnJobDialog(props: {
cronJob: CronJob;
applyFunc: (newItem: KubeObjectInterface) => Promise<KubeObjectInterface>;
openJobDialog: boolean;
setOpenJobDialog: (open: boolean) => void;
}) {
const { cronJob, openJobDialog, setOpenJobDialog, applyFunc } = props;
// method to generate a unique string
const uniqueString = () => {
const timestamp = Date.now().toString(36);
const randomNum = Math.random().toString(36).substr(2, 5);
return `${timestamp}-${randomNum}`;
};

function SpawnJobDialog(props: { cronJob: CronJob; onClose: () => void }) {
const { cronJob, onClose } = props;
const { namespace } = useParams<{ namespace: string }>();
const { t } = useTranslation(['translation']);
const dispatch: AppDispatch = useDispatch();

// method to generate a unique string
const uniqueString = () => {
const timestamp = Date.now().toString(36);
const randomNum = Math.random().toString(36).substr(2, 5);
return `${timestamp}-${randomNum}`;
};

const job = _.cloneDeep(cronJob.spec.jobTemplate);
const [jobName, setJobName] = useState(
`${cronJob?.metadata?.name}-manual-spawn-${uniqueString()}`
() => `${cronJob?.metadata?.name}-manual-spawn-${uniqueString()}`
);

// set all the fields that are assumed on the jobTemplate
job.kind = 'Job';
job.metadata = _.cloneDeep(job.metadata) || {};
job.metadata.namespace = namespace;
job.apiVersion = 'batch/v1';
job.metadata.name = jobName;
job.metadata.annotations = {
...job.metadata.annotations,
'cronjob.kubernetes.io/instantiate': 'manual',
};
if (!!cronJob.jsonData) {
job.metadata.ownerReferences = [
{
apiVersion: cronJob.jsonData.apiVersion,
blockOwnerDeletion: true,
controller: true,
kind: cronJob.jsonData.kind,
name: cronJob.metadata.name,
uid: cronJob.metadata.uid,
},
];
}

function handleClose() {
setOpenJobDialog(false);
function handleSpawn() {
const job = _.cloneDeep(cronJob.spec.jobTemplate);
// set all the fields that are assumed on the jobTemplate
job.kind = 'Job';
job.metadata = _.cloneDeep(job.metadata) || {};
job.metadata.namespace = namespace;
job.apiVersion = 'batch/v1';
job.metadata.name = jobName;
job.metadata.annotations = {
...job.metadata.annotations,
'cronjob.kubernetes.io/instantiate': 'manual',
};
if (!!cronJob.jsonData) {
job.metadata.ownerReferences = [
{
apiVersion: cronJob.jsonData.apiVersion,
blockOwnerDeletion: true,
controller: true,
kind: cronJob.jsonData.kind,
name: cronJob.metadata.name,
uid: cronJob.metadata.uid,
},
];
}
onClose();
dispatch(
clusterAction(() => apply(job), {
startMessage: t('translation|Spawning Job {{ newItemName }}…', {
newItemName: jobName,
}),
successMessage: t('translation|Job {{ newItemName }} spawned', {
newItemName: jobName,
}),
errorMessage: t('translation|Failed to spawn Job {{ newItemName }}', {
newItemName: jobName,
}),
})
);
}

return (
<Dialog
open={openJobDialog}
onClose={handleClose}
aria-labelledby="form-dialog-title"
maxWidth="sm"
>
<Dialog open onClose={onClose} aria-labelledby="form-dialog-title" maxWidth="sm">
<DialogTitle id="form-dialog-title">{t('translation|Spawn Job')}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('translation|This will trigger a new Job based on the CronJob {{ name }}', {
name,
name: cronJob.getName(),
})}
</DialogContentText>
<Box mb={1}>
Expand All @@ -105,28 +106,10 @@ function SpawnJobDialog(props: {
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
<Button onClick={onClose} color="primary">
{t('translation|Cancel')}
</Button>
<Button
onClick={() => {
handleClose();
dispatch(
clusterAction(() => applyFunc(job), {
startMessage: t('translation|Spawning Job {{ newItemName }}…', {
newItemName: job.metadata.name,
}),
successMessage: t('translation|Job {{ newItemName }} spawned', {
newItemName: job.metadata.name,
}),
errorMessage: t('translation|Failed to spawn Job {{ newItemName }}', {
newItemName: job.metadata.name,
}),
})
);
}}
color="primary"
>
<Button onClick={handleSpawn} color="primary">
{t('translation|Spawn')}
</Button>
</DialogActions>
Expand All @@ -137,138 +120,86 @@ function SpawnJobDialog(props: {
export default function CronJobDetails(props: { name?: string; namespace?: string }) {
const params = useParams<{ namespace: string; name: string }>();
const { name = params.name, namespace = params.namespace } = props;
const { t, i18n } = useTranslation('glossary');

const [jobs, jobsError] = Job.useList({ namespace });
const [cronJob, setCronJob] = useState<CronJob | null>(null);
const [isCronSuspended, setIsCronSuspended] = useState(false);
const [isCheckingCronSuspendStatus, setIsCheckingCronSuspendStatus] = useState(true);
const [openJobDialog, setOpenJobDialog] = useState(false);
const { t, i18n } = useTranslation('glossary');
const dispatch: AppDispatch = useDispatch();

useEffect(() => {
if (cronJob) {
setIsCronSuspended(cronJob.spec.suspend);
setIsCheckingCronSuspendStatus(false);
}
}, [cronJob]);

function filterOwnedJobs(jobs?: Job[] | null) {
if (!jobs) {
return null;
}

return jobs.filter(job => {
type OwnerRef = {
name: string;
kind: string;
};
return !!job.metadata?.ownerReferences?.find(
(ownerRef: OwnerRef) => ownerRef.kind === 'CronJob' && ownerRef.name === name
);
});
}

const ownedJobs = filterOwnedJobs(jobs);

function applyFunc(newItem: KubeObjectInterface): Promise<KubeObjectInterface> {
if (newItem.kind === 'CronJob') {
setIsCheckingCronSuspendStatus(true);
} else if (newItem.kind === 'Job') {
setOpenJobDialog(false);
}
const result = apply(newItem).finally(() => {
setIsCheckingCronSuspendStatus(false);
}) as unknown;

return result as Promise<KubeObjectInterface>;
}

function PauseResumeAction() {
if (!cronJob) {
return null;
}
return (
<ActionButton
description={isCronSuspended ? t('translation|Resume') : t('translation|Suspend')}
onClick={() => {
handleCron(cronJob, !isCronSuspended);
}}
icon={isCronSuspended ? 'mdi:play-circle' : 'mdi:pause-circle'}
iconButtonProps={{
disabled: isCheckingCronSuspendStatus,
}}
/>
);
}
const [jobs, jobsError] = Job.useList({ namespace });
const [cronJob] = CronJob.useGet(name, namespace);
const [isSpawnDialogOpen, setIsSpawnDialogOpen] = useState(false);
const [isPendingSuspend, setIsPendingSuspend] = useState(false);
const isCronSuspended = cronJob?.spec.suspend;

const ownedJobs = useMemo(
() =>
jobs?.filter(job =>
job.metadata.ownerReferences?.find(ref => ref.kind === 'CronJob' && ref.name === name)
) ?? [],
[jobs, name]
);

function handleCron(cronJob: CronJob, suspend: boolean) {
const clonedCronJob = _.cloneDeep(cronJob);
clonedCronJob.spec.suspend = suspend;
setIsCheckingCronSuspendStatus(true);
function applySuspend(cronJob: CronJob, suspend: boolean) {
setIsPendingSuspend(true);
dispatch(
clusterAction(() => applyFunc(clonedCronJob.jsonData), {
startMessage: suspend
? t('translation|Suspending CronJob {{ newItemName }}…', {
newItemName: clonedCronJob.metadata.name,
})
: t('translation|Resuming CronJob {{ newItemName }}…', {
newItemName: clonedCronJob.metadata.name,
}),
cancelledMessage: suspend
? t('translation|Cancelled suspending CronJob {{ newItemName }}.', {
newItemName: clonedCronJob.metadata.name,
})
: t('translation|Cancelled resuming CronJob {{ newItemName }}.', {
newItemName: clonedCronJob.metadata.name,
}),
successMessage: suspend
? t('translation|Suspended CronJob {{ newItemName }}.', {
newItemName: clonedCronJob.metadata.name,
})
: t('translation|Resumed CronJob {{ newItemName }}.', {
newItemName: clonedCronJob.metadata.name,
}),
errorMessage: suspend
? t('translation|Failed to suspend CronJob {{ newItemName }}.', {
newItemName: clonedCronJob.metadata.name,
})
: t('translation|Failed to resume CronJob {{ newItemName }}.', {
newItemName: clonedCronJob.metadata.name,
}),
})
clusterAction(
() => cronJob.patch({ spec: { suspend } }).finally(() => setIsPendingSuspend(false)),
{
cancelCallback: () => setIsPendingSuspend(false),
startMessage: suspend
? t('translation|Suspending CronJob {{ newItemName }}…', {
newItemName: cronJob.metadata.name,
})
: t('translation|Resuming CronJob {{ newItemName }}…', {
newItemName: cronJob.metadata.name,
}),
cancelledMessage: suspend
? t('translation|Cancelled suspending CronJob {{ newItemName }}.', {
newItemName: cronJob.metadata.name,
})
: t('translation|Cancelled resuming CronJob {{ newItemName }}.', {
newItemName: cronJob.metadata.name,
}),
successMessage: suspend
? t('translation|Suspended CronJob {{ newItemName }}.', {
newItemName: cronJob.metadata.name,
})
: t('translation|Resumed CronJob {{ newItemName }}.', {
newItemName: cronJob.metadata.name,
}),
errorMessage: suspend
? t('translation|Failed to suspend CronJob {{ newItemName }}.', {
newItemName: cronJob.metadata.name,
})
: t('translation|Failed to resume CronJob {{ newItemName }}.', {
newItemName: cronJob.metadata.name,
}),
}
)
);
}

const actions = [];

actions.push(
cronJob && (
<AuthVisible authVerb="create" item={Job} namespace={cronJob.getNamespace()}>
<ActionButton
description={t('translation|Spawn Job')}
onClick={() => {
setOpenJobDialog(true);
}}
icon="mdi:lightning-bolt-circle"
/>
{openJobDialog && (
<SpawnJobDialog
cronJob={cronJob}
openJobDialog={openJobDialog}
setOpenJobDialog={setOpenJobDialog}
applyFunc={applyFunc}
const actions = cronJob
? [
<AuthVisible authVerb="create" item={Job} namespace={cronJob.getNamespace()}>
<ActionButton
description={t('translation|Spawn Job')}
onClick={() => setIsSpawnDialogOpen(true)}
icon="mdi:lightning-bolt-circle"
/>
)}
</AuthVisible>
)
);

actions.push(
<AuthVisible authVerb="update" item={cronJob}>
<PauseResumeAction />
</AuthVisible>
);
{isSpawnDialogOpen && (
<SpawnJobDialog cronJob={cronJob} onClose={() => setIsSpawnDialogOpen(false)} />
)}
</AuthVisible>,
<AuthVisible authVerb="update" item={cronJob}>
<ActionButton
description={isCronSuspended ? t('translation|Resume') : t('translation|Suspend')}
onClick={() => applySuspend(cronJob, !isCronSuspended)}
icon={isCronSuspended ? 'mdi:play-circle' : 'mdi:pause-circle'}
iconButtonProps={{ disabled: isPendingSuspend }}
/>
</AuthVisible>,
]
: [];

return (
<DetailsGrid
Expand All @@ -277,7 +208,6 @@ export default function CronJobDetails(props: { name?: string; namespace?: strin
namespace={namespace}
withEvents
actions={actions}
onResourceUpdate={cronJob => setCronJob(cronJob)}
extraInfo={item =>
item && [
{
Expand Down

0 comments on commit 2c51759

Please sign in to comment.