diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java index 8e7c0dfc..31192c75 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/customizers/QCodeLoader.java @@ -93,4 +93,42 @@ public class QCodeLoader } } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static T getAdHoc(Class 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); + } + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractWidgetRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractWidgetRenderer.java new file mode 100644 index 00000000..bf8ca097 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractWidgetRenderer.java @@ -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; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoader.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoader.java new file mode 100644 index 00000000..a0e5d214 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoader.java @@ -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)); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java index 96340a69..9412e0d0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutput.java @@ -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) CustomizerLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD); + postQueryRecordCustomizer = (Function) QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), Customizers.POST_QUERY_RECORD); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/BarChart.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/BarChart.java new file mode 100644 index 00000000..f71f0c66 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/BarChart.java @@ -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 labels, List 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 labels; + private DataSet datasets; + + + + /******************************************************************************* + ** Getter for labels + ** + *******************************************************************************/ + public List getLabels() + { + return labels; + } + + + + /******************************************************************************* + ** Setter for labels + ** + *******************************************************************************/ + public void setLabels(List labels) + { + this.labels = labels; + } + + + + /******************************************************************************* + ** Fluent setter for labels + ** + *******************************************************************************/ + public Data withLabels(List 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 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 getData() + { + return data; + } + + + + /******************************************************************************* + ** Setter for data + ** + *******************************************************************************/ + public void setData(List data) + { + this.data = data; + } + + + + /******************************************************************************* + ** Fluent setter for data + ** + *******************************************************************************/ + public DataSet withData(List data) + { + this.data = data; + return (this); + } + } + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 3712d077..02718a01 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -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 processes = new LinkedHashMap<>(); private Map apps = new LinkedHashMap<>(); + private Map 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 getWidgets() + { + return widgets; + } + + + + /******************************************************************************* + ** Setter for widgets + ** + *******************************************************************************/ + public void setWidgets(Map 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)); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java new file mode 100644 index 00000000..0efb0dfe --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java @@ -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); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java index 6f792f97..a4bd3ba4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java @@ -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 children = new ArrayList<>(); + private List 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 getWidgets() + { + return widgets; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java index 36ac02d6..be38cb87 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java @@ -40,6 +40,7 @@ public class QAppMetaData implements QAppChildMetaData private String parentAppName; private QIcon icon; + private List widgets; /******************************************************************************* @@ -235,4 +236,38 @@ public class QAppMetaData implements QAppChildMetaData return (this); } + + + + /******************************************************************************* + ** Getter for widgets + ** + *******************************************************************************/ + public List getWidgets() + { + return widgets; + } + + + + /******************************************************************************* + ** Setter for widgets + ** + *******************************************************************************/ + public void setWidgets(List widgets) + { + this.widgets = widgets; + } + + + /******************************************************************************* + ** Fluent setter for widgets + ** + *******************************************************************************/ + public QAppMetaData withWidgets(List widgets) + { + this.widgets = widgets; + return (this); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/PersonsByCreateDateBarChart.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/PersonsByCreateDateBarChart.java new file mode 100644 index 00000000..da2c1ff7 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/PersonsByCreateDateBarChart.java @@ -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 labels = new ArrayList<>(); + List 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)); + } + } +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoaderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoaderTest.java new file mode 100644 index 00000000..c571e361 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoaderTest.java @@ -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()); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 915498d7..6b12b83b 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -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) diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java index 0a90182f..ae9ac969 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManager.java @@ -298,8 +298,6 @@ public class QueryManager *******************************************************************************/ public static List> executeStatementForRows(Connection connection, String sql, Object... params) throws SQLException { - throw (new NotImplementedException()); - /* List> rs = new ArrayList<>(); PreparedStatement statement = prepareStatementAndBindParams(connection, sql, params); @@ -318,7 +316,6 @@ public class QueryManager } return (rs); - */ } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java index b7be5476..5930ef84 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/QueryManagerTest.java @@ -36,6 +36,8 @@ import java.time.LocalTime; import java.time.Month; import java.time.OffsetDateTime; import java.util.GregorianCalendar; +import java.util.List; +import java.util.Map; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -381,4 +383,25 @@ class QueryManagerTest 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> 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")); + } + } \ No newline at end of file diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 72e0ee79..a780c379 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -36,6 +36,7 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; 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.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; @@ -278,6 +279,8 @@ public class QJavalinImplementation }); }); + get("/widget/{name}", QJavalinImplementation::widget); + //////////////////// // 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); + } + } + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/PersonsByCreateDateBarChart.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/PersonsByCreateDateBarChart.java new file mode 100644 index 00000000..09814113 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/PersonsByCreateDateBarChart.java @@ -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 labels = new ArrayList<>(); + List 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)); + } + } +} diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index a7351f52..76b660d7 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -470,4 +470,21 @@ class QJavalinImplementationTest extends QJavalinTestBase assertThat(response.getBody()).contains("Unsupported report format"); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWidget() + { + HttpResponse 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")); + } + } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index 818c2b53..831ecd25 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -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.actions.processes.BackendStep; 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.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; @@ -122,11 +123,24 @@ public class TestUtils qInstance.addProcess(defineProcessSimpleSleep()); qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessSimpleThrow()); + defineWidgets(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. ** diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java index 7c0e5c00..76086502 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java @@ -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.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.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.FilesystemTableBackendDetails; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; +import com.kingsrook.sampleapp.dashboard.widgets.PersonsByCreateDateBarChart; import io.github.cdimascio.dotenv.Dotenv; @@ -114,6 +116,8 @@ public class SampleMetaDataProvider qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessSimpleThrow()); + defineWidgets(qInstance); + defineApps(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) .withIcon(new QIcon().withName("location_city"))) .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() .withName(APP_NAME_PEOPLE) diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChart.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChart.java new file mode 100644 index 00000000..5d7c5270 --- /dev/null +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChart.java @@ -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> rows = QueryManager.executeStatementForRows(connection, sql); + + List labels = new ArrayList<>(); + List data = new ArrayList<>(); + + for(Map 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)); + } + } + +} diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChartTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChartTest.java new file mode 100644 index 00000000..09d8a3ae --- /dev/null +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChartTest.java @@ -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()); + } + +} \ No newline at end of file