QQQ-38 Initial build of app home page widgets

This commit is contained in:
2022-08-22 10:38:02 -05:00
parent 99f724e2c2
commit 937304e7f1
21 changed files with 939 additions and 13 deletions

View File

@ -93,4 +93,42 @@ public class QCodeLoader
}
}
/*******************************************************************************
**
*******************************************************************************/
public static <T> T getAdHoc(Class<T> expectedType, QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA code references are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((T) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer: " + codeReference);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
}

View File

@ -0,0 +1,20 @@
package com.kingsrook.qqq.backend.core.actions.dashboard;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
public abstract Object render(QInstance qInstance, QSession session) throws QException;
}

View File

@ -0,0 +1,26 @@
package com.kingsrook.qqq.backend.core.actions.dashboard;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
**
*******************************************************************************/
public class WidgetDataLoader
{
/*******************************************************************************
**
*******************************************************************************/
public Object execute(QInstance qInstance, QSession session, String name) throws QException
{
QWidgetMetaData widget = qInstance.getWidget(name);
AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, widget.getCodeReference());
return (widgetRenderer.render(qInstance, session));
}
}

View File

@ -25,8 +25,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.CustomizerLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.Customizers;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.apache.logging.log4j.LogManager;
@ -62,7 +62,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
storage = new QueryOutputList();
}
postQueryRecordCustomizer = (Function<QRecord, QRecord>) CustomizerLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD);
postQueryRecordCustomizer = (Function<QRecord, QRecord>) QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD);
}

View File

@ -0,0 +1,299 @@
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
import java.util.List;
/*******************************************************************************
**
*******************************************************************************/
public class BarChart
{
/*
type: "barChart",
title: "Parcel Invoice Lines per Month",
barChartData: {
labels: ["Feb 22", "Mar 22", "Apr 22", "May 22", "Jun 22", "Jul 22", "Aug 22"],
datasets: {label: "Parcel Invoice Lines", data: [50000, 22000, 11111, 22333, 40404, 9876, 2355]},
},
*/
private String type = "barChart";
private String title;
private Data barChartData;
/*******************************************************************************
**
*******************************************************************************/
public BarChart(String title, String seriesLabel, List<String> labels, List<Number> data)
{
setTitle(title);
setBarChartData(new BarChart.Data()
.withLabels(labels)
.withDatasets(new BarChart.Data.DataSet()
.withLabel("Parcel Invoice Lines")
.withData(data)));
}
/*******************************************************************************
** Getter for title
**
*******************************************************************************/
public String getTitle()
{
return title;
}
/*******************************************************************************
** Setter for title
**
*******************************************************************************/
public void setTitle(String title)
{
this.title = title;
}
/*******************************************************************************
** Fluent setter for title
**
*******************************************************************************/
public BarChart withTitle(String title)
{
this.title = title;
return (this);
}
/*******************************************************************************
** Getter for barChartData
**
*******************************************************************************/
public Data getBarChartData()
{
return barChartData;
}
/*******************************************************************************
** Setter for barChartData
**
*******************************************************************************/
public void setBarChartData(Data barChartData)
{
this.barChartData = barChartData;
}
/*******************************************************************************
** Fluent setter for barChartData
**
*******************************************************************************/
public BarChart withBarChartData(Data barChartData)
{
this.barChartData = barChartData;
return (this);
}
/*******************************************************************************
** Getter for type
**
*******************************************************************************/
public String getType()
{
return type;
}
/*******************************************************************************
** Setter for type
**
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
**
*******************************************************************************/
public BarChart withType(String type)
{
this.type = type;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public static class Data
{
private List<String> labels;
private DataSet datasets;
/*******************************************************************************
** Getter for labels
**
*******************************************************************************/
public List<String> getLabels()
{
return labels;
}
/*******************************************************************************
** Setter for labels
**
*******************************************************************************/
public void setLabels(List<String> labels)
{
this.labels = labels;
}
/*******************************************************************************
** Fluent setter for labels
**
*******************************************************************************/
public Data withLabels(List<String> labels)
{
this.labels = labels;
return (this);
}
/*******************************************************************************
** Getter for datasets
**
*******************************************************************************/
public DataSet getDatasets()
{
return datasets;
}
/*******************************************************************************
** Setter for datasets
**
*******************************************************************************/
public void setDatasets(DataSet datasets)
{
this.datasets = datasets;
}
/*******************************************************************************
** Fluent setter for datasets
**
*******************************************************************************/
public Data withDatasets(DataSet datasets)
{
this.datasets = datasets;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public static class DataSet
{
private String label;
private List<Number> data;
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return label;
}
/*******************************************************************************
** Setter for label
**
*******************************************************************************/
public void setLabel(String label)
{
this.label = label;
}
/*******************************************************************************
** Fluent setter for label
**
*******************************************************************************/
public DataSet withLabel(String label)
{
this.label = label;
return (this);
}
/*******************************************************************************
** Getter for data
**
*******************************************************************************/
public List<Number> getData()
{
return data;
}
/*******************************************************************************
** Setter for data
**
*******************************************************************************/
public void setData(List<Number> data)
{
this.data = data;
}
/*******************************************************************************
** Fluent setter for data
**
*******************************************************************************/
public DataSet withData(List<Number> data)
{
this.data = data;
return (this);
}
}
}
}

View File

@ -29,6 +29,7 @@ import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
@ -59,6 +60,8 @@ public class QInstance
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
private Map<String, QAppMetaData> apps = new LinkedHashMap<>();
private Map<String, QWidgetMetaData> widgets = new LinkedHashMap<>();
// todo - lock down the object (no more changes allowed) after it's been validated?
@JsonIgnore
@ -445,4 +448,60 @@ public class QInstance
{
this.authentication = authentication;
}
/*******************************************************************************
** Getter for widgets
**
*******************************************************************************/
public Map<String, QWidgetMetaData> getWidgets()
{
return widgets;
}
/*******************************************************************************
** Setter for widgets
**
*******************************************************************************/
public void setWidgets(Map<String, QWidgetMetaData> widgets)
{
this.widgets = widgets;
}
/*******************************************************************************
**
*******************************************************************************/
public void addWidget(QWidgetMetaData widget)
{
this.addWidget(widget.getName(), widget);
}
/*******************************************************************************
**
*******************************************************************************/
public void addWidget(String name, QWidgetMetaData widget)
{
if(this.widgets.containsKey(name))
{
throw (new IllegalArgumentException("Attempted to add a second widget with name: " + name));
}
this.widgets.put(name, widget);
}
/*******************************************************************************
**
*******************************************************************************/
public QWidgetMetaData getWidget(String name)
{
return (this.widgets.get(name));
}
}

