diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml index 268945bc..8a952f04 100644 --- a/qqq-backend-core/pom.xml +++ b/qqq-backend-core/pom.xml @@ -36,11 +36,26 @@ + + + + software.amazon.awssdk + bom + 2.17.259 + pom + import + + + + + software.amazon.awssdk + quicksight + com.fasterxml.jackson.core jackson-databind 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 4d478757..a0d3ec0a 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 @@ -103,6 +103,7 @@ public class QCodeLoader /******************************************************************************* ** *******************************************************************************/ + @SuppressWarnings("unchecked") public static T getBackendStep(Class expectedType, QCodeReference codeReference) { if(codeReference == null) @@ -121,9 +122,46 @@ public class QCodeLoader try { Class customizerClass = Class.forName(codeReference.getName()); - @SuppressWarnings("unchecked") - T t = (T) customizerClass.getConstructor().newInstance(); - return t; + 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); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("unchecked") + 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) { 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..7919b928 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/AbstractWidgetRenderer.java @@ -0,0 +1,21 @@ +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.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.session.QSession; + + +/******************************************************************************* + ** + *******************************************************************************/ +public abstract class AbstractWidgetRenderer +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public abstract Object render(QInstance qInstance, QSession session, QWidgetMetaDataInterface qWidgetMetaData) throws QException; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java new file mode 100644 index 00000000..74687c75 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/QuickSightChartRenderer.java @@ -0,0 +1,77 @@ +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.QuickSightChart; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QuickSightChartMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.quicksight.QuickSightClient; +import software.amazon.awssdk.services.quicksight.model.GenerateEmbedUrlForRegisteredUserRequest; +import software.amazon.awssdk.services.quicksight.model.GenerateEmbedUrlForRegisteredUserResponse; +import software.amazon.awssdk.services.quicksight.model.RegisteredUserDashboardEmbeddingConfiguration; +import software.amazon.awssdk.services.quicksight.model.RegisteredUserEmbeddingExperienceConfiguration; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QuickSightChartRenderer extends AbstractWidgetRenderer +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object render(QInstance qInstance, QSession session, QWidgetMetaDataInterface metaData) throws QException + { + try + { + QuickSightChartMetaData quickSightMetaData = (QuickSightChartMetaData) metaData; + QuickSightClient quickSightClient = getQuickSightClient(quickSightMetaData); + + final RegisteredUserEmbeddingExperienceConfiguration experienceConfiguration = RegisteredUserEmbeddingExperienceConfiguration.builder() + .dashboard( + RegisteredUserDashboardEmbeddingConfiguration.builder() + .initialDashboardId(quickSightMetaData.getDashboardId()) + .build()) + .build(); + + final GenerateEmbedUrlForRegisteredUserRequest generateEmbedUrlForRegisteredUserRequest = GenerateEmbedUrlForRegisteredUserRequest.builder() + .awsAccountId(quickSightMetaData.getAccountId()) + .userArn(quickSightMetaData.getUserArn()) + .experienceConfiguration(experienceConfiguration) + .build(); + + final GenerateEmbedUrlForRegisteredUserResponse generateEmbedUrlForRegisteredUserResponse = quickSightClient.generateEmbedUrlForRegisteredUser(generateEmbedUrlForRegisteredUserRequest); + + String embedUrl = generateEmbedUrlForRegisteredUserResponse.embedUrl(); + return (new QuickSightChart(metaData.getName(), quickSightMetaData.getLabel(), embedUrl)); + } + catch(Exception e) + { + throw (new QException("Error rendering widget", e)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QuickSightClient getQuickSightClient(QuickSightChartMetaData metaData) + { + AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(metaData.getAccessKey(), metaData.getSecretKey()); + + QuickSightClient amazonQuickSightClient = QuickSightClient.builder() + .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) + .region(Region.of(metaData.getRegion())) + .build(); + + return (amazonQuickSightClient); + } + +} 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..de346886 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/dashboard/WidgetDataLoader.java @@ -0,0 +1,27 @@ +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.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.session.QSession; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class WidgetDataLoader +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public Object execute(QInstance qInstance, QSession session, String name) throws QException + { + QWidgetMetaDataInterface widget = qInstance.getWidget(name); + AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, widget.getCodeReference()); + Object w = widgetRenderer.render(qInstance, session, widget); + return (widgetRenderer.render(qInstance, session, widget)); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java index d3ae8884..7549d047 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreter.java @@ -23,9 +23,12 @@ package com.kingsrook.qqq.backend.core.instances; import java.lang.reflect.Method; +import java.util.HashMap; import java.util.Locale; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; +import io.github.cdimascio.dotenv.Dotenv; +import io.github.cdimascio.dotenv.DotenvEntry; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -45,7 +48,22 @@ public class QMetaDataVariableInterpreter { private static final Logger LOG = LogManager.getLogger(QMetaDataVariableInterpreter.class); - private Map customEnvironment; + private Map environmentOverrides; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QMetaDataVariableInterpreter() + { + environmentOverrides = new HashMap<>(); + Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load(); + for(DotenvEntry e : dotenv.entries()) + { + environmentOverrides.put(e.getKey(), e.getValue()); + } + } @@ -86,7 +104,7 @@ public class QMetaDataVariableInterpreter ////////////////////////////////////////////////////////////////////////////////////////////// // get the value - if it's null, move on, else, interpret it, and put it back in the object // ////////////////////////////////////////////////////////////////////////////////////////////// - Object value = getter.invoke(o); + Object value = getter.invoke(o); if(value == null) { continue; @@ -124,7 +142,7 @@ public class QMetaDataVariableInterpreter if(value.startsWith(envPrefix) && value.endsWith("}")) { String envVarName = value.substring(envPrefix.length()).replaceFirst("}$", ""); - String envValue = getEnvironment().get(envVarName); + String envValue = getEnvironmentVariable(envVarName); return (envValue); } @@ -149,13 +167,13 @@ public class QMetaDataVariableInterpreter /******************************************************************************* - ** Setter for customEnvironment - protected - meant to be called (at least at this + ** Setter for environmentOverrides - protected - meant to be called (at least at this ** time), only in unit test ** *******************************************************************************/ - protected void setCustomEnvironment(Map customEnvironment) + protected void setEnvironmentOverrides(Map environmentOverrides) { - this.customEnvironment = customEnvironment; + this.environmentOverrides = environmentOverrides; } @@ -163,13 +181,13 @@ public class QMetaDataVariableInterpreter /******************************************************************************* ** *******************************************************************************/ - private Map getEnvironment() + private String getEnvironmentVariable(String key) { - if(this.customEnvironment != null) + if(this.environmentOverrides.containsKey(key)) { - return (this.customEnvironment); + return (this.environmentOverrides.get(key)); } - return System.getenv(); + return System.getenv(key); } } 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..2d8e8667 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/BarChart.java @@ -0,0 +1,276 @@ +package com.kingsrook.qqq.backend.core.model.dashboard.widgets; + + +import java.util.List; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class BarChart implements QWidget +{ + + /* + 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 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(seriesLabel) + .withData(data))); + } + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public String getType() + { + return "barChart"; + } + + + + /******************************************************************************* + ** 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); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + 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/dashboard/widgets/QWidget.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/QWidget.java new file mode 100644 index 00000000..8c76fc52 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/QWidget.java @@ -0,0 +1,14 @@ +package com.kingsrook.qqq.backend.core.model.dashboard.widgets; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface QWidget +{ + /******************************************************************************* + ** Getter for type + *******************************************************************************/ + String getType(); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/QuickSightChart.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/QuickSightChart.java new file mode 100644 index 00000000..9cccd508 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/dashboard/widgets/QuickSightChart.java @@ -0,0 +1,139 @@ +package com.kingsrook.qqq.backend.core.model.dashboard.widgets; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QuickSightChart implements QWidget +{ + private String label; + private String name; + private String url; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QuickSightChart(String name, String label, String url) + { + this.url = url; + this.name = name; + this.label = label; + } + + + + /******************************************************************************* + ** Getter for type + ** + *******************************************************************************/ + public String getType() + { + return "quickSightChart"; + } + + + + /******************************************************************************* + ** Getter for url + ** + *******************************************************************************/ + public String getUrl() + { + return url; + } + + + + /******************************************************************************* + ** Setter for url + ** + *******************************************************************************/ + public void setUrl(String url) + { + this.url = url; + } + + + + /******************************************************************************* + ** Fluent setter for url + ** + *******************************************************************************/ + public QuickSightChart withUrl(String url) + { + this.url = url; + return (this); + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return name; + } + + + + /******************************************************************************* + ** Setter for name + ** + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QuickSightChart withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public QuickSightChart withLabel(String label) + { + this.label = label; + 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 c2b08d6b..c221d013 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 @@ -30,6 +30,7 @@ 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.automation.QAutomationProviderMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; 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; @@ -62,6 +63,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 @@ -524,4 +527,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(QWidgetMetaDataInterface widget) + { + this.addWidget(widget.getName(), widget); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addWidget(String name, QWidgetMetaDataInterface widget) + { + if(this.widgets.containsKey(name)) + { + throw (new IllegalArgumentException("Attempted to add a second widget with name: " + name)); + } + this.widgets.put(name, widget); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QWidgetMetaDataInterface 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..2a9594b7 --- /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 implements QWidgetMetaDataInterface +{ + protected String name; + protected 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/dashboard/QWidgetMetaDataInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java new file mode 100644 index 00000000..771c8d33 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java @@ -0,0 +1,42 @@ +package com.kingsrook.qqq.backend.core.model.metadata.dashboard; + + +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface QWidgetMetaDataInterface +{ + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + String getName(); + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + void setName(String name); + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + QWidgetMetaDataInterface withName(String name); + + /******************************************************************************* + ** Getter for codeReference + *******************************************************************************/ + QCodeReference getCodeReference(); + + /******************************************************************************* + ** Setter for codeReference + *******************************************************************************/ + void setCodeReference(QCodeReference codeReference); + + /******************************************************************************* + ** Fluent setter for codeReference + *******************************************************************************/ + QWidgetMetaDataInterface withCodeReference(QCodeReference codeReference); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QuickSightChartMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QuickSightChartMetaData.java new file mode 100644 index 00000000..114804a4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QuickSightChartMetaData.java @@ -0,0 +1,305 @@ +package com.kingsrook.qqq.backend.core.model.metadata.dashboard; + + +import java.util.Collection; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QuickSightChartMetaData extends QWidgetMetaData implements QWidgetMetaDataInterface +{ + private String label; + private String accessKey; + private String secretKey; + private String dashboardId; + private String accountId; + private String userArn; + private String region; + private Collection allowedDomains; + + + + /******************************************************************************* + ** Fluent setter for name + ** + *******************************************************************************/ + public QuickSightChartMetaData withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for accessKey + ** + *******************************************************************************/ + public String getAccessKey() + { + return accessKey; + } + + + + /******************************************************************************* + ** Setter for accessKey + ** + *******************************************************************************/ + public void setAccessKey(String accessKey) + { + this.accessKey = accessKey; + } + + + + /******************************************************************************* + ** Fluent setter for accessKey + ** + *******************************************************************************/ + public QuickSightChartMetaData withAccessKey(String accessKey) + { + this.accessKey = accessKey; + return (this); + } + + + + /******************************************************************************* + ** Getter for label + ** + *******************************************************************************/ + public String getLabel() + { + + return label; + } + + + + /******************************************************************************* + ** Setter for label + ** + *******************************************************************************/ + public void setLabel(String label) + { + this.label = label; + } + + + + /******************************************************************************* + ** Fluent setter for label + ** + *******************************************************************************/ + public QuickSightChartMetaData withLabel(String label) + { + this.label = label; + return (this); + } + + + + /******************************************************************************* + ** Getter for secretKey + ** + *******************************************************************************/ + public String getSecretKey() + { + return secretKey; + } + + + + /******************************************************************************* + ** Setter for secretKey + ** + *******************************************************************************/ + public void setSecretKey(String secretKey) + { + this.secretKey = secretKey; + } + + + + /******************************************************************************* + ** Fluent setter for secretKey + ** + *******************************************************************************/ + public QuickSightChartMetaData withSecretKey(String secretKey) + { + this.secretKey = secretKey; + return (this); + } + + + + /******************************************************************************* + ** Getter for dashboardId + ** + *******************************************************************************/ + public String getDashboardId() + { + return dashboardId; + } + + + + /******************************************************************************* + ** Setter for dashboardId + ** + *******************************************************************************/ + public void setDashboardId(String dashboardId) + { + this.dashboardId = dashboardId; + } + + + + /******************************************************************************* + ** Fluent setter for dashboardId + ** + *******************************************************************************/ + public QuickSightChartMetaData withDashboardId(String dashboardId) + { + this.dashboardId = dashboardId; + return (this); + } + + + + /******************************************************************************* + ** Getter for accountId + ** + *******************************************************************************/ + public String getAccountId() + { + return accountId; + } + + + + /******************************************************************************* + ** Setter for accountId + ** + *******************************************************************************/ + public void setAccountId(String accountId) + { + this.accountId = accountId; + } + + + + /******************************************************************************* + ** Fluent setter for accountId + ** + *******************************************************************************/ + public QuickSightChartMetaData withAccountId(String accountId) + { + this.accountId = accountId; + return this; + } + + + + /******************************************************************************* + ** Getter for userArn + ** + *******************************************************************************/ + public String getUserArn() + { + return userArn; + } + + + + /******************************************************************************* + ** Setter for userArn + ** + *******************************************************************************/ + public void setUserArn(String userArn) + { + this.userArn = userArn; + } + + + + /******************************************************************************* + ** Fluent setter for userArn + ** + *******************************************************************************/ + public QuickSightChartMetaData withUserArn(String userArn) + { + this.userArn = userArn; + return this; + } + + + + /******************************************************************************* + ** Getter for region + ** + *******************************************************************************/ + public String getRegion() + { + return region; + } + + + + /******************************************************************************* + ** Setter for region + ** + *******************************************************************************/ + public void setRegion(String region) + { + this.region = region; + } + + + + /******************************************************************************* + ** Fluent setter for region + ** + *******************************************************************************/ + public QuickSightChartMetaData withRegion(String region) + { + this.region = region; + return this; + } + + + + /******************************************************************************* + ** Getter for allowedDomains + ** + *******************************************************************************/ + public Collection getAllowedDomains() + { + return allowedDomains; + } + + + + /******************************************************************************* + ** Setter for allowedDomains + ** + *******************************************************************************/ + public void setAllowedDomains(Collection allowedDomains) + { + this.allowedDomains = allowedDomains; + } + + + + /******************************************************************************* + ** Fluent setter for allowedDomains + ** + *******************************************************************************/ + public QuickSightChartMetaData withAllowedDomains(Collection allowedDomains) + { + this.allowedDomains = allowedDomains; + 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..06997d0a --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/PersonsByCreateDateBarChart.java @@ -0,0 +1,51 @@ +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.metadata.dashboard.QWidgetMetaDataInterface; +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, QWidgetMetaDataInterface metaData) 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/instances/QMetaDataVariableInterpreterTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java index e3881b00..d40a82d9 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QMetaDataVariableInterpreterTest.java @@ -102,7 +102,7 @@ class QMetaDataVariableInterpreterTest QMetaDataVariableInterpreter secretReader = new QMetaDataVariableInterpreter(); String key = "CUSTOM_PROPERTY"; String value = "ABCD-9876"; - secretReader.setCustomEnvironment(Map.of(key, value)); + secretReader.setEnvironmentOverrides(Map.of(key, value)); assertNull(secretReader.interpret(null)); assertEquals("foo", secretReader.interpret("foo")); @@ -278,4 +278,4 @@ class QMetaDataVariableInterpreterTest } } -} \ 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 b0079f5b..b762d42f 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 @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.List; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler; +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.InsertAction; @@ -52,6 +53,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProvi 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; @@ -147,6 +149,7 @@ public class TestUtils qInstance.addAutomationProvider(definePollingAutomationProvider()); + defineWidgets(qInstance); defineApps(qInstance); return (qInstance); @@ -154,6 +157,19 @@ public class TestUtils + + /******************************************************************************* + ** + *******************************************************************************/ + private static void defineWidgets(QInstance qInstance) + { + qInstance.addWidget(new QWidgetMetaData() + .withName(PersonsByCreateDateBarChart.class.getSimpleName()) + .withCodeReference(new QCodeReference(PersonsByCreateDateBarChart.class, null))); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -180,7 +196,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-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/model/metadata/S3BackendMetaData.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/model/metadata/S3BackendMetaData.java index 2475b6b5..652913f7 100644 --- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/model/metadata/S3BackendMetaData.java +++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/model/metadata/S3BackendMetaData.java @@ -50,6 +50,19 @@ public class S3BackendMetaData extends AbstractFilesystemBackendMetaData + /******************************************************************************* + ** Fluent setter for backendType + ** + *******************************************************************************/ + @Override + public S3BackendMetaData withBackendType(String backendType) + { + setBackendType(backendType); + return this; + } + + + /******************************************************************************* ** Getter for bucketName ** 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 ce761755..653e8731 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/ConnectionManagerTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManagerTest.java index a51a041b..64aa4da9 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManagerTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/jdbc/ConnectionManagerTest.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.module.rdbms.jdbc; import java.sql.Connection; import java.sql.SQLException; import java.util.Collections; +import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -101,13 +102,21 @@ class ConnectionManagerTest private RDBMSBackendMetaData getAuroraBacked() { + QMetaDataVariableInterpreter interpreter = new QMetaDataVariableInterpreter(); + String vendor = interpreter.interpret("${env.RDBMS_VENDOR}"); + String hostname = interpreter.interpret("${env.RDBMS_HOSTNAME}"); + Integer port = Integer.valueOf(interpreter.interpret("${env.RDBMS_PORT}")); + String databaseName = interpreter.interpret("${env.RDBMS_DATABASE_NAME}"); + String username = interpreter.interpret("${env.RDBMS_USERNAME}"); + String password= interpreter.interpret("${env.RDBMS_PASSWORD}"); + return new RDBMSBackendMetaData() .withName("aurora-test") - .withVendor("aurora") - .withHostName("nf-one-development-aurora.cwuhqcx1inwx.us-east-2.rds.amazonaws.com") - .withPort(3306) - .withDatabaseName("nutrifresh_one") - .withUsername("nf_admin") - .withPassword("%!2rwcH+fb#WgPg"); + .withVendor(vendor) + .withHostName(hostname) + .withPort(port) + .withDatabaseName(databaseName) + .withUsername(username) + .withPassword(password); } } 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 85b72727..dad72655 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; @@ -284,6 +285,8 @@ public class QJavalinImplementation }); }); + get("/widget/{name}", QJavalinImplementation::widget); + //////////////////// // process routes // //////////////////// @@ -668,6 +671,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..5afbf0d6 --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/PersonsByCreateDateBarChart.java @@ -0,0 +1,52 @@ +package com.kingsrook.qqq.backend.javalin; + + +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.metadata.dashboard.QWidgetMetaDataInterface; +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, QWidgetMetaDataInterface metaData) 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-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 10b8cc02..04cbb11c 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/pom.xml b/qqq-sample-project/pom.xml index 23f84047..e1d80a71 100644 --- a/qqq-sample-project/pom.xml +++ b/qqq-sample-project/pom.xml @@ -128,9 +128,9 @@ 4.10.0 /src/main/resources/liquibase/liquibase.properties - ${env.LB_DB_URL} - ${env.LB_DB_USERNAME} - ${env.LB_DB_PASSWORD} + ${env.RDBMS_URL} + ${env.RDBMS_USERNAME} + ${env.RDBMS_PASSWORD} ${env.LB_CONTEXTS} 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 63af904e..d90bec2e 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 @@ -26,10 +26,13 @@ import java.io.Serializable; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.amazonaws.regions.Regions; +import com.kingsrook.qqq.backend.core.actions.dashboard.QuickSightChartRenderer; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; +import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; 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.metadata.QAuthenticationType; @@ -37,6 +40,9 @@ 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.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QuickSightChartMetaData; 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; @@ -64,6 +70,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 com.kingsrook.sampleapp.processes.clonepeople.ClonePeopleTransformStep; import io.github.cdimascio.dotenv.Dotenv; @@ -78,10 +85,6 @@ public class SampleMetaDataProvider public static final String RDBMS_BACKEND_NAME = "rdbms"; public static final String FILESYSTEM_BACKEND_NAME = "filesystem"; - public static final String AUTH0_AUTHENTICATION_MODULE_NAME = "auth0"; - // public static final String AUTH0_BASE_URL = "https://kingsrook.us.auth0.com/"; - public static final String AUTH0_BASE_URL = "https://nutrifresh-one-development.us.auth0.com/"; - public static final String APP_NAME_GREETINGS = "greetingsApp"; public static final String APP_NAME_PEOPLE = "peopleApp"; public static final String APP_NAME_MISCELLANEOUS = "miscellaneous"; @@ -125,6 +128,8 @@ public class SampleMetaDataProvider qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessSimpleThrow()); + defineWidgets(qInstance); + defineApps(qInstance); return (qInstance); @@ -132,6 +137,37 @@ public class SampleMetaDataProvider + /******************************************************************************* + ** + *******************************************************************************/ + private static void defineWidgets(QInstance qInstance) + { + qInstance.addWidget(new QWidgetMetaData() + .withName(PersonsByCreateDateBarChart.class.getSimpleName()) + .withCodeReference(new QCodeReference(PersonsByCreateDateBarChart.class, null))); + + QMetaDataVariableInterpreter interpreter = new QMetaDataVariableInterpreter(); + String accountId = interpreter.interpret("${env.QUICKSIGHT_ACCOUNT_ID}"); + String accessKey = interpreter.interpret("${env.QUICKSIGHT_ACCESS_KEY}"); + String secretKey = interpreter.interpret("${env.QUICKSIGHT_SECRET_KEY}"); + String userArn = interpreter.interpret("${env.QUICKSIGHT_USER_ARN}"); + + QWidgetMetaDataInterface quickSightChartMetaData = new QuickSightChartMetaData() + .withAccountId(accountId) + .withAccessKey(accessKey) + .withSecretKey(secretKey) + .withUserArn(userArn) + .withDashboardId("9e452e78-8509-4c81-bb7f-967abfc356da") + .withRegion(Regions.US_EAST_2.getName()) + .withName(QuickSightChartRenderer.class.getSimpleName()) + .withLabel("Example Quicksight Chart") + .withCodeReference(new QCodeReference(QuickSightChartRenderer.class, null)); + + qInstance.addWidget(quickSightChartMetaData); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -144,6 +180,7 @@ public class SampleMetaDataProvider .withChild(qInstance.getTable(TABLE_NAME_PERSON).withIcon(new QIcon().withName("person"))) .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"))) + .withWidgets(List.of(PersonsByCreateDateBarChart.class.getSimpleName(), QuickSightChartRenderer.class.getSimpleName())) ); qInstance.addApp(new QAppMetaData() @@ -183,15 +220,22 @@ public class SampleMetaDataProvider { if(USE_MYSQL) { - Dotenv dotenv = Dotenv.configure().load(); + QMetaDataVariableInterpreter interpreter = new QMetaDataVariableInterpreter(); + String vendor = interpreter.interpret("${env.RDBMS_VENDOR}"); + String hostname = interpreter.interpret("${env.RDBMS_HOSTNAME}"); + Integer port = Integer.valueOf(interpreter.interpret("${env.RDBMS_PORT}")); + String databaseName = interpreter.interpret("${env.RDBMS_DATABASE_NAME}"); + String username = interpreter.interpret("${env.RDBMS_USERNAME}"); + String password= interpreter.interpret("${env.RDBMS_PASSWORD}"); + return new RDBMSBackendMetaData() .withName(RDBMS_BACKEND_NAME) - .withVendor("mysql") - .withHostName("127.0.0.1") - .withPort(3306) - .withDatabaseName("qqq") - .withUsername("root") - .withPassword(dotenv.get("RDBMS_PASSWORD")); + .withVendor(vendor) + .withHostName(hostname) + .withPort(port) + .withDatabaseName(databaseName) + .withUsername(username) + .withPassword(password); } else { 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..490ac39f --- /dev/null +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/dashboard/widgets/PersonsByCreateDateBarChart.java @@ -0,0 +1,79 @@ +package com.kingsrook.sampleapp.dashboard.widgets; + + +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.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.session.QSession; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class PersonsByCreateDateBarChart extends AbstractWidgetRenderer +{ + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object render(QInstance qInstance, QSession session, QWidgetMetaDataInterface metaData) throws QException + { + try + { + /* + // todo - always do this as SQL... if we had database in CI... + 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); + + for(Map row : rows) + { + labels.add(ValueUtils.getValueAsString(row.get("month"))); + data.add(ValueUtils.getValueAsInteger(row.get("count"))); + } + */ + + 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-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..b74f98ba --- /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(), null); + assertThat(widgetData).isInstanceOf(BarChart.class); + BarChart barChart = (BarChart) widgetData; + assertEquals("barChart", barChart.getType()); + assertThat(barChart.getTitle()).isNotBlank(); + assertNotNull(barChart.getBarChartData()); + } + +}