Initial version of run-report

This commit is contained in:
2022-09-20 13:00:47 -05:00
parent f1300d2db9
commit 734c2b4ea0
7 changed files with 248 additions and 28 deletions

View File

@ -49,6 +49,7 @@ import EntityEdit from "qqq/pages/entity-edit";
import EntityList from "qqq/pages/entity-list"; import EntityList from "qqq/pages/entity-list";
import EntityView from "qqq/pages/entity-view"; import EntityView from "qqq/pages/entity-view";
import ProcessRun from "qqq/pages/process-run"; import ProcessRun from "qqq/pages/process-run";
import ReportRun from "qqq/pages/process-run/ReportRun";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
import QProcessUtils from "qqq/utils/QProcessUtils"; import QProcessUtils from "qqq/utils/QProcessUtils";
@ -270,6 +271,17 @@ export default function App()
component: <ProcessRun process={process} />, component: <ProcessRun process={process} />,
}); });
}); });
const reportsForTable = QProcessUtils.getReportsForTable(metaData, table.name, true);
reportsForTable.forEach((report) =>
{
routeList.push({
name: report.label,
key: report.name,
route: `${path}/${report.name}`,
component: <ReportRun report={report} />,
});
});
} }
else if (app.type === QAppNodeType.PROCESS) else if (app.type === QAppNodeType.PROCESS)
{ {
@ -281,6 +293,16 @@ export default function App()
component: <ProcessRun process={process} />, component: <ProcessRun process={process} />,
}); });
} }
else if (app.type === QAppNodeType.REPORT)
{
const report = metaData.reports.get(app.name);
routeList.push({
name: `${app.label}`,
key: app.name,
route: path,
component: <ReportRun report={report} />,
});
}
} }
try try

View File