View File

@ -0,0 +1,83 @@
package com.kingsrook.qqq.backend.core.model.metadata.dashboard;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
**
*******************************************************************************/
public class QWidgetMetaData
{
private String name;
private QCodeReference codeReference;
/*******************************************************************************
** Getter for name
**
*******************************************************************************/
public String getName()
{
return name;
}
/*******************************************************************************
** Setter for name
**
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
**
*******************************************************************************/
public QWidgetMetaData withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for codeReference
**
*******************************************************************************/
public QCodeReference getCodeReference()
{
return codeReference;
}
/*******************************************************************************
** Setter for codeReference
**
*******************************************************************************/
public void setCodeReference(QCodeReference codeReference)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** Fluent setter for codeReference
**
*******************************************************************************/
public QWidgetMetaData withCodeReference(QCodeReference codeReference)
{
this.codeReference = codeReference;
return (this);
}
}

View File

@ -26,7 +26,8 @@ import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@ -40,6 +41,7 @@ public class QFrontendAppMetaData
private String label;
private List<AppTreeNode> children = new ArrayList<>();
private List<String> widgets = new ArrayList<>();
private String iconName;
@ -48,14 +50,19 @@ public class QFrontendAppMetaData
/*******************************************************************************
**
*******************************************************************************/
public QFrontendAppMetaData(QAppChildMetaData appChildMetaData)
public QFrontendAppMetaData(QAppMetaData appMetaData)
{
this.name = appChildMetaData.getName();
this.label = appChildMetaData.getLabel();
this.name = appMetaData.getName();
this.label = appMetaData.getLabel();
if(appChildMetaData.getIcon() != null)
if(appMetaData.getIcon() != null)
{
this.iconName = appChildMetaData.getIcon().getName();
this.iconName = appMetaData.getIcon().getName();
}
if(CollectionUtils.nullSafeHasContents(appMetaData.getWidgets()))
{
this.widgets = appMetaData.getWidgets();
}
}
@ -127,4 +134,15 @@ public class QFrontendAppMetaData
}
children.add(childAppTreeNode);
}
/*******************************************************************************
** Getter for widgets
**
*******************************************************************************/
public List<String> getWidgets()
{
return widgets;
}
}

