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
{
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);
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.QNoCodeWidgetMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -58,22 +59,30 @@ public class NoCodeWidgetRenderer extends AbstractWidgetRenderer
// build context by evaluating all values //
////////////////////////////////////////////
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())
{
LOG.trace("Computing: " + valueSource.getType() + " named " + valueSource.getName() + "...");
Object value = valueSource.evaluate(context);
LOG.trace("Computed: " + valueSource.getName() + " = " + value);
context.put(valueSource.getName(), value);
context.put(valueSource.getName() + ".source", valueSource);
try
{
LOG.trace("Computing: " + valueSource.getType() + " named " + valueSource.getName() + "...");
Object value = valueSource.evaluate(context, input);
LOG.trace("Computed: " + valueSource.getName() + " = " + value);
context.put(valueSource.getName(), value);
context.put(valueSource.getName() + ".source", valueSource);
}
catch(Exception e)
{
LOG.warn("Error evaluating widget value source", e, logPair("widgetName", input.getWidgetMetaData().getName()), logPair("valueSourceName", valueSource.getName()));
}
}
/////////////////////////////////////////////
// set default utils object in context too //
/////////////////////////////////////////////
context.put("utils", new NoCodeWidgetVelocityUtils(context));
/////////////////////////////////////////////
// build content by evaluating all outputs //
/////////////////////////////////////////////

View File

@ -22,11 +22,18 @@
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 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.logging.QLogger;
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.utils.StringUtils;
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 final Map<String, Object> context;
private Map<String, Object> context;
private RenderWidgetInput input;
@ -51,9 +55,10 @@ public class NoCodeWidgetVelocityUtils
** Constructor
**
*******************************************************************************/
public NoCodeWidgetVelocityUtils(Map<String, Object> context)
public NoCodeWidgetVelocityUtils(Map<String, Object> context, RenderWidgetInput input)
{
this.context = context;
this.input = input;
}
@ -61,7 +66,7 @@ public class NoCodeWidgetVelocityUtils
/*******************************************************************************
**
*******************************************************************************/
public final String errorIcon()
public String errorIcon()
{
return ("""
<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 ("""
<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");
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));
}
catch(Exception e)
@ -110,4 +255,14 @@ public class NoCodeWidgetVelocityUtils
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 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a");
private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a z");
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm a");
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 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 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

@ -33,9 +33,13 @@ public class HtmlWrapper
private String prefix;
private String suffix;
public static final HtmlWrapper SUBHEADER = new HtmlWrapper("<h4>", "</h4>");
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 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_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;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
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;
@ -51,8 +54,18 @@ public class WidgetCalculation extends AbstractWidgetValueSource
Integer sum = 0;
for(String valueName : valueNames)
{
Integer addend = ValueUtils.getValueAsInteger(context.get(valueName));
sum += addend;
try
{
Integer addend = ValueUtils.getValueAsInteger(context.get(valueName));
sum += addend;
}
catch(Exception e)
{
////////////////////////////////////////////////
// assume value to be null or 0, don't add it //
////////////////////////////////////////////////
e.printStackTrace();
}
}
return (sum);
}),
@ -62,6 +75,30 @@ public class WidgetCalculation extends AbstractWidgetValueSource
Instant now = Instant.now();
Instant then = ValueUtils.getValueAsInstant(context.get(valueNames.get(0)));
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
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));
}

View File

@ -22,22 +22,22 @@
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.CountAction;
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.CountOutput;
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
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.setTableName(tableName);
countInput.setFilter(filter);
countInput.setFilter(getEffectiveFilter(input));
CountOutput countOutput = new CountAction().execute(countInput);
return (countOutput.getCount());
}
/*******************************************************************************
**
*******************************************************************************/
public void supplementContext(Map<String, Object> context)
{
context.put(getName() + ".filter", filter);
}
/*******************************************************************************
** 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
*******************************************************************************/
@Override
public WidgetCount withTableName(String 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
*******************************************************************************/
@Override
public WidgetCount withFilter(QQueryFilter filter)
{
this.filter = filter;
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.QueryInput;
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;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetQueryField extends AbstractWidgetValueSource
public class WidgetQueryField extends AbstractWidgetValueSourceWithFilter
{
private String tableName;
private String selectFieldName;
private QQueryFilter filter;
private String selectFieldName;
@ -57,13 +56,11 @@ public class WidgetQueryField extends AbstractWidgetValueSource
**
*******************************************************************************/
@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.setTableName(tableName);
queryInput.setFilter(filter);
queryInput.setFilter(getEffectiveFilter(input));
queryInput.setLimit(1);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
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
*******************************************************************************/
@ -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
*******************************************************************************/
@ -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
*******************************************************************************/

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()
{
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 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", QValueFormatter.formatDateTime(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15)));
assertEquals("2023-02-01 07:15 PM CST", QValueFormatter.formatDateTimeWithZone(ZonedDateTime.of(LocalDateTime.of(2023, Month.FEBRUARY, 1, 19, 15), ZoneId.of("US/Central"))));
}
}