2nd iteration on no-code dashboards. add conditional filters, timeframes, more utils, calcualtions

This commit is contained in:
2023-02-14 08:56:15 -06:00
parent 7e07fd04a1
commit f01a1ac7a1
15 changed files with 876 additions and 148 deletions

View File

@ -234,7 +234,8 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/ *******************************************************************************/
public static String aHrefTableFilterNoOfRecords(RenderWidgetInput input, String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel) throws QException public static String aHrefTableFilterNoOfRecords(RenderWidgetInput input, String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel) throws QException
{ {
String displayText = QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords) + " " + StringUtils.plural(noOfRecords, singularLabel, pluralLabel); String plural = StringUtils.plural(noOfRecords, singularLabel, pluralLabel);
String displayText = QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords) + (StringUtils.hasContent(plural) ? (" " + plural) : "");
String tablePath = QContext.getQInstance().getTablePath(tableName); String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null) if(tablePath == null)
{ {

View File

@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWi
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWidgetValueSource; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWidgetValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.QNoCodeWidgetMetaData; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.QNoCodeWidgetMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils; import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/******************************************************************************* /*******************************************************************************
@ -58,21 +59,29 @@ public class NoCodeWidgetRenderer extends AbstractWidgetRenderer
// build context by evaluating all values // // build context by evaluating all values //
//////////////////////////////////////////// ////////////////////////////////////////////
Map<String, Object> context = new HashMap<>(); Map<String, Object> context = new HashMap<>();
context.put("utils", new NoCodeWidgetVelocityUtils(context, input));
context.put("input", input);
for(Map.Entry<String, String> entry : input.getQueryParams().entrySet())
{
context.put(entry.getKey(), entry.getValue());
}
for(AbstractWidgetValueSource valueSource : widgetMetaData.getValues()) for(AbstractWidgetValueSource valueSource : widgetMetaData.getValues())
{
try
{ {
LOG.trace("Computing: " + valueSource.getType() + " named " + valueSource.getName() + "..."); LOG.trace("Computing: " + valueSource.getType() + " named " + valueSource.getName() + "...");
Object value = valueSource.evaluate(context); Object value = valueSource.evaluate(context, input);
LOG.trace("Computed: " + valueSource.getName() + " = " + value); LOG.trace("Computed: " + valueSource.getName() + " = " + value);
context.put(valueSource.getName(), value); context.put(valueSource.getName(), value);
context.put(valueSource.getName() + ".source", valueSource); context.put(valueSource.getName() + ".source", valueSource);
} }
catch(Exception e)
///////////////////////////////////////////// {
// set default utils object in context too // LOG.warn("Error evaluating widget value source", e, logPair("widgetName", input.getWidgetMetaData().getName()), logPair("valueSourceName", valueSource.getName()));
///////////////////////////////////////////// }
context.put("utils", new NoCodeWidgetVelocityUtils(context)); }
///////////////////////////////////////////// /////////////////////////////////////////////
// build content by evaluating all outputs // // build content by evaluating all outputs //

View File

@ -22,11 +22,18 @@
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets; package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.dashboard.AbstractHTMLWidgetRenderer; import com.kingsrook.qqq.backend.core.actions.dashboard.AbstractHTMLWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetCount; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetCount;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -39,11 +46,8 @@ public class NoCodeWidgetVelocityUtils
{ {
private static final QLogger LOG = QLogger.getLogger(NoCodeWidgetVelocityUtils.class); private static final QLogger LOG = QLogger.getLogger(NoCodeWidgetVelocityUtils.class);
private Map<String, Object> context;
/******************************************************************************* private RenderWidgetInput input;
**
*******************************************************************************/
private final Map<String, Object> context;
@ -51,9 +55,10 @@ public class NoCodeWidgetVelocityUtils
** Constructor ** Constructor
** **
*******************************************************************************/ *******************************************************************************/
public NoCodeWidgetVelocityUtils(Map<String, Object> context) public NoCodeWidgetVelocityUtils(Map<String, Object> context, RenderWidgetInput input)
{ {
this.context = context; this.context = context;
this.input = input;
} }
@ -61,7 +66,7 @@ public class NoCodeWidgetVelocityUtils
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public final String errorIcon() public String errorIcon()
{ {
return (""" return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: red; position: relative; top: 6px;" aria-hidden="true">error_outline</span> <span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: red; position: relative; top: 6px;" aria-hidden="true">error_outline</span>
@ -73,7 +78,7 @@ public class NoCodeWidgetVelocityUtils
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public final String checkIcon() public String checkIcon()
{ {
return (""" return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: green; position: relative; top: 6px;" aria-hidden="true">check</span> <span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: green; position: relative; top: 6px;" aria-hidden="true">check</span>
@ -82,6 +87,42 @@ public class NoCodeWidgetVelocityUtils
/*******************************************************************************
**
*******************************************************************************/
public String spanColorGreen()
{
return ("""
<span style="color: green;">
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String spanColorOrange()
{
return ("""
<span style="color: orange;">
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String spanColorRed()
{
return ("""
<span style="color: red;">
""");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -92,6 +133,110 @@ public class NoCodeWidgetVelocityUtils
/*******************************************************************************
**
*******************************************************************************/
public String formatDateTime(Instant i)
{
return QValueFormatter.formatDateTimeWithZone(i.atZone(ZoneId.of(QContext.getQInstance().getDefaultTimeZoneId())));
}
/*******************************************************************************
**
*******************************************************************************/
public String formatSecondsAsDuration(Integer seconds)
{
StringBuilder rs = new StringBuilder();
if(seconds == null)
{
return ("");
}
int secondsPerDay = 24 * 60 * 60;
if(seconds >= secondsPerDay)
{
int days = seconds / (secondsPerDay);
seconds = seconds % secondsPerDay;
rs.append(days).append(StringUtils.plural(days, " day", " days")).append(" ");
}
int secondsPerHour = 60 * 60;
if(seconds >= secondsPerHour)
{
int hours = seconds / (secondsPerHour);
seconds = seconds % secondsPerHour;
rs.append(hours).append(StringUtils.plural(hours, " hour", " hours")).append(" ");
}
int secondsPerMinute = 60;
if(seconds >= secondsPerMinute)
{
int minutes = seconds / (secondsPerMinute);
seconds = seconds % secondsPerMinute;
rs.append(minutes).append(StringUtils.plural(minutes, " minute", " minutes")).append(" ");
}
if(seconds > 0 || rs.length() == 0)
{
rs.append(seconds).append(StringUtils.plural(seconds, " second", " seconds")).append(" ");
}
if(rs.length() > 0)
{
rs.deleteCharAt(rs.length() - 1);
}
return (rs.toString());
}
/*******************************************************************************
**
*******************************************************************************/
public String formatSecondsAsRoundedDuration(Integer seconds)
{
StringBuilder rs = new StringBuilder();
if(seconds == null)
{
return ("");
}
int secondsPerDay = 24 * 60 * 60;
if(seconds >= secondsPerDay)
{
int days = seconds / (secondsPerDay);
return (days + StringUtils.plural(days, " day", " days"));
}
int secondsPerHour = 60 * 60;
if(seconds >= secondsPerHour)
{
int hours = seconds / (secondsPerHour);
return (hours + StringUtils.plural(hours, " hour", " hours"));
}
int secondsPerMinute = 60;
if(seconds >= secondsPerMinute)
{
int minutes = seconds / (secondsPerMinute);
return (minutes + StringUtils.plural(minutes, " minute", " minutes"));
}
if(seconds > 0 || rs.length() == 0)
{
return (seconds + StringUtils.plural(seconds, " second", " seconds"));
}
return ("");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -101,7 +246,7 @@ public class NoCodeWidgetVelocityUtils
{ {
WidgetCount widgetCount = (WidgetCount) context.get(countVariableName + ".source"); WidgetCount widgetCount = (WidgetCount) context.get(countVariableName + ".source");
Integer count = ValueUtils.getValueAsInteger(context.get(countVariableName)); Integer count = ValueUtils.getValueAsInteger(context.get(countVariableName));
QQueryFilter filter = widgetCount.getFilter(); QQueryFilter filter = widgetCount.getEffectiveFilter(input);
return (AbstractHTMLWidgetRenderer.aHrefTableFilterNoOfRecords(null, widgetCount.getTableName(), filter, count, singular, plural)); return (AbstractHTMLWidgetRenderer.aHrefTableFilterNoOfRecords(null, widgetCount.getTableName(), filter, count, singular, plural));
} }
catch(Exception e) catch(Exception e)
@ -110,4 +255,14 @@ public class NoCodeWidgetVelocityUtils
return (""); return ("");
} }
} }
/*******************************************************************************
**
*******************************************************************************/
public String round(BigDecimal input, int digits)
{
return String.valueOf(input.setScale(digits, RoundingMode.HALF_UP));
}
} }

View File

@ -47,8 +47,8 @@ public class QValueFormatter
{ {
private static final QLogger LOG = QLogger.getLogger(QValueFormatter.class); private static final QLogger LOG = QLogger.getLogger(QValueFormatter.class);
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a"); private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a");
private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a z"); private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a z");
private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static DateTimeFormatter localTimeFormatter = DateTimeFormatter.ofPattern("h:mm a"); private static DateTimeFormatter localTimeFormatter = DateTimeFormatter.ofPattern("h:mm a");

View File

@ -0,0 +1,46 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractConditionalFilter
{
/*******************************************************************************
**
*******************************************************************************/
public abstract boolean testCondition(RenderWidgetInput renderWidgetInput);
/*******************************************************************************
**
*******************************************************************************/
public abstract QQueryFilter getFilter(RenderWidgetInput renderWidgetInput);
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
/******************************************************************************* /*******************************************************************************
@ -39,7 +40,7 @@ public abstract class AbstractWidgetValueSource
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public abstract Object evaluate(Map<String, Object> context) throws QException; public abstract Object evaluate(Map<String, Object> context, RenderWidgetInput input) throws QException;

View File

@ -0,0 +1,167 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractWidgetValueSourceWithFilter extends AbstractWidgetValueSource
{
protected String tableName;
protected QQueryFilter filter;
protected List<AbstractConditionalFilter> conditionalFilterList;
/*******************************************************************************
**
*******************************************************************************/
public QQueryFilter getEffectiveFilter(RenderWidgetInput input)
{
QQueryFilter effectiveFilter;
if(filter == null)
{
effectiveFilter = new QQueryFilter();
}
else
{
effectiveFilter = filter.clone();
}
for(AbstractConditionalFilter conditionalFilter : CollectionUtils.nonNullList(conditionalFilterList))
{
if(conditionalFilter.testCondition(input))
{
QQueryFilter additionalFilter = conditionalFilter.getFilter(input);
for(QFilterCriteria criterion : additionalFilter.getCriteria())
{
effectiveFilter.addCriteria(criterion);
}
}
}
return (effectiveFilter);
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public AbstractWidgetValueSourceWithFilter withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public AbstractWidgetValueSourceWithFilter withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for conditionalFilterList
*******************************************************************************/
public List<AbstractConditionalFilter> getConditionalFilterList()
{
return (this.conditionalFilterList);
}
/*******************************************************************************
** Setter for conditionalFilterList
*******************************************************************************/
public void setConditionalFilterList(List<AbstractConditionalFilter> conditionalFilterList)
{
this.conditionalFilterList = conditionalFilterList;
}
/*******************************************************************************
** Fluent setter for conditionalFilterList
*******************************************************************************/
public AbstractWidgetValueSourceWithFilter withConditionalFilterList(List<AbstractConditionalFilter> conditionalFilterList)
{
this.conditionalFilterList = conditionalFilterList;
return (this);
}
}

View File

@ -34,8 +34,12 @@ public class HtmlWrapper
private String suffix; private String suffix;
public static final HtmlWrapper SUBHEADER = new HtmlWrapper("<h4>", "</h4>"); public static final HtmlWrapper SUBHEADER = new HtmlWrapper("<h4>", "</h4>");
public static final HtmlWrapper BIG_CENTERED = new HtmlWrapper("<div style='font-size: 2rem; font-weight: 400; line-height: 1.625; text-align: center; padding-bottom: 8px;'>", "</div>");
public static final HtmlWrapper INDENT_1 = new HtmlWrapper("<div style='padding-left: 1rem;'>", "</div>"); public static final HtmlWrapper INDENT_1 = new HtmlWrapper("<div style='padding-left: 1rem;'>", "</div>");
public static final HtmlWrapper INDENT_2 = new HtmlWrapper("<div style='padding-left: 2rem;'>", "</div>"); public static final HtmlWrapper INDENT_2 = new HtmlWrapper("<div style='padding-left: 2rem;'>", "</div>");
public static final HtmlWrapper RULE_ABOVE = new HtmlWrapper("""
<hr style="opacity: 0.25; height: 0.0625rem; border-width: 0; margin-bottom: 1rem; background-image: linear-gradient(to right, rgba(52, 71, 103, 0), rgba(52, 71, 103, 0.4), rgba(52, 71, 103, 0));" />
""", "");

View File

@ -0,0 +1,99 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
/*******************************************************************************
**
*******************************************************************************/
public class IfInputVariableExistsFilter extends AbstractConditionalFilter
{
private String inputVariableName;
private QQueryFilter filter;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public IfInputVariableExistsFilter()
{
}
/*******************************************************************************
**
*******************************************************************************/
public IfInputVariableExistsFilter(String inputVariableName, QQueryFilter filter)
{
this.inputVariableName = inputVariableName;
this.filter = filter;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean testCondition(RenderWidgetInput renderWidgetInput)
{
return (renderWidgetInput.getQueryParams().get(inputVariableName) != null);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QQueryFilter getFilter(RenderWidgetInput renderWidgetInput)
{
for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria()))
{
if(criterion.getValues() != null)
{
criterion.setValues(new MutableList<>(criterion.getValues()));
for(int i = 0; i < criterion.getValues().size(); i++)
{
Serializable value = criterion.getValues().get(i);
if(value instanceof String valueString && valueString.equals("${input." + inputVariableName + "}"))
{
criterion.getValues().set(i, renderWidgetInput.getQueryParams().get(inputVariableName));
}
}
}
}
return (filter);
}
}

View File

@ -0,0 +1,175 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.metadata.dashboard.nocode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.Aggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateResult;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetAggregate extends AbstractWidgetValueSourceWithFilter
{
private Aggregate aggregate;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public WidgetAggregate()
{
setType(getClass().getSimpleName());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Object evaluate(Map<String, Object> context, RenderWidgetInput input) throws QException
{
AggregateInput aggregateInput = new AggregateInput();
aggregateInput.setTableName(tableName);
aggregateInput.setAggregates(List.of(aggregate));
aggregateInput.setFilter(getEffectiveFilter(input));
AggregateOutput aggregateOutput = new AggregateAction().execute(aggregateInput);
List<AggregateResult> results = aggregateOutput.getResults();
if(results.isEmpty())
{
return (null);
}
else
{
AggregateResult aggregateResult = results.get(0);
return (aggregateResult.getAggregateValue(aggregate));
}
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public WidgetAggregate withName(String name)
{
setName(name);
return (this);
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
@Override
public WidgetAggregate withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
@Override
public WidgetAggregate withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Fluent setter for conditionalFilterList
*******************************************************************************/
@Override
public WidgetAggregate withConditionalFilterList(List<AbstractConditionalFilter> conditionalFilterList)
{
this.conditionalFilterList = conditionalFilterList;
return (this);
}
/*******************************************************************************
** Fluent setter to add a single conditionalFilter
*******************************************************************************/
public WidgetAggregate withConditionalFilter(AbstractConditionalFilter conditionalFilter)
{
if(this.conditionalFilterList == null)
{
this.conditionalFilterList = new ArrayList<>();
}
this.conditionalFilterList.add(conditionalFilter);
return (this);
}
/*******************************************************************************
** Getter for aggregate
*******************************************************************************/
public Aggregate getAggregate()
{
return (this.aggregate);
}
/*******************************************************************************
** Setter for aggregate
*******************************************************************************/
public void setAggregate(Aggregate aggregate)
{
this.aggregate = aggregate;
}
/*******************************************************************************
** Fluent setter for aggregate
*******************************************************************************/
public WidgetAggregate withAggregate(Aggregate aggregate)
{
this.aggregate = aggregate;
return (this);
}
}

View File

@ -22,12 +22,15 @@
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -50,10 +53,20 @@ public class WidgetCalculation extends AbstractWidgetValueSource
{ {
Integer sum = 0; Integer sum = 0;
for(String valueName : valueNames) for(String valueName : valueNames)
{
try
{ {
Integer addend = ValueUtils.getValueAsInteger(context.get(valueName)); Integer addend = ValueUtils.getValueAsInteger(context.get(valueName));
sum += addend; sum += addend;
} }
catch(Exception e)
{
////////////////////////////////////////////////
// assume value to be null or 0, don't add it //
////////////////////////////////////////////////
e.printStackTrace();
}
}
return (sum); return (sum);
}), }),
@ -62,6 +75,30 @@ public class WidgetCalculation extends AbstractWidgetValueSource
Instant now = Instant.now(); Instant now = Instant.now();
Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0))); Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0)));
return (then.until(now, ChronoUnit.MINUTES)); return (then.until(now, ChronoUnit.MINUTES));
}),
AGE_SECONDS((List<String> valueNames, Map<String, Object> context) ->
{
Instant now = Instant.now();
Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0)));
return (then.until(now, ChronoUnit.SECONDS));
}),
PERCENT_CHANGE((List<String> valueNames, Map<String, Object> context) ->
{
BigDecimal current = ValueUtils.getValueAsBigDecimal(context.get(valueNames.get(0)));
BigDecimal previous = ValueUtils.getValueAsBigDecimal(context.get(valueNames.get(1)));
///////////////////////////////////////////////
// 100 * ( (current - previous) / previous ) //
///////////////////////////////////////////////
BigDecimal difference = current.subtract(previous);
if(BigDecimal.ZERO.equals(previous))
{
return (null);
}
BigDecimal quotient = difference.divide(previous, MathContext.DECIMAL32);
return new BigDecimal("100").multiply(quotient);
}); });
@ -105,7 +142,7 @@ public class WidgetCalculation extends AbstractWidgetValueSource
** **
*******************************************************************************/ *******************************************************************************/
@Override @Override
public Object evaluate(Map<String, Object> context) throws QException public Object evaluate(Map<String, Object> context, RenderWidgetInput input) throws QException
{ {
return (operator.execute(values, context)); return (operator.execute(values, context));
} }

