ability to start jobs individually

This commit is contained in:
orangecoding
2025-12-18 19:16:28 +01:00
parent 05f1bc61c9
commit 5dc976c7e3
17 changed files with 727 additions and 138 deletions

View File

@@ -6,7 +6,7 @@
import React from 'react';
import { Button, Empty, Table, Switch, Popover } from '@douyinfe/semi-ui';
import { IconAlertTriangle, IconDelete, IconDescend2, IconEdit } from '@douyinfe/semi-icons';
import { IconAlertTriangle, IconDelete, IconDescend2, IconEdit, IconPlayCircle } from '@douyinfe/semi-icons';
import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
import './JobTable.less';
@@ -21,7 +21,14 @@ const empty = (
const getPopoverContent = (text) => <article className="jobPopoverContent">{text}</article>;
export default function JobTable({ jobs = {}, onJobRemoval, onJobStatusChanged, onJobEdit, onListingRemoval } = {}) {
export default function JobTable({
jobs = {},
onJobRemoval,
onJobStatusChanged,
onJobEdit,
onListingRemoval,
onJobRun,
} = {}) {
return (
<Table
pagination={false}
@@ -91,6 +98,14 @@ export default function JobTable({ jobs = {}, onJobRemoval, onJobStatusChanged,
render: (_, job) => {
return (
<div className="interactions">
<Popover content={getPopoverContent('Run Job')}>
<Button
type="primary"
icon={<IconPlayCircle />}
disabled={job.isOnlyShared || job.running}
onClick={() => onJobRun && onJobRun(job.id)}
/>
</Popover>
<Popover content={getPopoverContent('Edit a Job')}>
<Button
type="secondary"

View File

@@ -100,6 +100,14 @@ export const useFredyState = create(
console.error(`Error while trying to get resource for api/jobs. Error:`, Exception);
}
},
setJobRunning(jobId, running) {
if (!jobId) return;
set((state) => {
const list = state.jobs.jobs || [];
const updated = list.map((j) => (j.id === jobId ? { ...j, running: !!running } : j));
return { jobs: { ...state.jobs, jobs: Object.freeze(updated) } };
});
},
},
user: {
async getUsers() {

View File

@@ -7,7 +7,7 @@ import React from 'react';
import JobTable from '../../components/table/JobTable';
import { useSelector, useActions } from '../../services/state/store';
import { xhrDelete, xhrPut } from '../../services/xhr';
import { xhrDelete, xhrPut, xhrPost } from '../../services/xhr';
import { useNavigate } from 'react-router-dom';
import { Button, Toast } from '@douyinfe/semi-ui';
import { IconPlusCircle } from '@douyinfe/semi-icons';
@@ -17,6 +17,47 @@ export default function Jobs() {
const jobs = useSelector((state) => state.jobs.jobs);
const navigate = useNavigate();
const actions = useActions();
const pendingJobIdRef = React.useRef(null);
const evtSourceRef = React.useRef(null);
// SSE connection for live job status updates
React.useEffect(() => {
// establish SSE connection
const src = new EventSource('/api/jobs/events');
evtSourceRef.current = src;
const onJobStatus = (e) => {
try {
const data = JSON.parse(e.data || '{}');
if (data && data.jobId) {
actions.jobs.setJobRunning(data.jobId, !!data.running);
// notify finish if it was triggered by this view
if (pendingJobIdRef.current === data.jobId && data.running === false) {
Toast.success('Job finished');
pendingJobIdRef.current = null;
}
}
} catch {
// ignore malformed events
}
};
src.addEventListener('jobStatus', onJobStatus);
src.onerror = () => {
// Let browser auto-reconnect; optionally log
};
return () => {
try {
src.removeEventListener('jobStatus', onJobStatus);
src.close();
} catch {
//noop
}
evtSourceRef.current = null;
pendingJobIdRef.current = null;
};
}, [actions.jobs]);
const onJobRemoval = async (jobId) => {
try {
@@ -48,6 +89,31 @@ export default function Jobs() {
}
};
const onJobRun = async (jobId) => {
try {
const response = await xhrPost(`/api/jobs/${jobId}/run`);
if (response.status === 202) {
Toast.success('Job run started');
} else {
Toast.info('Job run requested');
}
// remember so we can show a finish toast when SSE says it's done
pendingJobIdRef.current = jobId;
// optional: one initial refresh in case SSE arrives late
await actions.jobs.getJobs();
} catch (error) {
if (error?.status === 409) {
Toast.warning(error?.json?.message || 'Job is already running');
} else if (error?.status === 403) {
Toast.error('You are not allowed to run this job');
} else if (error?.status === 404) {
Toast.error('Job not found');
} else {
Toast.error('Failed to trigger job');
}
}
};
return (
<div>
<div>
@@ -66,6 +132,7 @@ export default function Jobs() {
onJobRemoval={onJobRemoval}
onListingRemoval={onListingRemoval}
onJobStatusChanged={onJobStatusChanged}
onJobRun={onJobRun}
onJobEdit={(jobId) => navigate(`/jobs/edit/${jobId}`)}
/>
</div>