@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
170 lines (169 loc) • 8.93 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import Input from '@mui/material/Input';
import InputLabel from '@mui/material/InputLabel';
import _ from 'lodash';
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 { clusterAction } from '../../redux/clusterActionSlice';
import { ActionButton } from '../common';
import { DetailsGrid } from '../common/Resource';
import AuthVisible from '../common/Resource/AuthVisible';
import { JobsListRenderer } from '../job/List';
import { getLastScheduleTime, getSchedule } from './List';
// 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 maxNameLength = 63;
function SpawnJobDialog(props) {
const { cronJob, onClose } = props;
const { namespace } = useParams();
const { t } = useTranslation(['translation']);
const dispatch = useDispatch();
const suffix = `-manual-spawn-${uniqueString()}`;
const [jobName, setJobName] = useState(() => `${cronJob?.metadata?.name.substring(0, maxNameLength - suffix.length)}${suffix}`);
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 (_jsxs(Dialog, { open: true, onClose: onClose, "aria-labelledby": "form-dialog-title", maxWidth: "sm", children: [_jsx(DialogTitle, { id: "form-dialog-title", children: t('translation|Spawn Job') }), _jsxs(DialogContent, { children: [_jsx(DialogContentText, { children: t('translation|This will trigger a new Job based on the CronJob {{ name }}', {
name: cronJob.getName(),
}) }), _jsx(Box, { mb: 1, children: _jsx(InputLabel, { htmlFor: "name", children: t('translation|Job Name') }) }), _jsx(Input, { margin: "dense", id: "name", type: "text", fullWidth: true, value: jobName, onChange: e => {
setJobName(e.target.value);
} })] }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: onClose, color: "primary", children: t('translation|Cancel') }), _jsx(Button, { onClick: handleSpawn, color: "primary", children: t('translation|Spawn') })] })] }));
}
export default function CronJobDetails(props) {
const params = useParams();
const { name = params.name, namespace = params.namespace, cluster } = props;
const { t, i18n } = useTranslation('glossary');
const dispatch = useDispatch();
const [cronJob] = CronJob.useGet(name, namespace);
const { items: jobs, errors } = Job.useList({ namespace, cluster: cronJob?.cluster });
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 applySuspend(cronJob, suspend) {
setIsPendingSuspend(true);
dispatch(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 = cronJob
? [
_jsxs(AuthVisible, { authVerb: "create", item: Job, namespace: cronJob.getNamespace(), children: [_jsx(ActionButton, { description: t('translation|Spawn Job'), onClick: () => setIsSpawnDialogOpen(true), icon: "mdi:lightning-bolt-circle" }), isSpawnDialogOpen && (_jsx(SpawnJobDialog, { cronJob: cronJob, onClose: () => setIsSpawnDialogOpen(false) }))] }),
_jsx(AuthVisible, { authVerb: "update", item: cronJob, children: _jsx(ActionButton, { description: isCronSuspended ? t('translation|Resume') : t('translation|Suspend'), onClick: () => applySuspend(cronJob, !isCronSuspended), icon: isCronSuspended ? 'mdi:play-circle' : 'mdi:pause-circle', iconButtonProps: { disabled: isPendingSuspend } }) }),
]
: [];
return (_jsx(DetailsGrid, { resourceType: CronJob, name: name, namespace: namespace, cluster: cluster, withEvents: true, actions: actions, extraInfo: item => item && [
{
name: t('Schedule'),
value: getSchedule(item, i18n.language),
},
{
name: t('translation|Suspend'),
value: item.spec.suspend.toString(),
},
{
name: t('Starting deadline'),
value: `${item.spec.startingDeadlineSeconds}s`,
hide: !item.spec.startingDeadlineSeconds,
},
{
name: t('Last Schedule'),
value: getLastScheduleTime(item),
},
], extraSections: cronJob => cronJob && [
_jsx(JobsListRenderer, { jobs: ownedJobs, errors: errors, hideColumns: ['namespace'], noNamespaceFilter: true }),
] }));
}