mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
ability to start jobs individually
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user