diff --git a/apps/cohort-request-api/src/main.ts b/apps/cohort-request-api/src/main.ts index df2d8b6..40b2677 100644 --- a/apps/cohort-request-api/src/main.ts +++ b/apps/cohort-request-api/src/main.ts @@ -94,6 +94,22 @@ app.get(`${API_ROOT}/job-detailed`, async (req, res) => { res.send(chain([response]).flatten().compact().value()); }); +app.get(`${API_ROOT}/terminate-job`, async (req, res) => { + const jobId = req.query['jobId'] as string; + const response = isEmpty(jobId) + ? undefined + : await requestTracker.terminateJobById(jobId).catch(() => undefined); + res.send(chain([response]).flatten().compact().value()); +}); + +app.get(`${API_ROOT}/recover-job`, async (req, res) => { + const jobId = req.query['jobId'] as string; + const response = isEmpty(jobId) + ? undefined + : await requestTracker.recoverJobById(jobId).catch(() => undefined); + res.send(chain([response]).flatten().compact().value()); +}); + app.listen(port, host, () => { console.log(`[ ready ] http://${host}:${port}`); }); diff --git a/apps/cohort-request-frontend/src/app/request-tracker/RequestTracker.tsx b/apps/cohort-request-frontend/src/app/request-tracker/RequestTracker.tsx index bc98145..e7e7f70 100644 --- a/apps/cohort-request-frontend/src/app/request-tracker/RequestTracker.tsx +++ b/apps/cohort-request-frontend/src/app/request-tracker/RequestTracker.tsx @@ -1,12 +1,16 @@ -import React, { useEffect, useState } from 'react'; -import { Container } from 'react-bootstrap'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Button, Container } from 'react-bootstrap'; +import { useSearchParams } from 'react-router-dom'; import { AgGridReact } from 'ag-grid-react'; import 'ag-grid-community/styles/ag-grid.css'; // Core CSS import 'ag-grid-community/styles/ag-theme-quartz.css'; // Theme +import { faTrashCan } from '@fortawesome/free-solid-svg-icons'; import { EnhancedJob, fetchJobsDetailed, + terminateJob, } from '@cbioportal-cohort-request/cohort-request-utils'; +import IconWithTooltip from '../icon-with-tooltip/IconWithTooltip'; import { dateFormatter, DefaultColumnDefinition, @@ -20,14 +24,51 @@ import { export interface RequestTrackerProps {} export function RequestTracker(props: RequestTrackerProps) { + const [searchParams] = useSearchParams(); + const handleJobFetch = useCallback( + (result: { data: EnhancedJob[] }) => { + // filter out terminated jobs + const data = searchParams.get('debug') + ? result.data + : result.data.filter((d) => !d.terminationTimestamp); + setRowData(data); + }, + [searchParams] + ); + useEffect(() => { - fetchJobsDetailed().then((result) => { - setRowData(result.data); - }); - }, []); + fetchJobsDetailed().then(handleJobFetch); + }, [handleJobFetch]); const [rowData, setRowData] = useState([]); + const DeleteJob = (props: { value?: number; data?: EnhancedJob }) => { + const handleDelete = () => { + if (props?.data?.jobId) { + terminateJob({ jobId: props?.data?.jobId }).then((result) => { + if (result.data.length > 0) { + // fetch jobs again to refresh the table (to remove deleted jobs) + fetchJobsDetailed().then(handleJobFetch); + } + }); + } + }; + + // TODO prompt "Are you sure?" before deleting + return ( + + ); + }; + const colDefs = [ { field: 'jobId', cellRenderer: JobIdColumn }, DefaultColumnDefinition.Status, @@ -44,6 +85,11 @@ export function RequestTracker(props: RequestTrackerProps) { DefaultColumnDefinition.Users, DefaultColumnDefinition.AdditionalData, { field: 'events', cellRenderer: EventColumn }, + { + headerName: 'Delete', + field: 'terminationTimestamp', + cellRenderer: DeleteJob, + }, ]; return ( diff --git a/libs/cohort-request-node-utils/src/lib/cohort-request-tracker.ts b/libs/cohort-request-node-utils/src/lib/cohort-request-tracker.ts index 1fccddc..d6493e8 100644 --- a/libs/cohort-request-node-utils/src/lib/cohort-request-tracker.ts +++ b/libs/cohort-request-node-utils/src/lib/cohort-request-tracker.ts @@ -218,6 +218,40 @@ export class CohortRequestTracker { return fetchJobById(jobId, this.jobDB); } + public terminateJobById(jobId: string): Promise { + return this.fetchJobById(jobId).then((job) => { + // if already terminated just return the job + if (job.terminationTimestamp) { + return job; + } + // if not terminated yet, terminate the job and update the DB + job.terminationTimestamp = Date.now(); + return insertJob(job, this.jobDB) + .then(() => job) + .catch((error) => { + console.log(JSON.stringify(error)); + return undefined; + }); + }); + } + + public recoverJobById(jobId: string): Promise { + return this.fetchJobById(jobId).then((job) => { + // if not terminated yet just return the job + if (!job.terminationTimestamp) { + return job; + } + // remove termination stamp so that the job can be recovered + job.terminationTimestamp = undefined; + return insertJob(job, this.jobDB) + .then(() => job) + .catch((error) => { + console.log(JSON.stringify(error)); + return undefined; + }); + }); + } + public fetchAllJobsDetailed(): Promise { return Promise.all([this.fetchAllEvents(), this.fetchAllJobs()]).then( (response) => { diff --git a/libs/cohort-request-utils/src/lib/cohort-request-api-client.ts b/libs/cohort-request-utils/src/lib/cohort-request-api-client.ts index d6655fc..58b8482 100644 --- a/libs/cohort-request-utils/src/lib/cohort-request-api-client.ts +++ b/libs/cohort-request-utils/src/lib/cohort-request-api-client.ts @@ -31,3 +31,15 @@ export async function fetchJobsDetailed( ): Promise> { return axios.get('/api/job-detailed', { params }); } + +export async function terminateJob( + params?: Dictionary +): Promise> { + return axios.get('/api/terminate-job', { params }); +} + +export async function recoverJob( + params?: Dictionary +): Promise> { + return axios.get('/api/recover-job', { params }); +} diff --git a/libs/cohort-request-utils/src/lib/cohort-request-model.ts b/libs/cohort-request-utils/src/lib/cohort-request-model.ts index f547e58..7a0a343 100644 --- a/libs/cohort-request-utils/src/lib/cohort-request-model.ts +++ b/libs/cohort-request-utils/src/lib/cohort-request-model.ts @@ -44,6 +44,7 @@ export interface Job { filename: string; size: number; }[]; + terminationTimestamp?: number; } export interface Event {