View File

@ -22,22 +22,22 @@
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode; package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public class WidgetCount extends AbstractWidgetValueSource public class WidgetCount extends AbstractWidgetValueSourceWithFilter
{ {
private String tableName;
private QQueryFilter filter;
/******************************************************************************* /*******************************************************************************
@ -55,29 +55,18 @@ public class WidgetCount extends AbstractWidgetValueSource
** **
*******************************************************************************/ *******************************************************************************/
@Override @Override
public Object evaluate(Map<String, Object> context) throws QException public Object evaluate(Map<String, Object> context, RenderWidgetInput input) throws QException
{ {
// todo - look for params in the filter (fields or values)
// make sure to update it in supplementContext below too!!
CountInput countInput = new CountInput(); CountInput countInput = new CountInput();
countInput.setTableName(tableName); countInput.setTableName(tableName);
countInput.setFilter(filter); countInput.setFilter(getEffectiveFilter(input));
CountOutput countOutput = new CountAction().execute(countInput); CountOutput countOutput = new CountAction().execute(countInput);
return (countOutput.getCount()); return (countOutput.getCount());
} }
/*******************************************************************************
**
*******************************************************************************/
public void supplementContext(Map<String, Object> context)
{
context.put(getName() + ".filter", filter);
}
/******************************************************************************* /*******************************************************************************
** Fluent setter for name ** Fluent setter for name
*******************************************************************************/ *******************************************************************************/
@ -89,29 +78,10 @@ public class WidgetCount extends AbstractWidgetValueSource
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/******************************************************************************* /*******************************************************************************
** Fluent setter for tableName ** Fluent setter for tableName
*******************************************************************************/ *******************************************************************************/
@Override
public WidgetCount withTableName(String tableName) public WidgetCount withTableName(String tableName)
{ {
this.tableName = tableName; this.tableName = tableName;
@ -120,33 +90,41 @@ public class WidgetCount extends AbstractWidgetValueSource
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/******************************************************************************* /*******************************************************************************
** Fluent setter for filter ** Fluent setter for filter
*******************************************************************************/ *******************************************************************************/
@Override
public WidgetCount withFilter(QQueryFilter filter) public WidgetCount withFilter(QQueryFilter filter)
{ {
this.filter = filter; this.filter = filter;
return (this); return (this);
} }
/*******************************************************************************
** Fluent setter for conditionalFilterList
*******************************************************************************/
@Override
public WidgetCount withConditionalFilterList(List<AbstractConditionalFilter> conditionalFilterList)
{
this.conditionalFilterList = conditionalFilterList;
return (this);
}
/*******************************************************************************
** Fluent setter to add a single conditionalFilter
*******************************************************************************/
public WidgetCount withConditionalFilter(AbstractConditionalFilter conditionalFilter)
{
if(this.conditionalFilterList == null)
{
this.conditionalFilterList = new ArrayList<>();
}
this.conditionalFilterList.add(conditionalFilter);
return (this);
}
} }

View File

@ -28,17 +28,16 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public class WidgetQueryField extends AbstractWidgetValueSource public class WidgetQueryField extends AbstractWidgetValueSourceWithFilter
{ {
private String tableName;
private String selectFieldName; private String selectFieldName;
private QQueryFilter filter;
@ -57,13 +56,11 @@ public class WidgetQueryField extends AbstractWidgetValueSource
** **
*******************************************************************************/ *******************************************************************************/
@Override @Override
public Object evaluate(Map<String, Object> context) throws QException public Object evaluate(Map<String, Object> context, RenderWidgetInput input) throws QException
{ {
// todo - look for params in the filter (fields or values)
// make sure to update it in supplementContext below too!!
QueryInput queryInput = new QueryInput(); QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName); queryInput.setTableName(tableName);
queryInput.setFilter(filter); queryInput.setFilter(getEffectiveFilter(input));
queryInput.setLimit(1); queryInput.setLimit(1);
QueryOutput queryOutput = new QueryAction().execute(queryInput); QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords())) if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
@ -76,16 +73,6 @@ public class WidgetQueryField extends AbstractWidgetValueSource
/*******************************************************************************
**
*******************************************************************************/
public void supplementContext(Map<String, Object> context)
{
context.put(getName() + ".filter", filter);
}
/******************************************************************************* /*******************************************************************************
** Fluent setter for name ** Fluent setter for name
*******************************************************************************/ *******************************************************************************/
@ -97,26 +84,6 @@ public class WidgetQueryField extends AbstractWidgetValueSource
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/******************************************************************************* /*******************************************************************************
** Fluent setter for tableName ** Fluent setter for tableName
*******************************************************************************/ *******************************************************************************/
@ -128,26 +95,6 @@ public class WidgetQueryField extends AbstractWidgetValueSource
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/******************************************************************************* /*******************************************************************************
** Fluent setter for filter ** Fluent setter for filter
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,109 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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.actions.dashboard.widgets;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/*******************************************************************************
** Unit test for NoCodeWidgetVelocityUtils
*******************************************************************************/
class NoCodeWidgetVelocityUtilsTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFormatSecondsAsDuration()
{
int HOUR = 60 * 60;
int DAY = 24 * 60 * 60;
NoCodeWidgetVelocityUtils utils = new NoCodeWidgetVelocityUtils(null, null);
assertEquals("", utils.formatSecondsAsDuration(null));
assertEquals("0 seconds", utils.formatSecondsAsDuration(0));
assertEquals("1 second", utils.formatSecondsAsDuration(1));
assertEquals("59 seconds", utils.formatSecondsAsDuration(59));
assertEquals("1 minute", utils.formatSecondsAsDuration(60));
assertEquals("1 minute 1 second", utils.formatSecondsAsDuration(61));
assertEquals("2 minutes 1 second", utils.formatSecondsAsDuration(121));
assertEquals("2 minutes 2 seconds", utils.formatSecondsAsDuration(122));
assertEquals("3 minutes", utils.formatSecondsAsDuration(180));
assertEquals("1 hour", utils.formatSecondsAsDuration(HOUR));
assertEquals("1 hour 1 second", utils.formatSecondsAsDuration(HOUR + 1));
assertEquals("1 hour 1 minute", utils.formatSecondsAsDuration(HOUR + 60));
assertEquals("1 hour 1 minute 1 second", utils.formatSecondsAsDuration(HOUR + 60 + 1));
assertEquals("1 hour 2 minutes 1 second", utils.formatSecondsAsDuration(HOUR + 120 + 1));
assertEquals("2 hours 2 minutes 2 seconds", utils.formatSecondsAsDuration(2 * 60 * 60 + 120 + 2));
assertEquals("23 hours 59 minutes 59 seconds", utils.formatSecondsAsDuration(DAY - 1));
assertEquals("1 day", utils.formatSecondsAsDuration(DAY));
assertEquals("1 day 1 second", utils.formatSecondsAsDuration(DAY + 1));
assertEquals("1 day 1 minute", utils.formatSecondsAsDuration(DAY + 60));
assertEquals("1 day 1 hour 1 minute 1 second", utils.formatSecondsAsDuration(DAY + HOUR + 60 + 1));
assertEquals("2 days 2 hours 2 minutes 2 seconds", utils.formatSecondsAsDuration(2 * DAY + 2 * 60 * 60 + 120 + 2));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testFormatSecondsAsRoundedDuration()
{
int HOUR = 60 * 60;
int DAY = 24 * 60 * 60;
NoCodeWidgetVelocityUtils utils = new NoCodeWidgetVelocityUtils(null, null);
assertEquals("", utils.formatSecondsAsRoundedDuration(null));
assertEquals("0 seconds", utils.formatSecondsAsRoundedDuration(0));
assertEquals("1 second", utils.formatSecondsAsRoundedDuration(1));
assertEquals("59 seconds", utils.formatSecondsAsRoundedDuration(59));
assertEquals("1 minute", utils.formatSecondsAsRoundedDuration(60));
assertEquals("1 minute", utils.formatSecondsAsRoundedDuration(61));
assertEquals("2 minutes", utils.formatSecondsAsRoundedDuration(121));
assertEquals("2 minutes", utils.formatSecondsAsRoundedDuration(122));
assertEquals("3 minutes", utils.formatSecondsAsRoundedDuration(180));
assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR));
assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR + 1));
assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR + 60));
assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR + 60 + 1));
assertEquals("1 hour", utils.formatSecondsAsRoundedDuration(HOUR + 120 + 1));
assertEquals("2 hours", utils.formatSecondsAsRoundedDuration(2 * 60 * 60 + 120 + 2));
assertEquals("23 hours", utils.formatSecondsAsRoundedDuration(DAY - 1));
assertEquals("1 day", utils.formatSecondsAsRoundedDuration(DAY));
assertEquals("1 day", utils.formatSecondsAsRoundedDuration(DAY + 1));
assertEquals("1 day", utils.formatSecondsAsRoundedDuration(DAY + 60));
assertEquals("1 day", utils.formatSecondsAsRoundedDuration(DAY + HOUR + 60 + 1));
assertEquals("2 days", utils.formatSecondsAsRoundedDuration(2 * DAY + 2 * 60 * 60 + 120 + 2));
}
}

View File

@ -196,8 +196,8 @@ class QValueFormatterTest extends BaseTest
void testFormatDates() void testFormatDates()
{ {
assertEquals("2023-02-01", QValueFormatter.formatDate(LocalDate.of(2023, Month.FEBRUARY, 1))); assertEquals("2023-02-01", QValueFormatter.formatDate(LocalDate.of(2023, Month.FEBRUARY, 1)));
assertEquals("2023-02-01 7:15 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15))); assertEquals("2023-02-01 07:15 PM", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15)));
assertEquals("2023-02-01 7:15 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15), ZoneId.of("US/Central")))); assertEquals("2023-02-01 07:15 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15), ZoneId.of("US/Central"))));
} }
} }