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.io.Serializable;
import java.util.List; import java.util.List;
import java.util.function.Function; 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.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.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@ -62,7 +62,7 @@ public class QueryOutput extends AbstractActionOutput implements Serializable
storage = new QueryOutputList(); 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 java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey; 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.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
@ -59,6 +60,8 @@ public class QInstance
private Map<String, QProcessMetaData> processes = new LinkedHashMap<>(); private Map<String, QProcessMetaData> processes = new LinkedHashMap<>();
private Map<String, QAppMetaData> apps = 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? // todo - lock down the object (no more changes allowed) after it's been validated?
@JsonIgnore @JsonIgnore
@ -445,4 +448,60 @@ public class QInstance
{ {
this.authentication = authentication; 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 java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include; 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 String label;
private List<AppTreeNode> children = new ArrayList<>(); private List<AppTreeNode> children = new ArrayList<>();
private List<String> widgets = new ArrayList<>();
private String iconName; private String iconName;
@ -48,14 +50,19 @@ public class QFrontendAppMetaData
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public QFrontendAppMetaData(QAppChildMetaData appChildMetaData) public QFrontendAppMetaData(QAppMetaData appMetaData)
{ {
this.name = appChildMetaData.getName(); this.name = appMetaData.getName();
this.label = appChildMetaData.getLabel(); 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); 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 String parentAppName;
private QIcon icon; private QIcon icon;
private List<String> widgets;
/******************************************************************************* /*******************************************************************************
@ -235,4 +236,38 @@ public class QAppMetaData implements QAppChildMetaData
return (this); 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.io.Serializable;
import java.util.List; 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.AddAge;
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; 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.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; 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.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.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; 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 BasicETLProcess().defineProcessMetaData());
qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData()); qInstance.addProcess(new StreamedETLProcess().defineProcessMetaData());
defineWidgets(qInstance);
defineApps(qInstance); defineApps(qInstance);
return (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) .withName(APP_NAME_PEOPLE)
.withChild(qInstance.getTable(TABLE_NAME_PERSON)) .withChild(qInstance.getTable(TABLE_NAME_PERSON))
.withChild(qInstance.getTable(TABLE_NAME_PERSON_FILE)) .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() qInstance.addApp(new QAppMetaData()
.withName(APP_NAME_MISCELLANEOUS) .withName(APP_NAME_MISCELLANEOUS)

View File

@ -298,8 +298,6 @@ public class QueryManager
*******************************************************************************/ *******************************************************************************/
public static List<Map<String, Object>> executeStatementForRows(Connection connection, String sql, Object... params) throws SQLException public static List<Map<String, Object>> executeStatementForRows(Connection connection, String sql, Object... params) throws SQLException
{ {
throw (new NotImplementedException());
/*
List<Map<String, Object>> rs = new ArrayList<>(); List<Map<String, Object>> rs = new ArrayList<>();
PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params);
@ -318,7 +316,6 @@ public class QueryManager
} }
return (rs); return (rs);
*/
} }

View File

@ -36,6 +36,8 @@ import java.time.LocalTime;
import java.time.Month; import java.time.Month;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -381,4 +383,25 @@ class QueryManagerTest
assertEquals("Q", simpleEntity.get("CHAR_COL")); assertEquals("Q", simpleEntity.get("CHAR_COL"));
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryForRows() throws SQLException
{
Connection connection = getConnection();
QueryManager.executeUpdate(connection, """
INSERT INTO test_table
( int_col, datetime_col, char_col, date_col, time_col )
VALUES
( 47, '2022-08-10 19:22:08', 'Q', '2022-08-10', '19:22:08')
""");
List<Map<String, Object>> rows = QueryManager.executeStatementForRows(connection, "SELECT * FROM test_table");
assertNotNull(rows);
assertEquals(47, rows.get(0).get("INT_COL"));
assertEquals("Q", rows.get(0).get("CHAR_COL"));
}
} }

View File

@ -36,6 +36,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier; import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.dashboard.WidgetDataLoader;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
@ -278,6 +279,8 @@ public class QJavalinImplementation
}); });
}); });
get("/widget/{name}", QJavalinImplementation::widget);
//////////////////// ////////////////////
// process routes // // process routes //
//////////////////// ////////////////////
@ -662,6 +665,27 @@ public class QJavalinImplementation
/*******************************************************************************
** Load the data for a widget of a given name.
*******************************************************************************/
private static void widget(Context context)
{
try
{
InsertInput insertInput = new InsertInput(qInstance);
setupSession(context, insertInput);
Object widgetData = new WidgetDataLoader().execute(qInstance, insertInput.getSession(), context.pathParam("name"));
context.result(JsonUtils.toJson(widgetData));
}
catch(Exception e)
{
handleException(context, e);
}
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,56 @@
package com.kingsrook.qqq.backend.javalin;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.dashboard.AbstractWidgetRenderer;
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;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
/*******************************************************************************
** Sample bar chart widget
*******************************************************************************/
public class PersonsByCreateDateBarChart extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Object render(QInstance qInstance, QSession session) throws QException
{
try
{
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.getConnection(TestUtils.defineBackend());
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

@ -470,4 +470,21 @@ class QJavalinImplementationTest extends QJavalinTestBase
assertThat(response.getBody()).contains("Unsupported report format"); assertThat(response.getBody()).contains("Unsupported report format");
} }
/*******************************************************************************
**
*******************************************************************************/
@Test
void testWidget()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/widget/" + PersonsByCreateDateBarChart.class.getSimpleName()).asString();
assertEquals(200, response.getStatus());
JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
assertNotNull(jsonObject);
assertEquals("barChart", jsonObject.getString("type"));
assertNotNull(jsonObject.getString("title"));
assertNotNull(jsonObject.getJSONObject("barChartData"));
}
} }

