packages/runtime-tools-process-enveloped-components/src/jobsManagement/envelope/components/JobsManagement/JobsManagement.tsx (396 lines of code) (raw):
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 React, { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "@patternfly/react-core/dist/js/components/Button";
import { Divider } from "@patternfly/react-core/dist/js/components/Divider";
import { ISortBy } from "@patternfly/react-table/dist/js/components/Table";
import { LoadMore } from "@kie-tools/runtime-tools-components/dist/components/LoadMore";
import { ServerErrors } from "@kie-tools/runtime-tools-components/dist/components/ServerErrors";
import {
KogitoEmptyState,
KogitoEmptyStateType,
} from "@kie-tools/runtime-tools-components/dist/components/KogitoEmptyState";
import { componentOuiaProps, OUIAProps } from "@kie-tools/runtime-tools-components/dist/ouiaTools/OuiaUtils";
import {
BulkCancel,
Job,
JobStatus,
JobsManagementState,
JobsSortBy,
} from "@kie-tools/runtime-tools-process-gateway-api/dist/types";
import {
BulkListItem,
BulkListType,
IOperationResults,
IOperations,
} from "@kie-tools/runtime-tools-components/dist/components/BulkList";
import { OperationType, OrderBy } from "@kie-tools/runtime-tools-shared-gateway-api/dist/types";
import { JobsManagementDriver } from "../../../api";
import JobsManagementTable from "../JobsManagementTable/JobsManagementTable";
import JobsManagementToolbar from "../JobsManagementToolbar/JobsManagementToolbar";
import "../styles.css";
import { setTitle } from "@kie-tools/runtime-tools-components/dist/utils/Utils";
import { JobsDetailsModal } from "../JobsDetailsModal";
import { JobsRescheduleModal } from "../JobsRescheduleModal";
import { JobsCancelModal } from "../JobsCancelModal";
import { useCancelableEffect } from "@kie-tools-core/react-hooks/dist/useCancelableEffect";
export const formatForBulkListJob = (jobsList: (Job & { errorMessage?: string })[]): BulkListItem[] => {
const formattedItems: BulkListItem[] = [];
jobsList.forEach((item: Job & { errorMessage?: string }) => {
const formattedObj: BulkListItem = {
id: item.id,
name: item.processId,
description: item.id,
errorMessage: item.errorMessage ? item.errorMessage : undefined,
};
formattedItems.push(formattedObj);
});
return formattedItems;
};
interface JobsManagementProps {
isEnvelopeConnectedToChannel: boolean;
driver: JobsManagementDriver;
initialState?: JobsManagementState;
}
const defaultPageSize: number = 10;
const defaultSortBy: ISortBy = { index: 6, direction: "asc" };
const JobsManagement: React.FC<JobsManagementProps & OUIAProps> = ({
ouiaId,
ouiaSafe,
driver,
isEnvelopeConnectedToChannel,
initialState,
}) => {
const defaultStatus: JobStatus[] = useMemo(
() => (initialState && initialState.filters ? [...initialState.filters] : [JobStatus.Scheduled]),
[initialState]
);
const defaultOrderBy: JobsSortBy = useMemo(
() =>
initialState && initialState.orderBy
? initialState.orderBy
: {
lastUpdate: OrderBy.DESC,
},
[initialState]
);
const [chips, setChips] = useState<JobStatus[]>(defaultStatus);
const [selectedStatus, setSelectedStatus] = useState<JobStatus[]>(defaultStatus);
const [selectedJobInstances, setSelectedJobInstances] = useState<Job[]>([]);
const [jobs, setJobs] = useState<Job[]>([]);
const [displayTable, setDisplayTable] = useState(true);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState("");
const [isActionPerformed, setIsActionPerformed] = useState<boolean>(false);
const [modalTitle, setModalTitle] = useState<JSX.Element>();
const [modalContent, setModalContent] = useState<string>("");
const [sortBy, setSortBy] = useState<ISortBy>(defaultSortBy);
const [orderBy, setOrderBy] = useState<JobsSortBy>(defaultOrderBy);
const [limit, setLimit] = useState<number>(defaultPageSize);
const [offset, setOffset] = useState<number>(0);
const [pageSize, setPageSize] = useState<number>(defaultPageSize);
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
const [isCancelModalOpen, setIsCancelModalOpen] = useState<boolean>(false);
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState<boolean>(false);
const [isRescheduleModalOpen, setIsRescheduleModalOpen] = useState<boolean>(false);
const [rescheduleError, setRescheduleError] = useState<string>("");
const [selectedJob, setSelectedJob] = useState<any>({});
const [jobOperationResults, setJobOperationResults] = useState<IOperationResults>({
CANCEL: {
successItems: [],
failedItems: [],
ignoredItems: [],
},
});
const [isInitialLoadDone, setIsInitialLoadDone] = useState(false);
const doQueryJobs = useCallback(
async (_offset: number, _limit: number) => {
try {
const jobsResponse: Job[] = await driver.query(_offset, _limit);
setLimit(jobsResponse.length);
setJobs((currentJobs) => {
if (_offset > 0 && currentJobs.length > 0) {
const tempData: Job[] = currentJobs.concat(jobsResponse);
return tempData;
} else {
return jobsResponse;
}
});
} catch (err) {
setError(err);
}
},
[driver]
);
const onRefresh = useCallback(async () => {
setIsLoading(true);
await driver.applyFilter(selectedStatus);
await driver.sortBy(orderBy);
setOffset(0);
await doQueryJobs(0, 10);
setIsLoading(false);
}, [doQueryJobs, driver, orderBy, selectedStatus]);
const onApplyFilter = useCallback(async () => {
setIsLoading(true);
await driver.applyFilter(selectedStatus);
await driver.sortBy(orderBy);
setChips(selectedStatus);
setOffset(0);
await doQueryJobs(0, 10);
setIsLoading(false);
}, [doQueryJobs, driver, orderBy, selectedStatus]);
useEffect(() => {
if (!isEnvelopeConnectedToChannel) {
setIsInitialLoadDone(false);
}
}, [isEnvelopeConnectedToChannel]);
useCancelableEffect(
useCallback(
({ canceled }) => {
if (isEnvelopeConnectedToChannel) {
setIsLoading(true);
setSelectedStatus(defaultStatus);
setChips(defaultStatus);
setOrderBy(defaultOrderBy);
if (canceled.get()) {
return;
}
driver
.initialLoad(defaultStatus, defaultOrderBy)
.then(() => {
if (canceled.get()) {
return;
}
return doQueryJobs(0, 10);
})
.then(() => {
if (canceled.get()) {
return;
}
setIsLoading(false);
setIsInitialLoadDone(true);
});
}
},
[defaultOrderBy, defaultStatus, doQueryJobs, driver, isEnvelopeConnectedToChannel]
)
);
const handleCancelModalToggle = useCallback((): void => {
setIsCancelModalOpen(!isCancelModalOpen);
}, [isCancelModalOpen]);
const handleCancelModalCloseToggle = useCallback(async () => {
setIsCancelModalOpen(!isCancelModalOpen);
setIsLoading(true);
await doQueryJobs(0, 10);
setIsLoading(false);
}, [doQueryJobs, isCancelModalOpen]);
const handleDetailsToggle = useCallback((): void => {
setIsDetailsModalOpen(!isDetailsModalOpen);
}, [isDetailsModalOpen]);
const handleRescheduleToggle = useCallback((): void => {
setIsRescheduleModalOpen(!isRescheduleModalOpen);
}, [isRescheduleModalOpen]);
const onGetMoreInstances = useCallback(
async (initVal: number, _pageSize: number): Promise<void> => {
setIsLoadingMore(true);
setOffset(initVal);
setPageSize(_pageSize);
await driver.initialLoad(selectedStatus, orderBy);
await doQueryJobs(initVal, _pageSize);
setIsLoadingMore(false);
},
[doQueryJobs, driver, orderBy, selectedStatus]
);
const handleBulkCancel = useCallback(
(cancelResults: BulkCancel, ignoredJobs: Job[]): void => {
setIsActionPerformed(true);
setModalTitle(setTitle("success", "Job Cancel"));
setModalContent("");
setJobOperationResults({
...jobOperationResults,
[OperationType.CANCEL]: {
...jobOperationResults[OperationType.CANCEL],
successItems: formatForBulkListJob(cancelResults.successJobs),
failedItems: formatForBulkListJob(cancelResults.failedJobs),
ignoredItems: formatForBulkListJob(ignoredJobs),
},
});
handleCancelModalToggle();
},
[handleCancelModalToggle, jobOperationResults]
);
const jobOperations: IOperations = useMemo(
() => ({
CANCEL: {
type: BulkListType.JOB,
results: jobOperationResults[OperationType.CANCEL],
messages: {
successMessage: "Canceled jobs: ",
noItemsMessage: "No jobs were canceled",
warningMessage:
"Note: The job status has been updated. The list may appear inconsistent until you refresh any applied filters.",
ignoredMessage: "These jobs were ignored because they were already canceled or executed.",
},
functions: {
perform: async () => {
const ignoredJobs: Job[] = [];
const remainingInstances = selectedJobInstances.filter((job) => {
if (job.status === JobStatus.Canceled || job.status === JobStatus.Executed) {
ignoredJobs.push(job);
} else {
return true;
}
});
const cancelResults = await driver.bulkCancel(remainingInstances);
handleBulkCancel(cancelResults, ignoredJobs);
},
},
},
}),
[driver, handleBulkCancel, jobOperationResults, selectedJobInstances]
);
const detailsAction: JSX.Element[] = useMemo(
() => [
<Button key="confirm-selection" variant="primary" onClick={handleDetailsToggle}>
OK
</Button>,
],
[handleDetailsToggle]
);
const rescheduleActions: JSX.Element[] = useMemo(
() => [
<Button key="cancel-reschedule" variant="secondary" onClick={handleRescheduleToggle}>
Cancel
</Button>,
],
[handleRescheduleToggle]
);
const onResetToDefault = useCallback(async () => {
const defaultState: any = {
filters: ["SCHEDULED"],
orderBy: { lastUpdate: "ASC" },
};
setSelectedStatus(defaultState.filters);
setChips(defaultState.filters);
setOrderBy(defaultState.orderBy), setDisplayTable(true);
setIsLoading(true);
await driver.initialLoad(defaultState.filters, defaultState.orderBy);
await doQueryJobs(0, 10);
setIsLoading(false);
}, [doQueryJobs, driver]);
const handleJobReschedule = useCallback(
async (job: Job, repeatInterval: string | number, repeatLimit: string | number, scheduleDate: Date) => {
const response = await driver.rescheduleJob(job, repeatInterval, repeatLimit, scheduleDate);
setIsLoading(true);
if (response && response.modalTitle === "success") {
handleRescheduleToggle();
await doQueryJobs(0, 10);
} else if (response && response.modalTitle === "failure") {
handleRescheduleToggle();
setRescheduleError(response.modalContent);
await doQueryJobs(0, 10);
}
setIsLoading(false);
},
[doQueryJobs, driver, handleRescheduleToggle]
);
return (
<div {...componentOuiaProps(ouiaId, "JobsManagementPage", ouiaSafe ? ouiaSafe : !isLoading)}>
{error.length === 0 ? (
<>
<JobsManagementToolbar
chips={chips}
onResetToDefault={onResetToDefault}
driver={driver}
doQueryJobs={doQueryJobs}
onApplyFilter={onApplyFilter}
jobOperations={jobOperations}
onRefresh={onRefresh}
selectedStatus={selectedStatus}
selectedJobInstances={selectedJobInstances}
setChips={setChips}
setDisplayTable={setDisplayTable}
setIsLoading={setIsLoading}
setSelectedJobInstances={setSelectedJobInstances}
setSelectedStatus={setSelectedStatus}
/>
<Divider />
{isInitialLoadDone && displayTable ? (
<JobsManagementTable
jobs={jobs}
driver={driver}
doQueryJobs={doQueryJobs}
handleCancelModalToggle={handleCancelModalToggle}
handleDetailsToggle={handleDetailsToggle}
handleRescheduleToggle={handleRescheduleToggle}
isActionPerformed={isActionPerformed}
isLoading={isLoadingMore ? false : isLoading}
setIsActionPerformed={setIsActionPerformed}
selectedJobInstances={selectedJobInstances}
setModalTitle={setModalTitle}
setModalContent={setModalContent}
setSelectedJobInstances={setSelectedJobInstances}
setSelectedJob={setSelectedJob}
setSortBy={setSortBy}
sortBy={sortBy}
setOrderBy={setOrderBy}
/>
) : (
<>
{selectedStatus.length === 0 && (
<div className="kogito-jobs-management__emptyState">
<KogitoEmptyState
type={KogitoEmptyStateType.Reset}
title="No filter applied."
body="Try applying at least one filter to see results"
onClick={() => onResetToDefault()}
/>
</div>
)}
</>
)}
{isInitialLoadDone && (!isLoading || isLoadingMore) && (limit === pageSize || isLoadingMore) && (
<LoadMore
offset={offset}
setOffset={setOffset}
getMoreItems={onGetMoreInstances}
pageSize={pageSize}
isLoadingMore={isLoadingMore}
/>
)}
</>
) : (
<ServerErrors error={error} variant="large" />
)}
{selectedJob && Object.keys(selectedJob).length > 0 && (
<JobsDetailsModal
actionType="Job Details"
modalTitle={setTitle("success", "Job Details")}
isModalOpen={isDetailsModalOpen}
handleModalToggle={handleDetailsToggle}
modalAction={detailsAction}
job={selectedJob}
/>
)}
{selectedJob && Object.keys(selectedJob).length > 0 && (
<JobsRescheduleModal
actionType="Job Reschedule"
isModalOpen={isRescheduleModalOpen}
handleModalToggle={handleRescheduleToggle}
modalAction={rescheduleActions}
job={selectedJob}
rescheduleError={rescheduleError}
setRescheduleError={setRescheduleError}
handleJobReschedule={handleJobReschedule}
/>
)}
<JobsCancelModal
actionType="Job Cancel"
isModalOpen={isCancelModalOpen}
handleModalToggle={handleCancelModalCloseToggle}
modalTitle={modalTitle ?? <></>}
modalContent={modalContent}
jobOperations={jobOperations[OperationType.CANCEL]}
/>
</div>
);
};
export default JobsManagement;