Files
qqq-frontend-material-dashb…/src/qqq/pages/process-run/index.tsx

559 lines
21 KiB
TypeScript

/**
=========================================================
* Material Dashboard 2 PRO React TS - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
Coded by www.creative-tim.com
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import {useEffect, useState} from "react";
// formik components
import {Formik, Form} from "formik";
// @mui material components
import Grid from "@mui/material/Grid";
import Card from "@mui/material/Card";
import Stepper from "@mui/material/Stepper";
import Step from "@mui/material/Step";
import StepLabel from "@mui/material/StepLabel";
// Material Dashboard 2 PRO React TS components
import MDBox from "components/MDBox";
import MDButton from "components/MDButton";
// Material Dashboard 2 PRO React TS examples components
import DashboardLayout from "examples/LayoutContainers/DashboardLayout";
import DashboardNavbar from "examples/Navbars/DashboardNavbar";
import Footer from "examples/Footer";
// ProcessRun layout schemas for form and form fields
import * as Yup from "yup";
import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
import {useLocation, useParams} from "react-router-dom";
import DynamicFormUtils from "qqq/components/QDynamicForm/utils/DynamicFormUtils";
import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobStarted";
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
import {
DataGridPro, GridColDef, GridRowParams, GridRowsProp,
} from "@mui/x-data-grid-pro";
import QDynamicForm from "../../components/QDynamicForm";
import MDTypography from "../../../components/MDTypography";
function getDynamicStepContent(
stepIndex: number,
step: any,
formData: any,
processError: string,
processValues: any,
recordConfig: any,
): JSX.Element
{
const {
formFields, values, errors, touched,
} = formData;
// console.log(`in getDynamicStepContent: step label ${step?.label}`);
if (processError)
{
return (
<>
<MDTypography color="error" variant="h3">
Error
</MDTypography>
<div>{processError}</div>
</>
);
}
if (step === null)
{
console.log("in getDynamicStepContent. No step yet, so returning 'loading'");
return <div>Loading...</div>;
}
console.log(`in getDynamicStepContent. the step looks like: ${JSON.stringify(step)}`);
return (
<>
{step.formFields && <QDynamicForm formData={formData} formLabel={step.label} />}
{step.viewFields && (
<div>
{step.viewFields.map((field: QFieldMetaData) => (
<div key={field.name}>
<b>
{field.label}
:
</b>
{" "}
{processValues[field.name]}
</div>
))}
</div>
)}
{step.recordListFields && (
<div>
<b>Records</b>
{" "}
<br />
<MDBox height="100%">
<DataGridPro
page={recordConfig.pageNo}
disableSelectionOnClick
autoHeight
rows={recordConfig.rows}
columns={recordConfig.columns}
rowBuffer={10}
rowCount={recordConfig.totalRecords}
pageSize={recordConfig.rowsPerPage}
rowsPerPageOptions={[10, 25, 50]}
onPageSizeChange={recordConfig.handleRowsPerPageChange}
onPageChange={recordConfig.handlePageChange}
onRowClick={recordConfig.handleRowClick}
paginationMode="server"
density="compact"
loading={recordConfig.loading}
/>
</MDBox>
</div>
)}
</>
);
}
function trace(name: string, isComponent: boolean = false)
{
if (isComponent)
{
console.log(`COMPONENT: ${name}`);
}
else
{
console.log(` function: ${name}`);
}
}
const qController = new QController("");
function ProcessRun(): JSX.Element
{
const {processName} = useParams();
const [processUUID, setProcessUUID] = useState(null as string);
const [jobUUID, setJobUUID] = useState(null as string);
const [activeStepIndex, setActiveStepIndex] = useState(0);
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
const [newStep, setNewStep] = useState(null);
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
const [needInitialLoad, setNeedInitialLoad] = useState(true);
const [processMetaData, setProcessMetaData] = useState(null);
const [processValues, setProcessValues] = useState({} as any);
const [lastProcessResponse, setLastProcessResponse] = useState(
null as QJobStarted | QJobComplete | QJobError | QJobRunning,
);
const [formId, setFormId] = useState("");
const [formFields, setFormFields] = useState({});
const [initialValues, setInitialValues] = useState({});
const [validationScheme, setValidationScheme] = useState(null);
const [validationFunction, setValidationFunction] = useState(null);
const [needToCheckJobStatus, setNeedToCheckJobStatus] = useState(false);
const [needRecords, setNeedRecords] = useState(false);
const [processError, setProcessError] = useState(null as string);
const [recordConfig, setRecordConfig] = useState({} as any);
const onLastStep = activeStepIndex === steps.length - 2;
const noMoreSteps = activeStepIndex === steps.length - 1;
trace("ProcessRun", true);
function buildNewRecordConfig()
{
const newRecordConfig = {} as any;
newRecordConfig.pageNo = 1;
newRecordConfig.rowsPerPage = 20;
newRecordConfig.columns = [] as GridColDef[];
newRecordConfig.rows = [];
newRecordConfig.totalRecords = 0;
newRecordConfig.handleRowsPerPageChange = null;
newRecordConfig.handlePageChange = null;
newRecordConfig.handleRowClick = null;
newRecordConfig.loading = true;
return (newRecordConfig);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle moving to another step in the process - e.g., after the backend told us what screen to show next. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
trace("updateActiveStep");
if (!processMetaData)
{
console.log("No process meta data yet, so returning early");
return;
}
// console.log(`Steps are: ${steps}`);
// console.log(`Setting step to ${newStep}`);
let newIndex = null;
if (typeof newStep === "number")
{
newIndex = newStep as number;
}
else if (typeof newStep === "string")
{
for (let i = 0; i < steps.length; i++)
{
if (steps[i].name === newStep)
{
newIndex = i;
break;
}
}
}
if (newIndex === null)
{
setProcessError(`Unknown process step ${newStep}.`);
}
setActiveStepIndex(newIndex);
if (steps)
{
const activeStep = steps[newIndex];
setActiveStep(activeStep);
setFormId(activeStep.name);
///////////////////////////////////////////////////
// if this step has form fields, set up the form //
///////////////////////////////////////////////////
if (activeStep.formFields)
{
const {dynamicFormFields, formValidations} = DynamicFormUtils.getFormData(
activeStep.formFields,
);
const initialValues: any = {};
activeStep.formFields.forEach((field) =>
{
initialValues[field.name] = processValues[field.name];
});
setFormFields(dynamicFormFields);
setInitialValues(initialValues);
setValidationScheme(Yup.object().shape(formValidations));
setValidationFunction(null);
}
else
{
/////////////////////////////////////////////////////////////////////////
// if there are no form fields, set a null validationScheme (Yup), and //
// instead use a validation function that always says true. //
/////////////////////////////////////////////////////////////////////////
setValidationScheme(null);
setValidationFunction(() => true);
}
////////////////////////////////////////////////////////////////////////////////////////////
// if there are fields to load, build a record config, and set the needRecords state flag //
////////////////////////////////////////////////////////////////////////////////////////////
if (activeStep.recordListFields)
{
const newRecordConfig = buildNewRecordConfig();
activeStep.recordListFields.forEach((field) =>
{
newRecordConfig.columns.push({field: field.name, headerName: field.label, width: 200});
});
setRecordConfig(newRecordConfig);
setNeedRecords(true);
}
}
}, [newStep]);
// when we need to load records, do so, async
useEffect(() =>
{
if (needRecords)
{
setNeedRecords(false);
(async () =>
{
const records = await qController.processRecords(
processName,
processUUID,
recordConfig.rowsPerPage * (recordConfig.pageNo - 1),
recordConfig.rowsPerPage,
);
/////////////////////////////////////////////////////////////////////////////////////////
// re-construct the recordConfig object, so the setState call triggers a new rendering //
/////////////////////////////////////////////////////////////////////////////////////////
const newRecordConfig = buildNewRecordConfig();
newRecordConfig.loading = false;
newRecordConfig.columns = recordConfig.columns;
newRecordConfig.rows = [];
let rowId = 0;
records.forEach((record) =>
{
const row = Object.fromEntries(record.values.entries());
if (!row.id)
{
row.id = ++rowId;
}
newRecordConfig.rows.push(row);
});
// todo count?
newRecordConfig.totalRecords = records.length;
setRecordConfig(newRecordConfig);
})();
}
}, [needRecords]);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle a response from the server - e.g., after starting a backend job, or getting its status/result //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
if (lastProcessResponse)
{
trace("handleProcessResponse");
setLastProcessResponse(null);
if (lastProcessResponse instanceof QJobComplete)
{
const qJobComplete = lastProcessResponse as QJobComplete;
console.log("Setting new step.");
setNewStep(qJobComplete.nextStep);
setProcessValues(qJobComplete.values);
// console.log(`Updated process values: ${JSON.stringify(qJobComplete.values)}`);
}
else if (lastProcessResponse instanceof QJobStarted)
{
const qJobStarted = lastProcessResponse as QJobStarted;
setJobUUID(qJobStarted.jobUUID);
setNeedToCheckJobStatus(true);
}
else if (lastProcessResponse instanceof QJobRunning)
{
const qJobRunning = lastProcessResponse as QJobRunning;
setNeedToCheckJobStatus(true);
}
else if (lastProcessResponse instanceof QJobError)
{
const qJobError = lastProcessResponse as QJobError;
console.log(`Got an error from the backend... ${qJobError.error}`);
setProcessError(qJobError.error);
}
}
}, [lastProcessResponse]);
/////////////////////////////////////////////////////////////////////////
// while a backend async job is running, periodically check its status //
/////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
if (needToCheckJobStatus)
{
trace("checkJobStatus");
setNeedToCheckJobStatus(false);
(async () =>
{
setTimeout(async () =>
{
const processResponse = await qController.processJobStatus(
processName,
processUUID,
jobUUID,
);
setLastProcessResponse(processResponse);
}, 1500);
})();
}
}, [needToCheckJobStatus]);
//////////////////////////////////////////////////////////////////////////////////////////
// do the initial load of data for the process - that is, meta data, plus the init step //
//////////////////////////////////////////////////////////////////////////////////////////
if (needInitialLoad)
{
trace("initialLoad");
setNeedInitialLoad(false);
(async () =>
{
const {search} = useLocation();
const urlSearchParams = new URLSearchParams(search);
let queryStringForInit = null;
if (urlSearchParams.get("recordIds"))
{
queryStringForInit = `recordsParam=recordIds&recordIds=${urlSearchParams.get(
"recordIds",
)}`;
}
else if (urlSearchParams.get("filterJSON"))
{
queryStringForInit = `recordsParam=filterJSON&filterJSON=${urlSearchParams.get(
"filterJSON",
)}`;
}
// todo once saved filters exist
//else if(urlSearchParams.get("filterId")) {
// queryStringForInit = `recordsParam=filterId&filterId=${urlSearchParams.get("filterId")}`
// }
console.log(`@dk: Query String for init: ${queryStringForInit}`);
const processMetaData = await qController.loadProcessMetaData(processName);
// console.log(processMetaData);
setProcessMetaData(processMetaData);
setSteps(processMetaData.frontendSteps);
const processResponse = await qController.processInit(processName, queryStringForInit);
setProcessUUID(processResponse.processUUID);
setLastProcessResponse(processResponse);
// console.log(processResponse);
})();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// handle the back button - todo - not really done at all //
// e.g., qqq needs to say when back is or isn't allowed, and we need to hit the backend upon backs. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
const handleBack = () =>
{
trace("handleBack");
setNewStep(activeStepIndex - 1);
};
//////////////////////////////////////////////////////////////////////////////////////////
// handle user submitting the form - which in qqq means moving forward from any screen. //
//////////////////////////////////////////////////////////////////////////////////////////
const handleSubmit = async (values: any, actions: any) =>
{
trace("handleSubmit");
// todo - post?
let queryString = "";
Object.keys(values).forEach((key) =>
{
queryString += `${key}=${encodeURIComponent(values[key])}&`;
});
actions.setSubmitting(false);
actions.resetForm();
const processResponse = await qController.processStep(
processName,
processUUID,
activeStep.name,
queryString,
);
setLastProcessResponse(processResponse);
};
return (
<DashboardLayout>
<DashboardNavbar />
<MDBox py={3} mb={20} height="65vh">
<Grid
container
justifyContent="center"
alignItems="center"
sx={{height: "100%", mt: 8}}
>
<Grid item xs={12} lg={8}>
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={validationScheme}
validation={validationFunction}
onSubmit={handleSubmit}
>
{({
values, errors, touched, isSubmitting,
}) => (
<Form id={formId} autoComplete="off">
<Card sx={{height: "100%"}}>
<MDBox mx={2} mt={-3}>
<Stepper activeStep={activeStepIndex} alternativeLabel>
{steps.map((step) => (
<Step key={step.name}>
<StepLabel>{step.label}</StepLabel>
</Step>
))}
</Stepper>
</MDBox>
<MDBox p={3}>
<MDBox>
{/***************************************************************************
** step content - e.g., the appropriate form or other screen for the step **
***************************************************************************/}
{getDynamicStepContent(
activeStepIndex,
activeStep,
{
values,
touched,
formFields,
errors,
},
processError,
processValues,
recordConfig,
)}
{/********************************
** back &| next/submit buttons **
********************************/}
<MDBox
mt={2}
width="100%"
display="flex"
justifyContent="space-between"
>
{true || activeStepIndex === 0 ? (
<MDBox />
) : (
<MDButton
variant="gradient"
color="light"
onClick={handleBack}
>
back
</MDButton>
)}
{noMoreSteps || processError ? (
<MDBox />
) : (
<MDButton
disabled={isSubmitting}
type="submit"
variant="gradient"
color="dark"
>
{onLastStep ? "submit" : "next"}
</MDButton>
)}
</MDBox>
</MDBox>
</MDBox>
</Card>
</Form>
)}
</Formik>
</Grid>
</Grid>
</MDBox>
<Footer />
</DashboardLayout>
);
}
export default ProcessRun;