View File

@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
@ -122,11 +123,24 @@ public class TestUtils
qInstance.addProcess(defineProcessSimpleSleep()); qInstance.addProcess(defineProcessSimpleSleep());
qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessScreenThenSleep());
qInstance.addProcess(defineProcessSimpleThrow()); qInstance.addProcess(defineProcessSimpleThrow());
defineWidgets(qInstance);
return (qInstance); return (qInstance);
} }
/*******************************************************************************
**
*******************************************************************************/
private static void defineWidgets(QInstance qInstance)
{
qInstance.addWidget(new QWidgetMetaData()
.withName(PersonsByCreateDateBarChart.class.getSimpleName())
.withCodeReference(new QCodeReference(PersonsByCreateDateBarChart.class, null)));
}
/******************************************************************************* /*******************************************************************************
** Define the authentication used in standard tests - using 'mock' type. ** Define the authentication used in standard tests - using 'mock' type.
** **

View File

@ -34,6 +34,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.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; 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.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.fields.QFieldType;
@ -56,6 +57,7 @@ import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.RecordFor
import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.FilesystemBackendMetaData; import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.FilesystemBackendMetaData;
import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.FilesystemTableBackendDetails; import com.kingsrook.qqq.backend.module.filesystem.local.model.metadata.FilesystemTableBackendDetails;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.sampleapp.dashboard.widgets.PersonsByCreateDateBarChart;
import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.Dotenv;
@ -114,6 +116,8 @@ public class SampleMetaDataProvider
qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessScreenThenSleep());
qInstance.addProcess(defineProcessSimpleThrow()); qInstance.addProcess(defineProcessSimpleThrow());
defineWidgets(qInstance);
defineApps(qInstance); defineApps(qInstance);
return (qInstance); return (qInstance);
@ -121,6 +125,18 @@ public class SampleMetaDataProvider
/*******************************************************************************
**
*******************************************************************************/
private static void defineWidgets(QInstance qInstance)
{
qInstance.addWidget(new QWidgetMetaData()
.withName(PersonsByCreateDateBarChart.class.getSimpleName())
.withCodeReference(new QCodeReference(PersonsByCreateDateBarChart.class, null)));
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -136,7 +152,9 @@ public class SampleMetaDataProvider
.withChild(qInstance.getTable(TABLE_NAME_CITY) .withChild(qInstance.getTable(TABLE_NAME_CITY)
.withIcon(new QIcon().withName("location_city"))) .withIcon(new QIcon().withName("location_city")))
.withChild(qInstance.getProcess(PROCESS_NAME_GREET_INTERACTIVE)) .withChild(qInstance.getProcess(PROCESS_NAME_GREET_INTERACTIVE))
.withIcon(new QIcon().withName("waving_hand"))); .withIcon(new QIcon().withName("waving_hand"))
.withWidgets(List.of(PersonsByCreateDateBarChart.class.getSimpleName()))
);
qInstance.addApp(new QAppMetaData() qInstance.addApp(new QAppMetaData()
.withName(APP_NAME_PEOPLE) .withName(APP_NAME_PEOPLE)

View File

@ -0,0 +1,66 @@
package com.kingsrook.sampleapp.dashboard.widgets;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.dashboard.AbstractWidgetRenderer;
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;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.sampleapp.SampleMetaDataProvider;
/*******************************************************************************
**
*******************************************************************************/
public class PersonsByCreateDateBarChart extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Object render(QInstance qInstance, QSession session) throws QException
{
try
{
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.getConnection(SampleMetaDataProvider.defineRdbmsBackend());
String sql = """
SELECT
COUNT(*) AS count,
DATE_FORMAT(create_date, '%m-%Y') AS month
FROM
person
GROUP BY
2
ORDER BY
2
""";
List<Map<String, Object>> rows = QueryManager.executeStatementForRows(connection, sql);
List<String> labels = new ArrayList<>();
List<Number> data = new ArrayList<>();
for(Map<String, Object> row : rows)
{
labels.add(ValueUtils.getValueAsString(row.get("month")));
data.add(ValueUtils.getValueAsInteger(row.get("count")));
}
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,34 @@
package com.kingsrook.sampleapp.dashboard.widgets;
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.session.QSession;
import com.kingsrook.sampleapp.SampleMetaDataProvider;
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 PersonsByCreateDateBarChart
*******************************************************************************/
class PersonsByCreateDateBarChartTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test() throws QException
{
Object widgetData = new PersonsByCreateDateBarChart().render(SampleMetaDataProvider.defineInstance(), new QSession());
assertThat(widgetData).isInstanceOf(BarChart.class);
BarChart barChart = (BarChart) widgetData;
assertEquals("barChart", barChart.getType());
assertThat(barChart.getTitle()).isNotBlank();
assertNotNull(barChart.getBarChartData());
}
}