diff --git a/web/src/app/api/jobs/jobs.ts b/web/src/app/api/jobs/jobs.ts index ecf7714..4e2ba8a 100644 --- a/web/src/app/api/jobs/jobs.ts +++ b/web/src/app/api/jobs/jobs.ts @@ -23,3 +23,10 @@ export const getJobStatus = async () => { const response = await fetch(`${API_URL}/job/statuses`) return await response.json() } + +export const cancelJob = async (id: string) => { + const response = await fetch(`${API_URL}/job/${id}/cancel`, { + method: 'POST', + }) + return await response.json() +} diff --git a/web/src/common/ClientLayout/ClientLayout.tsx b/web/src/common/ClientLayout/ClientLayout.tsx index 1eb5d65..58f8afe 100644 --- a/web/src/common/ClientLayout/ClientLayout.tsx +++ b/web/src/common/ClientLayout/ClientLayout.tsx @@ -33,9 +33,16 @@ const LeftNavContainer = dynamic( { ssr: false }, ) +const PatternToastContainer = dynamic( + () => import('@patterninc/react-ui').then((mod) => mod.PatternToastContainer), + { + ssr: false, + }, +) const ClientLayout = ({ children }: { children: ReactNode }) => { return ( +
diff --git a/web/src/modules/Jobs/Helper.tsx b/web/src/modules/Jobs/Helper.tsx index c47f205..cf6901f 100644 --- a/web/src/modules/Jobs/Helper.tsx +++ b/web/src/modules/Jobs/Helper.tsx @@ -30,6 +30,7 @@ export type JobType = { created_at: number updated_at: number status: string + is_sync: boolean command_criteria: string[] cluster_criteria: string[] command_id: string diff --git a/web/src/modules/Jobs/JobDetails/JobDetailsHeader.tsx b/web/src/modules/Jobs/JobDetails/JobDetailsHeader.tsx index d7ceb70..3128e18 100644 --- a/web/src/modules/Jobs/JobDetails/JobDetailsHeader.tsx +++ b/web/src/modules/Jobs/JobDetails/JobDetailsHeader.tsx @@ -1,14 +1,61 @@ 'use client' -import { Alert, PageHeader, SectionHeader } from '@patterninc/react-ui' +import { + Alert, + Ellipsis, + PageFooter, + PageHeader, + SectionHeader, + toast, +} from '@patterninc/react-ui' import { JobDataTypesProps } from '../Helper' import SyntaxHighlighter from 'react-syntax-highlighter' import { github } from 'react-syntax-highlighter/dist/esm/styles/hljs' import ApiResponseButton from '@/components/ApiResponseButton/ApiResponseButton' +import { cancelJob } from '@/app/api/jobs/jobs' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMemo } from 'react' + +const CANCELABLE_STATUSES = ['NEW', 'ACCEPTED', 'RUNNING'] const JobDetailsHeader = ({ jobData, }: JobDataTypesProps): React.JSX.Element => { + const queryClient = useQueryClient() + const cancelMutation = useMutation({ + mutationFn: (id: string) => cancelJob(id), + onSuccess: (response) => { + if (response.status === 'CANCELLING') { + toast({ + type: 'info', + message: `Job is being canceled...`, + }) + queryClient.invalidateQueries({ queryKey: ['job', jobData?.id] }) + } else { + toast({ + type: 'error', + message: response.error || 'Failed to cancel job', + }) + } + }, + onError: () => { + toast({ + type: 'error', + message: 'Failed to cancel job', + }) + }, + }) + + // Only async jobs in active states can be cancelled + const isCancelable = useMemo( + () => + CANCELABLE_STATUSES.includes(jobData?.status ?? '') && + jobData?.is_sync !== true, + [jobData?.status, jobData?.is_sync], + ) + + const isCancelling = jobData?.status === 'CANCELLING' + return (
} bottomSectionChildren={ -
+
{jobData?.status === 'FAILED' ? ( @@ -42,7 +87,9 @@ const JobDetailsHeader = ({
    - {jobData?.tags.map((value) =>
  • {value}
  • )} + {jobData?.tags.map((value) => ( +
  • {value}
  • + ))}
) : null} @@ -50,9 +97,9 @@ const JobDetailsHeader = ({ {jobData?.context?.query ? ( <> - - {jobData.context.query} - + + {jobData.context.query} + ) : null}
@@ -88,6 +135,32 @@ const JobDetailsHeader = ({ value: <>, }} /> + { + if (jobData?.id) cancelMutation.mutate(jobData.id) + }, + type: 'red', + }, + children: isCancelling ? ( + + Canceling job + + + ) : ( + 'Cancel Job' + ), + styleType: 'primary-red', + type: 'button', + disabled: !isCancelable || isCancelling, + }, + ]} + />
) }