Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2504129672
|
||
|
|
b1ede9f887 | ||
|
|
81552784e9 | ||
|
|
a68d557fb2 | ||
|
|
36527b06ff | ||
|
|
7c78003765
|
||
|
|
ce127037e4 | ||
|
|
20545be7fc
|
||
|
|
96d2e48e47 | ||
|
|
1d2c2b0029
|
||
|
|
21671b3df3
|
||
|
|
9d1d712040 | ||
|
|
2c1fa83cfd
|
||
|
|
25d03c1514 | ||
|
|
442eb4398c
|
||
|
|
3aa728d88a
|
||
|
|
51ab908b11 | ||
|
|
3474b0c188
|
||
|
|
b12cadfc75 |
No files matched your search
+11
-57
@@ -1,60 +1,14 @@
|
|||||||
version: 2.1
|
version: 2.1
|
||||||
|
setup: true
|
||||||
orbs:
|
orbs:
|
||||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
qqq-orb: kingsrook/qqq-orb@0.7.12
|
||||||
|
|
||||||
commands:
|
|
||||||
configure-package-install:
|
|
||||||
steps:
|
|
||||||
- run:
|
|
||||||
name: Keep package installation unattended
|
|
||||||
command: |
|
|
||||||
# These build machines are discarded after the job.
|
|
||||||
sudo install -d /etc/needrestart/conf.d
|
|
||||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
test_only:
|
setup:
|
||||||
jobs:
|
jobs:
|
||||||
- qqq-orb/mvn_test_only:
|
- qqq-orb/generate_pipeline:
|
||||||
pre-steps:
|
orb_version: 0.7.12
|
||||||
- configure-package-install
|
filters:
|
||||||
context: [qqq-maven-registry-credentials]
|
branches:
|
||||||
filters:
|
ignore: automation/qbit-rc-pending
|
||||||
branches:
|
tags:
|
||||||
ignore: /(develop|release\/.*)/
|
only: /^v\d+\.\d+\.\d+$/
|
||||||
|
|
||||||
publish_snapshot:
|
|
||||||
jobs:
|
|
||||||
- qqq-orb/mvn_publish:
|
|
||||||
pre-steps:
|
|
||||||
- configure-package-install
|
|
||||||
context: [qqq-maven-registry-credentials]
|
|
||||||
branch_type: snapshot
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: [develop]
|
|
||||||
|
|
||||||
publish_release_candidate:
|
|
||||||
jobs:
|
|
||||||
- qqq-orb/mvn_publish:
|
|
||||||
pre-steps:
|
|
||||||
- configure-package-install
|
|
||||||
context: [qqq-maven-registry-credentials]
|
|
||||||
branch_type: release_candidate
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: [/release\/.*/]
|
|
||||||
|
|
||||||
publish_release:
|
|
||||||
jobs:
|
|
||||||
- qqq-orb/mvn_publish:
|
|
||||||
pre-steps:
|
|
||||||
- configure-package-install
|
|
||||||
context: [qqq-maven-registry-credentials]
|
|
||||||
branch_type: release
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
ignore: /.*/
|
|
||||||
tags:
|
|
||||||
only: [/^v\d+\.\d+\.\d+$/]
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
@@ -1,25 +1,74 @@
|
|||||||
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
|
|
||||||
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
|
|
||||||
# the release line and the release/* push runs CircleCI publish_release_candidate.
|
|
||||||
name: Release candidate
|
name: Release candidate
|
||||||
|
'on':
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
dry_run:
|
dry_run:
|
||||||
description: Print the plan without merging or creating branches
|
description: Plan a cut without changing release state
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
finalize:
|
||||||
|
description: Retry finalization of an existing cut; do not cut another RC
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
recover:
|
||||||
|
description: Recover the same published RC after a deployment timeout; never
|
||||||
|
reupload
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
circle_build:
|
||||||
|
description: Original Circle publisher build number (recovery only)
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
merge_sha:
|
||||||
|
description: Existing cut merge_sha (finalize retry only)
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
bump_sha:
|
||||||
|
description: Existing cut bump_sha (finalize retry only)
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
branch:
|
||||||
|
description: Existing cut branch (finalize retry only)
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
version:
|
||||||
|
description: Existing cut version (finalize retry only)
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types:
|
||||||
branches: [develop]
|
- closed
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
repository_dispatch:
|
||||||
|
types:
|
||||||
|
- qbit-rc-published
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
actions: read
|
||||||
|
checks: read
|
||||||
jobs:
|
jobs:
|
||||||
release-rc:
|
release-rc:
|
||||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
|
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|
||||||
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
|
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
|
||||||
|
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
|
||||||
|
'release:rc'))
|
||||||
|
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
|
||||||
with:
|
with:
|
||||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
|
||||||
|
}}
|
||||||
|
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
|
||||||
|
== 'workflow_dispatch' && inputs.finalize) }}
|
||||||
|
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
|
||||||
|
}}
|
||||||
|
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|
||||||
|
|| '' }}
|
||||||
|
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|
||||||
|
|| inputs.merge_sha || '' }}
|
||||||
|
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|
||||||
|
|| inputs.bump_sha || '' }}
|
||||||
|
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|
||||||
|
|| inputs.branch || '' }}
|
||||||
|
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|
||||||
|
|| inputs.version || '' }}
|
||||||
|
secrets: inherit
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
profile: qbit
|
||||||
|
orb_version: 0.7.12
|
||||||
|
default_branch: develop
|
||||||
|
java_version: '21'
|
||||||
|
browsers: true
|
||||||
|
api_checks: false
|
||||||
|
docs_only: false
|
||||||
|
release_on_main: false
|
||||||
|
release_tag_policy: stable
|
||||||
|
test_branch_policy: non-release
|
||||||
|
publish:
|
||||||
|
snapshot: true
|
||||||
|
release_candidate: true
|
||||||
|
release: true
|
||||||
|
asciidoc: false
|
||||||
|
release_completion: true
|
||||||
@@ -4,6 +4,12 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Scheduled analyses and watches work under OAuth2. When the host declares the `data-analysis` service principal (QQQ service identities, `DataAnalysisQBitConfig.withServicePrincipal`), schedule and watch inventory runs as that principal, and owner and recipient runs go on their behalf through it: their current access intersected with the principal's. The self-made `data-analysis-scheduler` session was rejected under OAuth2 (`Invalid session in request` on every `data_analysis_run_schedules` run). With no principal declared, behavior is unchanged. Needs the QQQ release with service identities (qqq#1255, #1256).
|
||||||
|
- QBit metadata now reports the Maven artifact version, including release versions, from a filtered build resource.
|
||||||
|
- The Data Analysis home no longer shows the engine Setup card or "Unavailable: READ permission required." shells. Setup is admin-only (hidden from everyone else) and off the home; hosts place `DataAnalysisQBitProducer.SYSTEM_WIDGETS` on their Admin area. Home widgets the user cannot read are omitted from metadata, and unreadable groups are left out of the overview.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Complete host-neutral in-app help bundle with live metadata references and a core help coverage gate.
|
- Complete host-neutral in-app help bundle with live metadata references and a core help coverage gate.
|
||||||
@@ -42,6 +48,8 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- UI API extension data-analysis 1.5.0 (requires QQQ 4.1.0-RC.15, UI API 1.6.0): operationIds, standard error responses with BasicErrorResponseV1, extension tags, contract validation for operationIds and schema references, and HTTP error envelope regression assertions.
|
||||||
|
|
||||||
- Pin QQQ 4.1.0-RC.7 and replace the temporary help loader with core bundle registration and startup sync.
|
- Pin QQQ 4.1.0-RC.7 and replace the temporary help loader with core bundle registration and startup sync.
|
||||||
|
|
||||||
- Boolean pivot/crosstab headers now say Yes / No / (Blank). Pivot measures use human labels such as Count, Average Amount, Sum of Amount, Distinct Email, Median Amount and 90th percentile of Amount.
|
- Boolean pivot/crosstab headers now say Yes / No / (Blank). Pivot measures use human labels such as Count, Average Amount, Sum of Amount, Distinct Email, Median Amount and 90th percentile of Amount.
|
||||||
|
|||||||
@@ -135,6 +135,7 @@
|
|||||||
<filtering>true</filtering>
|
<filtering>true</filtering>
|
||||||
<includes>
|
<includes>
|
||||||
<include>help/*.yaml</include>
|
<include>help/*.yaml</include>
|
||||||
|
<include>qbit-data-analysis.properties</include>
|
||||||
</includes>
|
</includes>
|
||||||
</resource>
|
</resource>
|
||||||
<resource>
|
<resource>
|
||||||
@@ -142,10 +143,20 @@
|
|||||||
<filtering>false</filtering>
|
<filtering>false</filtering>
|
||||||
<excludes>
|
<excludes>
|
||||||
<exclude>help/*.yaml</exclude>
|
<exclude>help/*.yaml</exclude>
|
||||||
|
<exclude>qbit-data-analysis.properties</exclude>
|
||||||
</excludes>
|
</excludes>
|
||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</resources>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<systemPropertyVariables combine.children="append">
|
||||||
|
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
|
||||||
|
</systemPropertyVariables>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||||
@@ -232,7 +243,7 @@
|
|||||||
<activeByDefault>true</activeByDefault>
|
<activeByDefault>true</activeByDefault>
|
||||||
</activation>
|
</activation>
|
||||||
<properties>
|
<properties>
|
||||||
<qqq.snapshot.version>4.1.0-RC.7</qqq.snapshot.version>
|
<qqq.snapshot.version>4.1.0-RC.19</qqq.snapshot.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
|||||||
@@ -427,6 +427,28 @@ public class DataAnalysisQBitConfig implements QBitConfig
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String servicePrincipal = "data-analysis";
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** The service principal schedules and watches run as when the host declares
|
||||||
|
** it (QQQ service identities): inventory as the principal, owner and
|
||||||
|
** recipient runs on their behalf through it. Undeclared: the previous local
|
||||||
|
** sessions.
|
||||||
|
*******************************************************************************/
|
||||||
|
public String getServicePrincipal()
|
||||||
|
{
|
||||||
|
return (servicePrincipal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for servicePrincipal (default data-analysis).
|
||||||
|
*******************************************************************************/
|
||||||
|
public DataAnalysisQBitConfig withServicePrincipal(String servicePrincipal)
|
||||||
|
{
|
||||||
|
this.servicePrincipal = servicePrincipal;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
private String messagingProviderName;
|
private String messagingProviderName;
|
||||||
private String fromAddress;
|
private String fromAddress;
|
||||||
private transient java.util.function.Function<String, String> recipientAddressSupplier;
|
private transient java.util.function.Function<String, String> recipientAddressSupplier;
|
||||||
|
|||||||
@@ -21,7 +21,12 @@
|
|||||||
package com.kingsrook.qbits.dataanalysis;
|
package com.kingsrook.qbits.dataanalysis;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
import com.kingsrook.qbits.dataanalysis.api.DataAnalysisApi;
|
import com.kingsrook.qbits.dataanalysis.api.DataAnalysisApi;
|
||||||
|
import com.kingsrook.qbits.dataanalysis.app.DataAnalysisHome;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
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.QInstance;
|
||||||
@@ -40,13 +45,48 @@ public class DataAnalysisQBitProducer implements QBitMetaDataProducer<DataAnalys
|
|||||||
{
|
{
|
||||||
public static final String GROUP_ID = "com.kingsrook.qbits";
|
public static final String GROUP_ID = "com.kingsrook.qbits";
|
||||||
public static final String ARTIFACT_ID = "qbit-data-analysis";
|
public static final String ARTIFACT_ID = "qbit-data-analysis";
|
||||||
public static final String VERSION = "0.1.0";
|
public static final String VERSION = loadVersion();
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Widgets that show engine configuration. They are registered with the qbit //
|
||||||
|
// (admin-only, hidden from everyone else) but kept off the business home; the //
|
||||||
|
// host places them on its Admin area, e.g. withWidgets(SYSTEM_WIDGETS). //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
|
public static final List<String> SYSTEM_WIDGETS = List.of(DataAnalysisHome.SETUP);
|
||||||
|
|
||||||
private DataAnalysisQBitConfig qBitConfig;
|
private DataAnalysisQBitConfig qBitConfig;
|
||||||
private String namespace;
|
private String namespace;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Read the artifact version from the Maven-filtered properties resource.
|
||||||
|
*******************************************************************************/
|
||||||
|
private static String loadVersion()
|
||||||
|
{
|
||||||
|
try(InputStream inputStream = DataAnalysisQBitProducer.class.getClassLoader().getResourceAsStream("qbit-data-analysis.properties"))
|
||||||
|
{
|
||||||
|
if(inputStream == null)
|
||||||
|
{
|
||||||
|
throw (new IllegalStateException("Missing qbit-data-analysis.properties; build with Maven to generate the artifact version"));
|
||||||
|
}
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.load(inputStream);
|
||||||
|
String version = properties.getProperty("version");
|
||||||
|
if(version == null || version.isBlank() || version.contains("${"))
|
||||||
|
{
|
||||||
|
throw (new IllegalStateException("Missing or unfiltered version in qbit-data-analysis.properties; build with Maven resource filtering"));
|
||||||
|
}
|
||||||
|
return (version);
|
||||||
|
}
|
||||||
|
catch(IOException e)
|
||||||
|
{
|
||||||
|
throw (new IllegalStateException("Could not read artifact version from qbit-data-analysis.properties", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Fluent configuration setter.
|
** Fluent configuration setter.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public class DataAnalysisApi
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static MiddlewareSpecExtension extension()
|
public static MiddlewareSpecExtension extension()
|
||||||
{
|
{
|
||||||
return (new MiddlewareSpecExtension().withName("data-analysis").withSemanticVersion("1.4.0")
|
return (new MiddlewareSpecExtension().withName("data-analysis").withSemanticVersion("1.5.0")
|
||||||
.withDescription("Profiles, pivots, comparisons, distribution shifts, correlations, scatter samples, crosstabs and anomaly watch previews as the signed-in user.")
|
.withDescription("Profiles, pivots, comparisons, distribution shifts, correlations, scatter samples, crosstabs and anomaly watch previews as the signed-in user.")
|
||||||
.withSpec(SavedApi.Run.class).withSpec(SavedApi.ListSaved.class).withSpec(SavedApi.Star.class).withSpec(SavedApi.Unstar.class)
|
.withSpec(SavedApi.Run.class).withSpec(SavedApi.ListSaved.class).withSpec(SavedApi.Star.class).withSpec(SavedApi.Unstar.class)
|
||||||
.withSpec(SavedApi.Sql.class).withSpec(AnalysisFileSpec.class).withSpec(AnalysisFileSpec.Saved.class)
|
.withSpec(SavedApi.Sql.class).withSpec(AnalysisFileSpec.class).withSpec(AnalysisFileSpec.Saved.class)
|
||||||
|
|||||||
@@ -28,14 +28,17 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import com.kingsrook.qbits.dataanalysis.DataAnalysisQBitConfig;
|
import com.kingsrook.qbits.dataanalysis.DataAnalysisQBitConfig;
|
||||||
|
import com.kingsrook.qbits.dataanalysis.DataAnalysisQBitProducer;
|
||||||
import com.kingsrook.qbits.dataanalysis.DataAnalysisRuntime;
|
import com.kingsrook.qbits.dataanalysis.DataAnalysisRuntime;
|
||||||
import com.kingsrook.qbits.dataanalysis.engine.DrillFilters;
|
import com.kingsrook.qbits.dataanalysis.engine.DrillFilters;
|
||||||
import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables;
|
import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables;
|
||||||
|
import com.kingsrook.qbits.dataanalysis.permissions.DataAnalysisPermissions;
|
||||||
import com.kingsrook.qbits.dataanalysis.saved.SavedAnalyses;
|
import com.kingsrook.qbits.dataanalysis.saved.SavedAnalyses;
|
||||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
|
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
|
||||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.permissions.UseOtherPermissionNameCustomPermissionChecker;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
@@ -54,6 +57,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
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.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.model.metadata.permissions.PermissionLevel;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
@@ -82,7 +86,39 @@ public class DataAnalysisHome extends AbstractWidgetRenderer
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static List<QWidgetMetaData> widgets()
|
public static List<QWidgetMetaData> widgets()
|
||||||
{
|
{
|
||||||
return (List.of(widget(SETUP, "Setup", WidgetType.TABLE), widget(), widget(STARRED, "Starred analyses", WidgetType.TABLE), widget(FIRING, "Firing watches", WidgetType.TABLE), widget(RECENT, "My recent runs", WidgetType.TABLE)));
|
return (List.of(setupWidget(), widget().withPermissionRules(readRules(DataAnalysisTables.SAVED)),
|
||||||
|
widget(STARRED, "Starred analyses", WidgetType.TABLE).withPermissionRules(readRules(DataAnalysisTables.SAVED)),
|
||||||
|
widget(FIRING, "Firing watches", WidgetType.TABLE).withPermissionRules(readRules(DataAnalysisTables.WATCH)),
|
||||||
|
widget(RECENT, "My recent runs", WidgetType.TABLE).withPermissionRules(readRules(DataAnalysisTables.RUN))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Names of the widgets placed on the business home. Setup is system content and
|
||||||
|
** is deliberately absent; hosts place {@link DataAnalysisQBitProducer#SYSTEM_WIDGETS}
|
||||||
|
** on their Admin area instead.
|
||||||
|
*******************************************************************************/
|
||||||
|
public static List<String> homeWidgetNames()
|
||||||
|
{
|
||||||
|
return (widgets().stream().map(QWidgetMetaData::getName).filter(name -> !DataAnalysisQBitProducer.SYSTEM_WIDGETS.contains(name)).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** The Setup card shows engine configuration, so only Data Analysis admins get it;
|
||||||
|
** everyone else has it omitted from metadata.
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QWidgetMetaData setupWidget()
|
||||||
|
{
|
||||||
|
return (widget(SETUP, "Setup", WidgetType.TABLE).withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)
|
||||||
|
.withDenyBehavior(DenyBehavior.HIDDEN).withCustomPermissionChecker(UseOtherPermissionNameCustomPermissionChecker.build(DataAnalysisPermissions.ADMIN))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** A data widget is omitted from metadata for users who cannot read its table.
|
||||||
|
*******************************************************************************/
|
||||||
|
private static QPermissionRules readRules(String table)
|
||||||
|
{
|
||||||
|
return (new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withDenyBehavior(DenyBehavior.HIDDEN)
|
||||||
|
.withCustomPermissionChecker(TableReadWidgetPermissionChecker.build(table)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@@ -101,7 +137,7 @@ public class DataAnalysisHome extends AbstractWidgetRenderer
|
|||||||
public static QAppMetaData app(DataAnalysisQBitConfig config, List<QTableMetaData> tables)
|
public static QAppMetaData app(DataAnalysisQBitConfig config, List<QTableMetaData> tables)
|
||||||
{
|
{
|
||||||
QAppMetaData app = new QAppMetaData().withName(config.getAppName()).withLabel(config.getAppLabel())
|
QAppMetaData app = new QAppMetaData().withName(config.getAppName()).withLabel(config.getAppLabel())
|
||||||
.withIcon(new QIcon("insights")).withWidgets(widgets().stream().map(QWidgetMetaData::getName).toList())
|
.withIcon(new QIcon("insights")).withWidgets(homeWidgetNames())
|
||||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
|
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
|
||||||
.withSection(new QAppSection().withName("analyses").withLabel("Analyses").withIcon(new QIcon("insights")).withTables(DataAnalysisTables.NAV_TABLES));
|
.withSection(new QAppSection().withName("analyses").withLabel("Analyses").withIcon(new QIcon("insights")).withTables(DataAnalysisTables.NAV_TABLES));
|
||||||
app.setParentAppName(config.getParentAppName());
|
app.setParentAppName(config.getParentAppName());
|
||||||
@@ -197,46 +233,45 @@ public class DataAnalysisHome extends AbstractWidgetRenderer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** One standard group per subject, with real counts or an unavailable explanation.
|
** One standard group per subject the viewer can read; unreadable subjects are left out.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static MultiStatisticsData statistics(Instant now) throws QException
|
public static MultiStatisticsData statistics(Instant now) throws QException
|
||||||
{
|
{
|
||||||
List<MultiStatisticsData.StatisticsGroupData> groups = new ArrayList<>();
|
List<MultiStatisticsData.StatisticsGroupData> groups = new ArrayList<>();
|
||||||
var saved = group("Saved analyses", DataAnalysisTables.SAVED);
|
|
||||||
if(canRead(DataAnalysisTables.SAVED))
|
if(canRead(DataAnalysisTables.SAVED))
|
||||||
{
|
{
|
||||||
|
var saved = group("Saved analyses");
|
||||||
List<SavedAnalyses.Entry> entries = DataAnalysisRuntime.saved().list();
|
List<SavedAnalyses.Entry> entries = DataAnalysisRuntime.saved().list();
|
||||||
String user = SavedAnalyses.user();
|
String user = SavedAnalyses.user();
|
||||||
saved.withStatisticList(List.of(stat("Mine", (int) entries.stream().filter(e -> e.owner().equals(user)).count(), DataAnalysisTables.SAVED),
|
saved.withStatisticList(List.of(stat("Mine", (int) entries.stream().filter(e -> e.owner().equals(user)).count(), DataAnalysisTables.SAVED),
|
||||||
stat("Shared", (int) entries.stream().filter(e -> Boolean.TRUE.equals(e.shared())).count(), DataAnalysisTables.SAVED),
|
stat("Shared", (int) entries.stream().filter(e -> Boolean.TRUE.equals(e.shared())).count(), DataAnalysisTables.SAVED),
|
||||||
stat("Starred", (int) entries.stream().filter(SavedAnalyses.Entry::starred).count(), DataAnalysisTables.SAVED)));
|
stat("Starred", (int) entries.stream().filter(SavedAnalyses.Entry::starred).count(), DataAnalysisTables.SAVED)));
|
||||||
|
groups.add(saved);
|
||||||
}
|
}
|
||||||
groups.add(saved);
|
|
||||||
var watches = group("Watches", DataAnalysisTables.WATCH);
|
|
||||||
if(canRead(DataAnalysisTables.WATCH))
|
if(canRead(DataAnalysisTables.WATCH))
|
||||||
{
|
{
|
||||||
|
var watches = group("Watches");
|
||||||
watches.withStatisticList(List.of(stat("Enabled", count(DataAnalysisTables.WATCH, equal("enabled", true)), DataAnalysisTables.WATCH),
|
watches.withStatisticList(List.of(stat("Enabled", count(DataAnalysisTables.WATCH, equal("enabled", true)), DataAnalysisTables.WATCH),
|
||||||
stat("Firing", count(DataAnalysisTables.WATCH, equal("state", "FIRING")), DataAnalysisTables.WATCH)));
|
stat("Firing", count(DataAnalysisTables.WATCH, equal("state", "FIRING")), DataAnalysisTables.WATCH)));
|
||||||
|
groups.add(watches);
|
||||||
}
|
}
|
||||||
groups.add(watches);
|
|
||||||
var runs = group("My runs", DataAnalysisTables.RUN);
|
|
||||||
if(canRead(DataAnalysisTables.RUN))
|
if(canRead(DataAnalysisTables.RUN))
|
||||||
{
|
{
|
||||||
|
var runs = group("My runs");
|
||||||
runs.withStatisticList(List.of(stat("Last 7 days", count(DataAnalysisTables.RUN, equal("user_id", SavedAnalyses.user())
|
runs.withStatisticList(List.of(stat("Last 7 days", count(DataAnalysisTables.RUN, equal("user_id", SavedAnalyses.user())
|
||||||
.withCriteria("started_at", QCriteriaOperator.GREATER_THAN_OR_EQUALS, now.minus(7, ChronoUnit.DAYS))
|
.withCriteria("started_at", QCriteriaOperator.GREATER_THAN_OR_EQUALS, now.minus(7, ChronoUnit.DAYS))
|
||||||
.withCriteria("started_at", QCriteriaOperator.LESS_THAN_OR_EQUALS, now)), DataAnalysisTables.RUN)));
|
.withCriteria("started_at", QCriteriaOperator.LESS_THAN_OR_EQUALS, now)), DataAnalysisTables.RUN)));
|
||||||
|
groups.add(runs);
|
||||||
}
|
}
|
||||||
groups.add(runs);
|
|
||||||
return (new MultiStatisticsData("Analyses", groups));
|
return (new MultiStatisticsData("Analyses", groups));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Empty/denied data are different messages, not synthetic zeroes.
|
** A group is only built for a readable subject, so its subheader is always true.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
private static MultiStatisticsData.StatisticsGroupData group(String label, String table)
|
private static MultiStatisticsData.StatisticsGroupData group(String label)
|
||||||
{
|
{
|
||||||
return (new MultiStatisticsData.StatisticsGroupData().withHeader(label).withStatisticList(List.of())
|
return (new MultiStatisticsData.StatisticsGroupData().withHeader(label).withStatisticList(List.of()).withSubheader("Visible to you"));
|
||||||
.withSubheader(canRead(table) ? "Visible to you" : "Unavailable: READ permission required."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@@ -257,7 +292,7 @@ public class DataAnalysisHome extends AbstractWidgetRenderer
|
|||||||
{
|
{
|
||||||
columns.add(new TableData.Column(i == 0 ? "html" : "text", headers[i], "c" + i, null, "left"));
|
columns.add(new TableData.Column(i == 0 ? "html" : "text", headers[i], "c" + i, null, "left"));
|
||||||
}
|
}
|
||||||
return (new TableData(label, columns, new ArrayList<>()).withNoRowsFoundHTML(canRead(table) ? empty : "Unavailable: READ permission required.")
|
return (new TableData(label, columns, new ArrayList<>()).withNoRowsFoundHTML(empty)
|
||||||
.withLinkText("View all").withLinkURL(QContext.getQInstance().getTablePath(table)).withRowsPerPage(10));
|
.withLinkText("View all").withLinkURL(QContext.getQInstance().getTablePath(table)).withRowsPerPage(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* 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/
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.dataanalysis.app;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.permissions.CustomPermissionChecker;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.code.InitializableViaCodeReference;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReferenceWithProperties;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** A widget is available only to sessions that can READ the table it summarizes.
|
||||||
|
** Contributes no catalog entry of its own: the table's READ permission is the
|
||||||
|
** permission.
|
||||||
|
*******************************************************************************/
|
||||||
|
public class TableReadWidgetPermissionChecker implements CustomPermissionChecker, InitializableViaCodeReference
|
||||||
|
{
|
||||||
|
private static final String TABLE_NAME_PROPERTY = "tableName";
|
||||||
|
|
||||||
|
private String tableName;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Code reference for the given table.
|
||||||
|
*******************************************************************************/
|
||||||
|
public static QCodeReferenceWithProperties build(String tableName)
|
||||||
|
{
|
||||||
|
return (new QCodeReferenceWithProperties(TableReadWidgetPermissionChecker.class, MapBuilder.of(TABLE_NAME_PROPERTY, tableName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Read the table name from the code reference properties.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void initialize(QCodeReference codeReference)
|
||||||
|
{
|
||||||
|
if(codeReference instanceof QCodeReferenceWithProperties withProperties)
|
||||||
|
{
|
||||||
|
Map<String, Serializable> properties = withProperties.getProperties();
|
||||||
|
tableName = ValueUtils.getValueAsString(properties.get(TABLE_NAME_PROPERTY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Deny unless the session may READ the table.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Override
|
||||||
|
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException
|
||||||
|
{
|
||||||
|
AbstractTableActionInput tableInput = new AbstractTableActionInput();
|
||||||
|
tableInput.setTableName(tableName);
|
||||||
|
PermissionsHelper.checkTablePermissionThrowing(tableInput, TablePermissionSubType.READ);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2026. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kingsrook.qbits.dataanalysis.schedule;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import com.kingsrook.qbits.dataanalysis.DataAnalysisQBitConfig;
|
||||||
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
|
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
||||||
|
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentities;
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Sessions for scheduled analysis work. With the host's data-analysis service
|
||||||
|
** principal declared (QQQ service identities): inventory runs as the
|
||||||
|
** principal, and owner or recipient runs go on their behalf through it (their
|
||||||
|
** current access intersected with the principal's). Without it: the previous
|
||||||
|
** local sessions (single-node default unchanged). Never a system session.
|
||||||
|
*******************************************************************************/
|
||||||
|
public final class AnalysisSessions
|
||||||
|
{
|
||||||
|
/*******************************************************************************
|
||||||
|
** Utility class.
|
||||||
|
*******************************************************************************/
|
||||||
|
private AnalysisSessions()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** The session for schedule/watch inventory and status updates.
|
||||||
|
*******************************************************************************/
|
||||||
|
public static QSession maintenance(DataAnalysisQBitConfig config, Supplier<QSession> legacy) throws QException
|
||||||
|
{
|
||||||
|
if(usesPrincipal(config))
|
||||||
|
{
|
||||||
|
return (ServiceIdentities.session(QContext.getQInstance(), config.getServicePrincipal()));
|
||||||
|
}
|
||||||
|
return (legacy.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** The session for running as a user (owner or recipient).
|
||||||
|
*******************************************************************************/
|
||||||
|
public static QSession forUser(DataAnalysisQBitConfig config, String userId) throws QException
|
||||||
|
{
|
||||||
|
if(config.getOwnerSessionSupplier() != null)
|
||||||
|
{
|
||||||
|
return (config.getOwnerSessionSupplier().apply(userId));
|
||||||
|
}
|
||||||
|
if(usesPrincipal(config))
|
||||||
|
{
|
||||||
|
return (ServiceIdentities.sessionOnBehalfOf(QContext.getQInstance(), config.getServicePrincipal(), userId));
|
||||||
|
}
|
||||||
|
return (new QAuthenticationModuleDispatcher().getQModule(QContext.getQInstance().getAuthentication()).createAutomatedSessionForUser(QContext.getQInstance(), userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Whether the host declared the configured principal.
|
||||||
|
*******************************************************************************/
|
||||||
|
private static boolean usesPrincipal(DataAnalysisQBitConfig config)
|
||||||
|
{
|
||||||
|
return (config.getServicePrincipal() != null && ServiceIdentities.isDeclared(QContext.getQInstance(), config.getServicePrincipal()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,6 @@ 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.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Scheduled work uses owner and recipient sessions; maintenance only inventories rows.
|
** Scheduled work uses owner and recipient sessions; maintenance only inventories rows.
|
||||||
@@ -83,13 +82,17 @@ public class ScheduleRunner
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public synchronized Integer runDue(Instant now) throws QException
|
public synchronized Integer runDue(Instant now) throws QException
|
||||||
{
|
{
|
||||||
QSession maintenance = new QSession().withUser(new QUser().withIdReference("data-analysis-scheduler"))
|
QSession maintenance = AnalysisSessions.maintenance(config, () ->
|
||||||
.withPermissions("data_analysis.saved.read", "data_analysis.saved.edit")
|
|
||||||
.withSecurityKeyValue(DataAnalysisPermissions.ADMIN, true);
|
|
||||||
if(QContext.getQSession().getBackendVariants() != null)
|
|
||||||
{
|
{
|
||||||
maintenance.setBackendVariants(new java.util.HashMap<>(QContext.getQSession().getBackendVariants()));
|
QSession legacy = new QSession().withUser(new QUser().withIdReference("data-analysis-scheduler"))
|
||||||
}
|
.withPermissions("data_analysis.saved.read", "data_analysis.saved.edit")
|
||||||
|
.withSecurityKeyValue(DataAnalysisPermissions.ADMIN, true);
|
||||||
|
if(QContext.getQSession().getBackendVariants() != null)
|
||||||
|
{
|
||||||
|
legacy.setBackendVariants(new java.util.HashMap<>(QContext.getQSession().getBackendVariants()));
|
||||||
|
}
|
||||||
|
return (legacy);
|
||||||
|
});
|
||||||
int[] count = {0};
|
int[] count = {0};
|
||||||
QContext.withTemporaryContext(new CapturedContext(QContext.getQInstance(), maintenance), () ->
|
QContext.withTemporaryContext(new CapturedContext(QContext.getQInstance(), maintenance), () ->
|
||||||
{
|
{
|
||||||
@@ -161,9 +164,7 @@ public class ScheduleRunner
|
|||||||
QSession session;
|
QSession session;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
session = config.getOwnerSessionSupplier() == null
|
session = AnalysisSessions.forUser(config, id);
|
||||||
? new QAuthenticationModuleDispatcher().getQModule(QContext.getQInstance().getAuthentication()).createAutomatedSessionForUser(QContext.getQInstance(), id)
|
|
||||||
: config.getOwnerSessionSupplier().apply(id);
|
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ 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.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Transition watches through the core alerts SPI, computing exclusively as owner.
|
** Transition watches through the core alerts SPI, computing exclusively as owner.
|
||||||
@@ -94,13 +93,17 @@ public class WatchEvaluator
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public synchronized Integer evaluateEnabled(Instant now) throws QException
|
public synchronized Integer evaluateEnabled(Instant now) throws QException
|
||||||
{
|
{
|
||||||
QSession maintenance = new QSession().withUser(new QUser().withIdReference("data-analysis-scheduler"))
|
QSession maintenance = com.kingsrook.qbits.dataanalysis.schedule.AnalysisSessions.maintenance(config, () ->
|
||||||
.withPermissions("data_analysis.watch.read", "data_analysis.watch.edit")
|
|
||||||
.withSecurityKeyValue(DataAnalysisPermissions.ADMIN, true);
|
|
||||||
if(QContext.getQSession().getBackendVariants() != null)
|
|
||||||
{
|
{
|
||||||
maintenance.setBackendVariants(new java.util.HashMap<>(QContext.getQSession().getBackendVariants()));
|
QSession legacy = new QSession().withUser(new QUser().withIdReference("data-analysis-scheduler"))
|
||||||
}
|
.withPermissions("data_analysis.watch.read", "data_analysis.watch.edit")
|
||||||
|
.withSecurityKeyValue(DataAnalysisPermissions.ADMIN, true);
|
||||||
|
if(QContext.getQSession().getBackendVariants() != null)
|
||||||
|
{
|
||||||
|
legacy.setBackendVariants(new java.util.HashMap<>(QContext.getQSession().getBackendVariants()));
|
||||||
|
}
|
||||||
|
return (legacy);
|
||||||
|
});
|
||||||
int[] count = {0};
|
int[] count = {0};
|
||||||
QContext.withTemporaryContext(new CapturedContext(QContext.getQInstance(), maintenance), () ->
|
QContext.withTemporaryContext(new CapturedContext(QContext.getQInstance(), maintenance), () ->
|
||||||
{
|
{
|
||||||
@@ -128,9 +131,7 @@ public class WatchEvaluator
|
|||||||
QSession session;
|
QSession session;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
session = config.getOwnerSessionSupplier() == null
|
session = com.kingsrook.qbits.dataanalysis.schedule.AnalysisSessions.forUser(config, owner);
|
||||||
? new QAuthenticationModuleDispatcher().getQModule(QContext.getQInstance().getAuthentication()).createAutomatedSessionForUser(QContext.getQInstance(), owner)
|
|
||||||
: config.getOwnerSessionSupplier().apply(owner);
|
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
|||||||
|
version=${project.version}
|
||||||
@@ -136,6 +136,13 @@ class DataAnalysisApiTest extends EngineFixture
|
|||||||
}
|
}
|
||||||
var response = invoker.invoke(request);
|
var response = invoker.invoke(request);
|
||||||
assertThat(response.getStatus()).as(response.getBody()).isEqualTo(status);
|
assertThat(response.getStatus()).as(response.getBody()).isEqualTo(status);
|
||||||
|
if(status >= 400)
|
||||||
|
{
|
||||||
|
for(String field : List.of("error", "code", "message", "traceId"))
|
||||||
|
{
|
||||||
|
assertThat(response.getJson().path(field).asText()).as(field).isNotBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
return (response.getJson());
|
return (response.getJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +289,7 @@ class DataAnalysisApiTest extends EngineFixture
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Generated fragment is committed verbatim and has all fourteen operations.
|
** Generated fragment matches the committed contract and validates all references.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@Test
|
@Test
|
||||||
void contract() throws Exception
|
void contract() throws Exception
|
||||||
@@ -296,8 +303,9 @@ class DataAnalysisApiTest extends EngineFixture
|
|||||||
Files.createDirectories(path.getParent());
|
Files.createDirectories(path.getParent());
|
||||||
Files.writeString(path, yaml);
|
Files.writeString(path, yaml);
|
||||||
}
|
}
|
||||||
assertThat(Files.exists(path)).as("Regenerate with ./mvnw -o test -Dtest=DataAnalysisApiTest#contract -Dopenapi.write=true -Dqqq.snapshot.version=4.1.0-DATAANALYSIS-SNAPSHOT").isTrue();
|
assertThat(Files.exists(path)).as("Regenerate with ./mvnw -o test -Dtest=DataAnalysisApiTest#contract -Dopenapi.write=true -Dqqq.snapshot.version=4.1.99-SNAPSHOT").isTrue();
|
||||||
assertThat(Files.readString(path).replace("\r\n", "\n")).as("OpenAPI changed: regenerate with -Dopenapi.write=true").isEqualTo(yaml.replace("\r\n", "\n"));
|
assertThat(Files.readString(path).replace("\r\n", "\n")).as("OpenAPI changed: regenerate with -Dopenapi.write=true").isEqualTo(yaml.replace("\r\n", "\n"));
|
||||||
|
assertThat(UiApiContract.check(yaml, Files.readString(path), null, 1, "-Dopenapi.write=true")).isEmpty();
|
||||||
}
|
}
|
||||||
@Test
|
@Test
|
||||||
void pivotHttp() throws Exception
|
void pivotHttp() throws Exception
|
||||||
@@ -364,7 +372,7 @@ class DataAnalysisApiTest extends EngineFixture
|
|||||||
}
|
}
|
||||||
relationshipCountDrills(call(HttpMethod.POST, "/tables/{table}/crosstab", Map.of("rowField", "status", "columnField", "customer"), 200));
|
relationshipCountDrills(call(HttpMethod.POST, "/tables/{table}/crosstab", Map.of("rowField", "status", "columnField", "customer"), 200));
|
||||||
// All new schemas are discoverable from the same served OpenAPI catalog.
|
// All new schemas are discoverable from the same served OpenAPI catalog.
|
||||||
assertThat(com.kingsrook.qbits.dataanalysis.api.DataAnalysisApi.extension().getSemanticVersion()).isEqualTo("1.4.0");
|
assertThat(com.kingsrook.qbits.dataanalysis.api.DataAnalysisApi.extension().getSemanticVersion()).isEqualTo("1.5.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import java.time.temporal.ChronoUnit;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import com.kingsrook.qbits.dataanalysis.app.DataAnalysisHome;
|
import com.kingsrook.qbits.dataanalysis.app.DataAnalysisHome;
|
||||||
import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables;
|
import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables;
|
||||||
|
import com.kingsrook.qbits.dataanalysis.permissions.DataAnalysisPermissions;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.MultiStatisticsData;
|
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.MultiStatisticsData;
|
||||||
@@ -69,12 +72,43 @@ class DataAnalysisHomeTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
QContext.init(instance, TestSupport.user("alice"));
|
QContext.init(instance, TestSupport.user("alice"));
|
||||||
assertThat(DataAnalysisHome.statistics(Instant.now()).getStatisticsGroupData()).allSatisfy(g ->
|
assertThat(DataAnalysisHome.statistics(Instant.now()).getStatisticsGroupData()).isEmpty();
|
||||||
|
assertThat(((TableData) DataAnalysisHome.render(DataAnalysisHome.FIRING, Instant.now())).getNoRowsFoundHTML()).doesNotContain("Unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setupIsOffTheBusinessHomeAndAdminOnly() throws Exception
|
||||||
|
{
|
||||||
|
var instance = TestSupport.instance();
|
||||||
|
assertThat(DataAnalysisHome.homeWidgetNames()).doesNotContain(DataAnalysisHome.SETUP).containsExactly(DataAnalysisHome.HOME_WIDGET,
|
||||||
|
DataAnalysisHome.STARRED, DataAnalysisHome.FIRING, DataAnalysisHome.RECENT);
|
||||||
|
assertThat(instance.getApp("dataAnalysis").getWidgets()).doesNotContain(DataAnalysisHome.SETUP);
|
||||||
|
assertThat(DataAnalysisQBitProducer.SYSTEM_WIDGETS).containsExactly(DataAnalysisHome.SETUP);
|
||||||
|
assertThat(instance.getWidgets()).containsKey(DataAnalysisHome.SETUP);
|
||||||
|
|
||||||
|
QContext.init(instance, TestSupport.admin());
|
||||||
|
assertThat(PermissionsHelper.hasWidgetPermission(new AbstractActionInput(), DataAnalysisHome.SETUP)).isTrue();
|
||||||
|
assertThat(DataAnalysisHome.setup().getRows()).hasSize(5);
|
||||||
|
assertThat(new DataAnalysisHome().render(new RenderWidgetInput().withWidgetMetaData(instance.getWidget(DataAnalysisHome.SETUP))).getWidgetData()).isInstanceOf(TableData.class);
|
||||||
|
|
||||||
|
QContext.init(instance, TestSupport.user("alice").withPermissions(DataAnalysisPermissions.SAVED_READ));
|
||||||
|
assertThat(PermissionsHelper.hasWidgetPermission(new AbstractActionInput(), DataAnalysisHome.SETUP)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void savedReadOnlyUserGetsNoLockedShells() throws Exception
|
||||||
|
{
|
||||||
|
var instance = TestSupport.instance();
|
||||||
|
QContext.init(instance, TestSupport.user("alice").withPermissions(DataAnalysisPermissions.SAVED_READ));
|
||||||
|
List<String> visible = DataAnalysisHome.homeWidgetNames().stream().filter(name -> PermissionsHelper.hasWidgetPermission(new AbstractActionInput(), name)).toList();
|
||||||
|
assertThat(visible).containsExactly(DataAnalysisHome.HOME_WIDGET, DataAnalysisHome.STARRED);
|
||||||
|
for(String name : visible)
|
||||||
{
|
{
|
||||||
assertThat(g.getStatisticList()).isEmpty();
|
var data = DataAnalysisHome.render(name, Instant.now());
|
||||||
assertThat(g.getSubheader()).contains("Unavailable");
|
assertThat(com.kingsrook.qqq.backend.core.utils.JsonUtils.toJson(data)).doesNotContain("Unavailable").doesNotContain("permission required");
|
||||||
});
|
}
|
||||||
assertThat(((TableData) DataAnalysisHome.render(DataAnalysisHome.FIRING, Instant.now())).getNoRowsFoundHTML()).contains("Unavailable");
|
var groups = DataAnalysisHome.statistics(Instant.now()).getStatisticsGroupData();
|
||||||
|
assertThat(groups).extracting(g -> g.getHeader()).containsExactly("Saved analyses");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -116,11 +116,15 @@ class DataAnalysisMetadataTest
|
|||||||
assertThat(producer.isEnabled()).isTrue();
|
assertThat(producer.isEnabled()).isTrue();
|
||||||
assertThat(producer.getNamespace()).isEqualTo("analysis");
|
assertThat(producer.getNamespace()).isEqualTo("analysis");
|
||||||
assertThat(producer.getQBitConfig()).isSameAs(config);
|
assertThat(producer.getQBitConfig()).isSameAs(config);
|
||||||
assertThat(producer.getQBitMetaData().getVersion()).isEqualTo("0.1.0");
|
String artifactVersion = System.getProperty("qbit.artifactVersion");
|
||||||
|
assertThat(artifactVersion).isNotBlank();
|
||||||
|
assertThat(producer.getQBitMetaData().getVersion()).isEqualTo(artifactVersion).isEqualTo(DataAnalysisQBitProducer.VERSION);
|
||||||
assertThat(producer.getQBitMetaData().getGroupId()).isEqualTo("com.kingsrook.qbits");
|
assertThat(producer.getQBitMetaData().getGroupId()).isEqualTo("com.kingsrook.qbits");
|
||||||
assertThat(producer.getQBitMetaData().getArtifactId()).isEqualTo("qbit-data-analysis");
|
assertThat(producer.getQBitMetaData().getArtifactId()).isEqualTo("qbit-data-analysis");
|
||||||
assertThat(producer.getPackageNameForFindingMetaDataProducers()).endsWith(".metadata");
|
assertThat(producer.getPackageNameForFindingMetaDataProducers()).endsWith(".metadata");
|
||||||
producer.produce(instance);
|
producer.produce(instance);
|
||||||
|
assertThat(HelpBundles.of(instance).getBundles().getFirst().getVersion())
|
||||||
|
.isEqualTo(artifactVersion).isEqualTo(producer.getQBitMetaData().getVersion());
|
||||||
assertThat(HelpBundles.of(instance).getBundles().getFirst().getEntries()).anyMatch(entry -> entry.getKey().equals("app:customAnalysis;slot:help"))
|
assertThat(HelpBundles.of(instance).getBundles().getFirst().getEntries()).anyMatch(entry -> entry.getKey().equals("app:customAnalysis;slot:help"))
|
||||||
.noneMatch(entry -> entry.getKey().equals("app:dataAnalysis;slot:help"));
|
.noneMatch(entry -> entry.getKey().equals("app:dataAnalysis;slot:help"));
|
||||||
assertThat(instance.getApp("customAnalysis").getParentAppName()).isEqualTo("admin");
|
assertThat(instance.getApp("customAnalysis").getParentAppName()).isEqualTo("admin");
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables;
|
import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables;
|
||||||
|
import com.kingsrook.qbits.dataanalysis.permissions.DataAnalysisPermissions;
|
||||||
import com.kingsrook.qbits.dataanalysis.processes.ScheduleActions;
|
import com.kingsrook.qbits.dataanalysis.processes.ScheduleActions;
|
||||||
import com.kingsrook.qbits.dataanalysis.saved.AnalysisExport;
|
import com.kingsrook.qbits.dataanalysis.saved.AnalysisExport;
|
||||||
import com.kingsrook.qbits.dataanalysis.saved.SavedDefinition;
|
import com.kingsrook.qbits.dataanalysis.saved.SavedDefinition;
|
||||||
@@ -47,6 +48,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.messaging.QMessagingProvide
|
|||||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
import com.kingsrook.qqq.backend.core.modules.messaging.MessagingProviderInterface;
|
import com.kingsrook.qqq.backend.core.modules.messaging.MessagingProviderInterface;
|
||||||
import com.kingsrook.qqq.backend.core.modules.messaging.QMessagingProviderDispatcher;
|
import com.kingsrook.qqq.backend.core.modules.messaging.QMessagingProviderDispatcher;
|
||||||
|
import com.kingsrook.qqq.backend.core.serviceidentity.QServicePrincipalMetaData;
|
||||||
|
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentitiesMetaData;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -160,6 +163,27 @@ class SchedulesTest extends EngineFixture
|
|||||||
assertThat(WatchesTest.Sink.raised).isEmpty();
|
assertThat(WatchesTest.Sink.raised).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** With the host's data-analysis principal declared, schedule inventory runs
|
||||||
|
** as the principal (its grants decide - a principal without them finds
|
||||||
|
** nothing due), never as a self-made session.
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void inventoryRunsAsTheDeclaredPrincipal() throws Exception
|
||||||
|
{
|
||||||
|
schedule("PIVOT", Map.of("rows", List.of(Map.of("field", "status")), "values", List.of(Map.of("operator", "COUNT"))), "CSV");
|
||||||
|
config.withServicePrincipal("analysis-bare");
|
||||||
|
ServiceIdentitiesMetaData.declare(instance, new QServicePrincipalMetaData().withName("analysis-bare"));
|
||||||
|
assertThatThrownBy(() -> runner.runDue(NOW)).isInstanceOf(QException.class);
|
||||||
|
assertThat(Mail.sent).isEmpty();
|
||||||
|
|
||||||
|
ServiceIdentitiesMetaData.declare(instance, new QServicePrincipalMetaData().withName("data-analysis")
|
||||||
|
.withLocalPermissions("data_analysis.saved.read", "data_analysis.saved.edit").withLocalSecurityKeyValue(DataAnalysisPermissions.ADMIN, true));
|
||||||
|
config.withServicePrincipal("data-analysis");
|
||||||
|
assertThat(runner.runDue(NOW)).isEqualTo(1);
|
||||||
|
assertThat(Mail.sent).hasSize(2);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void providerFailureAndMissingOwnerRaiseVisibleAlerts() throws Exception
|
void providerFailureAndMissingOwnerRaiseVisibleAlerts() throws Exception
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user