@ -29,6 +29,7 @@ import MDTypography from "qqq/components/Temporary/MDTypography";
interface Props interface Props
{ {
color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark"; color?: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "light" | "dark";
isReport?: boolean;
title: string; title: string;
percentage?: { percentage?: {
color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark" | "white"; color: "primary" | "secondary" | "info" | "success" | "warning" | "error" | "dark" | "white";
@ -41,7 +42,7 @@ interface Props
} }
function ProcessLinkCard({ function ProcessLinkCard({
color, title, percentage, icon, color, isReport, title, percentage, icon,
}: Props): JSX.Element }: Props): JSX.Element
{ {
return ( return (
@ -81,10 +82,11 @@ function ProcessLinkCard({
> >
{percentage.amount} {percentage.amount}
</MDTypography> </MDTypography>
Click here to run the process called {
{" "} isReport
{title} ? `Click here to run the process called ${title}.`
. : `Click here to access the ${title} report.`
}
{percentage.label} {percentage.label}
</MDTypography> </MDTypography>
</MDBox> </MDBox>
@ -94,6 +96,7 @@ function ProcessLinkCard({
ProcessLinkCard.defaultProps = { ProcessLinkCard.defaultProps = {
color: "info", color: "info",
isReport: false,
percentage: { percentage: {
color: "success", color: "success",
text: "", text: "",

View File

@ -22,6 +22,7 @@ import {QAppMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QApp
import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType"; import {QAppNodeType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QAppNodeType";
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {Icon} from "@mui/material"; import {Icon} from "@mui/material";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
@ -53,6 +54,7 @@ function AppHome({app}: Props): JSX.Element
const [qInstance, setQInstance] = useState(null as QInstance); const [qInstance, setQInstance] = useState(null as QInstance);
const [tables, setTables] = useState([] as QTableMetaData[]); const [tables, setTables] = useState([] as QTableMetaData[]);
const [processes, setProcesses] = useState([] as QProcessMetaData[]); const [processes, setProcesses] = useState([] as QProcessMetaData[]);
const [reports, setReports] = useState([] as QReportMetaData[]);
const [childApps, setChildApps] = useState([] as QAppMetaData[]); const [childApps, setChildApps] = useState([] as QAppMetaData[]);
const [tableCounts, setTableCounts] = useState(new Map<string, { isLoading: boolean, value: number }>()); const [tableCounts, setTableCounts] = useState(new Map<string, { isLoading: boolean, value: number }>());
const [updatedTableCounts, setUpdatedTableCounts] = useState(new Date()); const [updatedTableCounts, setUpdatedTableCounts] = useState(new Date());
@ -78,6 +80,7 @@ function AppHome({app}: Props): JSX.Element
const newTables: QTableMetaData[] = []; const newTables: QTableMetaData[] = [];
const newProcesses: QProcessMetaData[] = []; const newProcesses: QProcessMetaData[] = [];
const newReports: QReportMetaData[] = [];
const newChildApps: QAppMetaData[] = []; const newChildApps: QAppMetaData[] = [];
app.children.forEach((child) => app.children.forEach((child) =>
@ -91,6 +94,9 @@ function AppHome({app}: Props): JSX.Element
case QAppNodeType.PROCESS: case QAppNodeType.PROCESS:
newProcesses.push(qInstance.processes.get(child.name)); newProcesses.push(qInstance.processes.get(child.name));
break; break;
case QAppNodeType.REPORT:
newReports.push(qInstance.reports.get(child.name));
break;
case QAppNodeType.APP: case QAppNodeType.APP:
newChildApps.push(qInstance.apps.get(child.name)); newChildApps.push(qInstance.apps.get(child.name));
break; break;
@ -101,6 +107,7 @@ function AppHome({app}: Props): JSX.Element
setTables(newTables); setTables(newTables);
setProcesses(newProcesses); setProcesses(newProcesses);
setReports(newReports);
setChildApps(newChildApps); setChildApps(newChildApps);
const tableCounts = new Map<string, { isLoading: boolean, value: number }>(); const tableCounts = new Map<string, { isLoading: boolean, value: number }>();
@ -231,7 +238,6 @@ function AppHome({app}: Props): JSX.Element
{ {
app.sections ? ( app.sections ? (
<Grid item xs={12} lg={widgetCount === 0 ? 12 : widgetCount === 1 ? 9 : 6}> <Grid item xs={12} lg={widgetCount === 0 ? 12 : widgetCount === 1 ? 9 : 6}>
{app.sections.map((section) => ( {app.sections.map((section) => (
<MDBox key={section.name} mb={3}> <MDBox key={section.name} mb={3}>
<Card sx={{overflow: "visible"}}> <Card sx={{overflow: "visible"}}>
@ -268,7 +274,42 @@ function AppHome({app}: Props): JSX.Element
) : null ) : null
} }
{ {
section.processes && section.tables ? ( section.processes && section.reports ? (
<Divider />
) : null
}
{
section.reports ? (
<MDBox p={3} pl={5} pt={0}>
<MDTypography variant="h6">Reports</MDTypography>
</MDBox>
) : null
}
{
section.reports ? (
<Grid container spacing={3} padding={3} paddingTop={0}>
{
section.reports.map((reportName) =>
{
let report = app.childMap.get(reportName);
return (
<Grid key={report.name} item xs={12} md={12} lg={tileSizeLg}>
<Link to={report.name}>
<ProcessLinkCard
icon={report.iconName || app.iconName}
title={report.label}
isReport={true}
/>
</Link>
</Grid>
);
})
}
</Grid>
) : null
}
{
section.reports && section.tables ? (
<Divider /> <Divider />
) : null ) : null
} }

View File

@ -0,0 +1,71 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData";
import React, {useEffect, useState} from "react";
import ProcessRun from "qqq/pages/process-run/index";
import QClient from "qqq/utils/QClient";
interface Props
{
report?: QReportMetaData;
}
function ReportRun({report}: Props): JSX.Element
{
// const reportNameParam = useParams().reportName;
// const processName = process === null ? processNameParam : process.name;
const [metaData, setMetaData] = useState(null as QInstance);
useEffect(() =>
{
if(!metaData)
{
(async () =>
{
const metaData = await QClient.getInstance().loadMetaData();
setMetaData(metaData);
})();
}
});
if(metaData)
{
console.log(`Report Process name is ${report.processName}`)
const process = metaData.processes.get(report.processName)
console.log(`Process is ${process.name}`)
const defaultProcessValues = {reportName: report.name}
return (<ProcessRun process={process} defaultProcessValues={defaultProcessValues} />);
}
else
{
// todo - loading?
return (<div/>);
}
}
ReportRun.defaultProps = {
process: null,
};
export default ReportRun;

View File

@ -35,6 +35,7 @@ import {Button, Icon, CircularProgress, TablePagination} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Link from "@mui/material/Link";
import Step from "@mui/material/Step"; import Step from "@mui/material/Step";
import StepLabel from "@mui/material/StepLabel"; import StepLabel from "@mui/material/StepLabel";
import Stepper from "@mui/material/Stepper"; import Stepper from "@mui/material/Stepper";
@ -42,7 +43,7 @@ import Typography from "@mui/material/Typography";
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro"; import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
import FormData from "form-data"; import FormData from "form-data";
import {Form, Formik} from "formik"; import {Form, Formik} from "formik";
import React, {Fragment, useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {useLocation, useParams, useNavigate} from "react-router-dom"; import {useLocation, useParams, useNavigate} from "react-router-dom";
import * as Yup from "yup"; import * as Yup from "yup";
import BaseLayout from "qqq/components/BaseLayout"; import BaseLayout from "qqq/components/BaseLayout";
@ -61,13 +62,14 @@ import QProcessSummaryResults from "./components/QProcessSummaryResults";
interface Props interface Props
{ {
process?: QProcessMetaData; process?: QProcessMetaData;
defaultProcessValues?: any;
} }
const INITIAL_RETRY_MILLIS = 1_500; const INITIAL_RETRY_MILLIS = 1_500;
const RETRY_MAX_MILLIS = 12_000; const RETRY_MAX_MILLIS = 12_000;
const BACKOFF_AMOUNT = 1.5; const BACKOFF_AMOUNT = 1.5;
function ProcessRun({process}: Props): JSX.Element function ProcessRun({process, defaultProcessValues}: Props): JSX.Element
{ {
const processNameParam = useParams().processName; const processNameParam = useParams().processName;
const processName = process === null ? processNameParam : process.name; const processName = process === null ? processNameParam : process.name;
@ -339,6 +341,29 @@ function ProcessRun({process}: Props): JSX.Element
</div> </div>
) )
} }
{
component.type === QComponentType.DOWNLOAD_FORM && (
<Grid container display="flex" justifyContent="center">
<Grid item xs={12} sm={12} xl={8} m={3} p={3} sx={{border: "1px solid gray", borderRadius: "1rem"}}>
<MDBox mt={-5} mb={1} p={1} sx={{width: "fit-content"}} bgColor="success" borderRadius=".25em" width="initial" color="white">
<MDBox display="flex" alignItems="center" color="white">
Download
</MDBox>
</MDBox>
<MDBox display="flex" py={1} pr={2}>
<MDTypography variant="button" fontWeight="bold">
<Link target="_blank" download href={`/download/${processValues.downloadFileName}?filePath=${processValues.serverFilePath}`} display="flex" alignItems="center">
<Icon fontSize="large">download_for_offline</Icon>
<Box pl={1}>
{processValues.downloadFileName}
</Box>
</Link>
</MDTypography>
</MDBox>
</Grid>
</Grid>
)
}
{ {
component.type === QComponentType.VALIDATION_REVIEW_SCREEN && ( component.type === QComponentType.VALIDATION_REVIEW_SCREEN && (
<QValidationReview <QValidationReview
@ -431,6 +456,30 @@ function ProcessRun({process}: Props): JSX.Element
return (newRecordConfig); return (newRecordConfig);
} }
const getFullFieldList = (activeStep: QFrontendStepMetaData, processValues: any) =>
{
let rs: QFieldMetaData[] = [];
if(activeStep && activeStep.formFields)
{
for(let i = 0; i<activeStep.formFields.length; i++)
{
rs.push(activeStep.formFields[i]);
}
}
if(processValues.inputFieldList)
{
for(let i = 0; i<processValues.inputFieldList.length; i++)
{
let inputField = new QFieldMetaData(processValues.inputFieldList[i]);
rs.push(inputField);
}
}
return (rs);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle moving to another step in the process - e.g., after the backend told us what screen to show next. // // handle moving to another step in the process - e.g., after the backend told us what screen to show next. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -474,14 +523,13 @@ function ProcessRun({process}: Props): JSX.Element
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
// if this step has form fields, set up the form // // if this step has form fields, set up the form //
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
if (activeStep.formFields) if (activeStep.formFields || processValues.inputFieldList)
{ {
const {dynamicFormFields, formValidations} = DynamicFormUtils.getFormData( let fullFieldList = getFullFieldList(activeStep, processValues);
activeStep.formFields, const {dynamicFormFields, formValidations} = DynamicFormUtils.getFormData(fullFieldList);
);
const initialValues: any = {}; const initialValues: any = {};
activeStep.formFields.forEach((field) => fullFieldList.forEach((field) =>
{ {
initialValues[field.name] = processValues[field.name]; initialValues[field.name] = processValues[field.name];
}); });
@ -492,7 +540,7 @@ function ProcessRun({process}: Props): JSX.Element
if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM)) if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM))
{ {
const newDisabledBulkEditFields: any = {}; const newDisabledBulkEditFields: any = {};
activeStep.formFields.forEach((field) => fullFieldList.forEach((field) =>
{ {
newDisabledBulkEditFields[field.name] = true; newDisabledBulkEditFields[field.name] = true;
dynamicFormFields[field.name].isRequired = false; dynamicFormFields[field.name].isRequired = false;
@ -562,11 +610,12 @@ function ProcessRun({process}: Props): JSX.Element
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
useEffect(() => useEffect(() =>
{ {
if (activeStep && activeStep.formFields) if (activeStep && (activeStep.formFields || processValues.inputFieldList))
{ {
let fullFieldList = getFullFieldList(activeStep, processValues);
const newDynamicFormFields: any = {}; const newDynamicFormFields: any = {};
const newFormValidations: any = {}; const newFormValidations: any = {};
activeStep.formFields.forEach((field) => fullFieldList.forEach((field) =>
{ {
const fieldName = field.name; const fieldName = field.name;
const isDisabled = disabledBulkEditFields[fieldName]; const isDisabled = disabledBulkEditFields[fieldName];
@ -738,22 +787,21 @@ function ProcessRun({process}: Props): JSX.Element
(async () => (async () =>
{ {
const urlSearchParams = new URLSearchParams(location.search); const urlSearchParams = new URLSearchParams(location.search);
let queryStringForInit = null; let queryStringPairsForInit = [];
if (urlSearchParams.get("recordIds")) if (urlSearchParams.get("recordIds"))
{ {
queryStringForInit = `recordsParam=recordIds&recordIds=${urlSearchParams.get( queryStringPairsForInit.push("recordsParam=recordIds");
"recordIds", queryStringPairsForInit.push(`recordIds=${urlSearchParams.get("recordIds")}`);
)}`;
} }
else if (urlSearchParams.get("filterJSON")) else if (urlSearchParams.get("filterJSON"))
{ {
queryStringForInit = `recordsParam=filterJSON&filterJSON=${urlSearchParams.get( queryStringPairsForInit.push("recordsParam=filterJSON");
"filterJSON", queryStringPairsForInit.push(`filterJSON=${urlSearchParams.get("filterJSON")}`);
)}`;
} }
// todo once saved filters exist // todo once saved filters exist
//else if(urlSearchParams.get("filterId")) { //else if(urlSearchParams.get("filterId")) {
// queryStringForInit = `recordsParam=filterId&filterId=${urlSearchParams.get("filterId")}` // queryStringPairsForInit.push("recordsParam=filterId");
// queryStringPairsForInit.push(`filterId=${urlSearchParams.get("filterId")}`);
// } // }
try try
@ -792,9 +840,17 @@ function ProcessRun({process}: Props): JSX.Element
return; return;
} }
if(defaultProcessValues)
{
for(let key in defaultProcessValues)
{
queryStringPairsForInit.push(`${key}=${encodeURIComponent(defaultProcessValues[key])}`);
}
}
try try
{ {
const processResponse = await QClient.getInstance().processInit(processName, queryStringForInit); const processResponse = await QClient.getInstance().processInit(processName, queryStringPairsForInit.join("&"));
setProcessUUID(processResponse.processUUID); setProcessUUID(processResponse.processUUID);
setLastProcessResponse(processResponse); setLastProcessResponse(processResponse);
} }
@ -830,7 +886,8 @@ function ProcessRun({process}: Props): JSX.Element
if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM)) if (doesStepHaveComponent(activeStep, QComponentType.BULK_EDIT_FORM))
{ {
const bulkEditEnabledFields: string[] = []; const bulkEditEnabledFields: string[] = [];
activeStep.formFields.forEach((field) => let fullFieldList = getFullFieldList(activeStep, processValues);
fullFieldList.forEach((field) =>
{ {
if (!disabledBulkEditFields[field.name]) if (!disabledBulkEditFields[field.name])
{ {
@ -904,7 +961,7 @@ function ProcessRun({process}: Props): JSX.Element
<BaseLayout> <BaseLayout>
<MDBox py={3} mb={20}> <MDBox py={3} mb={20}>
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}> <Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
<Grid item xs={12} lg={8}> <Grid item xs={12} lg={10} xl={8}>
<Formik <Formik
enableReinitialize enableReinitialize
initialValues={initialValues} initialValues={initialValues}
@ -995,6 +1052,7 @@ function ProcessRun({process}: Props): JSX.Element
ProcessRun.defaultProps = { ProcessRun.defaultProps = {
process: null, process: null,
defaultProcessValues: {}
}; };
export default ProcessRun; export default ProcessRun;

View File

@ -21,6 +21,7 @@
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData";
/******************************************************************************* /*******************************************************************************
** Utility class for working with QQQ Processes ** Utility class for working with QQQ Processes
@ -42,6 +43,22 @@ class QProcessUtils
}); });
return matchingProcesses; return matchingProcesses;
} }
public static getReportsForTable(metaData: QInstance, tableName: string, includeHidden = false): QReportMetaData[]
{
const matchingReports: QReportMetaData[] = [];
const reportKeys = [...metaData.reports.keys()];
reportKeys.forEach((key) =>
{
const process = metaData.reports.get(key);
if (process.tableName === tableName)
{
matchingReports.push(process);
}
});
return matchingReports;
}
} }
export default QProcessUtils; export default QProcessUtils;

View File

@ -36,4 +36,12 @@ module.exports = function (app)
changeOrigin: true, changeOrigin: true,
}), }),
); );
app.use(
"/download/*",
createProxyMiddleware({
target: "http://localhost:8000",
changeOrigin: true,
}),
);
}; };