Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
950f2a5e88
|
No files matched your search
@@ -1,14 +1,14 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
qqq-orb: kingsrook/qqq-orb@0.7.11
|
||||
workflows:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
orb_version: 0.7.11
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
only: /v.*/
|
||||
@@ -1,7 +0,0 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -1,23 +0,0 @@
|
||||
name: CodeQL
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- release/**
|
||||
- hotfix/**
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: 41 16 * * 0
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
packages: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
analyze:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
+3
-4
@@ -1,17 +1,16 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
orb_version: 0.7.11
|
||||
default_branch: main
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: stable
|
||||
test_branch_policy: feature-no-hotfix
|
||||
release_tag_policy: v-prefix
|
||||
test_branch_policy: feature
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
hotfix: true
|
||||
asciidoc: false
|
||||
release_completion: false
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
> **Status:** 0.1 release line, built on QQQ 4.1 (`4.1.0-RC.22`) and `qbit-user-role-permissions` `0.33.0-RC.1`. Published to the QRun-IO Gitea Maven registry only (`https://git.kof22.com/api/packages/QRun-IO/maven`); release candidates are `0.1.0-RC.N`.
|
||||
> **Status:** ⚠️ **Experimental / scaffold.** This qbit declares dependencies against `0.9999.0` sentinel versions of `qqq-backend-core` and `qqq-frontend-material-dashboard`, which means it does **not** consume a released framework version. Treat as exploratory until a maintainer confirms its intended state.
|
||||
|
||||
QBit that aims to let qqq apps register **custom application screens** declaratively — pages built from widgets, forms, and table views — without writing dedicated front-end components.
|
||||
|
||||
@@ -27,15 +27,15 @@ src/main/java/com/kingsrook/qbits/customapps/
|
||||
|
||||
## How it relates to other qqq repos
|
||||
|
||||
- **Declared dependencies:** `qqq-backend-core`, `qqq-middleware-api` (optional), `qqq-middleware-javalin`, `qbit-user-role-permissions`; test scope: `qqq-backend-module-rdbms`, `qqq-backend-module-postgres`. All versions come from the `qqq-bom-pom` import (`4.1.0-RC.22`).
|
||||
- **Declared dependencies:** `qqq-backend-core`, `qqq-backend-module-rdbms`, `qqq-frontend-material-dashboard`, `qqq-middleware-api`, `qqq-middleware-javalin` — all currently pinned to the `0.9999.0` sentinel version (development build).
|
||||
- **Inherits from:** `qbit-build-parent` (in the `qbit-bom` repo).
|
||||
- **Frontend pair:** Custom apps render via the standard qqq dashboard; no separate frontend component needed.
|
||||
|
||||
## Known gaps
|
||||
|
||||
- Integration tests (`CustomAppsPostgresTest`) use a PostgreSQL Testcontainer, so building needs Docker.
|
||||
- Pre-1.0: the metadata and permission model may still change between release candidates.
|
||||
- Depends on QQQ 4.1 release candidates, so it is not yet pinned to a final QQQ 4.1.0.
|
||||
- No automated tests at the time this README was added.
|
||||
- Sentinel `0.9999.0` versions indicate this qbit is not currently consuming any released qqq build — meaning it can't be cleanly installed by a downstream app.
|
||||
- This README was added by an audit pass; original maintainer should verify intent and either pin to a real qqq version or mark the repo archived.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -23,21 +23,13 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<revision>0.3.0-SNAPSHOT</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.showDeprecation>true</maven.compiler.showDeprecation>
|
||||
<maven.compiler.showWarnings>true</maven.compiler.showWarnings>
|
||||
<!-- jacoco sets argLine; the empty default keeps the UTC surefire argLine below valid when jacoco is not active -->
|
||||
<argLine></argLine>
|
||||
<!-- qbit-user-role-permissions built for the QQQ 4.1 family: 0.33.0-RC.1, latest in the QRun-IO Gitea Maven registry. -->
|
||||
<urp.version>0.33.0-RC.2</urp.version>
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<version>1.5.1</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
@@ -60,16 +52,6 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<!-- PostgreSQL instants (CustomAppsPostgresTest) are only correct under a UTC JVM -->
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -Duser.timezone=UTC</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
@@ -96,14 +78,13 @@
|
||||
</scm>
|
||||
|
||||
|
||||
<!-- Keep QQQ modules aligned with the published core/frontend release. -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-bom-pom</artifactId>
|
||||
<!-- The LOCAL_WIDGET backend, per-session metadata customizer and restricted-permission code need the QQQ 4.1 family: the published core
|
||||
(QRun-IO Gitea Maven registry), the one BOM version shared by the four app-QBit patches. -->
|
||||
<version>4.1.0-RC.25</version>
|
||||
<version>0.40.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@@ -114,17 +95,7 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
<artifactId>qbit-user-role-permissions</artifactId>
|
||||
<version>${urp.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<version>0.40.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
@@ -136,6 +107,11 @@
|
||||
<artifactId>qqq-backend-module-rdbms</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-frontend-material-dashboard</artifactId>
|
||||
<version>0.40.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
@@ -145,24 +121,7 @@
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.27.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-module-postgres</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<version>2.2.220</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers</artifactId>
|
||||
<version>2.0.3</version>
|
||||
<version>3.27.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
@@ -23,10 +23,13 @@ package com.kingsrook.qbits.customapps;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ClassPathUtils;
|
||||
|
||||
@@ -44,10 +47,6 @@ public class CustomAppsQBitConfig implements QBitConfig
|
||||
private boolean includeApiVersions = false;
|
||||
private String componentSourceUrl;
|
||||
|
||||
private String backendName;
|
||||
|
||||
private java.util.Set<String> allowedComponentHosts = new java.util.LinkedHashSet<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -61,13 +60,23 @@ public class CustomAppsQBitConfig implements QBitConfig
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public static boolean isApiModuleAvailableAndDoesQBitIncludeApiVersions()
|
||||
{
|
||||
Optional<QBitMetaData> optionalQBitConfig = QContext.getQInstance().getQBits().values().stream().filter(qb -> qb.getConfig() instanceof CustomAppsQBitConfig).findFirst();
|
||||
return (CustomAppsQBitConfig.getApiMiddlewareModuleAvailable() && optionalQBitConfig.isPresent() && ((CustomAppsQBitConfig) (optionalQBitConfig.get().getConfig())).getIncludeApiVersions());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void validate(QInstance qInstance, List<String> errors)
|
||||
{
|
||||
assertCondition(backendName != null && !backendName.isBlank(), "backendName is required for the Custom Apps QBit", errors);
|
||||
if(includeApiVersions && !apiMiddlewareModuleAvailable)
|
||||
{
|
||||
errors.add("Custom Apps QBit is configured to includeApiVersions, but the qqq-middleware-api module is available.");
|
||||
@@ -178,59 +187,4 @@ public class CustomAppsQBitConfig implements QBitConfig
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The backend the qbit's tables live on.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getDefaultBackendNameForTables()
|
||||
{
|
||||
return (backendName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for backendName
|
||||
*******************************************************************************/
|
||||
public String getBackendName()
|
||||
{
|
||||
return (this.backendName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for backendName
|
||||
*******************************************************************************/
|
||||
public CustomAppsQBitConfig withBackendName(String backendName)
|
||||
{
|
||||
this.backendName = backendName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for allowedComponentHosts (lower-case host names an https component
|
||||
** bundle may be loaded from; empty means paths on this application only)
|
||||
*******************************************************************************/
|
||||
public java.util.Set<String> getAllowedComponentHosts()
|
||||
{
|
||||
return (this.allowedComponentHosts);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for allowedComponentHosts
|
||||
*******************************************************************************/
|
||||
public CustomAppsQBitConfig withAllowedComponentHosts(java.util.Collection<String> hosts)
|
||||
{
|
||||
this.allowedComponentHosts = new java.util.LinkedHashSet<>();
|
||||
hosts.forEach(host -> this.allowedComponentHosts.add(host.toLowerCase()));
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,14 +24,43 @@ 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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -89,6 +118,214 @@ public class CustomAppsQBitProducer implements QBitMetaDataProducer<CustomAppsQB
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
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
|
||||
*******************************************************************************/
|
||||
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. 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.customizers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CustomAppContainerPlaceBeforeContainerPossibleValueSource implements QCustomPossibleValueProvider<String>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CustomAppContainerPlaceBeforeContainerPossibleValueSource.class);
|
||||
|
||||
public static final String NAME = "placeBeforeContainer";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QPossibleValue<String> getPossibleValue(Serializable idValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
for(QAppMetaData app : getNavApps())
|
||||
{
|
||||
if(app.getLabel().equals(idValue))
|
||||
{
|
||||
return appToPossibleValue(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Unable to find app with label: {}", idValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QPossibleValue<String>> search(SearchPossibleValueSourceInput input) throws QException
|
||||
{
|
||||
List<QPossibleValue<String>> rs = new ArrayList<>();
|
||||
for(QAppMetaData app : getNavApps())
|
||||
{
|
||||
rs.add(appToPossibleValue(app));
|
||||
}
|
||||
return rs;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static List<QAppMetaData> getNavApps() throws QException
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// determine which apps are in the nav bar based on not having a parent //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
List<QAppMetaData> navApps = new ArrayList<>();
|
||||
Map<String, QAppMetaData> apps = QContext.getQInstance().getApps();
|
||||
for(String appName : apps.keySet())
|
||||
{
|
||||
QAppMetaData app = apps.get(appName);
|
||||
if(app.getParentAppName() == null)
|
||||
{
|
||||
navApps.add(app);
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
// sort the navApps by their sort order //
|
||||
//////////////////////////////////////////
|
||||
navApps.sort(Comparator.comparing(QAppMetaData::getSortOrder));
|
||||
return (navApps);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QPossibleValue<String> appToPossibleValue(QAppMetaData app)
|
||||
{
|
||||
return new QPossibleValue<>(app.getLabel(), app.getLabel());
|
||||
}
|
||||
|
||||
}
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2022-2024. ColdTrack <contact@coldtrack.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.customapps.customizers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.customapps.model.CustomAppContainer;
|
||||
import com.kingsrook.qbits.customapps.utils.CustomAppPermissionUtils;
|
||||
import com.kingsrook.qbits.customapps.utils.CustomAppsUtils;
|
||||
import com.kingsrook.qbits.userrolepermissions.model.Permission;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.RecordCustomizerUtilityInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CustomAppContainerTableCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** custom actions to run after table meta data was created by the meta data action
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void postMetaDataAction(TableMetaDataInput tableMetaDataInput, TableMetaDataOutput tableMetaDataOutput) throws QException
|
||||
{
|
||||
/////////////////////////////////
|
||||
// add dynamic chips for icons //
|
||||
/////////////////////////////////
|
||||
CustomAppsUtils.addDynamicIconChipAdornments(tableMetaDataOutput.getTable(), "customAppIconId");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
if(!isPreview)
|
||||
{
|
||||
preInsertOrUpdate(records, Optional.empty());
|
||||
}
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
if(!isPreview)
|
||||
{
|
||||
preInsertOrUpdate(records, oldRecordList);
|
||||
}
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void preInsertOrUpdate(List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
Optional<Map<Serializable, QRecord>> oldRecordMap = oldRecordListToMap("id", oldRecordList);
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// create a permission for this container (or insert if none exists) //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
String name = ValueUtils.getValueAsString(RecordCustomizerUtilityInterface.getValueFromRecordElseFromOldRecord("name", record, record.getValue("id"), oldRecordMap));
|
||||
CustomAppContainer container = new CustomAppContainer(record).withName(name);
|
||||
Permission permission = CustomAppPermissionUtils.buildOrGetPermission(container.getName(), CustomAppContainer.TABLE_NAME);
|
||||
record.setValue("permissionId", permission.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
|
||||
{
|
||||
List<Serializable> permissionIds = new ArrayList<>();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
////////////////////////////////////////////
|
||||
// delete a permission for this container //
|
||||
////////////////////////////////////////////
|
||||
CustomAppContainer container = new CustomAppContainer(record);
|
||||
Integer permissionId = container.getPermissionId();
|
||||
permissionIds.add(permissionId);
|
||||
}
|
||||
new DeleteAction().execute(new DeleteInput(Permission.TABLE_NAME).withPrimaryKeys(permissionIds));
|
||||
|
||||
return TableCustomizerInterface.super.postDelete(deleteInput, records);
|
||||
}
|
||||
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2022-2025. ColdTrack <contact@coldtrack.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.customapps.customizers;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.customapps.metadata.CustomAppsGenericCompositeWidgetMetaDataProducer;
|
||||
import com.kingsrook.qbits.customapps.model.CustomAppIcon;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.CompositeWidgetData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.icon.IconBlockData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.icon.IconStyles;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.icon.IconValues;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextBlockData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextStyles;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.text.TextValues;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CustomAppIconTableCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
public static final String LABEL_COLOR = "rgb(52, 71, 103)";
|
||||
public static final String VALUE_COLOR = "rgb(123, 128, 154)";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postQuery(QueryOrGetInputInterface queryInput, List<QRecord> records) throws QException
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
setIconPreview(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private static IconBlockData iconBlock(String color, String iconName)
|
||||
{
|
||||
return new IconBlockData().withValues(new IconValues().withName(iconName))
|
||||
.withStyles(new IconStyles().withColor(color).withFontSize("125px"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public static TextBlockData valueTextBlock(String color, String text)
|
||||
{
|
||||
return new TextBlockData().withValues(new TextValues(text))
|
||||
.withStyles(new TextStyles().withColor(color).withSize("14").withWeight("bold"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private static void setIconPreview(QRecord record)
|
||||
{
|
||||
CustomAppIcon customAppIcon = new CustomAppIcon(record);
|
||||
if(customAppIcon.getIconId() != null && customAppIcon.getName() != null)
|
||||
{
|
||||
CustomAppsGenericCompositeWidgetMetaDataProducer.setDividerValue(record, "divider");
|
||||
|
||||
CompositeWidgetData previewComposite = new CompositeWidgetData().withLayout(CompositeWidgetData.Layout.FLEX_ROW)
|
||||
.withBlock(valueTextBlock(LABEL_COLOR, "Icon Preview: "))
|
||||
.withBlock(iconBlock(VALUE_COLOR, customAppIcon.getIconId()));
|
||||
|
||||
record.setValue("iconPreview", previewComposite);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2022-2024. ColdTrack <contact@coldtrack.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.customapps.customizers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.customapps.model.CustomApp;
|
||||
import com.kingsrook.qbits.customapps.utils.CustomAppPermissionUtils;
|
||||
import com.kingsrook.qbits.customapps.utils.CustomAppsUtils;
|
||||
import com.kingsrook.qbits.userrolepermissions.model.Permission;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.RecordCustomizerUtilityInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CustomAppTableCustomizer implements TableCustomizerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** custom actions to run after table meta data was created by the meta data action
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void postMetaDataAction(TableMetaDataInput tableMetaDataInput, TableMetaDataOutput tableMetaDataOutput) throws QException
|
||||
{
|
||||
CustomAppsUtils.addDynamicIconChipAdornments(tableMetaDataOutput.getTable(), "customAppIconId");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preInsert(InsertInput insertInput, List<QRecord> records, boolean isPreview) throws QException
|
||||
{
|
||||
if(!isPreview)
|
||||
{
|
||||
preInsertOrUpdate(records, Optional.empty());
|
||||
}
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> preUpdate(UpdateInput updateInput, List<QRecord> records, boolean isPreview, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
if(!isPreview)
|
||||
{
|
||||
preInsertOrUpdate(records, oldRecordList);
|
||||
}
|
||||
return (records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void preInsertOrUpdate(List<QRecord> records, Optional<List<QRecord>> oldRecordList) throws QException
|
||||
{
|
||||
Optional<Map<Serializable, QRecord>> oldRecordMap = oldRecordListToMap("id", oldRecordList);
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// create a permission for this app (or insert if none exists) //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
String appName = ValueUtils.getValueAsString(RecordCustomizerUtilityInterface.getValueFromRecordElseFromOldRecord("name", record, record.getValue("id"), oldRecordMap));
|
||||
CustomApp app = new CustomApp(record).withName(appName);
|
||||
Permission permission = CustomAppPermissionUtils.buildOrGetPermission(app.getName(), CustomApp.TABLE_NAME);
|
||||
record.setValue("permissionId", permission.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> postDelete(DeleteInput deleteInput, List<QRecord> records) throws QException
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
//////////////////////////////////////
|
||||
// delete a permission for this app //
|
||||
//////////////////////////////////////
|
||||
CustomApp app = new CustomApp(record);
|
||||
Integer permissionId = app.getPermissionId();
|
||||
new DeleteAction().execute(new DeleteInput(Permission.TABLE_NAME).withPrimaryKey(permissionId));
|
||||
}
|
||||
|
||||
return TableCustomizerInterface.super.postDelete(deleteInput, records);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -124,7 +124,7 @@ public class CustomAppsRegistry implements QSupplementalInstanceMetaData, QHelpC
|
||||
@Override
|
||||
public void acceptHelpContent(QInstance qInstance, QHelpContent helpContent, Map<String, String> nameValuePairs)
|
||||
{
|
||||
// not yet needed
|
||||
// todo?
|
||||
}
|
||||
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.customapps.model.CustomAppContainer;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The "users" section of a custom app container: the roles that hold the container's access
|
||||
** permission (rows of the permission-to-role table), as a child record list.
|
||||
*******************************************************************************/
|
||||
public class CustomAppContainerPermissionsWidgetMetaDataProducer extends MetaDataProducer<QWidgetMetaData>
|
||||
{
|
||||
public static final String NAME = "customAppContainerPermissions";
|
||||
public static final String JOIN_NAME = "customAppContainerRolePermission";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The widget, and (as a side effect, once) its join.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QWidgetMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
if(qInstance.getJoin(JOIN_NAME) == null)
|
||||
{
|
||||
qInstance.addJoin(new QJoinMetaData()
|
||||
.withName(JOIN_NAME)
|
||||
.withLeftTable(CustomAppContainer.TABLE_NAME)
|
||||
.withRightTable("rolePermissionInt")
|
||||
.withType(JoinType.ONE_TO_MANY)
|
||||
.withJoinOn(new JoinOn("permissionId", "permissionId")));
|
||||
}
|
||||
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin(JOIN_NAME))
|
||||
.withLabel("Roles with access")
|
||||
.getWidgetMetaData();
|
||||
widget.setName(NAME);
|
||||
return (widget);
|
||||
}
|
||||
}
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2022-2025. ColdTrack <contact@coldtrack.com>. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.customapps.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.customapps.customizers.CustomAppContainerPlaceBeforeContainerPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
|
||||
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.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Meta Data Producer for CustomAppContainerPlaceBeforeContainerPVS
|
||||
**
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CustomAppContainerPlaceBeforeContainerPVSMetaDataProducer extends MetaDataProducer<QPossibleValueSource>
|
||||
{
|
||||
public static final String NAME = "CustomAppContainerPlaceBeforeContainerPVS";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QPossibleValueSource produce(QInstance qInstance) throws QException
|
||||
{
|
||||
return (new QPossibleValueSource().withName(NAME).withType(QPossibleValueSourceType.CUSTOM).withCustomCodeReference(new QCodeReference(CustomAppContainerPlaceBeforeContainerPossibleValueSource.class)));
|
||||
}
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qbits.customapps.model.CustomApp;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.ChildRecordListRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The "users" section of a custom app: the roles that hold the app's access
|
||||
** permission (rows of the permission-to-role table), as a child record list.
|
||||
*******************************************************************************/
|
||||
public class CustomAppPermissionsWidgetMetaDataProducer extends MetaDataProducer<QWidgetMetaData>
|
||||
{
|
||||
public static final String NAME = "customAppPermissions";
|
||||
public static final String JOIN_NAME = "customAppRolePermission";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The widget, and (as a side effect, once) its join.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public QWidgetMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
if(qInstance.getJoin(JOIN_NAME) == null)
|
||||
{
|
||||
qInstance.addJoin(new QJoinMetaData()
|
||||
.withName(JOIN_NAME)
|
||||
.withLeftTable(CustomApp.TABLE_NAME)
|
||||
.withRightTable("rolePermissionInt")
|
||||
.withType(JoinType.ONE_TO_MANY)
|
||||
.withJoinOn(new JoinOn("permissionId", "permissionId")));
|
||||
}
|
||||
QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin(JOIN_NAME))
|
||||
.withLabel("Roles with access")
|
||||
.getWidgetMetaData();
|
||||
widget.setName(NAME);
|
||||
return (widget);
|
||||
}
|
||||
}
|
||||
-445
@@ -1,445 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. 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.metadata;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.customapps.CustomAppsQBitConfig;
|
||||
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.utils.CustomAppsUtils;
|
||||
import com.kingsrook.qbits.customapps.widgets.LookerWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
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.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.permissions.DenyBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Meta Data Producer for ViewScreenGenericCompositeWidget
|
||||
*
|
||||
* builds up a record containing data used to build the custom apps layout
|
||||
*******************************************************************************/
|
||||
public class CustomAppsDynamicMetaDataProducer
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CustomAppsDynamicMetaDataProducer.class);
|
||||
|
||||
public static final String NAME = "CustomAppsDynamicMetaDataProducer";
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public CustomAppData getCustomAppList(QInstance qInstance) throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// capture the viewer now: the build itself runs with the system session set //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
QSession viewer = QContext.getQSession();
|
||||
return (getCustomAppList(qInstance, name -> viewer != null && viewer.hasPermission(name + ".hasAccess")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Build the apps for whoever the predicate admits (by app/container name).
|
||||
** The definition rows are read as the system user (they are configuration,
|
||||
** not data of the viewer); what the viewer may see is decided only by the
|
||||
** predicate, which the metadata customizer ties to the session's permissions.
|
||||
***************************************************************************/
|
||||
public CustomAppData getCustomAppList(QInstance qInstance, java.util.function.Predicate<String> permitted) throws QException
|
||||
{
|
||||
QSession original = QContext.getQSession();
|
||||
try
|
||||
{
|
||||
QContext.setQSession(new QSystemUserSession());
|
||||
return (buildCustomAppList(qInstance, permitted, original));
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.setQSession(original);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** The build proper, with the system session in place for reads.
|
||||
***************************************************************************/
|
||||
private CustomAppData buildCustomAppList(QInstance qInstance, java.util.function.Predicate<String> permitted, QSession viewer) 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")).withOrderBy(new QFilterOrderBy("name")));
|
||||
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<>();
|
||||
Map<String, QAppMetaData> appMap = new HashMap<>();
|
||||
Map<String, QWidgetMetaDataInterface> widgetMap = new HashMap<>();
|
||||
Map<String, String> positionMap = new HashMap<>();
|
||||
for(CustomAppContainer container : customAppContainerList)
|
||||
{
|
||||
//////////////////////////
|
||||
// if no sections, skip //
|
||||
//////////////////////////
|
||||
if(sectionsMap.get(container.getId()) == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// iterate over sections for this app //
|
||||
////////////////////////////////////////
|
||||
List<QAppSection> sections = new ArrayList<>();
|
||||
for(CustomAppSection section : sectionsMap.get(container.getId()))
|
||||
{
|
||||
//////////////////////
|
||||
// if no apps, skip //
|
||||
//////////////////////
|
||||
if(appsMap.get(section.getId()) == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// iterate over apps //
|
||||
///////////////////////
|
||||
int appCount = 0;
|
||||
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 = CustomAppsUtils.getCamelCaseName(app.getName(), CustomApp.TABLE_NAME);
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// make sure user has permission to this app //
|
||||
////////////////////////////////////////////////////
|
||||
if(!permitted.test(appName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
// create a custom widget for this app //
|
||||
/////////////////////////////////////////
|
||||
String widgetName = appName + "Widget";
|
||||
QWidgetMetaDataInterface widget;
|
||||
try
|
||||
{
|
||||
widget = buildWidget(qInstance, app, customAppBackendConfigMap, widgetName);
|
||||
}
|
||||
catch(IllegalArgumentException e)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// one invalid row must not take the other apps (or startup) with it; //
|
||||
// it is simply not offered //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
LOG.warn("Custom app skipped: its definition is not valid", logPair("app", app.getName()), logPair("reason", e.getMessage()));
|
||||
continue;
|
||||
}
|
||||
CustomAppIcon appIcon = customAppIconMap.get(app.getCustomAppIconId());
|
||||
|
||||
QAppMetaData appMetaData = new QAppMetaData()
|
||||
.withPermissionRules(qInstance.getDefaultPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withPermissionBaseName(appName).withDenyBehavior(DenyBehavior.HIDDEN))
|
||||
.withIcon(new QIcon(appIcon.getIconId()))
|
||||
.withName(appName)
|
||||
.withLabel(app.getName())
|
||||
.withWidgets(List.of(widget.getName()));
|
||||
appNameList.add(appMetaData.getName());
|
||||
appCount++;
|
||||
|
||||
////////////////////////
|
||||
// add to output data //
|
||||
////////////////////////
|
||||
widgetMap.put(widgetName, widget);
|
||||
appMap.put(appMetaData.getName(), appMetaData);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// if no apps for this section, continue to next one //
|
||||
///////////////////////////////////////////////////////
|
||||
if(appCount == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// build section with its apps and add to list //
|
||||
/////////////////////////////////////////////////
|
||||
String sectionName = CustomAppsUtils.getCamelCaseName(section.getName(), CustomAppSection.TABLE_NAME);
|
||||
|
||||
QAppSection appSection = new QAppSection()
|
||||
.withApps(appNameList)
|
||||
.withName(sectionName)
|
||||
.withLabel(section.getName());
|
||||
sections.add(appSection);
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// build the container //
|
||||
/////////////////////////
|
||||
String containerName = CustomAppsUtils.getCamelCaseName(container.getName(), CustomAppContainer.TABLE_NAME);
|
||||
|
||||
//////////////////////////
|
||||
// if no sections, skip //
|
||||
//////////////////////////
|
||||
if(sections.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// make sure user has permission to this container //
|
||||
/////////////////////////////////////////////////////
|
||||
if(!permitted.test(containerName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CustomAppIcon containerIcon = customAppIconMap.get(container.getCustomAppIconId());
|
||||
QAppMetaData containerApp = new QAppMetaData()
|
||||
.withPermissionRules(qInstance.getDefaultPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withPermissionBaseName(containerName).withDenyBehavior(DenyBehavior.HIDDEN))
|
||||
.withName(containerName)
|
||||
.withLabel(container.getName())
|
||||
.withIcon(new QIcon().withName(containerIcon.getIconId()))
|
||||
.withSections(sections);
|
||||
containers.add(containerApp);
|
||||
positionMap.put(containerName, container.getPlaceBeforeContainer());
|
||||
}
|
||||
|
||||
return (new CustomAppData(containers, positionMap, appMap, widgetMap));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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));
|
||||
case LOCAL_WIDGET:
|
||||
return (localWidget(qInstance, customApp));
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown custom app backend: " + customAppBackendConfig.getCustomAppBackendId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** a custom app that shows a widget the application itself defines (a record
|
||||
** list, a chart, ...): the screen is composed from existing, permissioned
|
||||
** metadata, so no code or markup comes from the data, and the widget keeps its
|
||||
** own permission rules. Only widgets that exist on the instance can be named,
|
||||
** and never this qbit's own generated ones.
|
||||
*******************************************************************************/
|
||||
private QWidgetMetaDataInterface localWidget(QInstance qInstance, CustomApp customApp)
|
||||
{
|
||||
String name = customApp.getWidgetName();
|
||||
if(!StringUtils.hasContent(name))
|
||||
{
|
||||
throw new IllegalArgumentException("a local widget app needs a widget name");
|
||||
}
|
||||
QWidgetMetaDataInterface widget = qInstance.getWidget(name);
|
||||
if(widget == null)
|
||||
{
|
||||
throw new IllegalArgumentException("widget " + name + " is not defined on this application");
|
||||
}
|
||||
if(name.startsWith("qca"))
|
||||
{
|
||||
throw new IllegalArgumentException("widget " + name + " is generated by custom apps and cannot be chosen");
|
||||
}
|
||||
return (widget);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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-embed-looker-dashboards.bundle.js";
|
||||
}
|
||||
|
||||
if(!isAllowedComponentSource(componentSourceUrl))
|
||||
{
|
||||
throw new IllegalArgumentException("the component source must be a path on this application (or an https address the configuration allows)");
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// add the dashboard to url //
|
||||
/////////////////////////
|
||||
Integer dashboardId = customApp.getLookerDashboardId();
|
||||
if(dashboardId == null)
|
||||
{
|
||||
throw new IllegalArgumentException("a Looker app needs a dashboard id");
|
||||
}
|
||||
componentSourceUrl += "?dashboardId=" + dashboardId;
|
||||
|
||||
Map<String, Serializable> defaultValues = MapBuilder.of(
|
||||
"componentName", "EmbedLookerDashboards",
|
||||
"componentSourceUrl", componentSourceUrl,
|
||||
"dashboardId", dashboardId,
|
||||
// "authenticationUrl", this.customAppsQBitConfig.getAuthenticationUrl(),
|
||||
"sx", new HashMap<>(Map.of("height", "calc(90vh)", "minHeight", "600px"))
|
||||
);
|
||||
|
||||
QWidgetMetaData lookerWidget = new QWidgetMetaData()
|
||||
.withName(widgetName)
|
||||
.withPermissionRules(qInstance.getDefaultPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withPermissionBaseName(customApp.getName() == null ? widgetName : CustomAppsUtils.getCamelCaseName(customApp.getName(), CustomApp.TABLE_NAME)).withDenyBehavior(DenyBehavior.HIDDEN))
|
||||
.withType(WidgetType.CUSTOM_COMPONENT.getType())
|
||||
.withCodeReference(new QCodeReference(LookerWidgetRenderer.class))
|
||||
.withGridColumns(12)
|
||||
.withIsCard(false)
|
||||
.withLabel(null)
|
||||
.withDefaultValues(defaultValues);
|
||||
if(qInstance.getWidget(widgetName) == null)
|
||||
{
|
||||
qInstance.addWidget(lookerWidget);
|
||||
}
|
||||
|
||||
return (lookerWidget);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The component bundle is code the browser runs, so its source is never taken
|
||||
** from data: a path served by this application, or an https address whose host
|
||||
** the configuration lists. Anything else (script or data URLs, protocol
|
||||
** relative addresses, other hosts) is refused.
|
||||
*******************************************************************************/
|
||||
public boolean isAllowedComponentSource(String url)
|
||||
{
|
||||
if(url == null || url.chars().anyMatch(c -> c < 0x21 || c > 0x7e))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
if(url.startsWith("/") && !url.startsWith("//") && !url.contains("\\") && !url.contains(".."))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
try
|
||||
{
|
||||
java.net.URI uri = new java.net.URI(url);
|
||||
CustomAppsQBitConfig config = configFor(QContext.getQInstance());
|
||||
return ("https".equals(uri.getScheme()) && uri.getUserInfo() == null && uri.getHost() != null
|
||||
&& config != null && config.getAllowedComponentHosts().contains(uri.getHost().toLowerCase()));
|
||||
}
|
||||
catch(java.net.URISyntaxException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The qbit's configuration on the instance, if installed.
|
||||
*******************************************************************************/
|
||||
static CustomAppsQBitConfig configFor(QInstance qInstance)
|
||||
{
|
||||
if(qInstance == null || qInstance.getQBits() == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
return (qInstance.getQBits().values().stream().map(qBit -> qBit.getConfig()).filter(CustomAppsQBitConfig.class::isInstance)
|
||||
.map(CustomAppsQBitConfig.class::cast).findFirst().orElse(null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
public record CustomAppData(List<QAppMetaData> containerList, Map<String, String> positionMap, Map<String, QAppMetaData> appMap, Map<String, QWidgetMetaDataInterface> widgetMap) {}
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2024. 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.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.CompositeWidgetData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.divider.DividerBlockData;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer;
|
||||
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.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Meta Data Producer for ViewScreenGenericCompositeWidget
|
||||
*
|
||||
* e.g., generic widget to be used on table virtual fields, to display a composite
|
||||
* block widget.
|
||||
*******************************************************************************/
|
||||
public class CustomAppsGenericCompositeWidgetMetaDataProducer extends MetaDataProducer<QWidgetMetaData>
|
||||
{
|
||||
public static final String NAME = "CustomAppsGenericCompositeWidget";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QWidgetMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
QWidgetMetaData genericCompositeBlockWidget = new QWidgetMetaData()
|
||||
.withName(NAME)
|
||||
.withType(WidgetType.COMPOSITE.getType())
|
||||
.withShowReloadButton(false)
|
||||
.withShowExportButton(false)
|
||||
.withIsCard(false)
|
||||
.withCodeReference(new QCodeReference(NoopWidgetRenderer.class));
|
||||
|
||||
return genericCompositeBlockWidget;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public static <T extends QFieldMetaData> T applyToField(T field)
|
||||
{
|
||||
field.withFieldAdornment(new FieldAdornment(AdornmentType.WIDGET)
|
||||
.withValue(AdornmentType.WidgetValues.WIDGET_NAME, CustomAppsGenericCompositeWidgetMetaDataProducer.NAME));
|
||||
return (field);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
* for a virtual field that is being used to draw a horizontal divider line,
|
||||
* set the appropriate widget-data value for the field in a record.
|
||||
***************************************************************************/
|
||||
public static void setDividerValue(QRecord record, String fieldName)
|
||||
{
|
||||
CompositeWidgetData compositeWidgetData = new CompositeWidgetData()
|
||||
.withBlock(new DividerBlockData());
|
||||
record.setValue(fieldName, compositeWidgetData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** this widget - is not rendered in the traditional way, but rather,
|
||||
** its data comes from the record's field, so noop to render here.
|
||||
***************************************************************************/
|
||||
public static class NoopWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* 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.metadata;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.DefaultNoopMetaDataActionCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Offers the declared custom apps to the frontend, per session: each request's
|
||||
** metadata gets exactly the containers, apps and widgets whose
|
||||
** "<name>.hasAccess" permission the session holds, ahead of a container named
|
||||
** by its "place before" setting. Everything else is the application's own
|
||||
** metadata customizer behaviour, left unchanged (this extends the standard no-op
|
||||
** customizer, so an application that needs its own rules extends this class).
|
||||
*******************************************************************************/
|
||||
public class CustomAppsMetaDataActionCustomizer extends DefaultNoopMetaDataActionCustomizer
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CustomAppsMetaDataActionCustomizer.class);
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** Add the permitted custom apps to the finished metadata.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void postProcess(MetaDataOutput metaDataOutput) throws QException
|
||||
{
|
||||
super.postProcess(metaDataOutput);
|
||||
|
||||
CustomAppsDynamicMetaDataProducer.CustomAppData data;
|
||||
try
|
||||
{
|
||||
data = new CustomAppsDynamicMetaDataProducer().getCustomAppList(QContext.getQInstance());
|
||||
}
|
||||
catch(QException | RuntimeException e)
|
||||
{
|
||||
LOG.warn("Custom apps could not be read; none are offered", logPair("error", e.getClass().getSimpleName()));
|
||||
return;
|
||||
}
|
||||
|
||||
MetaDataInput input = new MetaDataInput();
|
||||
for(QWidgetMetaDataInterface widget : data.widgetMap().values())
|
||||
{
|
||||
if(QContext.getQInstance().getWidget(widget.getName()) != null)
|
||||
{
|
||||
metaDataOutput.getWidgets().put(widget.getName(), new QFrontendWidgetMetaData(input, widget));
|
||||
}
|
||||
}
|
||||
for(QAppMetaData app : data.appMap().values())
|
||||
{
|
||||
metaDataOutput.getApps().put(app.getName(), new QFrontendAppMetaData(app, metaDataOutput));
|
||||
}
|
||||
|
||||
List<AppTreeNode> tree = new ArrayList<>(metaDataOutput.getAppTree());
|
||||
for(QAppMetaData container : data.containerList())
|
||||
{
|
||||
AppTreeNode node = new AppTreeNode(container);
|
||||
for(var section : container.getSections())
|
||||
{
|
||||
for(String appName : section.getApps())
|
||||
{
|
||||
QAppMetaData child = data.appMap().get(appName);
|
||||
if(child != null)
|
||||
{
|
||||
node.addChild(new AppTreeNode(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
metaDataOutput.getApps().put(container.getName(), new QFrontendAppMetaData(container, metaDataOutput));
|
||||
int position = tree.size();
|
||||
String before = data.positionMap().get(container.getName());
|
||||
for(int i = 0; before != null && i < tree.size(); i++)
|
||||
{
|
||||
if(before.equals(tree.get(i).getName()))
|
||||
{
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
tree.add(position, node);
|
||||
}
|
||||
metaDataOutput.setAppTree(tree);
|
||||
}
|
||||
}
|
||||
@@ -24,29 +24,22 @@ package com.kingsrook.qbits.customapps.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qbits.customapps.customizers.CustomAppTableCustomizer;
|
||||
import com.kingsrook.qbits.userrolepermissions.model.Permission;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
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.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QRecord Entity for CustomApp table
|
||||
** QRecord Entity for CustomAppSection table
|
||||
*******************************************************************************/
|
||||
@QMetaDataProducingEntity(
|
||||
producePossibleValueSource = true,
|
||||
@@ -64,6 +57,7 @@ public class CustomApp extends QRecordEntity implements Serializable
|
||||
***************************************************************************/
|
||||
public static class TableMetaDataCustomizer implements MetaDataCustomizerInterface<QTableMetaData>
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@@ -77,16 +71,8 @@ public class CustomApp extends QRecordEntity implements Serializable
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withSection(SectionFactory.defaultT1("id"))
|
||||
.withSection(SectionFactory.defaultT2("name", "customAppIconId", "customAppBackendConfigId", "customAppSectionId", "sequenceNo", "lookerDashboardId", "widgetName", "permissionId"))
|
||||
.withSection(new QFieldSection("users", new QIcon().withName("person"), Tier.T2).withWidgetName("customAppPermissions"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"))
|
||||
|
||||
.withCustomizer(TableCustomizers.POST_META_DATA_ACTION, new QCodeReference(CustomAppTableCustomizer.class))
|
||||
.withCustomizer(TableCustomizers.POST_DELETE_RECORD, new QCodeReference(CustomAppTableCustomizer.class))
|
||||
.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(CustomAppTableCustomizer.class))
|
||||
.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(CustomAppTableCustomizer.class));
|
||||
|
||||
table.getField("customAppIconId").withFieldAdornment(AdornmentType.CHIP);
|
||||
.withSection(SectionFactory.defaultT2("name", "customAppIconId", "customAppBackendConfigId", "customAppSectionId", "sequenceNo", "lookerDashboardId"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
return (table);
|
||||
}
|
||||
@@ -100,27 +86,21 @@ public class CustomApp extends QRecordEntity implements Serializable
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true)
|
||||
private String name;
|
||||
|
||||
@QField(label = "Backend Config", isRequired = true, possibleValueSourceName = CustomAppBackendConfig.TABLE_NAME)
|
||||
@QField(isRequired = true, possibleValueSourceName = CustomAppBackendConfig.TABLE_NAME)
|
||||
private Integer customAppBackendConfigId;
|
||||
|
||||
@QField(label = "Section", isRequired = true, possibleValueSourceName = CustomAppSection.TABLE_NAME)
|
||||
@QField(isRequired = true, possibleValueSourceName = CustomAppSection.TABLE_NAME)
|
||||
private Integer customAppSectionId;
|
||||
|
||||
@QField(label = "Icon", isRequired = true, possibleValueSourceName = CustomAppIcon.TABLE_NAME)
|
||||
@QField(isRequired = true, possibleValueSourceName = CustomAppIcon.TABLE_NAME)
|
||||
private Integer customAppIconId;
|
||||
|
||||
@QField(isRequired = true)
|
||||
private Integer sequenceNo;
|
||||
|
||||
@QField(isEditable = false, possibleValueSourceName = Permission.TABLE_NAME)
|
||||
private Integer permissionId;
|
||||
|
||||
@QField(label = "Looker Dashboard Id")
|
||||
@QField(isRequired = true)
|
||||
private Integer lookerDashboardId;
|
||||
|
||||
@QField(label = "Local Widget", maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
|
||||
private String widgetName;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
@@ -425,66 +405,4 @@ public class CustomApp extends QRecordEntity implements Serializable
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for permissionId
|
||||
*******************************************************************************/
|
||||
public Integer getPermissionId()
|
||||
{
|
||||
return (this.permissionId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for permissionId
|
||||
*******************************************************************************/
|
||||
public void setPermissionId(Integer permissionId)
|
||||
{
|
||||
this.permissionId = permissionId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for permissionId
|
||||
*******************************************************************************/
|
||||
public CustomApp withPermissionId(Integer permissionId)
|
||||
{
|
||||
this.permissionId = permissionId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for widgetName
|
||||
*******************************************************************************/
|
||||
public String getWidgetName()
|
||||
{
|
||||
return (this.widgetName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for widgetName
|
||||
*******************************************************************************/
|
||||
public void setWidgetName(String widgetName)
|
||||
{
|
||||
this.widgetName = widgetName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for widgetName
|
||||
*******************************************************************************/
|
||||
public CustomApp withWidgetName(String widgetName)
|
||||
{
|
||||
this.widgetName = widgetName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -97,7 +97,7 @@ public class CustomAppBackendConfig extends QRecordEntity implements Serializabl
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true)
|
||||
private String name;
|
||||
|
||||
@QField(label = "Backend", isRequired = true, possibleValueSourceName = CustomAppBackendPossibleValueMetaDataProducer.NAME)
|
||||
@QField(isRequired = true, possibleValueSourceName = CustomAppBackendPossibleValueMetaDataProducer.NAME)
|
||||
private Integer customAppBackendId;
|
||||
|
||||
@QField(isEditable = false)
|
||||
|
||||
@@ -33,8 +33,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMeta
|
||||
public enum CustomAppBackendEnum implements PossibleValueEnum<Integer>
|
||||
{
|
||||
LOOKER(1, "Looker"),
|
||||
URL(2, "URL"),
|
||||
LOCAL_WIDGET(3, "Local widget");
|
||||
URL(2, "URL");
|
||||
|
||||
private final Integer id;
|
||||
private final String label;
|
||||
|
||||
@@ -24,19 +24,11 @@ package com.kingsrook.qbits.customapps.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.customapps.customizers.CustomAppContainerTableCustomizer;
|
||||
import com.kingsrook.qbits.customapps.metadata.CustomAppContainerPlaceBeforeContainerPVSMetaDataProducer;
|
||||
import com.kingsrook.qbits.userrolepermissions.model.Permission;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
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.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
@@ -45,11 +37,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.Child
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
@@ -70,8 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
)
|
||||
public class CustomAppContainer extends QRecordEntity implements Serializable
|
||||
{
|
||||
public static final String TABLE_NAME = "customAppContainer";
|
||||
public static final String ASSOCIATION_NAME_CUSTOM_APP_SECTIONS = "customAppSections";
|
||||
public static final String TABLE_NAME = "customAppContainer";
|
||||
|
||||
|
||||
|
||||
@@ -96,20 +84,9 @@ public class CustomAppContainer extends QRecordEntity implements Serializable
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withSection(SectionFactory.defaultT1("id"))
|
||||
.withSection(SectionFactory.defaultT2("name", "customAppIconId", "placeBeforeContainer", "sequenceNo", "permissionId"))
|
||||
.withSection(SectionFactory.defaultT2("name", "customAppIconId", "sequenceNo"))
|
||||
.withSection(SectionFactory.customT2("apps", new QIcon("polyline")).withWidgetName(sectionsChildJoinName))
|
||||
.withSection(new QFieldSection("users", new QIcon().withName("person"), Tier.T2).withWidgetName("customAppContainerPermissions"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"))
|
||||
|
||||
.withCustomizer(TableCustomizers.POST_META_DATA_ACTION, new QCodeReference(CustomAppContainerTableCustomizer.class))
|
||||
.withCustomizer(TableCustomizers.POST_DELETE_RECORD, new QCodeReference(CustomAppContainerTableCustomizer.class))
|
||||
.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(CustomAppContainerTableCustomizer.class))
|
||||
.withCustomizer(TableCustomizers.PRE_INSERT_RECORD, new QCodeReference(CustomAppContainerTableCustomizer.class))
|
||||
.withCustomizer(TableCustomizers.PRE_UPDATE_RECORD, new QCodeReference(CustomAppContainerTableCustomizer.class))
|
||||
|
||||
.withAssociation(new Association().withName(ASSOCIATION_NAME_CUSTOM_APP_SECTIONS).withJoinName(sectionsChildJoinName).withAssociatedTableName(CustomAppSection.TABLE_NAME));
|
||||
|
||||
table.getField("customAppIconId").withFieldAdornment(AdornmentType.CHIP);
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
return (table);
|
||||
}
|
||||
@@ -126,24 +103,15 @@ public class CustomAppContainer extends QRecordEntity implements Serializable
|
||||
@QField(label = "Icon", isRequired = true, possibleValueSourceName = CustomAppIcon.TABLE_NAME)
|
||||
private Integer customAppIconId;
|
||||
|
||||
@QField(possibleValueSourceName = CustomAppContainerPlaceBeforeContainerPVSMetaDataProducer.NAME)
|
||||
private String placeBeforeContainer;
|
||||
|
||||
@QField()
|
||||
@QField(isRequired = true)
|
||||
private Integer sequenceNo;
|
||||
|
||||
@QField(isEditable = false, possibleValueSourceName = Permission.TABLE_NAME)
|
||||
private Integer permissionId;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant createDate;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Instant modifyDate;
|
||||
|
||||
@QAssociation(name = ASSOCIATION_NAME_CUSTOM_APP_SECTIONS)
|
||||
private List<CustomAppSection> sections;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -290,31 +258,31 @@ public class CustomAppContainer extends QRecordEntity implements Serializable
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for placeBeforeContainer
|
||||
** Getter for sequenceNo
|
||||
*******************************************************************************/
|
||||
public String getPlaceBeforeContainer()
|
||||
public Integer getSequenceNo()
|
||||
{
|
||||
return (this.placeBeforeContainer);
|
||||
return (this.sequenceNo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for placeBeforeContainer
|
||||
** Setter for sequenceNo
|
||||
*******************************************************************************/
|
||||
public void setPlaceBeforeContainer(String placeBeforeContainer)
|
||||
public void setSequenceNo(Integer sequenceNo)
|
||||
{
|
||||
this.placeBeforeContainer = placeBeforeContainer;
|
||||
this.sequenceNo = sequenceNo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for placeBeforeContainer
|
||||
** Fluent setter for sequenceNo
|
||||
*******************************************************************************/
|
||||
public CustomAppContainer withPlaceBeforeContainer(String placeBeforeContainer)
|
||||
public CustomAppContainer withSequenceNo(Integer sequenceNo)
|
||||
{
|
||||
this.placeBeforeContainer = placeBeforeContainer;
|
||||
this.sequenceNo = sequenceNo;
|
||||
return (this);
|
||||
}
|
||||
|
||||
@@ -349,97 +317,4 @@ public class CustomAppContainer extends QRecordEntity implements Serializable
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for permissionId
|
||||
*******************************************************************************/
|
||||
public Integer getPermissionId()
|
||||
{
|
||||
return (this.permissionId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for permissionId
|
||||
*******************************************************************************/
|
||||
public void setPermissionId(Integer permissionId)
|
||||
{
|
||||
this.permissionId = permissionId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for permissionId
|
||||
*******************************************************************************/
|
||||
public CustomAppContainer withPermissionId(Integer permissionId)
|
||||
{
|
||||
this.permissionId = permissionId;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sequenceNo
|
||||
*******************************************************************************/
|
||||
public Integer getSequenceNo()
|
||||
{
|
||||
return (this.sequenceNo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sequenceNo
|
||||
*******************************************************************************/
|
||||
public void setSequenceNo(Integer sequenceNo)
|
||||
{
|
||||
this.sequenceNo = sequenceNo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sequenceNo
|
||||
*******************************************************************************/
|
||||
public CustomAppContainer withSequenceNo(Integer sequenceNo)
|
||||
{
|
||||
this.sequenceNo = sequenceNo;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sections
|
||||
*******************************************************************************/
|
||||
public List<CustomAppSection> getSections()
|
||||
{
|
||||
return (this.sections);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sections
|
||||
*******************************************************************************/
|
||||
public void setSections(List<CustomAppSection> sections)
|
||||
{
|
||||
this.sections = sections;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sections
|
||||
*******************************************************************************/
|
||||
public CustomAppContainer withSections(List<CustomAppSection> sections)
|
||||
{
|
||||
this.sections = sections;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,22 +24,15 @@ package com.kingsrook.qbits.customapps.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qbits.customapps.customizers.CustomAppIconTableCustomizer;
|
||||
import com.kingsrook.qbits.customapps.metadata.CustomAppsGenericCompositeWidgetMetaDataProducer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
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.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QVirtualFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSectionAlternativeType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
@@ -78,25 +71,9 @@ public class CustomAppIcon extends QRecordEntity implements Serializable
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("name")
|
||||
.withSection(SectionFactory.defaultT1("id"))
|
||||
.withSection(SectionFactory.defaultT2("name", "iconId").withGridColumns(12)
|
||||
.withAlternative(QFieldSectionAlternativeType.RECORD_VIEW, (s) ->
|
||||
{
|
||||
s.getFieldNames().add("divider");
|
||||
s.getFieldNames().add("iconPreview");
|
||||
}
|
||||
))
|
||||
.withSection(SectionFactory.defaultT2("name", "iconId"))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
table.withCustomizer(TableCustomizers.POST_QUERY_RECORD, new QCodeReference(CustomAppIconTableCustomizer.class));
|
||||
|
||||
////////////////////
|
||||
// virtual fields //
|
||||
////////////////////
|
||||
table.withVirtualField(CustomAppsGenericCompositeWidgetMetaDataProducer.applyToField(
|
||||
new QVirtualFieldMetaData("iconPreview", QFieldType.STRING)));
|
||||
table.withVirtualField(CustomAppsGenericCompositeWidgetMetaDataProducer.applyToField(
|
||||
new QVirtualFieldMetaData("divider", QFieldType.STRING)));
|
||||
|
||||
return (table);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,7 @@ package com.kingsrook.qbits.customapps.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
@@ -39,7 +37,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.Child
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildRecordListWidget;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.ChildTable;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.SectionFactory;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
@@ -62,8 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
)
|
||||
public class CustomAppSection extends QRecordEntity implements Serializable
|
||||
{
|
||||
public static final String TABLE_NAME = "customAppSection";
|
||||
public static final String ASSOCIATION_NAME_CUSTOM_APPS = "customApps";
|
||||
public static final String TABLE_NAME = "customAppSection";
|
||||
|
||||
|
||||
|
||||
@@ -89,9 +85,7 @@ public class CustomAppSection extends QRecordEntity implements Serializable
|
||||
.withSection(SectionFactory.defaultT1("id"))
|
||||
.withSection(SectionFactory.defaultT2("customAppContainerId", "name", "sequenceNo"))
|
||||
.withSection(SectionFactory.customT2("apps", new QIcon("polyline")).withWidgetName(appsChildJoinName))
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"))
|
||||
|
||||
.withAssociation(new Association().withName(ASSOCIATION_NAME_CUSTOM_APPS).withJoinName(appsChildJoinName).withAssociatedTableName(CustomApp.TABLE_NAME));
|
||||
.withSection(SectionFactory.defaultT3("createDate", "modifyDate"));
|
||||
|
||||
return (table);
|
||||
}
|
||||
@@ -105,7 +99,7 @@ public class CustomAppSection extends QRecordEntity implements Serializable
|
||||
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR, isRequired = true)
|
||||
private String name;
|
||||
|
||||
@QField(label = "Container", isRequired = true, possibleValueSourceName = CustomAppContainer.TABLE_NAME)
|
||||
@QField(isRequired = true, possibleValueSourceName = CustomAppContainer.TABLE_NAME)
|
||||
private Integer customAppContainerId;
|
||||
|
||||
@QField(isRequired = true)
|
||||
@@ -117,9 +111,6 @@ public class CustomAppSection extends QRecordEntity implements Serializable
|
||||
@QField(isEditable = false)
|
||||
private Instant modifyDate;
|
||||
|
||||
@QAssociation(name = ASSOCIATION_NAME_CUSTOM_APPS)
|
||||
private List<CustomApp> apps;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -325,35 +316,4 @@ public class CustomAppSection extends QRecordEntity implements Serializable
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for apps
|
||||
*******************************************************************************/
|
||||
public List<CustomApp> getApps()
|
||||
{
|
||||
return (this.apps);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for apps
|
||||
*******************************************************************************/
|
||||
public void setApps(List<CustomApp> apps)
|
||||
{
|
||||
this.apps = apps;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for apps
|
||||
*******************************************************************************/
|
||||
public CustomAppSection withApps(List<CustomApp> apps)
|
||||
{
|
||||
this.apps = apps;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* 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.utils;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.userrolepermissions.model.Permission;
|
||||
import com.kingsrook.qbits.userrolepermissions.model.PermissionObjectType;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CustomAppPermissionUtils
|
||||
{
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static Permission buildOrGetPermission(String name, String tableName) throws QException
|
||||
{
|
||||
String objectName = CustomAppsUtils.getCamelCaseName(name, tableName);
|
||||
String permissionName = objectName + ".hasAccess";
|
||||
|
||||
/////////////////////////////////////
|
||||
// if permission exists, return it //
|
||||
/////////////////////////////////////
|
||||
GetOutput output = new GetAction().execute(new GetInput(Permission.TABLE_NAME).withUniqueKey(Map.of("name", permissionName)));
|
||||
if(output.getRecord() != null)
|
||||
{
|
||||
return (new Permission(output.getRecord()));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// otherwise create a new permission to be inserted //
|
||||
//////////////////////////////////////////////////////
|
||||
Permission permission = new Permission()
|
||||
.withName(permissionName)
|
||||
.withDescription("Permission to access " + name)
|
||||
.withObjectType(PermissionObjectType.APP.getId())
|
||||
.withObjectLabel(name);
|
||||
|
||||
InsertInput permissionInput = new InsertInput(Permission.TABLE_NAME).withRecordEntity(permission);
|
||||
InsertOutput permissionOutput = new InsertAction().execute(permissionInput);
|
||||
return (new Permission(permissionOutput.getRecords().get(0)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* 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.utils;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qbits.customapps.model.CustomApp;
|
||||
import com.kingsrook.qbits.customapps.model.CustomAppContainer;
|
||||
import com.kingsrook.qbits.customapps.model.CustomAppIcon;
|
||||
import com.kingsrook.qbits.customapps.model.CustomAppSection;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
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.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType.ChipValues.iconAndColorValues;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CustomAppsUtils
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static void addDynamicIconChipAdornments(QFrontendTableMetaData table, String fieldName) throws QException
|
||||
{
|
||||
QFrontendFieldMetaData field = table.getFields().get(fieldName);
|
||||
Optional<FieldAdornment> chipAdornmentOpt = field.getAdornments().stream().filter(a -> a.getType().equals(AdornmentType.CHIP)).findFirst();
|
||||
if(chipAdornmentOpt.isPresent())
|
||||
{
|
||||
////////////////////////////////////////////////////////////
|
||||
// look up all icons in the system and add chips for them //
|
||||
////////////////////////////////////////////////////////////
|
||||
List<CustomAppIcon> customAppIcons = QueryAction.execute(CustomAppIcon.TABLE_NAME, CustomAppIcon.class, new QQueryFilter().withOrderBy(new QFilterOrderBy("name")));
|
||||
for(CustomAppIcon icon : customAppIcons)
|
||||
{
|
||||
chipAdornmentOpt.get().withValues(iconAndColorValues(icon.getId(), icon.getIconId(), AdornmentType.ChipValues.COLOR_SECONDARY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static String getCamelCaseName(String name, String tableName)
|
||||
{
|
||||
name = nameToCamelCase(name);
|
||||
switch(tableName)
|
||||
{
|
||||
case CustomApp.TABLE_NAME -> name = "qca" + StringUtils.ucFirst(name);
|
||||
case CustomAppSection.TABLE_NAME -> name = "qcas" + StringUtils.ucFirst(name);
|
||||
case CustomAppContainer.TABLE_NAME -> name = "qcac" + StringUtils.ucFirst(name);
|
||||
default ->
|
||||
{
|
||||
////////////////////////////////////////////////////////////
|
||||
// dont add a prefix in this case and lowercase the start //
|
||||
////////////////////////////////////////////////////////////
|
||||
name = StringUtils.lcFirst(name);
|
||||
}
|
||||
}
|
||||
|
||||
return (name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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();
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ package com.kingsrook.qbits.customapps.widgets;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
|
||||
@@ -29,18 +28,9 @@ public class LookerWidgetRenderer extends AbstractWidgetRenderer
|
||||
try
|
||||
{
|
||||
Map<String, String> queryParams = input.getQueryParams();
|
||||
if(!queryParams.containsKey("dashboardId") || queryParams.get("dashboardId") == null)
|
||||
{
|
||||
throw (new QUserFacingException("LookerWidgetRenderer requires a dashboardId to be provided."));
|
||||
}
|
||||
|
||||
int dashboardId = Integer.parseInt(queryParams.get("dashboardId"));
|
||||
Integer dashboardId = Integer.parseInt(queryParams.get("dashboardId"));
|
||||
return (new RenderWidgetOutput(new LookerWidgetData().withDashboardId(dashboardId)));
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
throw (qe);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Error rendering LookerWidget", e));
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
/*
|
||||
* 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.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.customapps.metadata.CustomAppsDynamicMetaDataProducer;
|
||||
import com.kingsrook.qbits.customapps.metadata.CustomAppsMetaDataActionCustomizer;
|
||||
import com.kingsrook.qbits.customapps.model.CustomApp;
|
||||
import com.kingsrook.qbits.customapps.model.CustomAppBackendEnum;
|
||||
import com.kingsrook.qbits.userrolepermissions.UserRolePermissionsQBitConfig;
|
||||
import com.kingsrook.qbits.userrolepermissions.UserRolePermissionsQBitProducer;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.StatisticsData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
|
||||
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.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.ProvidedOrSuppliedTableConfig;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
import com.kingsrook.qqq.backend.module.postgres.model.metadata.PostgreSQLBackendMetaData;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Custom apps on a real PostgreSQL 17: the schema owner creates the tables, the
|
||||
** application runs as a DML-only role, apps declared as rows reach the frontend
|
||||
** metadata per session, a custom screen shows a widget over local data, and the
|
||||
** apps' permissions (rows of the permission table the roles qbit manages) decide
|
||||
** who sees and renders it.
|
||||
*******************************************************************************/
|
||||
class CustomAppsPostgresTest
|
||||
{
|
||||
private static final String OWNER = "ca_owner";
|
||||
private static final String RUNTIME = "ca_runtime";
|
||||
private static final String BACKEND = "customAppsPg";
|
||||
private static final String ITEMS = "demoItem";
|
||||
private static final String WIDGET = "demoItemCountWidget";
|
||||
|
||||
private static GenericContainer<?> postgres;
|
||||
private static String host;
|
||||
private static int port;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** PG17 with an owner-created schema (derived from the installed metadata) and a
|
||||
** DML-only runtime role.
|
||||
*******************************************************************************/
|
||||
@BeforeAll
|
||||
static void start() throws Exception
|
||||
{
|
||||
postgres = new GenericContainer<>("postgres:17-alpine")
|
||||
.withEnv("POSTGRES_PASSWORD", "super-synthetic")
|
||||
.withExposedPorts(5432)
|
||||
.waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*\\n", 2).withStartupTimeout(java.time.Duration.ofMinutes(2)));
|
||||
postgres.start();
|
||||
host = postgres.getHost();
|
||||
port = postgres.getMappedPort(5432);
|
||||
|
||||
try(Connection admin = connect("postgres", "super-synthetic", "postgres"); Statement statement = admin.createStatement())
|
||||
{
|
||||
statement.execute("CREATE ROLE " + OWNER + " LOGIN PASSWORD 'owner-synthetic'");
|
||||
statement.execute("CREATE ROLE " + RUNTIME + " LOGIN PASSWORD 'runtime-synthetic'");
|
||||
statement.execute("CREATE DATABASE customapps OWNER " + OWNER);
|
||||
statement.execute("REVOKE ALL ON DATABASE customapps FROM PUBLIC");
|
||||
statement.execute("GRANT CONNECT ON DATABASE customapps TO " + RUNTIME);
|
||||
}
|
||||
|
||||
QInstance scratch = instance(OWNER, "owner-synthetic");
|
||||
try(Connection owner = connect(OWNER, "owner-synthetic", "customapps"); Statement statement = owner.createStatement())
|
||||
{
|
||||
statement.execute("REVOKE ALL ON SCHEMA public FROM PUBLIC");
|
||||
statement.execute("GRANT USAGE ON SCHEMA public TO " + RUNTIME);
|
||||
for(QTableMetaData table : scratch.getTables().values())
|
||||
{
|
||||
if(BACKEND.equals(table.getBackendName()))
|
||||
{
|
||||
statement.execute(TestDdl.create(table));
|
||||
statement.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON \"" + table.getName() + "\" TO " + RUNTIME);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stop the database.
|
||||
*******************************************************************************/
|
||||
@AfterAll
|
||||
static void stop()
|
||||
{
|
||||
if(postgres != null)
|
||||
{
|
||||
postgres.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Clear the context.
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void clear()
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Declared rows reach the frontend metadata per session; the screen shows a
|
||||
** widget over local data; the app is closed to anyone without its permission;
|
||||
** invalid rows are skipped rather than taking the others down.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testDeclaredAppRendersAndIsPermissioned() throws Exception
|
||||
{
|
||||
QInstance instance = instance(RUNTIME, "runtime-synthetic");
|
||||
QContext.init(instance, new QSystemUserSession());
|
||||
seed();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// the customizers created one permission per app and container (URP rows) //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
assertThat(new CountAction().execute(new CountInput("permission")).getCount()).isGreaterThanOrEqualTo(2);
|
||||
|
||||
var all = new CustomAppsDynamicMetaDataProducer().getCustomAppList(instance, name -> true);
|
||||
assertThat(all.appMap()).containsKey("qcaOperationsBoard");
|
||||
assertThat(all.appMap()).doesNotContainKeys("qcaMissingWidget", "qcaGeneratedName", "qcaNoWidget");
|
||||
assertThat(all.appMap().get("qcaOperationsBoard").getWidgets()).containsExactly(WIDGET);
|
||||
assertThat(all.appMap().get("qcaOperationsBoard").getPermissionRules().getLevel()).isEqualTo(PermissionLevel.HAS_ACCESS_PERMISSION);
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// a user without the app permission sees none //
|
||||
/////////////////////////////////////////////////
|
||||
QContext.init(instance, new QSession().withPermissions(Set.of("unrelated.read")));
|
||||
var nothing = new MetaDataAction().execute(new MetaDataInput());
|
||||
assertThat(nothing.getApps()).doesNotContainKeys("qcaOperationsBoard", "qcacOperations");
|
||||
assertThat(nothing.getAppTree().stream().map(n -> n.getName())).doesNotContain("qcacOperations");
|
||||
assertThatThrownBy(() -> new RenderWidgetAction().execute(new RenderWidgetInput().withWidgetMetaData(instance.getWidget(WIDGET))))
|
||||
.isInstanceOf(QPermissionDeniedException.class);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// a user granted the app (and its container) sees it and renders real DB data //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
QContext.init(instance, new QSession().withPermissions(Set.of("qcaOperationsBoard.hasAccess", "qcacOperations.hasAccess", ITEMS + ".read")));
|
||||
var visible = new MetaDataAction().execute(new MetaDataInput());
|
||||
assertThat(visible.getApps()).containsKeys("qcaOperationsBoard", "qcacOperations");
|
||||
assertThat(visible.getWidgets()).containsKey(WIDGET);
|
||||
assertThat(visible.getAppTree().stream().map(n -> n.getName())).contains("qcacOperations");
|
||||
RenderWidgetOutput rendered = new RenderWidgetAction().execute(new RenderWidgetInput().withWidgetMetaData(instance.getWidget(WIDGET)));
|
||||
assertThat(((StatisticsData) rendered.getWidgetData()).getCount()).isEqualTo(3);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// a user granted the app but not the container does not see it in the tree //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
QContext.init(instance, new QSession().withPermissions(Set.of("qcaOperationsBoard.hasAccess")));
|
||||
assertThat(new MetaDataAction().execute(new MetaDataInput()).getAppTree().stream().map(n -> n.getName())).doesNotContain("qcacOperations");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The runtime role does DML only.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRuntimeRoleHasNoDdl() throws Exception
|
||||
{
|
||||
try(Connection runtime = connect(RUNTIME, "runtime-synthetic", "customapps"); Statement statement = runtime.createStatement())
|
||||
{
|
||||
assertThatThrownBy(() -> statement.execute("CREATE TABLE intruder (id int)")).isInstanceOf(SQLException.class).hasMessageContaining("permission denied");
|
||||
assertThatThrownBy(() -> statement.execute("DROP TABLE \"" + CustomApp.TABLE_NAME + "\"")).isInstanceOf(SQLException.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Component bundles are code the browser runs: only paths on this application,
|
||||
** or https hosts the configuration lists, are accepted.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testComponentSourceIsRestricted() throws Exception
|
||||
{
|
||||
QInstance instance = instance(RUNTIME, "runtime-synthetic");
|
||||
QContext.init(instance, new QSystemUserSession());
|
||||
CustomAppsDynamicMetaDataProducer producer = new CustomAppsDynamicMetaDataProducer();
|
||||
assertThat(producer.isAllowedComponentSource("/dynamic-qfmd-components/q-embed-looker-dashboards.bundle.js")).isTrue();
|
||||
assertThat(producer.isAllowedComponentSource("//evil.example/x.js")).isFalse();
|
||||
assertThat(producer.isAllowedComponentSource("https://evil.example/x.js")).isFalse();
|
||||
assertThat(producer.isAllowedComponentSource("javascript:alert(1)")).isFalse();
|
||||
assertThat(producer.isAllowedComponentSource("data:text/javascript,alert(1)")).isFalse();
|
||||
assertThat(producer.isAllowedComponentSource("/ok/../../etc/passwd")).isFalse();
|
||||
assertThat(producer.isAllowedComponentSource("/a b.js")).isFalse();
|
||||
assertThat(producer.isAllowedComponentSource(null)).isFalse();
|
||||
|
||||
QInstance allowing = instance(RUNTIME, "runtime-synthetic", List.of("Cdn.Example.com"));
|
||||
QContext.init(allowing, new QSystemUserSession());
|
||||
assertThat(producer.isAllowedComponentSource("https://cdn.example.com/x.js")).isTrue();
|
||||
assertThat(producer.isAllowedComponentSource("https://cdn.example.com@evil.example/x.js")).isFalse();
|
||||
assertThat(producer.isAllowedComponentSource("http://cdn.example.com/x.js")).isFalse();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The configuration requires its backend.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testConfigValidation() throws Exception
|
||||
{
|
||||
List<String> errors = new java.util.ArrayList<>();
|
||||
new CustomAppsQBitConfig().validate(new QInstance(), errors);
|
||||
assertThat(errors).anyMatch(e -> e.contains("backendName"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Synthetic definitions: one valid local-widget app and three invalid ones.
|
||||
*******************************************************************************/
|
||||
private static void seed() throws QException
|
||||
{
|
||||
insert("customAppIcon", new QRecord().withValue("id", 1).withValue("name", "Dashboard").withValue("iconId", "dashboard"));
|
||||
insert("customAppBackendConfig", new QRecord().withValue("id", 1).withValue("name", "Local widget").withValue("customAppBackendId", CustomAppBackendEnum.LOCAL_WIDGET.getId()));
|
||||
insert("customAppContainer", new QRecord().withValue("id", 1).withValue("name", "Operations").withValue("customAppIconId", 1).withValue("sequenceNo", 1));
|
||||
insert("customAppSection", new QRecord().withValue("id", 1).withValue("name", "Boards").withValue("customAppContainerId", 1).withValue("sequenceNo", 1));
|
||||
insertApp(1, "Operations Board", WIDGET);
|
||||
insertApp(2, "Missing Widget", "noSuchWidget");
|
||||
insertApp(3, "Generated Name", "qcaOperationsBoardWidget");
|
||||
insertApp(4, "No Widget", null);
|
||||
for(int i = 1; i <= 3; i++)
|
||||
{
|
||||
insert(ITEMS, new QRecord().withValue("name", "Item " + i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One app definition row.
|
||||
*******************************************************************************/
|
||||
private static void insertApp(int id, String name, String widgetName) throws QException
|
||||
{
|
||||
insert("customApp", new QRecord().withValue("id", id).withValue("name", name).withValue("customAppBackendConfigId", 1).withValue("customAppSectionId", 1)
|
||||
.withValue("customAppIconId", 1).withValue("sequenceNo", id).withValue("widgetName", widgetName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert a record.
|
||||
*******************************************************************************/
|
||||
private static void insert(String table, QRecord record) throws QException
|
||||
{
|
||||
List<QRecord> records = new InsertAction().execute(new InsertInput(table).withRecord(record)).getRecords();
|
||||
assertThat(records.get(0).getErrors()).isEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An application on the given role.
|
||||
*******************************************************************************/
|
||||
private static QInstance instance(String user, String password) throws QException
|
||||
{
|
||||
return instance(user, password, List.of());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An application: native PG backend as a role, the roles qbit (whose permission
|
||||
** table the apps use), this qbit with its metadata customizer, a local table and
|
||||
** a permissioned widget over it.
|
||||
*******************************************************************************/
|
||||
private static QInstance instance(String user, String password, List<String> allowedHosts) throws QException
|
||||
{
|
||||
QInstance instance = new QInstance();
|
||||
instance.setEnforcePermissionsInCoreActions(true);
|
||||
instance.withInstanceDefaultAuthentication(new QAuthenticationMetaData().withName("anonymous").withType(QAuthenticationType.FULLY_ANONYMOUS));
|
||||
PostgreSQLBackendMetaData backend = new PostgreSQLBackendMetaData();
|
||||
backend.setName(BACKEND);
|
||||
backend.setHostName(host);
|
||||
backend.setPort(port);
|
||||
backend.setDatabaseName("customapps");
|
||||
backend.setUsername(user);
|
||||
backend.setPassword(password);
|
||||
instance.addBackend(backend);
|
||||
|
||||
new UserRolePermissionsQBitProducer().withUserRolePermissionsQBitConfig(new UserRolePermissionsQBitConfig()
|
||||
.withUserTableConfig(ProvidedOrSuppliedTableConfig.provideTableUsingBackendNamed(BACKEND))
|
||||
.withTableMetaDataCustomizer((i, table) ->
|
||||
{
|
||||
table.setBackendName(BACKEND);
|
||||
return (table);
|
||||
})).produce(instance);
|
||||
new CustomAppsQBitProducer().withQBitConfig(new CustomAppsQBitConfig().withBackendName(BACKEND).withAllowedComponentHosts(allowedHosts)).produce(instance).addSelfToInstance(instance);
|
||||
instance.setMetaDataActionCustomizer(new QCodeReference(CustomAppsMetaDataActionCustomizer.class));
|
||||
|
||||
instance.addTable(new QTableMetaData().withName(ITEMS).withBackendName(BACKEND).withPrimaryKeyField("id")
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.READ_WRITE_PERMISSIONS).withPermissionBaseName(ITEMS))
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING).withMaxLength(100)));
|
||||
instance.addWidget(new QWidgetMetaData().withName(WIDGET).withType(WidgetType.STATISTICS.getType())
|
||||
.withCodeReference(new QCodeReference(ItemCountWidgetRenderer.class))
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withPermissionBaseName("qcaOperationsBoard")));
|
||||
new QInstanceValidator().validate(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A JDBC connection as a named role.
|
||||
*******************************************************************************/
|
||||
private static Connection connect(String user, String password, String database) throws SQLException
|
||||
{
|
||||
return DriverManager.getConnection("jdbc:postgresql://" + host + ":" + port + "/" + database, user, password);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A widget that counts the rows of the local table, as the viewing user.
|
||||
*******************************************************************************/
|
||||
public static class ItemCountWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
/***************************************************************************
|
||||
** Render the count.
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
Integer count = new CountAction().execute(new CountInput(ITEMS)).getCount();
|
||||
return (new RenderWidgetOutput(new StatisticsData().withCount(count)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* 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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** PostgreSQL DDL derived from QQQ table metadata (exact, quoted names, as QQQ
|
||||
** quotes them), so a test schema is exactly what the producer defined.
|
||||
*******************************************************************************/
|
||||
final class TestDdl
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Utility class.
|
||||
*******************************************************************************/
|
||||
private TestDdl()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One CREATE TABLE.
|
||||
*******************************************************************************/
|
||||
static String create(QTableMetaData table)
|
||||
{
|
||||
List<String> lines = new ArrayList<>();
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
lines.add(" \"" + field.getName() + "\" " + type(table, field));
|
||||
}
|
||||
for(UniqueKey key : table.getUniqueKeys() == null ? List.<UniqueKey>of() : table.getUniqueKeys())
|
||||
{
|
||||
lines.add(" UNIQUE (" + String.join(", ", key.getFieldNames().stream().map(n -> "\"" + n + "\"").toList()) + ")");
|
||||
}
|
||||
return "CREATE TABLE \"" + table.getName() + "\" (\n" + String.join(",\n", lines) + "\n)";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Column type and constraints.
|
||||
*******************************************************************************/
|
||||
private static String type(QTableMetaData table, QFieldMetaData field)
|
||||
{
|
||||
if(field.getName().equals(table.getPrimaryKeyField()))
|
||||
{
|
||||
return "INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY";
|
||||
}
|
||||
String base = switch(field.getType())
|
||||
{
|
||||
case STRING, PASSWORD -> field.getMaxLength() == null ? "TEXT" : "VARCHAR(" + field.getMaxLength() + ")";
|
||||
case TEXT, HTML -> "TEXT";
|
||||
case INTEGER -> "INTEGER";
|
||||
case LONG -> "BIGINT";
|
||||
case BOOLEAN -> "BOOLEAN";
|
||||
case DATE_TIME -> "TIMESTAMP WITH TIME ZONE";
|
||||
default -> throw new IllegalStateException("Unsupported type " + field.getType());
|
||||
};
|
||||
return base + (Boolean.TRUE.equals(field.getIsRequired()) ? " NOT NULL" : "");
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
DROP TABLE IF EXISTS custom_app_container;
|
||||
CREATE TABLE custom_app_container
|
||||
(
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
create_date TIMESTAMP DEFAULT now(),
|
||||
modify_date TIMESTAMP DEFAULT now(),
|
||||
custom_app_icon_id INTEGER NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sequence_no INTEGER NOT NULL,
|
||||
permission_id INTEGER,
|
||||
place_before_container VARCHAR(100)
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS custom_app_section;
|
||||
CREATE TABLE custom_app_section
|
||||
(
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
create_date TIMESTAMP DEFAULT now(),
|
||||
modify_date TIMESTAMP DEFAULT now(),
|
||||
custom_app_container_id INTEGER NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sequence_no INTEGER NOT NULL
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS custom_app;
|
||||
CREATE TABLE custom_app
|
||||
(
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
create_date TIMESTAMP DEFAULT now(),
|
||||
modify_date TIMESTAMP DEFAULT now(),
|
||||
custom_app_section_id INTEGER NOT NULL,
|
||||
custom_app_backend_config_id INTEGER NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
custom_app_icon_id VARCHAR(100) NOT NULL,
|
||||
sequence_no INTEGER NOT NULL,
|
||||
looker_dashboard_id INTEGER,
|
||||
permission_id INTEGER
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS custom_app_backend_config;
|
||||
CREATE TABLE custom_app_backend_config
|
||||
(
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
create_date TIMESTAMP DEFAULT now(),
|
||||
modify_date TIMESTAMP DEFAULT now(),
|
||||
custom_app_backend_id INTEGER NOT NULL,
|
||||
name VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
DROP TABLE IF EXISTS custom_app_icon;
|
||||
CREATE TABLE custom_app_icon
|
||||
(
|
||||
id INTEGER AUTO_INCREMENT PRIMARY KEY,
|
||||
create_date TIMESTAMP DEFAULT now(),
|
||||
modify_date TIMESTAMP DEFAULT now(),
|
||||
icon_id VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user