View File

@ -40,6 +40,7 @@ public class QAppMetaData implements QAppChildMetaData
private String parentAppName;
private QIcon icon;
private List<String> widgets;
/*******************************************************************************
@ -235,4 +236,38 @@ public class QAppMetaData implements QAppChildMetaData
return (this);
}
/*******************************************************************************
** Getter for widgets
**
*******************************************************************************/
public List<String> getWidgets()
{
return widgets;
}
/*******************************************************************************
** Setter for widgets
**
*******************************************************************************/
public void setWidgets(List<String> widgets)
{
this.widgets = widgets;
}
/*******************************************************************************
** Fluent setter for widgets
**
*******************************************************************************/
public QAppMetaData withWidgets(List<String> widgets)
{
this.widgets = widgets;
return (this);
}
}

View File

@ -0,0 +1,50 @@
package com.kingsrook.qqq.backend.core.actions.dashboard;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.BarChart;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** Sample bar chart widget
*******************************************************************************/
public class PersonsByCreateDateBarChart extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Object render(QInstance qInstance, QSession session) throws QException
{
try
{
List<String> labels = new ArrayList<>();
List<Number> data = new ArrayList<>();
labels.add("Jan. 2022");
data.add(17);
labels.add("Feb. 2022");
data.add(42);
labels.add("Mar. 2022");
data.add(47);
labels.add("Apr. 2022");
data.add(0);
labels.add("May 2022");
data.add(64);
return (new BarChart("Persons created per Month", "Person records", labels, data));
}
catch(Exception e)
{
throw (new QException("Error rendering widget", e));
}
}
}

View File

@ -0,0 +1,32 @@
package com.kingsrook.qqq.backend.core.actions.dashboard;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.BarChart;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/*******************************************************************************
** Unit test for WidgetDataLoader
*******************************************************************************/
class WidgetDataLoaderTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
Object widgetData = new WidgetDataLoader().execute(TestUtils.defineInstance(), TestUtils.getMockSession(), PersonsByCreateDateBarChart.class.getSimpleName());
assertThat(widgetData).isInstanceOf(BarChart.class);
BarChart barChart = (BarChart) widgetData;
assertEquals("barChart", barChart.getType());
assertThat(barChart.getTitle()).isNotBlank();
assertNotNull(barChart.getBarChartData());
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.utils;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarChart;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -38,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
@ -115,6 +117,7 @@ public class TestUtils
qInstance.addProcess(new BasicETLProcess().defineProcessMetaData());
qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData());
defineWidgets(qInstance);
defineApps(qInstance);
return (qInstance);
@ -122,6 +125,19 @@ public class TestUtils
/*******************************************************************************
**
*******************************************************************************/
private static void defineWidgets(QInstance qInstance)
{
qInstance.addWidget(new QWidgetMetaData()
.withName(PersonsByCreateDateBarChart.class.getSimpleName())
.withCodeReference(new QCodeReference(PersonsByCreateDateBarChart.class, null)));
}
/*******************************************************************************
**
*******************************************************************************/
@ -136,7 +152,8 @@ public class TestUtils
.withName(APP_NAME_PEOPLE)
.withChild(qInstance.getTable(TABLE_NAME_PERSON))
.withChild(qInstance.getTable(TABLE_NAME_PERSON_FILE))
.withChild(qInstance.getApp(APP_NAME_GREETINGS)));
.withChild(qInstance.getApp(APP_NAME_GREETINGS))
.withWidgets(List.of(PersonsByCreateDateBarChart.class.getSimpleName())));
qInstance.addApp(new QAppMetaData()
.withName(APP_NAME_MISCELLANEOUS)