360 lines
16 KiB
Java
360 lines
16 KiB
Java
/*
|
|
* QQQ - Low-code Application Framework for Engineers.
|
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
* contact@kingsrook.com
|
|
* https://github.com/Kingsrook/
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package com.kingsrook.qbits.customapps;
|
|
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.Serializable;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Properties;
|
|
import java.util.function.Function;
|
|
import java.util.stream.Collectors;
|
|
import com.kingsrook.qbits.customapps.definition.CustomAppsRegistry;
|
|
import com.kingsrook.qbits.customapps.model.CustomApp;
|
|
import com.kingsrook.qbits.customapps.model.CustomAppBackendConfig;
|
|
import com.kingsrook.qbits.customapps.model.CustomAppBackendEnum;
|
|
import com.kingsrook.qbits.customapps.model.CustomAppContainer;
|
|
import com.kingsrook.qbits.customapps.model.CustomAppIcon;
|
|
import com.kingsrook.qbits.customapps.model.CustomAppSection;
|
|
import com.kingsrook.qbits.customapps.widgets.LookerWidgetRenderer;
|
|
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
|
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
|
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
|
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.dashboard.QWidgetMetaData;
|
|
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.layout.QAppSection;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer;
|
|
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
|
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
|
|
|
|
|
/*******************************************************************************
|
|
**
|
|
*******************************************************************************/
|
|
public class CustomAppsQBitProducer implements QBitMetaDataProducer<CustomAppsQBitConfig>
|
|
{
|
|
private static final QLogger LOG = QLogger.getLogger(CustomAppsQBitProducer.class);
|
|
|
|
private CustomAppsQBitConfig customAppsQBitConfig;
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
@Override
|
|
public QBitMetaData getQBitMetaData()
|
|
{
|
|
QBitMetaData qBitMetaData = new QBitMetaData()
|
|
.withNamespace(getNamespace())
|
|
.withConfig(getQBitConfig());
|
|
|
|
/////////////////////////////////////////////////////////////////////////
|
|
// look up details from properties file (which gets data from pom.xml) //
|
|
/////////////////////////////////////////////////////////////////////////
|
|
Properties properties = new Properties();
|
|
try(InputStream in = getClass().getResourceAsStream("/qbit.properties"))
|
|
{
|
|
properties.load(in);
|
|
qBitMetaData
|
|
.withGroupId(properties.getProperty("qbit.groupId"))
|
|
.withArtifactId(properties.getProperty("qbit.artifactId"))
|
|
.withVersion(properties.getProperty("qbit.version"));
|
|
}
|
|
catch(IOException ioe)
|
|
{
|
|
LOG.error("Error reading qbit.properties", ioe);
|
|
}
|
|
|
|
return (qBitMetaData);
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
**
|
|
***************************************************************************/
|
|
@Override
|
|
public void postProduceActions(MetaDataProducerMultiOutput metaDataProducerMultiOutput, QInstance qInstance) throws QException
|
|
{
|
|
CustomAppsRegistry customAppsRegistry = metaDataProducerMultiOutput.get(CustomAppsRegistry.class, CustomAppsRegistry.NAME);
|
|
qInstance.add(customAppsRegistry);
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
*
|
|
***************************************************************************/
|
|
public List<QAppMetaData> getCustomAppList(QInstance qInstance) throws QException
|
|
{
|
|
///////////////////////////////////////////////////
|
|
// look up data needed to set up the custom apps //
|
|
///////////////////////////////////////////////////
|
|
List<CustomAppContainer> customAppContainerList = QueryAction.execute(CustomAppContainer.TABLE_NAME, CustomAppContainer.class, new QQueryFilter().withOrderBy(new QFilterOrderBy("sequenceNo")));
|
|
List<CustomAppSection> customAppSectionList = QueryAction.execute(CustomAppSection.TABLE_NAME, CustomAppSection.class, new QQueryFilter().withOrderBy(new QFilterOrderBy("sequenceNo")));
|
|
List<CustomApp> customAppList = QueryAction.execute(CustomApp.TABLE_NAME, CustomApp.class, new QQueryFilter().withOrderBy(new QFilterOrderBy("sequenceNo")));
|
|
List<CustomAppIcon> customAppIcons = QueryAction.execute(CustomAppIcon.TABLE_NAME, CustomAppIcon.class, new QQueryFilter());
|
|
List<CustomAppBackendConfig> customAppBackendConfigList = QueryAction.execute(CustomAppBackendConfig.TABLE_NAME, CustomAppBackendConfig.class, new QQueryFilter());
|
|
Map<Integer, CustomAppBackendConfig> customAppBackendConfigMap = customAppBackendConfigList.stream().collect(Collectors.toMap(CustomAppBackendConfig::getId, Function.identity()));
|
|
|
|
/////////////////////
|
|
// icon id to icon //
|
|
/////////////////////
|
|
Map<Integer, CustomAppIcon> customAppIconMap = customAppIcons.stream().collect(Collectors.toMap(CustomAppIcon::getId, Function.identity()));
|
|
|
|
//////////////////////////////
|
|
// container id to sections //
|
|
//////////////////////////////
|
|
ListingHash<Integer, CustomAppSection> sectionsMap = new ListingHash<>();
|
|
customAppSectionList.forEach(section -> sectionsMap.add(section.getCustomAppContainerId(), section));
|
|
|
|
////////////////////////
|
|
// section id to apps //
|
|
////////////////////////
|
|
ListingHash<Integer, CustomApp> appsMap = new ListingHash<>();
|
|
customAppList.forEach(app -> appsMap.add(app.getCustomAppSectionId(), app));
|
|
|
|
///////////////////////////////////
|
|
// iterate over containers first //
|
|
///////////////////////////////////
|
|
List<QAppMetaData> containers = new ArrayList<>();
|
|
for(CustomAppContainer container : customAppContainerList)
|
|
{
|
|
////////////////////////////////////////
|
|
// iterate over sections for this app //
|
|
////////////////////////////////////////
|
|
List<QAppSection> sections = new ArrayList<>();
|
|
for(CustomAppSection section : sectionsMap.get(container.getId()))
|
|
{
|
|
///////////////////////
|
|
// iterate over apps //
|
|
///////////////////////
|
|
List<String> appNameList = new ArrayList<>();
|
|
if(appsMap.containsKey(section.getId()))
|
|
{
|
|
for(CustomApp app : appsMap.get(section.getId()))
|
|
{
|
|
//////////////////////////////////////////////////
|
|
// turn the label into a valid name for the app //
|
|
//////////////////////////////////////////////////
|
|
String appName = nameToCamelCase(app.getName());
|
|
appName = "qca" + StringUtils.ucFirst(appName);
|
|
|
|
/////////////////////////////////////////
|
|
// create a custom widget for this app //
|
|
/////////////////////////////////////////
|
|
String widgetName = appName + "Widget";
|
|
QWidgetMetaDataInterface widget = buildWidget(qInstance, app, customAppBackendConfigMap, widgetName);
|
|
CustomAppIcon appIcon = customAppIconMap.get(app.getCustomAppIconId());
|
|
|
|
QAppMetaData appMetaData = new QAppMetaData()
|
|
.withIcon(new QIcon(appIcon.getIconId()))
|
|
.withName(appName)
|
|
.withLabel(app.getName())
|
|
.withWidgets(List.of(widget.getName()));
|
|
appNameList.add(appMetaData.getName());
|
|
|
|
/////////////////////////////////////
|
|
// add to qinstance if not already //
|
|
/////////////////////////////////////
|
|
if(!qInstance.getApps().containsKey(appMetaData.getName()))
|
|
{
|
|
qInstance.getApps().put(appMetaData.getName(), appMetaData);
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////
|
|
// build section with its apps and add to list //
|
|
/////////////////////////////////////////////////
|
|
String sectionName = nameToCamelCase(section.getName());
|
|
sectionName = "cas" + StringUtils.ucFirst(sectionName);
|
|
|
|
QAppSection appSection = new QAppSection()
|
|
.withApps(appNameList)
|
|
.withName(sectionName)
|
|
.withLabel(section.getName());
|
|
sections.add(appSection);
|
|
}
|
|
|
|
///////////////////////
|
|
// build the new app //
|
|
///////////////////////
|
|
String containerName = nameToCamelCase(container.getName());
|
|
containerName = "cac" + StringUtils.ucFirst(containerName);
|
|
CustomAppIcon containerIcon = customAppIconMap.get(container.getCustomAppIconId());
|
|
QAppMetaData containerApp = new QAppMetaData()
|
|
.withName(containerName)
|
|
.withLabel(container.getName())
|
|
.withIcon(new QIcon().withName(containerIcon.getIconId()))
|
|
.withSections(sections);
|
|
containers.add(containerApp);
|
|
|
|
/////////////////////////////////////
|
|
// add to qinstance if not already //
|
|
/////////////////////////////////////
|
|
if(!qInstance.getApps().containsKey(containerApp.getName()))
|
|
{
|
|
qInstance.getApps().put(containerApp.getName(), containerApp);
|
|
}
|
|
}
|
|
|
|
return (containers);
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** builds a widget to be used for a customapps app
|
|
*******************************************************************************/
|
|
private QWidgetMetaDataInterface buildWidget(QInstance qInstance, CustomApp customApp, Map<Integer, CustomAppBackendConfig> customAppBackendConfigMap, String widgetName)
|
|
{
|
|
CustomAppBackendConfig customAppBackendConfig = customAppBackendConfigMap.get(customApp.getCustomAppBackendConfigId());
|
|
switch(CustomAppBackendEnum.getById(customAppBackendConfig.getCustomAppBackendId()))
|
|
{
|
|
case LOOKER:
|
|
return (buildLookerWidget(qInstance, customApp, widgetName));
|
|
default:
|
|
throw new IllegalArgumentException("Unknown custom app backend: " + customAppBackendConfig.getCustomAppBackendId());
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** builds a widget to be used for a customapps app
|
|
*******************************************************************************/
|
|
private QWidgetMetaDataInterface buildLookerWidget(QInstance qInstance, CustomApp customApp, String widgetName)
|
|
{
|
|
String componentSourceUrl = new QMetaDataVariableInterpreter().interpret("${env.Q_CUSTOM_APPS_SOURCE_URL}");
|
|
if(StringUtils.hasContent(componentSourceUrl))
|
|
{
|
|
LOG.debug("Using component source url from environment", logPair("url", componentSourceUrl));
|
|
}
|
|
else
|
|
{
|
|
componentSourceUrl = "/dynamic-qfmd-components/q-custom-apps.bundle.js";
|
|
}
|
|
|
|
//////////////////////////////
|
|
// add the dashboard to url //
|
|
//////////////////////////////
|
|
Integer dashboardId = customApp.getLookerDashboardId();
|
|
componentSourceUrl += "?dashboardId=" + dashboardId;
|
|
|
|
Map<String, Serializable> defaultValues = MapBuilder.of(
|
|
"componentName", "CustomApp",
|
|
"componentSourceUrl", componentSourceUrl,
|
|
"dashboardId", dashboardId,
|
|
"sx", new HashMap<>(Map.of("height", "calc(90vh)", "minHeight", "600px"))
|
|
);
|
|
|
|
QWidgetMetaData lookerWidget = new QWidgetMetaData()
|
|
.withName(widgetName)
|
|
.withType(WidgetType.CUSTOM_COMPONENT.getType())
|
|
.withCodeReference(new QCodeReference(LookerWidgetRenderer.class))
|
|
.withGridColumns(12)
|
|
.withIsCard(false)
|
|
.withLabel(null)
|
|
.withDefaultValues(defaultValues);
|
|
if(qInstance.getWidget(widgetName) == null)
|
|
{
|
|
qInstance.add(lookerWidget);
|
|
}
|
|
|
|
return (lookerWidget);
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** transform an app name into a camelCase name
|
|
*******************************************************************************/
|
|
private static String nameToCamelCase(String label)
|
|
{
|
|
String[] parts = label
|
|
.replaceAll("[^a-zA-Z0-9]+", " ")
|
|
.trim()
|
|
.split("\\s+");
|
|
|
|
StringBuilder result = new StringBuilder(parts[0].toLowerCase());
|
|
|
|
for(int i = 1; i < parts.length; i++)
|
|
{
|
|
result.append(parts[i].substring(0, 1).toUpperCase()).append(parts[i].substring(1).toLowerCase());
|
|
}
|
|
|
|
return result.toString();
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Getter for qBitConfig
|
|
*******************************************************************************/
|
|
@Override
|
|
public CustomAppsQBitConfig getQBitConfig()
|
|
{
|
|
return (this.customAppsQBitConfig);
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Setter for qBitConfig
|
|
*******************************************************************************/
|
|
public void setQBitConfig(CustomAppsQBitConfig customAppsQBitConfig)
|
|
{
|
|
this.customAppsQBitConfig = customAppsQBitConfig;
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Fluent setter for qBitConfig
|
|
*******************************************************************************/
|
|
public CustomAppsQBitProducer withQBitConfig(CustomAppsQBitConfig customAppsQBitConfig)
|
|
{
|
|
this.customAppsQBitConfig = customAppsQBitConfig;
|
|
return (this);
|
|
}
|
|
|
|
}
|