CE-881 Add RenderedReport records

This commit is contained in:
2024-04-02 15:44:50 -05:00
parent 4dadff7fc2
commit 3a1125f668
4 changed files with 702 additions and 13 deletions

View File

@ -0,0 +1,506 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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/>.
*/
package com.kingsrook.qqq.backend.core.model.savedreports;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
/*******************************************************************************
** Entity bean for the rendered report table
*******************************************************************************/
public class RenderedReport extends QRecordEntity
{
public static final String TABLE_NAME = "renderedReport";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR, dynamicDefaultValueBehavior = DynamicDefaultValueBehavior.USER_ID)
private String userId;
@QField(possibleValueSourceName = SavedReport.TABLE_NAME)
private Integer savedReportId;
@QField(possibleValueSourceName = RenderedReportStatus.NAME, label = "Status")
private Integer renderedReportStatusId;
@QField(maxLength = 40, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String jobUuid;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String resultPath;
@QField(maxLength = 10, valueTooLongBehavior = ValueTooLongBehavior.ERROR, possibleValueSourceName = ReportFormatPossibleValueEnum.NAME)
private String reportFormat;
@QField()
private Instant startTime;
@QField()
private Instant endTime;
@QField(displayFormat = DisplayFormat.COMMAS)
private Integer rowCount;
@QField(maxLength = 250, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String errorMessage;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public RenderedReport()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public RenderedReport(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public RenderedReport withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public RenderedReport withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public RenderedReport withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for userId
*******************************************************************************/
public String getUserId()
{
return (this.userId);
}
/*******************************************************************************
** Setter for userId
*******************************************************************************/
public void setUserId(String userId)
{
this.userId = userId;
}
/*******************************************************************************
** Fluent setter for userId
*******************************************************************************/
public RenderedReport withUserId(String userId)
{
this.userId = userId;
return (this);
}
/*******************************************************************************
** Getter for savedReportId
*******************************************************************************/
public Integer getSavedReportId()
{
return (this.savedReportId);
}
/*******************************************************************************
** Setter for savedReportId
*******************************************************************************/
public void setSavedReportId(Integer savedReportId)
{
this.savedReportId = savedReportId;
}
/*******************************************************************************
** Fluent setter for savedReportId
*******************************************************************************/
public RenderedReport withSavedReportId(Integer savedReportId)
{
this.savedReportId = savedReportId;
return (this);
}
/*******************************************************************************
** Getter for renderedReportStatusId
*******************************************************************************/
public Integer getRenderedReportStatusId()
{
return (this.renderedReportStatusId);
}
/*******************************************************************************
** Setter for renderedReportStatusId
*******************************************************************************/
public void setRenderedReportStatusId(Integer renderedReportStatusId)
{
this.renderedReportStatusId = renderedReportStatusId;
}
/*******************************************************************************
** Fluent setter for renderedReportStatusId
*******************************************************************************/
public RenderedReport withRenderedReportStatusId(Integer renderedReportStatusId)
{
this.renderedReportStatusId = renderedReportStatusId;
return (this);
}
/*******************************************************************************
** Getter for jobUuid
*******************************************************************************/
public String getJobUuid()
{
return (this.jobUuid);
}
/*******************************************************************************
** Setter for jobUuid
*******************************************************************************/
public void setJobUuid(String jobUuid)
{
this.jobUuid = jobUuid;
}
/*******************************************************************************
** Fluent setter for jobUuid
*******************************************************************************/
public RenderedReport withJobUuid(String jobUuid)
{
this.jobUuid = jobUuid;
return (this);
}
/*******************************************************************************
** Getter for resultPath
*******************************************************************************/
public String getResultPath()
{
return (this.resultPath);
}
/*******************************************************************************
** Setter for resultPath
*******************************************************************************/
public void setResultPath(String resultPath)
{
this.resultPath = resultPath;
}
/*******************************************************************************
** Fluent setter for resultPath
*******************************************************************************/
public RenderedReport withResultPath(String resultPath)
{
this.resultPath = resultPath;
return (this);
}
/*******************************************************************************
** Getter for reportFormat
*******************************************************************************/
public String getReportFormat()
{
return (this.reportFormat);
}
/*******************************************************************************
** Setter for reportFormat
*******************************************************************************/
public void setReportFormat(String reportFormat)
{
this.reportFormat = reportFormat;
}
/*******************************************************************************
** Fluent setter for reportFormat
*******************************************************************************/
public RenderedReport withReportFormat(String reportFormat)
{
this.reportFormat = reportFormat;
return (this);
}
/*******************************************************************************
** Getter for startTime
*******************************************************************************/
public Instant getStartTime()
{
return (this.startTime);
}
/*******************************************************************************
** Setter for startTime
*******************************************************************************/
public void setStartTime(Instant startTime)
{
this.startTime = startTime;
}
/*******************************************************************************
** Fluent setter for startTime
*******************************************************************************/
public RenderedReport withStartTime(Instant startTime)
{
this.startTime = startTime;
return (this);
}
/*******************************************************************************
** Getter for endTime
*******************************************************************************/
public Instant getEndTime()
{
return (this.endTime);
}
/*******************************************************************************
** Setter for endTime
*******************************************************************************/
public void setEndTime(Instant endTime)
{
this.endTime = endTime;
}
/*******************************************************************************
** Fluent setter for endTime
*******************************************************************************/
public RenderedReport withEndTime(Instant endTime)
{
this.endTime = endTime;
return (this);
}
/*******************************************************************************
** Getter for rowCount
*******************************************************************************/
public Integer getRowCount()
{
return (this.rowCount);
}
/*******************************************************************************
** Setter for rowCount
*******************************************************************************/
public void setRowCount(Integer rowCount)
{
this.rowCount = rowCount;
}
/*******************************************************************************
** Fluent setter for rowCount
*******************************************************************************/
public RenderedReport withRowCount(Integer rowCount)
{
this.rowCount = rowCount;
return (this);
}
/*******************************************************************************
** Getter for errorMessage
*******************************************************************************/
public String getErrorMessage()
{
return (this.errorMessage);
}
/*******************************************************************************
** Setter for errorMessage
*******************************************************************************/
public void setErrorMessage(String errorMessage)
{
this.errorMessage = errorMessage;
}
/*******************************************************************************
** Fluent setter for errorMessage
*******************************************************************************/
public RenderedReport withErrorMessage(String errorMessage)
{
this.errorMessage = errorMessage;
return (this);
}
}

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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/>.
*/
package com.kingsrook.qqq.backend.core.model.savedreports;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
/*******************************************************************************
**
*******************************************************************************/
public enum RenderedReportStatus implements PossibleValueEnum<Integer>
{
RUNNING(1, "Running"),
COMPLETE(2, "Complete"),
FAILED(3, "Failed");
public static final String NAME = "renderedReportStatus";
private final Integer id;
private final String label;
/*******************************************************************************
**
*******************************************************************************/
RenderedReportStatus(int id, String label)
{
this.id = id;
this.label = label;
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public Integer getId()
{
return id;
}
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return label;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Integer getPossibleValueId()
{
return id;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueLabel()
{
return label;
}
}

View File

@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
@ -55,8 +56,10 @@ public class SavedReportsMetaDataProvider
public void defineAll(QInstance instance, String recordTablesBackendName, String reportStorageBackendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
instance.addTable(defineSavedReportTable(recordTablesBackendName, backendDetailEnricher));
instance.addTable(defineRenderedReportTable(recordTablesBackendName, backendDetailEnricher));
instance.addPossibleValueSource(QPossibleValueSource.newForTable(SavedReport.TABLE_NAME));
instance.addPossibleValueSource(QPossibleValueSource.newForEnum(ReportFormatPossibleValueEnum.NAME, ReportFormatPossibleValueEnum.values()));
instance.addPossibleValueSource(QPossibleValueSource.newForEnum(RenderedReportStatus.NAME, RenderedReportStatus.values()));
instance.addTable(defineReportStorageTable(reportStorageBackendName, backendDetailEnricher));
@ -92,8 +95,6 @@ public class SavedReportsMetaDataProvider
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
@ -118,7 +119,6 @@ public class SavedReportsMetaDataProvider
{
QTableMetaData table = new QTableMetaData()
.withName(SavedReport.TABLE_NAME)
.withLabel("Saved Report")
.withIcon(new QIcon().withName("article"))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
@ -145,4 +145,38 @@ public class SavedReportsMetaDataProvider
return (table);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineRenderedReportTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData table = new QTableMetaData()
.withName(RenderedReport.TABLE_NAME)
.withIcon(new QIcon().withName("print"))
.withRecordLabelFormat("%s - %s")
.withRecordLabelFields("savedReportId", "startTime")
.withBackendName(backendName)
.withPrimaryKeyField("id")
.withFieldsFromEntity(RenderedReport.class)
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "savedReportId", "renderedReportStatusId")))
.withSection(new QFieldSection("input", new QIcon().withName("input"), Tier.T2, List.of("userId", "reportFormat")))
.withSection(new QFieldSection("output", new QIcon().withName("output"), Tier.T2, List.of("jobUuid", "resultPath", "rowCount", "errorMessage", "startTime", "endTime")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
.withoutCapabilities(Capability.allWriteCapabilities());
table.getField("renderedReportStatusId").setAdornments(List.of(new FieldAdornment(AdornmentType.CHIP)
.withValues(AdornmentType.ChipValues.iconAndColorValues(RenderedReportStatus.RUNNING.getId(), "pending", AdornmentType.ChipValues.COLOR_SECONDARY))
.withValues(AdornmentType.ChipValues.iconAndColorValues(RenderedReportStatus.COMPLETE.getId(), "check", AdornmentType.ChipValues.COLOR_SUCCESS))
.withValues(AdornmentType.ChipValues.iconAndColorValues(RenderedReportStatus.FAILED.getId(), "error", AdornmentType.ChipValues.COLOR_ERROR))));
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(table);
}
return (table);
}
}

View File

@ -25,23 +25,35 @@ package com.kingsrook.qqq.backend.core.processes.implementations.savedreports;
import java.io.OutputStream;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportDestination;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormatPossibleValueEnum;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.savedreports.RenderedReport;
import com.kingsrook.qqq.backend.core.model.savedreports.RenderedReportStatus;
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -60,22 +72,43 @@ public class RenderSavedReportExecuteStep implements BackendStep
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
QRecord renderedReportRecord = null;
try
{
String storageTableName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME);
ReportFormat reportFormat = ReportFormat.fromString(runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT));
SavedReport savedReport = new SavedReport(runBackendStepInput.getRecords().get(0));
String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport);
String storageReference = UUID.randomUUID() + "/" + downloadFileBaseName + "." + reportFormat.getExtension();
OutputStream outputStream = new StorageAction().createOutputStream(new StorageInput(storageTableName).withReference(storageReference));
////////////////////////////////
// read inputs, set up params //
////////////////////////////////
String storageTableName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME);
ReportFormat reportFormat = ReportFormat.fromString(runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT));
SavedReport savedReport = new SavedReport(runBackendStepInput.getRecords().get(0));
String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport);
String storageReference = LocalDate.now() + "/" + LocalTime.now().toString().replaceAll(":", "").replaceFirst("\\..*", "") + "/" + UUID.randomUUID() + "/" + downloadFileBaseName + "." + reportFormat.getExtension();
OutputStream outputStream = new StorageAction().createOutputStream(new StorageInput(storageTableName).withReference(storageReference));
runBackendStepInput.getAsyncJobCallback().updateStatus("Generating Report");
//////////////////////////////////////////////////////////////////
// insert a rendered-report record indicating that it's running //
//////////////////////////////////////////////////////////////////
renderedReportRecord = new InsertAction().execute(new InsertInput(RenderedReport.TABLE_NAME).withRecordEntity(new RenderedReport()
.withSavedReportId(savedReport.getId())
.withStartTime(Instant.now())
// todo .withJobUuid(runBackendStepInput.get)
.withRenderedReportStatusId(RenderedReportStatus.RUNNING.getId())
.withReportFormat(ReportFormatPossibleValueEnum.valueOf(reportFormat.name()).getPossibleValueId())
)).getRecords().get(0);
////////////////////////////////////////////////////////////////////////////////////////////
// convert the report record to report meta-data, which the GenerateReportAction works on //
////////////////////////////////////////////////////////////////////////////////////////////
QReportMetaData reportMetaData = new SavedReportToReportMetaDataAdapter().adapt(savedReport, reportFormat);
/////////////////////////////////////
// setup & run the generate action //
/////////////////////////////////////
ReportInput reportInput = new ReportInput();
reportInput.setAsyncJobCallback(runBackendStepInput.getAsyncJobCallback());
reportInput.setReportMetaData(reportMetaData);
reportInput.setReportDestination(new ReportDestination()
.withReportFormat(reportFormat)
@ -84,7 +117,18 @@ public class RenderSavedReportExecuteStep implements BackendStep
Map<String, Serializable> values = runBackendStepInput.getValues();
reportInput.setInputValues(values);
new GenerateReportAction().execute(reportInput);
ReportOutput reportOutput = new GenerateReportAction().execute(reportInput);
///////////////////////////////////
// update record to show success //
///////////////////////////////////
new UpdateAction().execute(new UpdateInput(RenderedReport.TABLE_NAME).withRecord(new QRecord()
.withValue("id", renderedReportRecord.getValue("id"))
.withValue("resultPath", storageReference)
.withValue("renderedReportStatusId", RenderedReportStatus.COMPLETE.getPossibleValueId())
.withValue("endTime", Instant.now())
.withValue("rowCount", reportOutput.getTotalRecordCount())
));
runBackendStepOutput.addValue("downloadFileName", downloadFileBaseName + "." + reportFormat.getExtension());
runBackendStepOutput.addValue("storageTableName", storageTableName);
@ -92,9 +136,18 @@ public class RenderSavedReportExecuteStep implements BackendStep
}
catch(Exception e)
{
// todo - render error screen?
if(renderedReportRecord != null)
{
new UpdateAction().execute(new UpdateInput(RenderedReport.TABLE_NAME).withRecord(new QRecord()
.withValue("id", renderedReportRecord.getValue("id"))
.withValue("renderedReportStatusId", RenderedReportStatus.FAILED.getPossibleValueId())
.withValue("endTime", Instant.now())
.withValue("errorMessage", ExceptionUtils.concatenateMessagesFromChain(e))
));
}
LOG.warn("Error rendering saved report", e);
throw (e);
}
}