mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 06:00:44 +00:00
Compare commits
67 Commits
snapshot-i
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
b0aaf61e99 | |||
8056e49e24 | |||
2bdb5911e7 | |||
9680812828 | |||
049f5a6b42 | |||
15c0d6a45a | |||
6919d9d810 | |||
815f160a94 | |||
0aba833af3 | |||
2522bdcf1b | |||
e27ef7b835 | |||
45899400ad | |||
7d25fc7390 | |||
878f374cb5 | |||
29a54f5293 | |||
a035bbe18f | |||
edf942a01b | |||
ed6319ff53 | |||
55b4e2154c | |||
aabe9e315e | |||
217acc954a | |||
557824c572 | |||
85ac2c10e7 | |||
b506150842 | |||
3b8eef0f9c | |||
c07d9a779a | |||
4609ff8976 | |||
bf11ea5185 | |||
a6d84c66b7 | |||
69015be2f6 | |||
0f16446f69 | |||
9464a308f2 | |||
51f021193d | |||
0090601166 | |||
cf78f1cb94 | |||
0b1b570f28 | |||
73e54dd56f | |||
e936468f29 | |||
73ac3448dd | |||
d54010e89d | |||
dae23c2a04 | |||
6e3ef2254a | |||
b03de8ec0f | |||
f18ddcf188 | |||
a7dfad5b28 | |||
2f83b7ba7d | |||
62560ab82c | |||
718701aaff | |||
b8f9469477 | |||
4bf29807e3 | |||
b28000932b | |||
ee1c20b1c6 | |||
e3ce1cdbc3 | |||
a9999ee8ce | |||
f50e6d1a94 | |||
72735607c6 | |||
bbba43ef80 | |||
efa84d03e6 | |||
8572c5cb7e | |||
871d133a37 | |||
61c9f1fe75 | |||
2a68478405 | |||
459510bba4 | |||
18e1852ce4 | |||
0dd7f5e1d2 | |||
601c66ddff | |||
e1ca85c746 |
@ -1,7 +1,7 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
localstack: localstack/platform@1.0
|
||||
localstack: localstack/platform@2.1
|
||||
|
||||
commands:
|
||||
store_jacoco_site:
|
||||
|
@ -26,12 +26,12 @@ include::metaData/Reports.adoc[leveloffset=+1]
|
||||
include::metaData/Icons.adoc[leveloffset=+1]
|
||||
include::metaData/PermissionRules.adoc[leveloffset=+1]
|
||||
|
||||
|
||||
== Custom Application Code
|
||||
include::misc/QContext.adoc[leveloffset=+1]
|
||||
include::misc/QRecords.adoc[leveloffset=+1]
|
||||
include::misc/QRecordEntities.adoc[leveloffset=+1]
|
||||
include::misc/ProcessBackendSteps.adoc[leveloffset=+1]
|
||||
include::misc/RenderingWidgets.adoc[leveloffset=+1]
|
||||
|
||||
=== Table Customizers
|
||||
#todo#
|
||||
@ -61,3 +61,6 @@ include::actions/InsertAction.adoc[leveloffset=+1]
|
||||
== QQQ Default Implementations
|
||||
include::implementations/TableSync.adoc[leveloffset=+1]
|
||||
// later... include::actions/RenderTemplateAction.adoc[leveloffset=+1]
|
||||
|
||||
== QQQ Utility Classes
|
||||
include::utilities/RecordLookupHelper.adoc[leveloffset=+1]
|
@ -2,16 +2,79 @@
|
||||
== Security Key Types
|
||||
include::../variables.adoc[]
|
||||
|
||||
#TODO#
|
||||
In QQQ, record-level security is provided by using a lock & key metaphor.
|
||||
|
||||
The use-case being handled here is:
|
||||
|
||||
* A user has full permission on a table (query, insert, update, and delete).
|
||||
* However, they should only be allowed to read a sub-set of the rows in the table.
|
||||
** e.g., maybe it's a multi-tenant system, or the table has user-specific records.
|
||||
|
||||
The lock & key metaphor is realized by the user being associated with one or more "Keys"
|
||||
(as values in their session), and records in tables being associated with one or more "Locks"
|
||||
(as values in fields).
|
||||
A user is only allowed to access records where the user's key(s) match the record's security lock(s).
|
||||
|
||||
For a practical example, picture a multi-tenant Order Management System,where all orders are assigned to a "client".
|
||||
Users (customers) should only be able to see orders associated with the client which that user works for.
|
||||
|
||||
In this scenario, the `order` table would have a "lock" on its `clientId` field.
|
||||
Customer-users would have a `clientId` key in their session.
|
||||
When the QQQ backend did a search for records (e.g., an SQL query) it would implicitly
|
||||
(without any code being written by the application developer) filter the table to only
|
||||
allow the user to see records with their `clientId`.
|
||||
|
||||
To implement this scenario, the application would define the following pieces of meta-data:
|
||||
|
||||
* At the QQQ-Instance level, a `SecurityKeyType`,
|
||||
to define a domain of possible locks & keys within an application.
|
||||
** An application can define multiple Security Key Types.
|
||||
For example, maybe `clientId` and `userId` as key types.
|
||||
* At the per-table level, a `RecordSecurityLock`,
|
||||
which references a security key type, and how that key type should be applied to the table.
|
||||
** For example, what field stores the `clientId` value in the `order` table.
|
||||
* Finally, when a user's session is constructed via a QQQ Authentication provider,
|
||||
security key values are set, based on data from the authentication backend.
|
||||
|
||||
=== Additional Scenarios
|
||||
|
||||
==== All Access Key
|
||||
A "super-user" may be allowed to access all records in a table regardless of their record locks,
|
||||
if the Security Key Type specifies an `allAccessKeyName`,
|
||||
and if the user has a key in their session with that key name, and a value of `true`.
|
||||
Going back to the lock & key metaphor, this can be thought of as a "skeleton key",
|
||||
that can unlock any record lock (of the security key's type).
|
||||
|
||||
==== Null Value Behaviors
|
||||
In a record security lock, different behaviors can be defined for handling rows with a null key value.
|
||||
|
||||
For example:
|
||||
|
||||
* Sometimes orders may be loaded into the OMS system described above, where the application doesn't yet know what client the order belongs to.
|
||||
In this case, the application may need to ensure that such records, with a `null` value in `clientId` are hidden from customer-users,
|
||||
to avoid potentially leaking a different client's data.
|
||||
** This can be accomplished with a record security lock on the `order` table, with a `nullValueBehavior` of `DENY`.
|
||||
** Furthermore, if internal/admin users _should_ be given access to such records, then the security key type can be
|
||||
configured with a `nullValueBehaviorKeyName` (e.g., `"clientIdNullValueBehavior"`), which can be set per-user to allow
|
||||
access to records, overriding the table lock's specified `nullValueBehavior`.
|
||||
*** This could also be done by giving internal/admin users an `allAccessKey`, but sometimes that is not what is required.
|
||||
|
||||
* Maybe a warehouse locations table is assigned a `clientId` once inventory for a client is placed in the location,
|
||||
at which point in time, only the client's users should be allowed to see the record.
|
||||
But, if no client has been assigned to the location, and `clientId` is `null`,
|
||||
then you may want to allow any user to see such records.
|
||||
** This can be accomplished with a record security lock on the Warehouse Locations table, with a `nullValueBehavior` of `ALLOW`.
|
||||
|
||||
=== QSecurityKeyType
|
||||
A Security Key Type is defined in a QQQ Instance in a `*QSecurityKeyType*` object.
|
||||
|
||||
#TODO#
|
||||
|
||||
*QSecurityKeyType Properties:*
|
||||
|
||||
* `name` - *String, Required* - Unique name for the security key type within the QQQ Instance.
|
||||
* `name` - *String, Required* - Unique name for this security key type within the QQQ Instance.
|
||||
* `allAccessKeyName` - *String* - Optional name of the all-access security key associated with this key type.
|
||||
* `nullValueBehaviorKeyName` - *String* - Optional name of the null-value-behavior overriding security key associated with this key type.
|
||||
** Note, `name`, `allAccessKeyName`, and `nullValueBehaviorKeyName` are all checked against each other for uniqueness.
|
||||
A `QInstanceValidationException` will be thrown if any name collisions occur.
|
||||
* `possibleValueSourceName` - *String* - Optional reference to a possible value source from which value for the key can come.
|
||||
|
||||
#TODO#
|
||||
|
||||
|
@ -244,3 +244,41 @@ QQQ provides the mechanism for UI's to present and manage such scripts (e.g., th
|
||||
* `scriptTypeId` - *Serializable (typically Integer), Required* - primary key value from the `"scriptType"` table in the instance, to designate the type of the Script.
|
||||
* `scriptTester` - *QCodeReference* - reference to a class which implements `TestScriptActionInterface`, that can be used by UI's for running an associated script to test it.
|
||||
|
||||
|
||||
|
||||
=== Supplemental Meta Data
|
||||
==== QQQ Frontend Material Dashboard
|
||||
When running a QQQ application with the QQQ Frontend Material Dashboard module (QFMD),
|
||||
there are various pieces of supplemental meta-data which can be assigned to a Table,
|
||||
to modify some behaviors for the table in this UI.
|
||||
|
||||
===== Default Quick Filter Field Names
|
||||
QFMD's table query has a "Basic" mode, which will always display a subset of the table's fields as quick-access filters.
|
||||
By default, the "Tier 1" fields on a table (e.g., fields in a Section that is marked as T1) will be used for this purpose.
|
||||
|
||||
However, you can customize which fields are shown as the default quick-filter fields, by providing a list of field names in a
|
||||
`MaterialDashboardTableMetaData` object, placed in the table's `supplementalMetaData`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
table.withSupplementalMetaData(new MaterialDashboardTableMetaData()
|
||||
.withDefaultQuickFilterFieldNames(List.of("id", "warehouseId", "statusId", "orderDate")));
|
||||
----
|
||||
|
||||
===== Go To Field Names
|
||||
QFMD has a feature where a table's query screen can include a "Go To" button,
|
||||
which a user can hit to open a modal popup, into which the user can enter a record's identifier,
|
||||
to be brought directly to the record matching that identifier.
|
||||
|
||||
To use this feature, the table must have a List of `GotoFieldNames` set in its
|
||||
`MaterialDashboardTableMetaData` object in the table's `supplementalMetaData`.
|
||||
|
||||
Each entry in this list is actually a list of fields, e.g., to account for a multi-value unique-key.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
table.withSupplementalMetaData(new MaterialDashboardTableMetaData()
|
||||
.withGotoFieldNames(List.of(
|
||||
List.of("id"),
|
||||
List.of("partnerName", "partnerOrderId"))));
|
||||
----
|
||||
|
@ -1,553 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="generator" content="Asciidoctor 2.0.18">
|
||||
<title>QQQ Tables</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
|
||||
<style>
|
||||
/*! Asciidoctor default stylesheet | MIT License | https://asciidoctor.org */
|
||||
/* Uncomment the following line when using as a custom stylesheet */
|
||||
/* @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700"; */
|
||||
html{font-family:sans-serif;-webkit-text-size-adjust:100%}
|
||||
a{background:none}
|
||||
a:focus{outline:thin dotted}
|
||||
a:active,a:hover{outline:0}
|
||||
h1{font-size:2em;margin:.67em 0}
|
||||
b,strong{font-weight:bold}
|
||||
abbr{font-size:.9em}
|
||||
abbr[title]{cursor:help;border-bottom:1px dotted #dddddf;text-decoration:none}
|
||||
dfn{font-style:italic}
|
||||
hr{height:0}
|
||||
mark{background:#ff0;color:#000}
|
||||
code,kbd,pre,samp{font-family:monospace;font-size:1em}
|
||||
pre{white-space:pre-wrap}
|
||||
q{quotes:"\201C" "\201D" "\2018" "\2019"}
|
||||
small{font-size:80%}
|
||||
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
|
||||
sup{top:-.5em}
|
||||
sub{bottom:-.25em}
|
||||
img{border:0}
|
||||
svg:not(:root){overflow:hidden}
|
||||
figure{margin:0}
|
||||
audio,video{display:inline-block}
|
||||
audio:not([controls]){display:none;height:0}
|
||||
fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
|
||||
legend{border:0;padding:0}
|
||||
button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
|
||||
button,input{line-height:normal}
|
||||
button,select{text-transform:none}
|
||||
button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}
|
||||
button[disabled],html input[disabled]{cursor:default}
|
||||
input[type=checkbox],input[type=radio]{padding:0}
|
||||
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
|
||||
textarea{overflow:auto;vertical-align:top}
|
||||
table{border-collapse:collapse;border-spacing:0}
|
||||
*,::before,::after{box-sizing:border-box}
|
||||
html,body{font-size:100%}
|
||||
body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;line-height:1;position:relative;cursor:auto;-moz-tab-size:4;-o-tab-size:4;tab-size:4;word-wrap:anywhere;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}
|
||||
a:hover{cursor:pointer}
|
||||
img,object,embed{max-width:100%;height:auto}
|
||||
object,embed{height:100%}
|
||||
img{-ms-interpolation-mode:bicubic}
|
||||
.left{float:left!important}
|
||||
.right{float:right!important}
|
||||
.text-left{text-align:left!important}
|
||||
.text-right{text-align:right!important}
|
||||
.text-center{text-align:center!important}
|
||||
.text-justify{text-align:justify!important}
|
||||
.hide{display:none}
|
||||
img,object,svg{display:inline-block;vertical-align:middle}
|
||||
textarea{height:auto;min-height:50px}
|
||||
select{width:100%}
|
||||
.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
|
||||
div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0}
|
||||
a{color:#2156a5;text-decoration:underline;line-height:inherit}
|
||||
a:hover,a:focus{color:#1d4b8f}
|
||||
a img{border:0}
|
||||
p{line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
|
||||
p aside{font-size:.875em;line-height:1.35;font-style:italic}
|
||||
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
|
||||
h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
|
||||
h1{font-size:2.125em}
|
||||
h2{font-size:1.6875em}
|
||||
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
|
||||
h4,h5{font-size:1.125em}
|
||||
h6{font-size:1em}
|
||||
hr{border:solid #dddddf;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em}
|
||||
em,i{font-style:italic;line-height:inherit}
|
||||
strong,b{font-weight:bold;line-height:inherit}
|
||||
small{font-size:60%;line-height:inherit}
|
||||
code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
|
||||
ul,ol,dl{line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
|
||||
ul,ol{margin-left:1.5em}
|
||||
ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0}
|
||||
ul.circle{list-style-type:circle}
|
||||
ul.disc{list-style-type:disc}
|
||||
ul.square{list-style-type:square}
|
||||
ul.circle ul:not([class]),ul.disc ul:not([class]),ul.square ul:not([class]){list-style:inherit}
|
||||
ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
|
||||
dl dt{margin-bottom:.3125em;font-weight:bold}
|
||||
dl dd{margin-bottom:1.25em}
|
||||
blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
|
||||
blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
|
||||
@media screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
|
||||
h1{font-size:2.75em}
|
||||
h2{font-size:2.3125em}
|
||||
h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
|
||||
h4{font-size:1.4375em}}
|
||||
table{background:#fff;margin-bottom:1.25em;border:1px solid #dedede;word-wrap:normal}
|
||||
table thead,table tfoot{background:#f7f8f7}
|
||||
table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
|
||||
table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
|
||||
table tr.even,table tr.alt{background:#f8f8f7}
|
||||
table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{line-height:1.6}
|
||||
h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
|
||||
h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
|
||||
.center{margin-left:auto;margin-right:auto}
|
||||
.stretch{width:100%}
|
||||
.clearfix::before,.clearfix::after,.float-group::before,.float-group::after{content:" ";display:table}
|
||||
.clearfix::after,.float-group::after{clear:both}
|
||||
:not(pre).nobreak{word-wrap:normal}
|
||||
:not(pre).nowrap{white-space:nowrap}
|
||||
:not(pre).pre-wrap{white-space:pre-wrap}
|
||||
:not(pre):not([class^=L])>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background:#f7f7f8;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}
|
||||
pre{color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;line-height:1.45;text-rendering:optimizeSpeed}
|
||||
pre code,pre pre{color:inherit;font-size:inherit;line-height:inherit}
|
||||
pre>code{display:block}
|
||||
pre.nowrap,pre.nowrap pre{white-space:pre;word-wrap:normal}
|
||||
em em{font-style:normal}
|
||||
strong strong{font-weight:400}
|
||||
.keyseq{color:rgba(51,51,51,.8)}
|
||||
kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background:#f7f7f7;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 0 rgba(0,0,0,.2),inset 0 0 0 .1em #fff;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
|
||||
.keyseq kbd:first-child{margin-left:0}
|
||||
.keyseq kbd:last-child{margin-right:0}
|
||||
.menuseq,.menuref{color:#000}
|
||||
.menuseq b:not(.caret),.menuref{font-weight:inherit}
|
||||
.menuseq{word-spacing:-.02em}
|
||||
.menuseq b.caret{font-size:1.25em;line-height:.8}
|
||||
.menuseq i.caret{font-weight:bold;text-align:center;width:.45em}
|
||||
b.button::before,b.button::after{position:relative;top:-1px;font-weight:400}
|
||||
b.button::before{content:"[";padding:0 3px 0 2px}
|
||||
b.button::after{content:"]";padding:0 2px 0 3px}
|
||||
p a>code:hover{color:rgba(0,0,0,.9)}
|
||||
#header,#content,#footnotes,#footer{width:100%;margin:0 auto;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
|
||||
#header::before,#header::after,#content::before,#content::after,#footnotes::before,#footnotes::after,#footer::before,#footer::after{content:" ";display:table}
|
||||
#header::after,#content::after,#footnotes::after,#footer::after{clear:both}
|
||||
#content{margin-top:1.25em}
|
||||
#content::before{content:none}
|
||||
#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
|
||||
#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #dddddf}
|
||||
#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #dddddf;padding-bottom:8px}
|
||||
#header .details{border-bottom:1px solid #dddddf;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:flex;flex-flow:row wrap}
|
||||
#header .details span:first-child{margin-left:-.125em}
|
||||
#header .details span.email a{color:rgba(0,0,0,.85)}
|
||||
#header .details br{display:none}
|
||||
#header .details br+span::before{content:"\00a0\2013\00a0"}
|
||||
#header .details br+span.author::before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
|
||||
#header .details br+span#revremark::before{content:"\00a0|\00a0"}
|
||||
#header #revnumber{text-transform:capitalize}
|
||||
#header #revnumber::after{content:"\00a0"}
|
||||
#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #dddddf;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
|
||||
#toc{border-bottom:1px solid #e7e7e9;padding-bottom:.5em}
|
||||
#toc>ul{margin-left:.125em}
|
||||
#toc ul.sectlevel0>li>a{font-style:italic}
|
||||
#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
|
||||
#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
|
||||
#toc li{line-height:1.3334;margin-top:.3334em}
|
||||
#toc a{text-decoration:none}
|
||||
#toc a:active{text-decoration:underline}
|
||||
#toctitle{color:#7a2518;font-size:1.2em}
|
||||
@media screen and (min-width:768px){#toctitle{font-size:1.375em}
|
||||
body.toc2{padding-left:15em;padding-right:0}
|
||||
#toc.toc2{margin-top:0!important;background:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #e7e7e9;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
|
||||
#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
|
||||
#toc.toc2>ul{font-size:.9em;margin-bottom:0}
|
||||
#toc.toc2 ul ul{margin-left:0;padding-left:1em}
|
||||
#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
|
||||
body.toc2.toc-right{padding-left:0;padding-right:15em}
|
||||
body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #e7e7e9;left:auto;right:0}}
|
||||
@media screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
|
||||
#toc.toc2{width:20em}
|
||||
#toc.toc2 #toctitle{font-size:1.375em}
|
||||
#toc.toc2>ul{font-size:.95em}
|
||||
#toc.toc2 ul ul{padding-left:1.25em}
|
||||
body.toc2.toc-right{padding-left:0;padding-right:20em}}
|
||||
#content #toc{border:1px solid #e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;border-radius:4px}
|
||||
#content #toc>:first-child{margin-top:0}
|
||||
#content #toc>:last-child{margin-bottom:0}
|
||||
#footer{max-width:none;background:rgba(0,0,0,.8);padding:1.25em}
|
||||
#footer-text{color:hsla(0,0%,100%,.8);line-height:1.44}
|
||||
#content{margin-bottom:.625em}
|
||||
.sect1{padding-bottom:.625em}
|
||||
@media screen and (min-width:768px){#content{margin-bottom:1.25em}
|
||||
.sect1{padding-bottom:1.25em}}
|
||||
.sect1:last-child{padding-bottom:0}
|
||||
.sect1+.sect1{border-top:1px solid #e7e7e9}
|
||||
#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
|
||||
#content h1>a.anchor::before,h2>a.anchor::before,h3>a.anchor::before,#toctitle>a.anchor::before,.sidebarblock>.content>.title>a.anchor::before,h4>a.anchor::before,h5>a.anchor::before,h6>a.anchor::before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
|
||||
#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
|
||||
#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
|
||||
#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
|
||||
details,.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
|
||||
details{margin-left:1.25rem}
|
||||
details>summary{cursor:pointer;display:block;position:relative;line-height:1.6;margin-bottom:.625rem;outline:none;-webkit-tap-highlight-color:transparent}
|
||||
details>summary::-webkit-details-marker{display:none}
|
||||
details>summary::before{content:"";border:solid transparent;border-left:solid;border-width:.3em 0 .3em .5em;position:absolute;top:.5em;left:-1.25rem;transform:translateX(15%)}
|
||||
details[open]>summary::before{border:solid transparent;border-top:solid;border-width:.5em .3em 0;transform:translateY(15%)}
|
||||
details>summary::after{content:"";width:1.25rem;height:1em;position:absolute;top:.3em;left:-1.25rem}
|
||||
.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
|
||||
table.tableblock.fit-content>caption.title{white-space:nowrap;width:0}
|
||||
.paragraph.lead>p,#preamble>.sectionbody>[class=paragraph]:first-of-type p{font-size:1.21875em;line-height:1.6;color:rgba(0,0,0,.85)}
|
||||
.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
|
||||
.admonitionblock>table td.icon{text-align:center;width:80px}
|
||||
.admonitionblock>table td.icon img{max-width:none}
|
||||
.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
|
||||
.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #dddddf;color:rgba(0,0,0,.6);word-wrap:anywhere}
|
||||
.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
|
||||
.exampleblock>.content{border:1px solid #e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;border-radius:4px}
|
||||
.exampleblock>.content>:first-child{margin-top:0}
|
||||
.exampleblock>.content>:last-child{margin-bottom:0}
|
||||
.sidebarblock{border:1px solid #dbdbd6;margin-bottom:1.25em;padding:1.25em;background:#f3f3f2;border-radius:4px}
|
||||
.sidebarblock>:first-child{margin-top:0}
|
||||
.sidebarblock>:last-child{margin-bottom:0}
|
||||
.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
|
||||
.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
|
||||
.literalblock pre,.listingblock>.content>pre{border-radius:4px;overflow-x:auto;padding:1em;font-size:.8125em}
|
||||
@media screen and (min-width:768px){.literalblock pre,.listingblock>.content>pre{font-size:.90625em}}
|
||||
@media screen and (min-width:1280px){.literalblock pre,.listingblock>.content>pre{font-size:1em}}
|
||||
.literalblock pre,.listingblock>.content>pre:not(.highlight),.listingblock>.content>pre[class=highlight],.listingblock>.content>pre[class^="highlight "]{background:#f7f7f8}
|
||||
.literalblock.output pre{color:#f7f7f8;background:rgba(0,0,0,.9)}
|
||||
.listingblock>.content{position:relative}
|
||||
.listingblock code[data-lang]::before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:inherit;opacity:.5}
|
||||
.listingblock:hover code[data-lang]::before{display:block}
|
||||
.listingblock.terminal pre .command::before{content:attr(data-prompt);padding-right:.5em;color:inherit;opacity:.5}
|
||||
.listingblock.terminal pre .command:not([data-prompt])::before{content:"$"}
|
||||
.listingblock pre.highlightjs{padding:0}
|
||||
.listingblock pre.highlightjs>code{padding:1em;border-radius:4px}
|
||||
.listingblock pre.prettyprint{border-width:0}
|
||||
.prettyprint{background:#f7f7f8}
|
||||
pre.prettyprint .linenums{line-height:1.45;margin-left:2em}
|
||||
pre.prettyprint li{background:none;list-style-type:inherit;padding-left:0}
|
||||
pre.prettyprint li code[data-lang]::before{opacity:1}
|
||||
pre.prettyprint li:not(:first-child) code[data-lang]::before{display:none}
|
||||
table.linenotable{border-collapse:separate;border:0;margin-bottom:0;background:none}
|
||||
table.linenotable td[class]{color:inherit;vertical-align:top;padding:0;line-height:inherit;white-space:normal}
|
||||
table.linenotable td.code{padding-left:.75em}
|
||||
table.linenotable td.linenos,pre.pygments .linenos{border-right:1px solid;opacity:.35;padding-right:.5em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
|
||||
pre.pygments span.linenos{display:inline-block;margin-right:.75em}
|
||||
.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
|
||||
.quoteblock:not(.excerpt)>.title{margin-left:-1.5em;margin-bottom:.75em}
|
||||
.quoteblock blockquote,.quoteblock p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
|
||||
.quoteblock blockquote{margin:0;padding:0;border:0}
|
||||
.quoteblock blockquote::before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
|
||||
.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
|
||||
.quoteblock .attribution{margin-top:.75em;margin-right:.5ex;text-align:right}
|
||||
.verseblock{margin:0 1em 1.25em}
|
||||
.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans-serif;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
|
||||
.verseblock pre strong{font-weight:400}
|
||||
.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
|
||||
.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
|
||||
.quoteblock .attribution br,.verseblock .attribution br{display:none}
|
||||
.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
|
||||
.quoteblock.abstract blockquote::before,.quoteblock.excerpt blockquote::before,.quoteblock .quoteblock blockquote::before{display:none}
|
||||
.quoteblock.abstract blockquote,.quoteblock.abstract p,.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{line-height:1.6;word-spacing:0}
|
||||
.quoteblock.abstract{margin:0 1em 1.25em;display:block}
|
||||
.quoteblock.abstract>.title{margin:0 0 .375em;font-size:1.15em;text-align:center}
|
||||
.quoteblock.excerpt>blockquote,.quoteblock .quoteblock{padding:0 0 .25em 1em;border-left:.25em solid #dddddf}
|
||||
.quoteblock.excerpt,.quoteblock .quoteblock{margin-left:0}
|
||||
.quoteblock.excerpt blockquote,.quoteblock.excerpt p,.quoteblock .quoteblock blockquote,.quoteblock .quoteblock p{color:inherit;font-size:1.0625rem}
|
||||
.quoteblock.excerpt .attribution,.quoteblock .quoteblock .attribution{color:inherit;font-size:.85rem;text-align:left;margin-right:0}
|
||||
p.tableblock:last-child{margin-bottom:0}
|
||||
td.tableblock>.content{margin-bottom:1.25em;word-wrap:anywhere}
|
||||
td.tableblock>.content>:last-child{margin-bottom:-1.25em}
|
||||
table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
|
||||
table.grid-all>*>tr>*{border-width:1px}
|
||||
table.grid-cols>*>tr>*{border-width:0 1px}
|
||||
table.grid-rows>*>tr>*{border-width:1px 0}
|
||||
table.frame-all{border-width:1px}
|
||||
table.frame-ends{border-width:1px 0}
|
||||
table.frame-sides{border-width:0 1px}
|
||||
table.frame-none>colgroup+*>:first-child>*,table.frame-sides>colgroup+*>:first-child>*{border-top-width:0}
|
||||
table.frame-none>:last-child>:last-child>*,table.frame-sides>:last-child>:last-child>*{border-bottom-width:0}
|
||||
table.frame-none>*>tr>:first-child,table.frame-ends>*>tr>:first-child{border-left-width:0}
|
||||
table.frame-none>*>tr>:last-child,table.frame-ends>*>tr>:last-child{border-right-width:0}
|
||||
table.stripes-all>*>tr,table.stripes-odd>*>tr:nth-of-type(odd),table.stripes-even>*>tr:nth-of-type(even),table.stripes-hover>*>tr:hover{background:#f8f8f7}
|
||||
th.halign-left,td.halign-left{text-align:left}
|
||||
th.halign-right,td.halign-right{text-align:right}
|
||||
th.halign-center,td.halign-center{text-align:center}
|
||||
th.valign-top,td.valign-top{vertical-align:top}
|
||||
th.valign-bottom,td.valign-bottom{vertical-align:bottom}
|
||||
th.valign-middle,td.valign-middle{vertical-align:middle}
|
||||
table thead th,table tfoot th{font-weight:bold}
|
||||
tbody tr th{background:#f7f8f7}
|
||||
tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
|
||||
p.tableblock>code:only-child{background:none;padding:0}
|
||||
p.tableblock{font-size:1em}
|
||||
ol{margin-left:1.75em}
|
||||
ul li ol{margin-left:1.5em}
|
||||
dl dd{margin-left:1.125em}
|
||||
dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
|
||||
li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
|
||||
ul.checklist,ul.none,ol.none,ul.no-bullet,ol.no-bullet,ol.unnumbered,ul.unstyled,ol.unstyled{list-style-type:none}
|
||||
ul.no-bullet,ol.no-bullet,ol.unnumbered{margin-left:.625em}
|
||||
ul.unstyled,ol.unstyled{margin-left:0}
|
||||
li>p:empty:only-child::before{content:"";display:inline-block}
|
||||
ul.checklist>li>p:first-child{margin-left:-1em}
|
||||
ul.checklist>li>p:first-child>.fa-square-o:first-child,ul.checklist>li>p:first-child>.fa-check-square-o:first-child{width:1.25em;font-size:.8em;position:relative;bottom:.125em}
|
||||
ul.checklist>li>p:first-child>input[type=checkbox]:first-child{margin-right:.25em}
|
||||
ul.inline{display:flex;flex-flow:row wrap;list-style:none;margin:0 0 .625em -1.25em}
|
||||
ul.inline>li{margin-left:1.25em}
|
||||
.unstyled dl dt{font-weight:400;font-style:normal}
|
||||
ol.arabic{list-style-type:decimal}
|
||||
ol.decimal{list-style-type:decimal-leading-zero}
|
||||
ol.loweralpha{list-style-type:lower-alpha}
|
||||
ol.upperalpha{list-style-type:upper-alpha}
|
||||
ol.lowerroman{list-style-type:lower-roman}
|
||||
ol.upperroman{list-style-type:upper-roman}
|
||||
ol.lowergreek{list-style-type:lower-greek}
|
||||
.hdlist>table,.colist>table{border:0;background:none}
|
||||
.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
|
||||
td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
|
||||
td.hdlist1{font-weight:bold;padding-bottom:1.25em}
|
||||
td.hdlist2{word-wrap:anywhere}
|
||||
.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
|
||||
.colist td:not([class]):first-child{padding:.4em .75em 0;line-height:1;vertical-align:top}
|
||||
.colist td:not([class]):first-child img{max-width:none}
|
||||
.colist td:not([class]):last-child{padding:.25em 0}
|
||||
.thumb,.th{line-height:0;display:inline-block;border:4px solid #fff;box-shadow:0 0 0 1px #ddd}
|
||||
.imageblock.left{margin:.25em .625em 1.25em 0}
|
||||
.imageblock.right{margin:.25em 0 1.25em .625em}
|
||||
.imageblock>.title{margin-bottom:0}
|
||||
.imageblock.thumb,.imageblock.th{border-width:6px}
|
||||
.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
|
||||
.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
|
||||
.image.left{margin-right:.625em}
|
||||
.image.right{margin-left:.625em}
|
||||
a.image{text-decoration:none;display:inline-block}
|
||||
a.image object{pointer-events:none}
|
||||
sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
|
||||
sup.footnote a,sup.footnoteref a{text-decoration:none}
|
||||
sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
|
||||
#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
|
||||
#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em;border-width:1px 0 0}
|
||||
#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;margin-bottom:.2em}
|
||||
#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none;margin-left:-1.05em}
|
||||
#footnotes .footnote:last-of-type{margin-bottom:0}
|
||||
#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
|
||||
div.unbreakable{page-break-inside:avoid}
|
||||
.big{font-size:larger}
|
||||
.small{font-size:smaller}
|
||||
.underline{text-decoration:underline}
|
||||
.overline{text-decoration:overline}
|
||||
.line-through{text-decoration:line-through}
|
||||
.aqua{color:#00bfbf}
|
||||
.aqua-background{background:#00fafa}
|
||||
.black{color:#000}
|
||||
.black-background{background:#000}
|
||||
.blue{color:#0000bf}
|
||||
.blue-background{background:#0000fa}
|
||||
.fuchsia{color:#bf00bf}
|
||||
.fuchsia-background{background:#fa00fa}
|
||||
.gray{color:#606060}
|
||||
.gray-background{background:#7d7d7d}
|
||||
.green{color:#006000}
|
||||
.green-background{background:#007d00}
|
||||
.lime{color:#00bf00}
|
||||
.lime-background{background:#00fa00}
|
||||
.maroon{color:#600000}
|
||||
.maroon-background{background:#7d0000}
|
||||
.navy{color:#000060}
|
||||
.navy-background{background:#00007d}
|
||||
.olive{color:#606000}
|
||||
.olive-background{background:#7d7d00}
|
||||
.purple{color:#600060}
|
||||
.purple-background{background:#7d007d}
|
||||
.red{color:#bf0000}
|
||||
.red-background{background:#fa0000}
|
||||
.silver{color:#909090}
|
||||
.silver-background{background:#bcbcbc}
|
||||
.teal{color:#006060}
|
||||
.teal-background{background:#007d7d}
|
||||
.white{color:#bfbfbf}
|
||||
.white-background{background:#fafafa}
|
||||
.yellow{color:#bfbf00}
|
||||
.yellow-background{background:#fafa00}
|
||||
span.icon>.fa{cursor:default}
|
||||
a span.icon>.fa{cursor:inherit}
|
||||
.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
|
||||
.admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c}
|
||||
.admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
|
||||
.admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900}
|
||||
.admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400}
|
||||
.admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000}
|
||||
.conum[data-value]{display:inline-block;color:#fff!important;background:rgba(0,0,0,.8);border-radius:50%;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
|
||||
.conum[data-value] *{color:#fff!important}
|
||||
.conum[data-value]+b{display:none}
|
||||
.conum[data-value]::after{content:attr(data-value)}
|
||||
pre .conum[data-value]{position:relative;top:-.125em}
|
||||
b.conum *{color:inherit!important}
|
||||
.conum:not([data-value]):empty{display:none}
|
||||
dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
|
||||
h1,h2,p,td.content,span.alt,summary{letter-spacing:-.01em}
|
||||
p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
|
||||
p,blockquote,dt,td.content,span.alt,summary{font-size:1.0625rem}
|
||||
p{margin-bottom:1.25rem}
|
||||
.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
|
||||
.exampleblock>.content{background:#fffef7;border-color:#e0e0dc;box-shadow:0 1px 4px #e0e0dc}
|
||||
.print-only{display:none!important}
|
||||
@page{margin:1.25cm .75cm}
|
||||
@media print{*{box-shadow:none!important;text-shadow:none!important}
|
||||
html{font-size:80%}
|
||||
a{color:inherit!important;text-decoration:underline!important}
|
||||
a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
|
||||
a[href^="http:"]:not(.bare)::after,a[href^="https:"]:not(.bare)::after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
|
||||
abbr[title]{border-bottom:1px dotted}
|
||||
abbr[title]::after{content:" (" attr(title) ")"}
|
||||
pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
|
||||
thead{display:table-header-group}
|
||||
svg{max-width:100%}
|
||||
p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
|
||||
h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
|
||||
#header,#content,#footnotes,#footer{max-width:none}
|
||||
#toc,.sidebarblock,.exampleblock>.content{background:none!important}
|
||||
#toc{border-bottom:1px solid #dddddf!important;padding-bottom:0!important}
|
||||
body.book #header{text-align:center}
|
||||
body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em}
|
||||
body.book #header .details{border:0!important;display:block;padding:0!important}
|
||||
body.book #header .details span:first-child{margin-left:0!important}
|
||||
body.book #header .details br{display:block}
|
||||
body.book #header .details br+span::before{content:none!important}
|
||||
body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
|
||||
body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
|
||||
.listingblock code[data-lang]::before{display:block}
|
||||
#footer{padding:0 .9375em}
|
||||
.hide-on-print{display:none!important}
|
||||
.print-only{display:block!important}
|
||||
.hide-for-print{display:none!important}
|
||||
.show-for-print{display:inherit!important}}
|
||||
@media amzn-kf8,print{#header>h1:first-child{margin-top:1.25rem}
|
||||
.sect1{padding:0!important}
|
||||
.sect1+.sect1{border:0}
|
||||
#footer{background:none}
|
||||
#footer-text{color:rgba(0,0,0,.6);font-size:.9em}}
|
||||
@media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="article">
|
||||
<div id="header">
|
||||
</div>
|
||||
<div id="content">
|
||||
<div class="sect1">
|
||||
<h2 id="_qqq_tables">QQQ Tables</h2>
|
||||
<div class="sectionbody">
|
||||
<div class="paragraph">
|
||||
<p>The core type of object in a QQQ Instance is the Table.
|
||||
In the most common use-case, a QQQ Table may be the in-app representation of a Database table.
|
||||
That is, it is a collection of records (or rows) of data, each of which has a set of fields (or columns).</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<p>QQQ also allows other types of data sources (<a href="Backends{relfilesuffix}">QQQ Backends</a>) to be used as tables, such as File systems, API’s, Java enums or objects, etc.
|
||||
All of these backend types present the same interfaces (both user-interfaces, and application programming interfaces), regardless of their backend type.</p>
|
||||
</div>
|
||||
<div class="sect2">
|
||||
<h3 id="_qtablemetadata">QTableMetaData</h3>
|
||||
<div class="paragraph">
|
||||
<p>Tables are defined in a QQQ Instance in a <code><strong>QTableMetaData</strong></code> object.
|
||||
All tables must reference a <a href="Backends{relfilesuffix}">QQQ Backend</a>, a list of fields that define the shape of records in the table, and additional data to describe how to work with the table within its backend.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<p><strong>QTableMetaData Properties:</strong></p>
|
||||
</div>
|
||||
<div class="ulist">
|
||||
<ul>
|
||||
<li>
|
||||
<p><code>name</code> - <strong>String, Required</strong> - Unique name for the table within the QQQ Instance.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>label</code> - <strong>String</strong> - User-facing label for the table, presented in User Interfaces.
|
||||
Inferred from <code>name</code> if not set.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>backendName</code> - <strong>String, Required</strong> - Name of a <a href="Backends{relfilesuffix}">QQQ Backend</a> in which this table’s data is managed.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>fields</code> - <strong>Map of String → <a href="Fields{relfilesuffix}">QQQ Field</a>, Required</strong> - The columns of data that make up all records in this table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>primaryKeyField</code> - <strong>String, Conditional</strong> - Name of a <a href="Fields{relfilesuffix}">QQQ Field</a> that serves as the primary key (e.g., unique identifier) for records in this table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>uniqueKeys</code> - <strong>List of UniqueKey</strong> - Definition of additional unique constraints (from an RDBMS point of view) from the table.
|
||||
e.g., sets of columns which must have unique values for each record in the table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>backendDetails</code> - <strong>QTableBackendDetails or subclass</strong> - Additional data to configure the table within its <a href="Backends{relfilesuffix}">QQQ Backend</a>.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>automationDetails</code> - <strong>QTableAutomationDetails</strong> - Configuration of automated jobs that run against records in the table, e.g., upon insert or update.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>customizers</code> - <strong>Map of String → QCodeReference</strong> - References to custom code that are injected into standard table actions, that allow applications to customize certain parts of how the table works.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>parentAppName</code> - <strong>String</strong> - Name of a <a href="Apps{relfilesuffix}">QQQ App</a> that this table exists within.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>icon</code> - <strong>QIcon</strong> - Icon associated with this table in certain user interfaces.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>recordLabelFormat</code> - <strong>String</strong> - Java Format String, used with <code>recordLabelFields</code> to produce a label shown for records from the table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>recordLabelFields</code> - <strong>List of String, Conditional</strong> - Used with <code>recordLabelFormat</code> to provide values for any format specifiers in the format string.
|
||||
These strings must be field names within the table.</p>
|
||||
<div class="ulist">
|
||||
<ul>
|
||||
<li>
|
||||
<p>Example of using <code>recordLabelFormat</code> and <code>recordLabelFields</code>:</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="listingblock">
|
||||
<div class="content">
|
||||
<pre class="CodeRay highlight"><code data-lang="java"><span style="color:#777">// given these fields in the table:</span>
|
||||
<span style="color:#080;font-weight:bold">new</span> QFieldMetaData(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">name</span><span style="color:#710">"</span></span>, QFieldType.STRING)
|
||||
<span style="color:#080;font-weight:bold">new</span> QFieldMetaData(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">birthDate</span><span style="color:#710">"</span></span>, QFieldType.DATE)
|
||||
|
||||
<span style="color:#777">// We can produce a record label such as "Darin Kelkhoff (1980-05-31)" via:</span>
|
||||
.withRecordLabelFormat(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">%s (%s)</span><span style="color:#710">"</span></span>)
|
||||
.withRecordLabelFields(<span style="color:#0a8;font-weight:bold">List</span>.of(<span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">name</span><span style="color:#710">"</span></span>, <span style="background-color:hsla(0,100%,50%,0.05)"><span style="color:#710">"</span><span style="color:#D20">birthDate</span><span style="color:#710">"</span></span>))</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ulist">
|
||||
<ul>
|
||||
<li>
|
||||
<p><code>sections</code> - <strong>List of QFieldSection</strong> - Mechanism to organize fields within user interfaces, into logical sections.
|
||||
If any sections are present in the table meta data, then all fields in the table must be listed in exactly 1 section.
|
||||
If no sections are defined, then instance enrichment will define default sections.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>associatedScripts</code> - <strong>List of AssociatedScript</strong> - Definition of user-defined scripts that can be associated with records within the table.</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><code>enabledCapabilities</code> and <code>disabledCapabilities</code> - <strong>Set of Capability enum values</strong> - Overrides from the backend level, for capabilities that this table does or does not possess.</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<div id="footer-text">
|
||||
Last updated 2022-11-21 09:02:56 -0600
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -2,16 +2,50 @@
|
||||
== Widgets
|
||||
include::../variables.adoc[]
|
||||
|
||||
#TODO#
|
||||
Widgets are the most customizable UI components in QQQ.
|
||||
They can be used either on App Home Screens (e.g., as Dashboard screens),
|
||||
or they can be included into Record View screens.
|
||||
|
||||
QQQ defines several types of widgets, such as charts (pie, bar, line),
|
||||
numeric displays, application-populated tables, or even fully custom HTML.
|
||||
|
||||
=== QWidgetMetaData
|
||||
A Widget is defined in a QQQ Instance in a `*QWidgetMetaData*` object.
|
||||
|
||||
#TODO#
|
||||
|
||||
*QWidgetMetaData Properties:*
|
||||
|
||||
* `name` - *String, Required* - Unique name for the widget within the QQQ Instance.
|
||||
* `type` - *String, Required* - Specifies the UI & data type for the widget.
|
||||
* `label` - *String* - User-facing header or title for a widget.
|
||||
* `tooltip` - *String* - Text contents to be placed in a tooltip associated with the widget's label in the UI.
|
||||
** Values should come from the `WidgetType` enum's `getType()` method (e.g., `WidgetType.BAR_CHART.getType()`)
|
||||
* `gridColumns` - *Integer* - for a desktop-sized screen, in a 12-based grid,
|
||||
how many columns the widget should consume.
|
||||
* `codeReference` - *QCodeReference, Required* - Reference to the custom code,
|
||||
a subclass of `AbstractWidgetRenderer`, which is responsible for loading data to render the widget.
|
||||
* `footerHTML` - *String* - HTML String, which, if present, will be displayed in the
|
||||
footer of the widget (not supported by all widget types).
|
||||
* `isCard` - *boolean, default false* #TODO#
|
||||
* `showReloadButton` - *boolean, default true* #TODO#
|
||||
* `showExportButton` - *boolean, default false* #TODO#
|
||||
* `dropdowns` - #TODO#
|
||||
* `storeDropdownSelections` - *boolean* #TODO#
|
||||
* `icons` - *Map<String, QIcon>* #TODO#
|
||||
* `defaultValues` - *Map<String, Serializable>* #TODO#
|
||||
|
||||
#TODO#
|
||||
There are also some subclasses of `QWidgetMetaData`, for some specific widget types:
|
||||
|
||||
*ParentWidgetMetaData Properties:*
|
||||
|
||||
* `title` - *String* #TODO - how does this differ from label?#
|
||||
* `childWidgetNameList` - *List<String>, Required*
|
||||
* `childProcessNameList` - *List<String>* #TODO appears unused - check, and delete#
|
||||
* `laytoutType` - *enum of GRID or TABS, default GRID*
|
||||
|
||||
*QNoCodeWidgetMetaData Properties:*
|
||||
|
||||
* `values` - *List<AbstractWidgetValueSource>* #TODO#
|
||||
* `outputs` - *List<AbstractWidgetOutput>* #TODO#
|
||||
|
||||
#TODO - Examples#
|
||||
|
||||
|
224
docs/misc/RenderingWidgets.adoc
Normal file
224
docs/misc/RenderingWidgets.adoc
Normal file
@ -0,0 +1,224 @@
|
||||
== Rendering Widgets
|
||||
include::../variables.adoc[]
|
||||
|
||||
=== WidgetRenderer classes
|
||||
In general, to fully implement a Widget, you must define its `QWidgetMetaData`,
|
||||
and supply a subclass of `AbstractWidgetRenderer`, to provide the data to the widget.
|
||||
(Note the "No Code" category of widgets, which are an exception to this generalization).
|
||||
|
||||
The only method required in a subclass of `AbstractWidgetRenderer` is:
|
||||
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
|
||||
The fields available in `RenderWidgetInput` are:
|
||||
|
||||
- `Map<String, String> queryParams` - These are parameters supplied by the frontend, for example,
|
||||
if a user selected values from dropdowns to control a dimension of your widget, those name/value
|
||||
pairs would be in this map. Similarly, if your widget is being included on a record view screen, then
|
||||
the record's primary key will be in this map.
|
||||
- `QWidgetMetaDataInterface widgetMetaData` - This is the meta-data for the widget being
|
||||
rendered. This can be useful in case you are using the same renderer class for multiple widgets.
|
||||
|
||||
The only field in `RenderWidgetOutput` is:
|
||||
|
||||
- `QWidgetData widgetData` - This is a base class, with several attributes, and more importantly,
|
||||
several subclasses, specific to the type of widget that is being rendered.
|
||||
|
||||
==== Widget-Type Specific Rendering Details
|
||||
Different widget types expect & require different types of values to be set in the `RenderWidgetOutput` by their renderers.
|
||||
|
||||
===== Pie Chart
|
||||
The `WidgetType.PIE_CHART` requires an object of type `ChartData`.
|
||||
The fields on this type are:
|
||||
|
||||
* `chartData` an instance of `ChartData.Data`, which has the following fields:
|
||||
** `labels` - *List<String>, required* - the labels for the slices of the pie.
|
||||
** `datasets` - *List<Dataset> required* - the data for each slice of the pie.
|
||||
For a Pie chart, only a single entry in this list is used
|
||||
(other chart types using `ChartData` may support more than 1 entry in this list).
|
||||
Fields in this object are:
|
||||
*** `label` - *String, required* - a label to describe the dataset as a whole.
|
||||
e.g., "Orders" for a pie showing orders of different statuses.
|
||||
*** `data` - *List<Number>, required* - the data points for each slice of the pie.
|
||||
*** `color` - *String* - HTML color for the slice
|
||||
*** `urls` - *List<String>* - Optional URLs for slices of the pie to link to when clicked.
|
||||
*** `backgroundColors` - *List<String>* - Optional HTML color codes for each slice of the pie.
|
||||
|
||||
[source,java]
|
||||
.Pie chart widget example
|
||||
----
|
||||
// meta data
|
||||
new QWidgetMetaData()
|
||||
.withName("pieChartExample")
|
||||
.withType(WidgetType.PIE_CHART.getType())
|
||||
.withGridColumns(4)
|
||||
.withIsCard(true)
|
||||
.withLabel("Pie Chart Example")
|
||||
.withCodeReference(new QCodeReference(PieChartExampleRenderer.class));
|
||||
|
||||
// renderer
|
||||
private List<String> labels = new ArrayList<>();
|
||||
private List<String> colors = new ArrayList<>();
|
||||
private List<Number> data = new ArrayList<>();
|
||||
|
||||
/*******************************************************************************
|
||||
** helper method - to add values for a slice to the lists
|
||||
*******************************************************************************/
|
||||
private void addSlice(String label, String color, Number datum)
|
||||
{
|
||||
labels.add(label);
|
||||
colors.add(color);
|
||||
data.add(datum);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** main method of the widget renderer
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
addSlice("Apple", "#FF0000", 100);
|
||||
addSlice("Orange", "#FF8000", 150);
|
||||
addSlice("Banana", "#FFFF00", 75);
|
||||
addSlice("Lime", "#00FF00", 100);
|
||||
addSlice("Blueberry", "#0000FF", 200);
|
||||
|
||||
ChartData chartData = new ChartData()
|
||||
.withChartData(new ChartData.Data()
|
||||
.withLabels(labels)
|
||||
.withDatasets(List.of(
|
||||
new ChartData.Data.Dataset()
|
||||
.withLabel("Flavor")
|
||||
.withData(data)
|
||||
.withBackgroundColors(colors)
|
||||
.withUrls(urls))));
|
||||
|
||||
return (new RenderWidgetOutput(chartData));
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
===== Bar Chart
|
||||
#todo#
|
||||
|
||||
===== Stacked Bar Chart
|
||||
#todo#
|
||||
|
||||
===== Horizontal Bar Chart
|
||||
#todo#
|
||||
|
||||
===== Child Record List
|
||||
#todo#
|
||||
|
||||
===== Line Chart
|
||||
#todo#
|
||||
|
||||
===== Small Line Chart
|
||||
#todo#
|
||||
|
||||
===== Statistics
|
||||
#todo#
|
||||
|
||||
===== Parent Widget
|
||||
#todo#
|
||||
|
||||
===== Composite
|
||||
A `WidgetType.COMPOSITE` is built by using one or more smaller elements, known as `Blocks`.
|
||||
Note that `Blocks` can also be used in the data of some other widget types
|
||||
(specifically, within a cell of a Table-type widget, or (in the future?) as a header above a pie or bar chart).
|
||||
|
||||
A composite widget renderer must return data of type `CompositeWidgetData`,
|
||||
which has the following fields:
|
||||
|
||||
* `blocks` - *List<AbstractBlockWidgetData>, required* - The blocks (1 or more) being composited together to make the widget.
|
||||
See below for details on the specific Block types.
|
||||
* `styleOverrides` - *Map<String, Serializable>* - Optional map of CSS attributes
|
||||
(named following javascriptStyleCamelCase) to apply to the `<div>` element that wraps the rendered blocks.
|
||||
* `layout` - *Layout enum* - Optional specifier for how the blocks should be laid out.
|
||||
e.g., predefined sets of CSS attributes to achieve specific layouts.
|
||||
** Note that some blocks are designed to work within composites with specific layouts.
|
||||
Look for matching names, such as `Layout.BADGES_WRAPPER` to go with `NumberIconBadgeBlock`.
|
||||
|
||||
[source,java]
|
||||
.Composite widget example - consisting of 3 Progress Bar Blocks, and one Divider Block
|
||||
----
|
||||
// meta data
|
||||
new QWidgetMetaData()
|
||||
.withName("compositeExample")
|
||||
.withType(WidgetType.COMPOSITE.getType())
|
||||
.withGridColumns(4)
|
||||
.withIsCard(true)
|
||||
.withLabel("Composite Example")
|
||||
.withCodeReference(new QCodeReference(CompositeExampleRenderer.class));
|
||||
|
||||
// renderer
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
CompositeWidgetData data = new CompositeWidgetData();
|
||||
|
||||
data.addBlock(new ProgressBarBlockData()
|
||||
.withValues(new ProgressBarValues()
|
||||
.withHeading("Blocks")
|
||||
.withPercent(new BigDecimal("78.5"))));
|
||||
|
||||
data.addBlock(new ProgressBarBlockData()
|
||||
.withValues(new ProgressBarValues()
|
||||
.withHeading("Progress")
|
||||
.withPercent(new BigDecimal(0))));
|
||||
|
||||
data.addBlock(new DividerBlockData());
|
||||
|
||||
data.addBlock(new ProgressBarBlockData()
|
||||
.withStyles(new ProgressBarStyles().withBarColor("#C0C000"))
|
||||
.withValues(new ProgressBarValues()
|
||||
.withHeading("Custom Color")
|
||||
.withPercent(new BigDecimal("75.3"))));
|
||||
|
||||
return (new RenderWidgetOutput(data));
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
===== Table
|
||||
#todo#
|
||||
|
||||
===== HTML
|
||||
#todo#
|
||||
|
||||
===== Divider
|
||||
#todo#
|
||||
|
||||
===== Process
|
||||
#todo#
|
||||
|
||||
===== Stepper
|
||||
#todo#
|
||||
|
||||
===== Data Bag Viewer
|
||||
#todo#
|
||||
|
||||
===== Script Viewer
|
||||
#todo#
|
||||
|
||||
=== Block-type Specific Rendering Details
|
||||
For Composite-type widgets (or other widgets which can include blocks),
|
||||
there are specific data classes required to be returned by the widget renderer.
|
||||
|
||||
Each block type defines a subclass of `AbstractBlockWidgetData`,
|
||||
which is a generic class with 3 type parameters:
|
||||
|
||||
* `V` - an implementation of `BlockValuesInterface` - to define the type of values that the block uses.
|
||||
* `S` - an implementation of `BlockSlotsInterface` (expected to be an `enum`) - to define the "slots" in the block,
|
||||
that can have Tooltips and/or Links applied to them.
|
||||
* `SX` - an implementation of `BlockStylesInterface` - to define the types of style customizations that the block supports.
|
||||
|
||||
These type parameters are designed to ensure type-safety for the application developer,
|
||||
to ensure that only
|
||||
|
||||
=== Additional Tips
|
||||
|
||||
* To make a Dashboard page (e.g., an App consisting of Widgets) with a parent widget use the parent widget's label as the page's label:
|
||||
** On the `QAppMetaData` that contains the Parent widget, call
|
||||
`.withSupplementalMetaData(new MaterialDashboardAppMetaData().withShowAppLabelOnHomeScreen(false))`.
|
||||
** In the Parent widget's renderer, on the `ParentWidgetData`, call `setLabel("My Label")` and
|
||||
`setIsLabelPageTitle(true)`.
|
272
docs/utilities/RecordLookupHelper.adoc
Normal file
272
docs/utilities/RecordLookupHelper.adoc
Normal file
@ -0,0 +1,272 @@
|
||||
== RecordLookupHelper
|
||||
include::../variables.adoc[]
|
||||
|
||||
`RecordLookupHelper` is a utility class that exists to help you... lookup records :)
|
||||
|
||||
OK, I'll try to give a little more context:
|
||||
|
||||
=== Motivation 1: Performance
|
||||
One of the most significant performance optimizations that the team behind QQQ has found time and time again,
|
||||
is to minimize the number of times you have to perform an I/O operation.
|
||||
To just say it more plainly:
|
||||
Make fewer calls to your database (or other backend).
|
||||
|
||||
This is part of why the DML actions in QQQ (InsertAction, UpdateAction, DeleteAction) are all written to work on multiple records:
|
||||
If you've got to insert 1,000 records, the performance difference between doing that as 1,000 SQL INSERT statements vs. just 1 statement cannot be overstated.
|
||||
|
||||
Similarly then, for looking up records:
|
||||
If we can do 1 round-trip to the database backend - that is - 1 query to fetch _n_ records,
|
||||
then in almost all cases it will be significantly faster than doing _n_ queries, one-by-one, for those _n_ records.
|
||||
|
||||
The primary reason why `RecordLookupHelper` exists is to help you cut down on the number of times you have to make a round-trip to a backend data store to fetch records within a process.
|
||||
|
||||
[sidebar]
|
||||
This basically is version of caching, isn't it?
|
||||
Take a set of data from "far away" (e.g., database), and bring it "closer" (local or instance variables), for faster access.
|
||||
So we may describe this as a "cache" through the rest of this document.
|
||||
|
||||
=== Motivation 2: Convenience
|
||||
|
||||
So, given that one wants to try to minimize the number of queries being executed to look up data in a QQQ processes,
|
||||
one can certainly do this "by-hand" in each process that they write.
|
||||
|
||||
Doing this kind of record caching in a QQQ Process `BackendStep` may be done as:
|
||||
|
||||
* Adding a `Map<Integer, QRecord>` as a field in your class.
|
||||
* Setting up and running a `QueryAction`, with a filter based on the collection of the keys you need to look up, then iterating over (or streaming) the results into the map field.
|
||||
* Getting values out of the map when you need to use them (dealing with missing values as needed).
|
||||
|
||||
That's not so bad, but, it does get a little verbose, especially if you're going to have several such caches in your class.
|
||||
|
||||
As such, the second reason that `RecordLookupHelper` exists, is to be a reusable and convenient way to do this kind of optimization,
|
||||
by providing methods to perform the bulk query & map building operation described above,
|
||||
while also providing some convenient methods for accessing such data after it's been fetched.
|
||||
In addition, a single instance of `RecordLookupHelper` can provide this service for multiple tables at once
|
||||
(e.g., so you don't need to add a field to your class for each type of data that you're trying to cache).
|
||||
|
||||
=== Use Cases
|
||||
==== Preload records, then access them
|
||||
Scenario:
|
||||
|
||||
* We're writing a process `BackendStep` that uses `shipment` records as input.
|
||||
* We need to know the `order` record associated with each `shipment` (via an `orderId` foreign key), for some business logic that isn't germaine to the explanation of `RecordLookupHelper`.
|
||||
* We also to access some field on the `shippingPartner` record assigned to each `shipment`.
|
||||
** Note that here, the `shipment` table has a `partnerCode` field, which relates to the `code` unique-key in the `shippingPartner` table.
|
||||
** It's also worth mentioning, we only have a handful of `shippingPartner` records in our database, and we never expect to have very many more than that.
|
||||
|
||||
[source,java]
|
||||
.Example of a process step using a RecordLookupHelper to preload records
|
||||
----
|
||||
public class MyShipmentProcessStep implements BackendStep
|
||||
{
|
||||
// Add a new RecordLookupHelper field, which will "cache" both orders and shippingPartners
|
||||
private RecordLookupHelper recordLookupHelper = new RecordLookupHelper();
|
||||
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException;
|
||||
{
|
||||
// lookup the full shippingPartner table (because it's cheap & easy to do so)
|
||||
// use the partner's "code" as the key field (e.g,. they key in the helper's internal map).
|
||||
recordLookupHelper.preloadRecords("shippingPartner", "code");
|
||||
|
||||
// get all of the orderIds from the input shipments
|
||||
List<Serializable> orderIds = input.getRecords().stream()
|
||||
.map(r -> r.getValue("id")).toList();
|
||||
|
||||
// fetch the orders related to by these shipments
|
||||
recordLookupHelper.preloadRecords("order", "id", orderIds);
|
||||
|
||||
for(QRecord shipment : input.getRecords())
|
||||
{
|
||||
// get someConfigField from the shippingPartner assigned to the shipment
|
||||
Boolean someConfig = recordLookupHelper.getRecordValue("shippingPartner", "someConfigField", "code", shipment.getValue("partnerCode"));
|
||||
|
||||
// get the order record assigned to the shipment
|
||||
QRecord order = recordLookupHelper.getRecordByKey("order", "id", shipment.getValue("orderId"));
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
==== Lazy fetching records
|
||||
Scenario:
|
||||
|
||||
* We have a `BackendStep` that is taking in `purchaseOrderHeader` records, from an API partner.
|
||||
* For each record, we need to make an API call to the partner to fetch the `purchaseOrderLine` records under that header.
|
||||
** In this contrived example, the partner's API forces us to do these lookups order-by-order...
|
||||
* Each `purchaseOrderLine` that we fetch will have a `sku` on it - a reference to our `item` table.
|
||||
** We need to look up each `item` to apply some business logic.
|
||||
** We assume there are very many item records in the backend database, so we don't want to pre-load the full table.
|
||||
Also, we don't know what `sku` values we will need until we fetch the `purchaseOrderLine`.
|
||||
|
||||
This is a situation where we can use `RecordLookupHelper` to lazily fetch the `item` records as we discover them,
|
||||
and it will take care of not re-fetching ones that it has already loaded.
|
||||
|
||||
[source,java]
|
||||
.Example of a process step using a RecordLookupHelper to lazy fetch records
|
||||
----
|
||||
public class MyPurchaseOrderProcessStep implements BackendStep
|
||||
{
|
||||
// Add a new RecordLookupHelper field, which will "cache" lazy-loaded item records
|
||||
private RecordLookupHelper recordLookupHelper = new RecordLookupHelper();
|
||||
|
||||
@Override
|
||||
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException;
|
||||
{
|
||||
for(QRecord poHeader : input.getRecords())
|
||||
{
|
||||
// fetch the lines under the header
|
||||
Serializable poNo = poHeader.getValue("poNo");
|
||||
List<QRecord> poLines = new QueryAction().execute(new QueryInput("purchaseOrderLine")
|
||||
.withFilter(new QQueryFilter(new QFilterCriteria("poNo", EQUALS, poNo))));
|
||||
|
||||
for(QRecord poLine : poLines)
|
||||
{
|
||||
// use recordLookupHelper to lazy-load item records by SKU.
|
||||
QRecord item = recordLookupHelper.getRecordByKey("item", "sku", poLine.getValue("sku"));
|
||||
|
||||
// business logic related to item performed here.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
In this example, we will be doing exactly 1 query on the `item` table for each unique `sku` that is found across all of the `poLine` records we process.
|
||||
That is to say, if the same `sku` appears on only 1 `poLine`, or if it appears on 100 `poLines`, we will still only query once for that `sku`.
|
||||
|
||||
A slight tweak could be made to the example above, to make 1 `item` table lookup for each `poHeader` record:
|
||||
|
||||
[source,java]
|
||||
.Tweaked example doing 1 item lookup per poLine
|
||||
----
|
||||
// continuing from above, after the List<QRecord> poLines has been built
|
||||
|
||||
// get all of the skus from the lines
|
||||
List<Serializable> skus = poLines.stream().map(r -> r.getValue("sku")).toList();
|
||||
|
||||
// preload the items for the skus
|
||||
recordLookupHelper.preloadRecords("item", "sku", new QQueryFilter(new QFilterCriteria("sku", IN, skus)));
|
||||
|
||||
for(QRecord poLine : poLines)
|
||||
{
|
||||
// get the items from the helper
|
||||
QRecord item = recordLookupHelper.getRecordByKey("item", "sku", poLine.getValue("sku"));
|
||||
|
||||
----
|
||||
|
||||
In this example, we've made a trade-off: We will query the `item` table exactly 1 time for each `poHeader` that we process.
|
||||
However, if the same `sku` is on every PO that we process, we will end up fetching it multiple times.
|
||||
|
||||
This could end up being better or worse than the previous example, depending on the distribution of the data we are dealing with.
|
||||
|
||||
A further tweak, a hybrid approach, could potentially reap the benefits of both of these examples (at the tradeoff of, more code, more complexity):
|
||||
|
||||
[source,java]
|
||||
.Tweaked example doing 1 item lookup per poLine, but only for not-previously-encountered skus
|
||||
----
|
||||
// Critically - we must tell our recordLookupHelper to NOT do any one-off lookups in this table
|
||||
recordLookupHelper.setMayNotDoOneOffLookups("item", "sku");
|
||||
|
||||
// continuing from above, after the List<QRecord> poLines has been built
|
||||
|
||||
// get all of the skus from the lines
|
||||
List<Serializable> skus = poLines.stream().map(r -> r.getValue("sku")).toList();
|
||||
|
||||
// determine which skus have not yet been loaded - e.g., they are not in the recordLookupHelper.
|
||||
// this is why we needed to tell it above not to do one-off lookups; else it would lazy-load each sku here.
|
||||
List<Serializable> skusToLoad = new ArrayList<>();
|
||||
for(Serializable sku : skus)
|
||||
{
|
||||
if(recordLookupHelper.getRecordByKey("item", "sku", sku) == null)
|
||||
{
|
||||
skusToLoad.add(sku);
|
||||
}
|
||||
}
|
||||
|
||||
// preload the item records for any skus that are still needed
|
||||
if(!skusToLoad.isEmpty())
|
||||
{
|
||||
recordLookupHelper.preloadRecords("item", "sku",
|
||||
new QQueryFilter(new QFilterCriteria("sku", IN, skusToLoad)));
|
||||
}
|
||||
|
||||
// continue as above
|
||||
|
||||
----
|
||||
|
||||
In this example, we will start by querying the `item` table once for each `poHeader`, but,
|
||||
if we eventually encounter a PO where all of its `skus` have already been loaded, then we may be able to avoid any `item` queries for such a PO.
|
||||
|
||||
=== Implementation Details
|
||||
|
||||
* Internally, an instance of `RecordLookupHelper` maintains a number of `Maps`,
|
||||
with QQQ table names and field names as keys.
|
||||
* The accessing/lazy-fetching methods (e.g., any method whose name starts with `getRecord`)
|
||||
all begin by looking in these internal maps for the `tableName` and `keyFieldName` that they take as parameters.
|
||||
** If they find an entry in the maps, then it is used for producing a return value.
|
||||
** If they do not find an entry, then they will perform the a `QueryAction`,
|
||||
to try to fetch the requested record from the table's backend.
|
||||
*** Unless the `setMayNotDoOneOffLookups` method has been called for the `(tableName, keyFieldName)` pair.
|
||||
|
||||
=== Full API
|
||||
|
||||
==== Methods for accessing and lazy-fetching
|
||||
* `getRecordByKey(String tableName, String keyFieldName, Serializable key)`
|
||||
|
||||
Get a `QRecord` from `tableName`, where `keyFieldName` = `key`.
|
||||
|
||||
* `getRecordValue(String tableName, String requestedField, String keyFieldName, Serializable key)`
|
||||
|
||||
Get the field `requestedField` from the record in `tableName`, where `keyFieldName` = `key`, as a `Serializable`.
|
||||
If the record is not found, `null` is returned.
|
||||
|
||||
* `getRecordValue(String tableName, String requestedField, String keyFieldName, Serializable key, Class<T> type)`
|
||||
|
||||
Get the field `requestedField` from the record in `tableName`, where `keyFieldName` = `key`, as an instance of `type`.
|
||||
If the record is not found, `null` is returned.
|
||||
|
||||
* `getRecordId(String tableName, String keyFieldName, Serializable key)`
|
||||
|
||||
Get the primary key of the record in `tableName`, where `keyFieldName` = `key`, as a `Serializable`.
|
||||
If the record is not found, `null` is returned.
|
||||
|
||||
* `getRecordId(String tableName, String keyFieldName, Serializable key, Class<T> type)`
|
||||
|
||||
Get the primary key of the record in `tableName`, where `keyFieldName` = `key`, as an instance of `type`.
|
||||
If the record is not found, `null` is returned.
|
||||
|
||||
* `getRecordByUniqueKey(String tableName, Map<String, Serializable> uniqueKey)`
|
||||
|
||||
Get a `QRecord` from `tableName`, where the record matches the field/value pairs in `uniqueKey`.
|
||||
|
||||
_Note: this method does not use the same internal map as the rest of the class.
|
||||
As such, it does not take advantage of any data fetched via the preload methods.
|
||||
It is only used for caching lazy-fetches._
|
||||
|
||||
==== Methods for preloading
|
||||
* `preloadRecords(String tableName, String keyFieldName)`
|
||||
|
||||
Query for all records from `tableName`, storing them in an internal map keyed by the field `keyFieldName`.
|
||||
|
||||
* `preloadRecords(String tableName, String keyFieldName, QQueryFilter filter)`
|
||||
|
||||
Query for records matching `filter` from `tableName`,
|
||||
storing them in an internal map keyed by the field `keyFieldName`.
|
||||
|
||||
* `preloadRecords(String tableName, String keyFieldName, List<Serializable> inList)`
|
||||
|
||||
Query for records with the field `keyFieldName` having a value in `inList` from `tableName`,
|
||||
storing them in an internal map keyed by the field `keyFieldName`.
|
||||
|
||||
==== Config Methods
|
||||
* `setMayNotDoOneOffLookups(String tableName, String fieldName)`
|
||||
|
||||
For cases where you know that you have preloaded records for `tableName`, keyed by `fieldName`,
|
||||
and you know that some of the keys may not have been found,
|
||||
so you want to avoid doing a query when a missed key is found in one of the `getRecord...` methods,
|
||||
then if you call this method, an internal flag is set, which will prevent any such one-off lookups.
|
||||
|
||||
In other words, if this method has been called for a `(tableName, fieldName)` pair,
|
||||
then the `getRecord...` methods will only look in the internal map for records,
|
||||
and no queries will be performed to look for records.
|
4
pom.xml
4
pom.xml
@ -80,12 +80,12 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>2.17.1</version>
|
||||
<version>2.23.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>2.17.1</version>
|
||||
<version>2.23.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
|
@ -78,6 +78,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
private static Set<String> loggedUnauditableTableNames = new HashSet<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -210,6 +211,19 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
contextSuffix.append(" ").append(input.getAuditContext());
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// look for a context value place directly into the session //
|
||||
//////////////////////////////////////////////////////////////
|
||||
QSession qSession = QContext.getQSession();
|
||||
if(qSession != null)
|
||||
{
|
||||
String sessionContext = qSession.getValue(AUDIT_CONTEXT_FIELD_NAME);
|
||||
if(StringUtils.hasContent(sessionContext))
|
||||
{
|
||||
contextSuffix.append(" ").append(sessionContext);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// note process label (and a possible context from the process's state) if present //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -233,17 +247,20 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
///////////////////////////////////////////////////
|
||||
// use api label & version if present in session //
|
||||
///////////////////////////////////////////////////
|
||||
QSession qSession = QContext.getQSession();
|
||||
String apiVersion = qSession.getValue("apiVersion");
|
||||
if(apiVersion != null)
|
||||
if(qSession != null)
|
||||
{
|
||||
String apiLabel = qSession.getValue("apiLabel");
|
||||
if(!StringUtils.hasContent(apiLabel))
|
||||
String apiVersion = qSession.getValue("apiVersion");
|
||||
if(apiVersion != null)
|
||||
{
|
||||
apiLabel = "API";
|
||||
String apiLabel = qSession.getValue("apiLabel");
|
||||
if(!StringUtils.hasContent(apiLabel))
|
||||
{
|
||||
apiLabel = "API";
|
||||
}
|
||||
contextSuffix.append(" via ").append(apiLabel).append(" Version: ").append(apiVersion);
|
||||
}
|
||||
contextSuffix.append(" via ").append(apiLabel).append(" Version: ").append(apiVersion);
|
||||
}
|
||||
|
||||
return (contextSuffix.toString());
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.automation;
|
||||
|
||||
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
|
||||
|
||||
|
||||
@ -55,6 +56,30 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get instance by id
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static AutomationStatus getById(Integer id)
|
||||
{
|
||||
if(id == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
for(AutomationStatus value : AutomationStatus.values())
|
||||
{
|
||||
if(Objects.equals(value.id, id))
|
||||
{
|
||||
return (value);
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
**
|
||||
@ -106,10 +131,10 @@ public enum AutomationStatus implements PossibleValueEnum<Integer>
|
||||
public String getInsertOrUpdate()
|
||||
{
|
||||
return switch(this)
|
||||
{
|
||||
case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert";
|
||||
case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update";
|
||||
case OK -> "";
|
||||
};
|
||||
{
|
||||
case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert";
|
||||
case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update";
|
||||
case OK -> "";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -22,30 +22,40 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.automation;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -55,19 +65,37 @@ public class RecordAutomationStatusUpdater
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(RecordAutomationStatusUpdater.class);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// feature flag - by default, will be true - before setting records to PENDING_UPDATE_AUTOMATIONS, //
|
||||
// we will fetch them (if we didn't take them in from the caller, which, UpdateAction does if its //
|
||||
// backend supports it), to check their current automationStatus - and if they are currently PENDING //
|
||||
// or RUNNING inserts or updates, we won't update them. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static boolean allowPreUpdateFetch = new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.recordAutomationStatusUpdater.allowPreUpdateFetch", "QQQ_RECORD_AUTOMATION_STATUS_UPDATER_ALLOW_PRE_UPDATE_FETCH", true);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// feature flag - by default, we'll memoize the check for triggers - but we can turn it off. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static boolean memoizeCheckForTriggers = new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.recordAutomationStatusUpdater.memoizeCheckForTriggers", "QQQ_RECORD_AUTOMATION_STATUS_UPDATER_MEMOIZE_CHECK_FOR_TRIGGERS", true);
|
||||
|
||||
private static Memoization<Key, Boolean> areThereTableTriggersForTableMemoization = new Memoization<Key, Boolean>().withTimeout(Duration.of(60, ChronoUnit.SECONDS));
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** for a list of records from a table, set their automation status - based on
|
||||
** how the table is configured.
|
||||
*******************************************************************************/
|
||||
public static boolean setAutomationStatusInRecords(QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus)
|
||||
public static boolean setAutomationStatusInRecords(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus, QBackendTransaction transaction, List<QRecord> oldRecordList)
|
||||
{
|
||||
if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
Set<Serializable> pkeysWeMayNotUpdate = new HashSet<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// In case an automation is running, and it updates records - don't let those records be marked //
|
||||
// as PENDING_UPDATE_AUTOMATIONS... this is meant to avoid having a record's automation update //
|
||||
@ -81,12 +109,60 @@ public class RecordAutomationStatusUpdater
|
||||
for(StackTraceElement stackTraceElement : e.getStackTrace())
|
||||
{
|
||||
String className = stackTraceElement.getClassName();
|
||||
if(className.contains("com.kingsrook.qqq.backend.core.actions.automation") && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test"))
|
||||
if(className.contains(RecordAutomationStatusUpdater.class.getPackageName()) && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test") && !className.contains("Test$"))
|
||||
{
|
||||
LOG.debug("Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// if table uses field-in-table status tracking, then check the old records, //
|
||||
// before we set them to pending-updates, to avoid losing other pending or //
|
||||
// running status information. We will allow moving from OK or the 2 //
|
||||
// failed statuses into pending-updates - which seems right. //
|
||||
// This is added to fix cases where an update that comes in before insert //
|
||||
// -automations have run, will cause the pending-insert status to be missed. //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
|
||||
{
|
||||
try
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(oldRecordList))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we didn't get the oldRecordList as input (though UpdateAction should usually pass it?) //
|
||||
// then check feature-flag if we're allowed to do a lookup here & now. If so, then do. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(allowPreUpdateFetch)
|
||||
{
|
||||
List<Serializable> pkeysToLookup = records.stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
|
||||
oldRecordList = new QueryAction().execute(new QueryInput(table.getName())
|
||||
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, pkeysToLookup)))
|
||||
.withTransaction(transaction)
|
||||
).getRecords();
|
||||
}
|
||||
}
|
||||
|
||||
for(QRecord freshRecord : CollectionUtils.nonNullList(oldRecordList))
|
||||
{
|
||||
Serializable recordStatus = freshRecord.getValue(automationDetails.getStatusTracking().getFieldName());
|
||||
if(AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId().equals(recordStatus)
|
||||
|| AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId().equals(recordStatus)
|
||||
|| AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId().equals(recordStatus)
|
||||
|| AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId().equals(recordStatus))
|
||||
{
|
||||
Serializable primaryKey = freshRecord.getValue(table.getPrimaryKeyField());
|
||||
LOG.debug("May not update automation status", logPair("table", table.getName()), logPair("id", primaryKey), logPair("currentStatus", recordStatus), logPair("requestedStatus", automationStatus.getId()));
|
||||
pkeysWeMayNotUpdate.add(primaryKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
LOG.error("Error checking existing automation status before setting new automation status - more records will be updated than maybe should be...", qe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -98,19 +174,15 @@ public class RecordAutomationStatusUpdater
|
||||
automationStatus = AutomationStatus.OK;
|
||||
}
|
||||
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - seems like there's some case here, where if an order was in PENDING_INSERT, but then some other job updated the record, that we'd //
|
||||
// lose that pending status, which would be a Bad Thing™... //
|
||||
// problem is - we may not have the full record in here, so we can't necessarily check the record to see what status it's currently in... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
|
||||
// todo - another field - for the automation timestamp??
|
||||
if(!pkeysWeMayNotUpdate.contains(record.getValue(table.getPrimaryKeyField())))
|
||||
{
|
||||
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
|
||||
// todo - another field - for the automation timestamp??
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,11 +260,29 @@ public class RecordAutomationStatusUpdater
|
||||
return (false);
|
||||
}
|
||||
|
||||
if(memoizeCheckForTriggers)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// as within the lookup method, error on the side of "yes, maybe there are triggers" //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
Optional<Boolean> result = areThereTableTriggersForTableMemoization.getResult(new Key(table, triggerEvent), key -> lookupIfThereAreTriggersForTable(table, triggerEvent));
|
||||
return result.orElse(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return lookupIfThereAreTriggersForTable(table, triggerEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Boolean lookupIfThereAreTriggersForTable(QTableMetaData table, TriggerEvent triggerEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
///////////////////
|
||||
// todo - cache? //
|
||||
///////////////////
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(TableTrigger.TABLE_NAME);
|
||||
countInput.setFilter(new QQueryFilter(
|
||||
@ -207,6 +297,7 @@ public class RecordAutomationStatusUpdater
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the count query failed, we're a bit safer to err on the side of "yeah, there might be automations" //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.warn("Error looking if there are triggers for table", e, logPair("tableName", table.getName()));
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
@ -217,12 +308,12 @@ public class RecordAutomationStatusUpdater
|
||||
** for a list of records, update their automation status and actually Update the
|
||||
** backend as well.
|
||||
*******************************************************************************/
|
||||
public static void setAutomationStatusInRecordsAndUpdate(QInstance instance, QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus) throws QException
|
||||
public static void setAutomationStatusInRecordsAndUpdate(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus, QBackendTransaction transaction) throws QException
|
||||
{
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
if(automationDetails != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
|
||||
{
|
||||
boolean didSetStatusField = setAutomationStatusInRecords(session, table, records, automationStatus);
|
||||
boolean didSetStatusField = setAutomationStatusInRecords(table, records, automationStatus, transaction, null);
|
||||
if(didSetStatusField)
|
||||
{
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
@ -237,6 +328,7 @@ public class RecordAutomationStatusUpdater
|
||||
.withValue(table.getPrimaryKeyField(), r.getValue(table.getPrimaryKeyField()))
|
||||
.withValue(automationDetails.getStatusTracking().getFieldName(), r.getValue(automationDetails.getStatusTracking().getFieldName()))).toList());
|
||||
updateInput.setAreAllValuesBeingUpdatedTheSame(true);
|
||||
updateInput.setTransaction(transaction);
|
||||
updateInput.setOmitDmlAudit(true);
|
||||
|
||||
new UpdateAction().execute(updateInput);
|
||||
@ -250,4 +342,8 @@ public class RecordAutomationStatusUpdater
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private record Key(QTableMetaData table, TriggerEvent triggerEvent) {}
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
||||
@ -60,18 +61,21 @@ import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
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.DynamicDefaultValueBehavior;
|
||||
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.automation.AutomationStatusTrackingType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
|
||||
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import org.json.JSONObject;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -251,12 +255,11 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
|
||||
try
|
||||
{
|
||||
QSession session = sessionSupplier != null ? sessionSupplier.get() : new QSession();
|
||||
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), session, tableActions.status());
|
||||
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), tableActions.status());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error running automations", e);
|
||||
LOG.warn("Error running automations", e, logPair("tableName", tableActions.tableName()), logPair("status", tableActions.status()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -270,7 +273,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
/*******************************************************************************
|
||||
** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table.
|
||||
*******************************************************************************/
|
||||
public void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus) throws QException
|
||||
public void processTableInsertOrUpdate(QTableMetaData table, AutomationStatus automationStatus) throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// get the actions to run against this table in this automation status //
|
||||
@ -300,7 +303,9 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType();
|
||||
if(AutomationStatusTrackingType.FIELD_IN_TABLE.equals(statusTrackingType))
|
||||
{
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))));
|
||||
QQueryFilter filter = new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId())));
|
||||
addOrderByToQueryFilter(table, automationStatus, filter);
|
||||
queryInput.setFilter(filter);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -321,7 +326,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
}, () ->
|
||||
{
|
||||
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||
applyActionsToRecords(session, table, records, actions, automationStatus);
|
||||
applyActionsToRecords(table, records, actions, automationStatus);
|
||||
return (records.size());
|
||||
}
|
||||
);
|
||||
@ -329,6 +334,38 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static void addOrderByToQueryFilter(QTableMetaData table, AutomationStatus automationStatus, QQueryFilter filter)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// look for a field in the table with either create-date or modify-date behavior, //
|
||||
// based on if doing insert or update automations //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
DynamicDefaultValueBehavior dynamicDefaultValueBehavior = automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS) ? DynamicDefaultValueBehavior.CREATE_DATE : DynamicDefaultValueBehavior.MODIFY_DATE;
|
||||
Optional<QFieldMetaData> field = table.getFields().values().stream()
|
||||
.filter(f -> dynamicDefaultValueBehavior.equals(f.getBehaviorOrDefault(QContext.getQInstance(), DynamicDefaultValueBehavior.class)))
|
||||
.findFirst();
|
||||
|
||||
if(field.isPresent())
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// if a create/modify date field was found, order by it (ascending) //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
filter.addOrderBy(new QFilterOrderBy(field.get().getName()));
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////
|
||||
// else, order by the primary key //
|
||||
////////////////////////////////////
|
||||
filter.addOrderBy(new QFilterOrderBy(table.getPrimaryKeyField()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get the actions to run against a table in an automation status. both from
|
||||
** metaData and tableTriggers/data.
|
||||
@ -388,13 +425,15 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
if(filterId != null)
|
||||
{
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(SavedFilter.TABLE_NAME);
|
||||
getInput.setTableName(SavedView.TABLE_NAME);
|
||||
getInput.setPrimaryKey(filterId);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
if(getOutput.getRecord() != null)
|
||||
{
|
||||
SavedFilter savedFilter = new SavedFilter(getOutput.getRecord());
|
||||
filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class);
|
||||
SavedView savedView = new SavedView(getOutput.getRecord());
|
||||
JSONObject viewJson = new JSONObject(savedView.getViewJson());
|
||||
JSONObject queryFilter = viewJson.getJSONObject("queryFilter");
|
||||
filter = JsonUtils.toObject(queryFilter.toString(), QQueryFilter.class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -427,7 +466,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
** table's actions against them - IF they are found to match the action's filter
|
||||
** (assuming it has one - if it doesn't, then all records match).
|
||||
*******************************************************************************/
|
||||
private void applyActionsToRecords(QSession session, QTableMetaData table, List<QRecord> records, List<TableAutomationAction> actions, AutomationStatus automationStatus) throws QException
|
||||
private void applyActionsToRecords(QTableMetaData table, List<QRecord> records, List<TableAutomationAction> actions, AutomationStatus automationStatus) throws QException
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(records))
|
||||
{
|
||||
@ -437,7 +476,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
///////////////////////////////////////////////////
|
||||
// mark the records as RUNNING their automations //
|
||||
///////////////////////////////////////////////////
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToRunningStatusMap.get(automationStatus));
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, pendingToRunningStatusMap.get(automationStatus), null);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach action - run it against the records (but only if they match the action's filter, if there is one) //
|
||||
@ -455,13 +494,15 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
////////////////////////////////////////
|
||||
// update status on all these records //
|
||||
////////////////////////////////////////
|
||||
if(anyActionsFailed)
|
||||
AutomationStatus statusToUpdateTo = anyActionsFailed ? pendingToFailedStatusMap.get(automationStatus) : AutomationStatus.OK;
|
||||
try
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToFailedStatusMap.get(automationStatus));
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(table, records, statusToUpdateTo, null);
|
||||
}
|
||||
else
|
||||
catch(Exception e)
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, AutomationStatus.OK);
|
||||
LOG.warn("Error updating automationStatus after running automations", logPair("tableName", table), logPair("count", records.size()), logPair("status", statusToUpdateTo));
|
||||
throw (e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,7 +532,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
|
||||
LOG.warn("Caught exception processing automations", e, logPair("tableName", table), logPair("action", action.getName()));
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.QQueryFilterDeduper;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@ -176,11 +177,13 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
return (totalString);
|
||||
}
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return ("<a href='" + tablePath + "?filter=" + JsonUtils.toJson(filter) + "'>" + totalString + "</a>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -192,6 +195,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return;
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
urls.add(tablePath + "?filter=" + JsonUtils.toJson(filter));
|
||||
}
|
||||
|
||||
@ -208,6 +212,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return (null);
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "?filter=" + JsonUtils.toJson(filter));
|
||||
}
|
||||
|
||||
@ -224,6 +229,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return (null);
|
||||
}
|
||||
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
}
|
||||
|
||||
@ -326,6 +332,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
}
|
||||
|
||||
String tablePath = QContext.getQInstance().getTablePath(tableName);
|
||||
filter = QQueryFilterDeduper.dedupeFilter(filter);
|
||||
return (tablePath + "/" + processName + "?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
@ -297,4 +297,21 @@ public enum DateTimeGroupBy
|
||||
ZonedDateTime zoned = instant.atZone(zoneId);
|
||||
return (zoned.plus(noOfChronoUnitsToAdd, chronoUnitToAdd).toInstant());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static DateTimeFormatter sqlDateFormatToSelectedDateTimeFormatter(String sqlDateFormat)
|
||||
{
|
||||
for(DateTimeGroupBy value : values())
|
||||
{
|
||||
if(value.sqlDateFormat.equals(sqlDateFormat))
|
||||
{
|
||||
return (value.selectedStringFormatter);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
@ -47,12 +48,14 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.Script;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -65,6 +68,8 @@ public class RunAdHocRecordScriptAction
|
||||
private Map<Integer, ScriptRevision> scriptRevisionCacheByScriptRevisionId = new HashMap<>();
|
||||
private Map<Integer, ScriptRevision> scriptRevisionCacheByScriptId = new HashMap<>();
|
||||
|
||||
private static Memoization<Integer, Script> scriptMemoizationById = new Memoization<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -85,6 +90,12 @@ public class RunAdHocRecordScriptAction
|
||||
throw (new QException("Script revision was not found."));
|
||||
}
|
||||
|
||||
Optional<Script> script = getScript(scriptRevision);
|
||||
|
||||
QContext.getQSession().setValue(DMLAuditAction.AUDIT_CONTEXT_FIELD_NAME, script.isPresent()
|
||||
? "via Script \"%s\"".formatted(script.get().getName())
|
||||
: "via Script id " + scriptRevision.getScriptId());
|
||||
|
||||
////////////////////////////
|
||||
// figure out the records //
|
||||
////////////////////////////
|
||||
@ -124,6 +135,10 @@ public class RunAdHocRecordScriptAction
|
||||
{
|
||||
output.setException(Optional.of(e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.getQSession().removeValue(DMLAuditAction.AUDIT_CONTEXT_FIELD_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -217,4 +232,44 @@ public class RunAdHocRecordScriptAction
|
||||
throw (new QException("Code reference did not contain a scriptRevision, scriptRevisionId, or scriptId"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Optional<Script> getScript(ScriptRevision scriptRevision)
|
||||
{
|
||||
if(scriptRevision == null || scriptRevision.getScriptId() == null)
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return scriptMemoizationById.getResult(scriptRevision.getScriptId(), scriptId ->
|
||||
{
|
||||
try
|
||||
{
|
||||
QRecord scriptRecord = new GetAction().executeForRecord(new GetInput(Script.TABLE_NAME).withPrimaryKey(scriptRevision.getScriptId()));
|
||||
if(scriptRecord != null)
|
||||
{
|
||||
Script script = new Script(scriptRecord);
|
||||
scriptMemoizationById.storeResult(scriptRevision.getScriptId(), script);
|
||||
return (script);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.info("");
|
||||
}
|
||||
|
||||
return (null);
|
||||
});
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -444,7 +444,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
*******************************************************************************/
|
||||
private void setAutomationStatusField(InsertInput insertInput)
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getSession(), insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS);
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS, insertInput.getTransaction(), null);
|
||||
}
|
||||
|
||||
|
||||
|
@ -113,7 +113,6 @@ public class UpdateAction
|
||||
public UpdateOutput execute(UpdateInput updateInput) throws QException
|
||||
{
|
||||
ActionHelper.validateSession(updateInput);
|
||||
setAutomationStatusField(updateInput);
|
||||
|
||||
QTableMetaData table = updateInput.getTable();
|
||||
|
||||
@ -130,6 +129,16 @@ public class UpdateAction
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Optional<List<QRecord>> oldRecordList = fetchOldRecords(updateInput, updateInterface);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// allow caller to specify that we don't want to trigger automations. this isn't //
|
||||
// isn't expected to be used much - by design, only for the process that is meant to //
|
||||
// heal automation status, so that it can force us into status=Pending-inserts //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!updateInput.getOmitTriggeringAutomations())
|
||||
{
|
||||
setAutomationStatusField(updateInput, oldRecordList);
|
||||
}
|
||||
|
||||
performValidations(updateInput, oldRecordList, false);
|
||||
|
||||
////////////////////////////////////
|
||||
@ -561,9 +570,9 @@ public class UpdateAction
|
||||
/*******************************************************************************
|
||||
** If the table being updated uses an automation-status field, populate it now.
|
||||
*******************************************************************************/
|
||||
private void setAutomationStatusField(UpdateInput updateInput)
|
||||
private void setAutomationStatusField(UpdateInput updateInput, Optional<List<QRecord>> oldRecordList)
|
||||
{
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getSession(), updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS);
|
||||
RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS, updateInput.getTransaction(), oldRecordList.orElse(null));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ 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.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
@ -230,7 +231,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
{
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
inputRecord.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."));
|
||||
}
|
||||
@ -298,7 +299,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// handle null values - error if the NullValueBehavior is DENY //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
|
||||
if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
|
||||
{
|
||||
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
|
||||
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
|
||||
|
@ -22,6 +22,9 @@
|
||||
package com.kingsrook.qqq.backend.core.context;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Stack;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
@ -31,6 +34,7 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -47,6 +51,7 @@ public class QContext
|
||||
private static ThreadLocal<QBackendTransaction> qBackendTransactionThreadLocal = new ThreadLocal<>();
|
||||
private static ThreadLocal<Stack<AbstractActionInput>> actionStackThreadLocal = new ThreadLocal<>();
|
||||
|
||||
private static ThreadLocal<Map<String, Serializable>> objectsThreadLocal = new ThreadLocal<>();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -132,6 +137,7 @@ public class QContext
|
||||
qSessionThreadLocal.remove();
|
||||
qBackendTransactionThreadLocal.remove();
|
||||
actionStackThreadLocal.remove();
|
||||
objectsThreadLocal.remove();
|
||||
}
|
||||
|
||||
|
||||
@ -259,4 +265,92 @@ public class QContext
|
||||
|
||||
return (Optional.of(actionStackThreadLocal.get().get(0)));
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get one named object from the Context for the current thread. may return null.
|
||||
*******************************************************************************/
|
||||
public static Serializable getObject(String key)
|
||||
{
|
||||
if(objectsThreadLocal.get() == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return objectsThreadLocal.get().get(key);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get one named object from the Context for the current thread, cast to the
|
||||
** specified type if possible. if not found, or wrong type, empty is returned.
|
||||
*******************************************************************************/
|
||||
public static <T extends Serializable> Optional<T> getObject(String key, Class<T> type)
|
||||
{
|
||||
Serializable object = getObject(key);
|
||||
|
||||
if(type.isInstance(object))
|
||||
{
|
||||
return Optional.of(type.cast(object));
|
||||
}
|
||||
else if(object == null)
|
||||
{
|
||||
return Optional.empty();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unexpected type of object found in session under key [" + key + "]",
|
||||
logPair("expectedType", type.getName()),
|
||||
logPair("actualType", object.getClass().getName())
|
||||
);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** put a named object into the Context for the current thread.
|
||||
*******************************************************************************/
|
||||
public static void setObject(String key, Serializable object)
|
||||
{
|
||||
if(objectsThreadLocal.get() == null)
|
||||
{
|
||||
objectsThreadLocal.set(new HashMap<>());
|
||||
}
|
||||
objectsThreadLocal.get().put(key, object);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** remove a named object from the Context of the current thread.
|
||||
*******************************************************************************/
|
||||
public static void removeObject(String key)
|
||||
{
|
||||
if(objectsThreadLocal.get() != null)
|
||||
{
|
||||
objectsThreadLocal.get().remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** get the full map of named objects for the current thread (possibly null).
|
||||
*******************************************************************************/
|
||||
public static Map<String, Serializable> getObjects()
|
||||
{
|
||||
return objectsThreadLocal.get();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** fully replace the map of named objets for the current thread.
|
||||
*******************************************************************************/
|
||||
public static void setObjects(Map<String, Serializable> objects)
|
||||
{
|
||||
objectsThreadLocal.set(objects);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,12 +30,14 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.helpcontent.HelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.HelpRole;
|
||||
@ -109,6 +111,8 @@ public class QInstanceHelpContentManager
|
||||
String processName = nameValuePairs.get("process");
|
||||
String fieldName = nameValuePairs.get("field");
|
||||
String sectionName = nameValuePairs.get("section");
|
||||
String widgetName = nameValuePairs.get("widget");
|
||||
String slotName = nameValuePairs.get("slot");
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// build a help content meta-data object from the record //
|
||||
@ -137,89 +141,16 @@ public class QInstanceHelpContentManager
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
if(StringUtils.hasContent(tableName))
|
||||
{
|
||||
QTableMetaData table = qInstance.getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
LOG.info("Unrecognized table in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
//////////////////////////
|
||||
// handle a table field //
|
||||
//////////////////////////
|
||||
QFieldMetaData field = table.getFields().get(fieldName);
|
||||
if(field == null)
|
||||
{
|
||||
LOG.info("Unrecognized table field in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
field.withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
field.removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else if(StringUtils.hasContent(sectionName))
|
||||
{
|
||||
////////////////////////////
|
||||
// handle a table section //
|
||||
////////////////////////////
|
||||
Optional<QFieldSection> optionalSection = table.getSections().stream().filter(s -> sectionName.equals(s.getName())).findFirst();
|
||||
if(optionalSection.isEmpty())
|
||||
{
|
||||
LOG.info("Unrecognized table section in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
optionalSection.get().withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
optionalSection.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
processHelpContentForTable(key, tableName, sectionName, fieldName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(processName))
|
||||
{
|
||||
QProcessMetaData process = qInstance.getProcess(processName);
|
||||
if(process == null)
|
||||
{
|
||||
LOG.info("Unrecognized process in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
processHelpContentForProcess(key, processName, fieldName, roles, helpContent);
|
||||
}
|
||||
else if(StringUtils.hasContent(widgetName))
|
||||
{
|
||||
processHelpContentForWidget(key, widgetName, slotName, helpContent);
|
||||
|
||||
if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
////////////////////////////
|
||||
// handle a process field //
|
||||
////////////////////////////
|
||||
Optional<QFieldMetaData> optionalField = CollectionUtils.mergeLists(process.getInputFields(), process.getOutputFields())
|
||||
.stream().filter(f -> fieldName.equals(f.getName()))
|
||||
.findFirst();
|
||||
|
||||
if(optionalField.isEmpty())
|
||||
{
|
||||
LOG.info("Unrecognized process field in help content", logPair("key", key));
|
||||
return;
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
optionalField.get().withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
optionalField.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -230,6 +161,131 @@ public class QInstanceHelpContentManager
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForTable(String key, String tableName, String sectionName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
LOG.info("Unrecognized table in help content", logPair("key", key));
|
||||
}
|
||||
else if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
//////////////////////////
|
||||
// handle a table field //
|
||||
//////////////////////////
|
||||
QFieldMetaData field = table.getFields().get(fieldName);
|
||||
if(field == null)
|
||||
{
|
||||
LOG.info("Unrecognized table field in help content", logPair("key", key));
|
||||
}
|
||||
else if(helpContent != null)
|
||||
{
|
||||
field.withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
field.removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
else if(StringUtils.hasContent(sectionName))
|
||||
{
|
||||
////////////////////////////
|
||||
// handle a table section //
|
||||
////////////////////////////
|
||||
Optional<QFieldSection> optionalSection = table.getSections().stream().filter(s -> sectionName.equals(s.getName())).findFirst();
|
||||
if(optionalSection.isEmpty())
|
||||
{
|
||||
LOG.info("Unrecognized table section in help content", logPair("key", key));
|
||||
}
|
||||
else if(helpContent != null)
|
||||
{
|
||||
optionalSection.get().withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
optionalSection.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForProcess(String key, String processName, String fieldName, Set<HelpRole> roles, QHelpContent helpContent)
|
||||
{
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(process == null)
|
||||
{
|
||||
LOG.info("Unrecognized process in help content", logPair("key", key));
|
||||
}
|
||||
else if(StringUtils.hasContent(fieldName))
|
||||
{
|
||||
////////////////////////////
|
||||
// handle a process field //
|
||||
////////////////////////////
|
||||
Optional<QFieldMetaData> optionalField = CollectionUtils.mergeLists(process.getInputFields(), process.getOutputFields())
|
||||
.stream().filter(f -> fieldName.equals(f.getName()))
|
||||
.findFirst();
|
||||
|
||||
if(optionalField.isEmpty())
|
||||
{
|
||||
LOG.info("Unrecognized process field in help content", logPair("key", key));
|
||||
}
|
||||
else if(helpContent != null)
|
||||
{
|
||||
optionalField.get().withHelpContent(helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
optionalField.get().removeHelpContent(roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processHelpContentForWidget(String key, String widgetName, String slotName, QHelpContent helpContent)
|
||||
{
|
||||
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
|
||||
if(!StringUtils.hasContent(slotName))
|
||||
{
|
||||
LOG.info("Missing slot name in help content", logPair("key", key));
|
||||
}
|
||||
else if(widget == null)
|
||||
{
|
||||
LOG.info("Unrecognized widget in help content", logPair("key", key));
|
||||
}
|
||||
else
|
||||
{
|
||||
Map<String, QHelpContent> widgetHelpContent = widget.getHelpContent();
|
||||
if(widgetHelpContent == null)
|
||||
{
|
||||
widgetHelpContent = new HashMap<>();
|
||||
}
|
||||
|
||||
if(helpContent != null)
|
||||
{
|
||||
widgetHelpContent.put(slotName, helpContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
widgetHelpContent.remove(slotName);
|
||||
}
|
||||
|
||||
widget.setHelpContent(widgetHelpContent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** add a help content object to a list - replacing an entry in the list with the
|
||||
** same roles if one is found.
|
||||
|
@ -37,6 +37,7 @@ import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.metadata.JoinGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
|
||||
@ -50,8 +51,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
|
||||
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.code.QCodeType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
|
||||
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;
|
||||
@ -76,6 +79,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
@ -84,9 +88,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeLambda;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -151,11 +157,13 @@ public class QInstanceValidator
|
||||
try
|
||||
{
|
||||
validateBackends(qInstance);
|
||||
validateAuthentication(qInstance);
|
||||
validateAutomationProviders(qInstance);
|
||||
validateTables(qInstance, joinGraph);
|
||||
validateProcesses(qInstance);
|
||||
validateReports(qInstance);
|
||||
validateApps(qInstance);
|
||||
validateWidgets(qInstance);
|
||||
validatePossibleValueSources(qInstance);
|
||||
validateQueuesAndProviders(qInstance);
|
||||
validateJoins(qInstance);
|
||||
@ -205,14 +213,23 @@ public class QInstanceValidator
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyType.getName()), "Missing name for a securityKeyType"))
|
||||
{
|
||||
assertCondition(Objects.equals(name, securityKeyType.getName()), "Inconsistent naming for securityKeyType: " + name + "/" + securityKeyType.getName() + ".");
|
||||
assertCondition(!usedNames.contains(name), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + name);
|
||||
|
||||
String duplicateNameMessagePrefix = "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: ";
|
||||
assertCondition(!usedNames.contains(name), duplicateNameMessagePrefix + name);
|
||||
usedNames.add(name);
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
|
||||
{
|
||||
assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + securityKeyType.getAllAccessKeyName());
|
||||
assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), duplicateNameMessagePrefix + securityKeyType.getAllAccessKeyName());
|
||||
usedNames.add(securityKeyType.getAllAccessKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
|
||||
{
|
||||
assertCondition(!usedNames.contains(securityKeyType.getNullValueBehaviorKeyName()), duplicateNameMessagePrefix + securityKeyType.getNullValueBehaviorKeyName());
|
||||
usedNames.add(securityKeyType.getNullValueBehaviorKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getPossibleValueSourceName()))
|
||||
{
|
||||
assertCondition(qInstance.getPossibleValueSource(securityKeyType.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName in securityKeyType: " + name);
|
||||
@ -381,6 +398,23 @@ public class QInstanceValidator
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateAuthentication(QInstance qInstance)
|
||||
{
|
||||
QAuthenticationMetaData authentication = qInstance.getAuthentication();
|
||||
if(authentication != null)
|
||||
{
|
||||
if(authentication.getCustomizer() != null)
|
||||
{
|
||||
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -493,6 +527,11 @@ public class QInstanceValidator
|
||||
validateTableRecordSecurityLocks(qInstance, table);
|
||||
validateTableAssociations(qInstance, table);
|
||||
validateExposedJoins(qInstance, joinGraph, table);
|
||||
|
||||
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
|
||||
{
|
||||
supplementalTableMetaData.validate(qInstance, table, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1539,12 +1578,53 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// validate widgets //
|
||||
//////////////////////
|
||||
for(String widgetName : CollectionUtils.nonNullList(app.getWidgets()))
|
||||
{
|
||||
assertCondition(qInstance.getWidget(widgetName) != null, "App " + appName + " widget " + widgetName + " is not a recognized widget.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateWidgets(QInstance qInstance)
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(qInstance.getWidgets()))
|
||||
{
|
||||
qInstance.getWidgets().forEach((widgetName, widget) ->
|
||||
{
|
||||
assertCondition(Objects.equals(widgetName, widget.getName()), "Inconsistent naming for widget: " + widgetName + "/" + widget.getName() + ".");
|
||||
|
||||
if(assertCondition(widget.getCodeReference() != null, "Missing codeReference for widget: " + widgetName))
|
||||
{
|
||||
validateSimpleCodeReference("Widget " + widgetName + " code reference: ", widget.getCodeReference(), AbstractWidgetRenderer.class);
|
||||
}
|
||||
|
||||
if(widget instanceof ParentWidgetMetaData parentWidgetMetaData)
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(parentWidgetMetaData.getChildWidgetNameList()), "Missing child widgets for parent widget: " + widget.getName()))
|
||||
{
|
||||
for(String childWidgetName : parentWidgetMetaData.getChildWidgetNameList())
|
||||
{
|
||||
assertCondition(qInstance.getWidget(childWidgetName) != null, "Unrecognized child widget name [" + childWidgetName + "] in parent widget: " + widget.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -1784,20 +1864,6 @@ public class QInstanceValidator
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@FunctionalInterface
|
||||
interface UnsafeLambda
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.actions.audits;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.tables.QTableMetaData;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Object to accumulate multiple audit-details to be recorded under a single
|
||||
** audit per-record, within a process step. Especially useful if/when the
|
||||
** process step spreads its work out through multiple classes.
|
||||
**
|
||||
** Pattern of usage looks like:
|
||||
**
|
||||
** <pre>
|
||||
** // declare as a field (or local) w/ message for the audit headers
|
||||
** private AuditDetailAccumulator auditDetailAccumulator = new AuditDetailAccumulator("Audit header message");
|
||||
**
|
||||
** // put into thread context
|
||||
** AuditDetailAccumulator.setInContext(auditDetailAccumulator);
|
||||
**
|
||||
** // add a detail message for a record
|
||||
** auditDetailAccumulator.addAuditDetail(tableName, record, "Detail message");
|
||||
**
|
||||
** // in another class, get the accumulator from context and safely add a detail message
|
||||
** AuditDetailAccumulator.getFromContext().ifPresent(ada -> ada.addAuditDetail(tableName, record, "More Details"));
|
||||
**
|
||||
** // at the end of a step run/runOnePage method, add the accumulated audit details to step output
|
||||
** auditDetailAccumulator.getAccumulatedAuditSingleInputs().forEach(runBackendStepOutput::addAuditSingleInput);
|
||||
** auditDetailAccumulator.clear();
|
||||
** </pre>
|
||||
*******************************************************************************/
|
||||
public class AuditDetailAccumulator implements Serializable
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AuditDetailAccumulator.class);
|
||||
|
||||
private static final String objectKey = AuditDetailAccumulator.class.getSimpleName();
|
||||
|
||||
private String header;
|
||||
|
||||
private Map<TableNameAndPrimaryKey, AuditSingleInput> recordAuditInputMap = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AuditDetailAccumulator(String header)
|
||||
{
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setInContext()
|
||||
{
|
||||
QContext.setObject(objectKey, this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Optional<AuditDetailAccumulator> getFromContext()
|
||||
{
|
||||
return QContext.getObject(objectKey, AuditDetailAccumulator.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addAuditDetail(String tableName, QRecordEntity entity, String message)
|
||||
{
|
||||
if(entity != null)
|
||||
{
|
||||
addAuditDetail(tableName, entity.toQRecord(), message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addAuditDetail(String tableName, QRecord record, String message)
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
|
||||
if(primaryKey == null)
|
||||
{
|
||||
LOG.info("Missing primary key in input record - audit detail message will not be recorded.", logPair("message", message));
|
||||
return;
|
||||
}
|
||||
|
||||
AuditSingleInput auditSingleInput = recordAuditInputMap.computeIfAbsent(new TableNameAndPrimaryKey(tableName, primaryKey), (key) -> new AuditSingleInput(table, record, header));
|
||||
auditSingleInput.addDetail(message);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Collection<AuditSingleInput> getAccumulatedAuditSingleInputs()
|
||||
{
|
||||
return (recordAuditInputMap.values());
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void clear()
|
||||
{
|
||||
recordAuditInputMap.clear();
|
||||
}
|
||||
|
||||
|
||||
private record TableNameAndPrimaryKey(String tableName, Serializable primaryKey) {}
|
||||
}
|
@ -41,7 +41,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
/*******************************************************************************
|
||||
** Input data to insert a single audit record (with optional child record)..
|
||||
*******************************************************************************/
|
||||
public class AuditSingleInput
|
||||
public class AuditSingleInput implements Serializable
|
||||
{
|
||||
private String auditTableName;
|
||||
private String auditUserName;
|
||||
|
@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
|
||||
@ -346,4 +347,37 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(o == null || getClass() != o.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QFilterCriteria that = (QFilterCriteria) o;
|
||||
return Objects.equals(fieldName, that.fieldName) && operator == that.operator && Objects.equals(values, that.values) && Objects.equals(otherFieldName, that.otherFieldName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(fieldName, operator, values, otherFieldName);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -323,6 +324,18 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for adding a single subFilter
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QQueryFilter withSubFilter(QQueryFilter subFilter)
|
||||
{
|
||||
addSubFilter(subFilter);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -359,7 +372,7 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
rs.append(")");
|
||||
|
||||
rs.append("OrderBy[");
|
||||
for(QFilterOrderBy orderBy : orderBys)
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(orderBys))
|
||||
{
|
||||
rs.append(orderBy).append(",");
|
||||
}
|
||||
@ -467,4 +480,36 @@ public class QQueryFilter implements Serializable, Cloneable
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if(o == null || getClass() != o.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QQueryFilter that = (QQueryFilter) o;
|
||||
return Objects.equals(criteria, that.criteria) && Objects.equals(orderBys, that.orderBys) && booleanOperator == that.booleanOperator && Objects.equals(subFilters, that.subFilters) && Objects.equals(skip, that.skip) && Objects.equals(limit, that.limit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(criteria, orderBys, booleanOperator, subFilters, skip, limit);
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private Boolean areAllValuesBeingUpdatedTheSame = null;
|
||||
|
||||
private boolean omitTriggeringAutomations = false;
|
||||
private boolean omitDmlAudit = false;
|
||||
private String auditContext = null;
|
||||
|
||||
@ -321,4 +322,35 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for omitTriggeringAutomations
|
||||
*******************************************************************************/
|
||||
public boolean getOmitTriggeringAutomations()
|
||||
{
|
||||
return (this.omitTriggeringAutomations);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for omitTriggeringAutomations
|
||||
*******************************************************************************/
|
||||
public void setOmitTriggeringAutomations(boolean omitTriggeringAutomations)
|
||||
{
|
||||
this.omitTriggeringAutomations = omitTriggeringAutomations;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for omitTriggeringAutomations
|
||||
*******************************************************************************/
|
||||
public UpdateInput withOmitTriggeringAutomations(boolean omitTriggeringAutomations)
|
||||
{
|
||||
this.omitTriggeringAutomations = omitTriggeringAutomations;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ 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.tables.TablesPossibleValueSourceMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.Script;
|
||||
|
||||
|
||||
@ -51,7 +51,7 @@ public class TableTrigger extends QRecordEntity
|
||||
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
|
||||
private String tableName;
|
||||
|
||||
@QField(possibleValueSourceName = SavedFilter.TABLE_NAME)
|
||||
@QField(possibleValueSourceName = SavedView.TABLE_NAME)
|
||||
private Integer filterId;
|
||||
|
||||
@QField(possibleValueSourceName = Script.TABLE_NAME)
|
||||
|
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseValues;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Data used to render a Composite Widget - e.g., a collection of blocks
|
||||
*******************************************************************************/
|
||||
public class CompositeWidgetData extends AbstractBlockWidgetData<CompositeWidgetData, BaseValues, BaseSlots, BaseStyles>
|
||||
{
|
||||
private List<AbstractBlockWidgetData<?, ?, ?, ?>> blocks = new ArrayList<>();
|
||||
|
||||
private Map<String, Serializable> styleOverrides = new HashMap<>();
|
||||
|
||||
private Layout layout;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum Layout
|
||||
{
|
||||
/////////////////////////////////////////////////////////////
|
||||
// note, these are used in QQQ FMD CompositeWidgetData.tsx //
|
||||
/////////////////////////////////////////////////////////////
|
||||
FLEX_ROW_WRAPPED,
|
||||
FLEX_ROW_SPACE_BETWEEN,
|
||||
TABLE_SUB_ROW_DETAILS,
|
||||
BADGES_WRAPPER
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getBlockTypeName()
|
||||
{
|
||||
return "COMPOSITE";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for blocks
|
||||
**
|
||||
*******************************************************************************/
|
||||
public List<AbstractBlockWidgetData<?, ?, ?, ?>> getBlocks()
|
||||
{
|
||||
return blocks;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for blocks
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setBlocks(List<AbstractBlockWidgetData<?, ?, ?, ?>> blocks)
|
||||
{
|
||||
this.blocks = blocks;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for blocks
|
||||
**
|
||||
*******************************************************************************/
|
||||
public CompositeWidgetData withBlocks(List<AbstractBlockWidgetData<?, ?, ?, ?>> blocks)
|
||||
{
|
||||
this.blocks = blocks;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public CompositeWidgetData withBlock(AbstractBlockWidgetData<?, ?, ?, ?> block)
|
||||
{
|
||||
addBlock(block);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addBlock(AbstractBlockWidgetData<?, ?, ?, ?> block)
|
||||
{
|
||||
if(this.blocks == null)
|
||||
{
|
||||
this.blocks = new ArrayList<>();
|
||||
}
|
||||
this.blocks.add(block);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for styleOverrides
|
||||
*******************************************************************************/
|
||||
public Map<String, Serializable> getStyleOverrides()
|
||||
{
|
||||
return (this.styleOverrides);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for styleOverrides
|
||||
*******************************************************************************/
|
||||
public void setStyleOverrides(Map<String, Serializable> styleOverrides)
|
||||
{
|
||||
this.styleOverrides = styleOverrides;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for styleOverrides
|
||||
*******************************************************************************/
|
||||
public CompositeWidgetData withStyleOverrides(Map<String, Serializable> styleOverrides)
|
||||
{
|
||||
this.styleOverrides = styleOverrides;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public CompositeWidgetData withStyleOverride(String key, Serializable value)
|
||||
{
|
||||
addStyleOverride(key, value);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addStyleOverride(String key, Serializable value)
|
||||
{
|
||||
if(this.styleOverrides == null)
|
||||
{
|
||||
this.styleOverrides = new HashMap<>();
|
||||
}
|
||||
this.styleOverrides.put(key, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for layout
|
||||
*******************************************************************************/
|
||||
public Layout getLayout()
|
||||
{
|
||||
return (this.layout);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for layout
|
||||
*******************************************************************************/
|
||||
public void setLayout(Layout layout)
|
||||
{
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for layout
|
||||
*******************************************************************************/
|
||||
public CompositeWidgetData withLayout(Layout layout)
|
||||
{
|
||||
this.layout = layout;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -35,6 +35,8 @@ public class ParentWidgetData extends QWidgetData
|
||||
private List<String> childWidgetNameList;
|
||||
private ParentWidgetMetaData.LayoutType layoutType = ParentWidgetMetaData.LayoutType.GRID;
|
||||
|
||||
private boolean isLabelPageTitle = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -121,4 +123,34 @@ public class ParentWidgetData extends QWidgetData
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isLabelPageTitle
|
||||
*******************************************************************************/
|
||||
public boolean getIsLabelPageTitle()
|
||||
{
|
||||
return (this.isLabelPageTitle);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isLabelPageTitle
|
||||
*******************************************************************************/
|
||||
public void setIsLabelPageTitle(boolean isLabelPageTitle)
|
||||
{
|
||||
this.isLabelPageTitle = isLabelPageTitle;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isLabelPageTitle
|
||||
*******************************************************************************/
|
||||
public ParentWidgetData withIsLabelPageTitle(boolean isLabelPageTitle)
|
||||
{
|
||||
this.isLabelPageTitle = isLabelPageTitle;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -47,6 +48,7 @@ public abstract class QWidgetData
|
||||
private List<List<Map<String, String>>> dropdownDataList;
|
||||
private String dropdownNeedsSelectedText;
|
||||
|
||||
private List<List<Serializable>> csvData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -324,4 +326,34 @@ public abstract class QWidgetData
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for csvData
|
||||
*******************************************************************************/
|
||||
public List<List<Serializable>> getCsvData()
|
||||
{
|
||||
return (this.csvData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for csvData
|
||||
*******************************************************************************/
|
||||
public void setCsvData(List<List<Serializable>> csvData)
|
||||
{
|
||||
this.csvData = csvData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for csvData
|
||||
*******************************************************************************/
|
||||
public QWidgetData withCsvData(List<List<Serializable>> csvData)
|
||||
{
|
||||
this.csvData = csvData;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,19 +22,24 @@
|
||||
package com.kingsrook.qqq.backend.core.model.dashboard.widgets;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Model containing datastructure expected by frontend statistics widget
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class StatisticsData extends QWidgetData
|
||||
{
|
||||
private Number count;
|
||||
private String countFontSize;
|
||||
private String countURL;
|
||||
private Number percentageAmount;
|
||||
private String percentageLabel;
|
||||
private boolean isCurrency = false;
|
||||
private boolean increaseIsGood = true;
|
||||
private Serializable count;
|
||||
private String countFontSize;
|
||||
private String countURL;
|
||||
private String countContext;
|
||||
private Number percentageAmount;
|
||||
private String percentageLabel;
|
||||
private String percentageURL;
|
||||
private boolean isCurrency = false;
|
||||
private boolean increaseIsGood = true;
|
||||
|
||||
|
||||
|
||||
@ -50,7 +55,7 @@ public class StatisticsData extends QWidgetData
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public StatisticsData(Number count, Number percentageAmount, String percentageLabel)
|
||||
public StatisticsData(Serializable count, Number percentageAmount, String percentageLabel)
|
||||
{
|
||||
this.count = count;
|
||||
this.percentageLabel = percentageLabel;
|
||||
@ -142,7 +147,7 @@ public class StatisticsData extends QWidgetData
|
||||
** Getter for count
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Number getCount()
|
||||
public Serializable getCount()
|
||||
{
|
||||
return count;
|
||||
}
|
||||
@ -153,7 +158,7 @@ public class StatisticsData extends QWidgetData
|
||||
** Setter for count
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setCount(Number count)
|
||||
public void setCount(Serializable count)
|
||||
{
|
||||
this.count = count;
|
||||
}
|
||||
@ -164,7 +169,7 @@ public class StatisticsData extends QWidgetData
|
||||
** Fluent setter for count
|
||||
**
|
||||
*******************************************************************************/
|
||||
public StatisticsData withCount(Number count)
|
||||
public StatisticsData withCount(Serializable count)
|
||||
{
|
||||
this.count = count;
|
||||
return (this);
|
||||
@ -306,4 +311,66 @@ public class StatisticsData extends QWidgetData
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for countContext
|
||||
*******************************************************************************/
|
||||
public String getCountContext()
|
||||
{
|
||||
return (this.countContext);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for countContext
|
||||
*******************************************************************************/
|
||||
public void setCountContext(String countContext)
|
||||
{
|
||||
this.countContext = countContext;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for countContext
|
||||
*******************************************************************************/
|
||||
public StatisticsData withCountContext(String countContext)
|
||||
{
|
||||
this.countContext = countContext;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for percentageURL
|
||||
*******************************************************************************/
|
||||
public String getPercentageURL()
|
||||
{
|
||||
return (this.percentageURL);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for percentageURL
|
||||
*******************************************************************************/
|
||||
public void setPercentageURL(String percentageURL)
|
||||
{
|
||||
this.percentageURL = percentageURL;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for percentageURL
|
||||
*******************************************************************************/
|
||||
public StatisticsData withPercentageURL(String percentageURL)
|
||||
{
|
||||
this.percentageURL = percentageURL;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ public enum WidgetType
|
||||
STEPPER("stepper"),
|
||||
TABLE("table"),
|
||||
USA_MAP("usaMap"),
|
||||
COMPOSITE("composite"),
|
||||
DATA_BAG_VIEWER("dataBagViewer"),
|
||||
SCRIPT_VIEWER("scriptViewer");
|
||||
|
||||
|
@ -0,0 +1,386 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Base class for the data returned in rendering a block of a specific type.
|
||||
**
|
||||
** The type parameters define the structure of the block's data, and should
|
||||
** generally be defined along with a sub-class of this class, in a block-specific
|
||||
** sub-package.
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractBlockWidgetData<
|
||||
T extends AbstractBlockWidgetData<T, V, S, SX>,
|
||||
V extends BlockValuesInterface,
|
||||
S extends BlockSlotsInterface,
|
||||
SX extends BlockStylesInterface> extends QWidgetData
|
||||
{
|
||||
private BlockTooltip tooltip;
|
||||
private BlockLink link;
|
||||
|
||||
private Map<S, BlockTooltip> tooltipMap;
|
||||
private Map<S, BlockLink> linkMap;
|
||||
|
||||
private V values;
|
||||
private SX styles;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public final String getType()
|
||||
{
|
||||
return "block";
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract String getBlockTypeName();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T withTooltip(S key, String value)
|
||||
{
|
||||
addTooltip(key, value);
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addTooltip(S key, String value)
|
||||
{
|
||||
if(this.tooltipMap == null)
|
||||
{
|
||||
this.tooltipMap = new HashMap<>();
|
||||
}
|
||||
this.tooltipMap.put(key, new BlockTooltip().withTitle(value));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T withTooltip(S key, BlockTooltip value)
|
||||
{
|
||||
addTooltip(key, value);
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addTooltip(S key, BlockTooltip value)
|
||||
{
|
||||
if(this.tooltipMap == null)
|
||||
{
|
||||
this.tooltipMap = new HashMap<>();
|
||||
}
|
||||
this.tooltipMap.put(key, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tooltipMap
|
||||
*******************************************************************************/
|
||||
public Map<S, BlockTooltip> getTooltipMap()
|
||||
{
|
||||
return (this.tooltipMap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tooltipMap
|
||||
*******************************************************************************/
|
||||
public void setTooltipMap(Map<S, BlockTooltip> tooltipMap)
|
||||
{
|
||||
this.tooltipMap = tooltipMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tooltipMap
|
||||
*******************************************************************************/
|
||||
public T withTooltipMap(Map<S, BlockTooltip> tooltipMap)
|
||||
{
|
||||
this.tooltipMap = tooltipMap;
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tooltip
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BlockTooltip getTooltip()
|
||||
{
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tooltip
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setTooltip(BlockTooltip tooltip)
|
||||
{
|
||||
this.tooltip = tooltip;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tooltip
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T withTooltip(String tooltip)
|
||||
{
|
||||
this.tooltip = new BlockTooltip(tooltip);
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tooltip
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T withTooltip(BlockTooltip tooltip)
|
||||
{
|
||||
this.tooltip = tooltip;
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T withLink(S key, String value)
|
||||
{
|
||||
addLink(key, value);
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addLink(S key, String value)
|
||||
{
|
||||
if(this.linkMap == null)
|
||||
{
|
||||
this.linkMap = new HashMap<>();
|
||||
}
|
||||
this.linkMap.put(key, new BlockLink(value));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T withLink(S key, BlockLink value)
|
||||
{
|
||||
addLink(key, value);
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addLink(S key, BlockLink value)
|
||||
{
|
||||
if(this.linkMap == null)
|
||||
{
|
||||
this.linkMap = new HashMap<>();
|
||||
}
|
||||
this.linkMap.put(key, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for linkMap
|
||||
*******************************************************************************/
|
||||
public Map<S, BlockLink> getLinkMap()
|
||||
{
|
||||
return (this.linkMap);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for linkMap
|
||||
*******************************************************************************/
|
||||
public void setLinkMap(Map<S, BlockLink> linkMap)
|
||||
{
|
||||
this.linkMap = linkMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for linkMap
|
||||
*******************************************************************************/
|
||||
public T withLinkMap(Map<S, BlockLink> linkMap)
|
||||
{
|
||||
this.linkMap = linkMap;
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for link
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BlockLink getLink()
|
||||
{
|
||||
return link;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for link
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setLink(BlockLink link)
|
||||
{
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for link
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T withLink(String link)
|
||||
{
|
||||
this.link = new BlockLink(link);
|
||||
return (T) (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for link
|
||||
**
|
||||
*******************************************************************************/
|
||||
public T withLink(BlockLink link)
|
||||
{
|
||||
this.link = link;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for values
|
||||
*******************************************************************************/
|
||||
public V getValues()
|
||||
{
|
||||
return (this.values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for values
|
||||
*******************************************************************************/
|
||||
public void setValues(V values)
|
||||
{
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for values
|
||||
*******************************************************************************/
|
||||
public T withValues(V values)
|
||||
{
|
||||
this.values = values;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for styles
|
||||
*******************************************************************************/
|
||||
public SX getStyles()
|
||||
{
|
||||
return (this.styles);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for styles
|
||||
*******************************************************************************/
|
||||
public void setStyles(SX styles)
|
||||
{
|
||||
this.styles = styles;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for styles
|
||||
*******************************************************************************/
|
||||
public T withStyles(SX styles)
|
||||
{
|
||||
this.styles = styles;
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A link used within a (widget) block.
|
||||
**
|
||||
** Right now, just a href - but target is an obvious next-thing to add here.
|
||||
*******************************************************************************/
|
||||
public class BlockLink
|
||||
{
|
||||
private String href;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BlockLink()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BlockLink(String href)
|
||||
{
|
||||
this.href = href;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for href
|
||||
*******************************************************************************/
|
||||
public String getHref()
|
||||
{
|
||||
return (this.href);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for href
|
||||
*******************************************************************************/
|
||||
public void setHref(String href)
|
||||
{
|
||||
this.href = href;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for href
|
||||
*******************************************************************************/
|
||||
public BlockLink withHref(String href)
|
||||
{
|
||||
this.href = href;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** marker-interface for classes (enums, actually) used to define the "slots"
|
||||
** within a widget-block that can have links or tooltips applied to them.
|
||||
*******************************************************************************/
|
||||
public interface BlockSlotsInterface
|
||||
{
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Marker interface for classes that define the "styles" that can be customized
|
||||
** within a particular widget-block type.
|
||||
*******************************************************************************/
|
||||
public interface BlockStylesInterface
|
||||
{
|
||||
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A tooltip used within a (widget) block.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class BlockTooltip
|
||||
{
|
||||
private String title;
|
||||
private Placement placement = Placement.BOTTOM;
|
||||
|
||||
|
||||
|
||||
public enum Placement
|
||||
{BOTTOM, LEFT, RIGHT, TOP}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BlockTooltip()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BlockTooltip(String title)
|
||||
{
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for title
|
||||
*******************************************************************************/
|
||||
public String getTitle()
|
||||
{
|
||||
return (this.title);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for title
|
||||
*******************************************************************************/
|
||||
public void setTitle(String title)
|
||||
{
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for title
|
||||
*******************************************************************************/
|
||||
public BlockTooltip withTitle(String title)
|
||||
{
|
||||
this.title = title;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for placement
|
||||
*******************************************************************************/
|
||||
public Placement getPlacement()
|
||||
{
|
||||
return (this.placement);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for placement
|
||||
*******************************************************************************/
|
||||
public void setPlacement(Placement placement)
|
||||
{
|
||||
this.placement = placement;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for placement
|
||||
*******************************************************************************/
|
||||
public BlockTooltip withPlacement(Placement placement)
|
||||
{
|
||||
this.placement = placement;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Marker interface for classes that define the values that can be returned for
|
||||
** a particular widget-block type.
|
||||
*******************************************************************************/
|
||||
public interface BlockValuesInterface
|
||||
{
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.base;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockSlotsInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum BaseSlots implements BlockSlotsInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.base;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class BaseStyles implements BlockStylesInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.base;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class BaseValues implements BlockValuesInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.bignumberblock;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class BigNumberBlockData extends AbstractBlockWidgetData<BigNumberBlockData, BigNumberValues, BigNumberSlots, BigNumberStyles>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getBlockTypeName()
|
||||
{
|
||||
return "BIG_NUMBER";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.bignumberblock;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockSlotsInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum BigNumberSlots implements BlockSlotsInterface
|
||||
{
|
||||
HEADING,
|
||||
NUMBER,
|
||||
CONTEXT
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.bignumberblock;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class BigNumberStyles implements BlockStylesInterface
|
||||
{
|
||||
private String numberColor;
|
||||
private String width;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for numberColor
|
||||
*******************************************************************************/
|
||||
public String getNumberColor()
|
||||
{
|
||||
return (this.numberColor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for numberColor
|
||||
*******************************************************************************/
|
||||
public void setNumberColor(String numberColor)
|
||||
{
|
||||
this.numberColor = numberColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for numberColor
|
||||
*******************************************************************************/
|
||||
public BigNumberStyles withNumberColor(String numberColor)
|
||||
{
|
||||
this.numberColor = numberColor;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for width
|
||||
*******************************************************************************/
|
||||
public String getWidth()
|
||||
{
|
||||
return (this.width);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for width
|
||||
*******************************************************************************/
|
||||
public void setWidth(String width)
|
||||
{
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for width
|
||||
*******************************************************************************/
|
||||
public BigNumberStyles withWidth(String width)
|
||||
{
|
||||
this.width = width;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.bignumberblock;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class BigNumberValues implements BlockValuesInterface
|
||||
{
|
||||
private String heading;
|
||||
private Serializable number;
|
||||
private String context;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for heading
|
||||
*******************************************************************************/
|
||||
public String getHeading()
|
||||
{
|
||||
return (this.heading);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for heading
|
||||
*******************************************************************************/
|
||||
public void setHeading(String heading)
|
||||
{
|
||||
this.heading = heading;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for heading
|
||||
*******************************************************************************/
|
||||
public BigNumberValues withHeading(String heading)
|
||||
{
|
||||
this.heading = heading;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for number
|
||||
*******************************************************************************/
|
||||
public Serializable getNumber()
|
||||
{
|
||||
return (this.number);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for number
|
||||
*******************************************************************************/
|
||||
public void setNumber(Serializable number)
|
||||
{
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for number
|
||||
*******************************************************************************/
|
||||
public BigNumberValues withNumber(Serializable number)
|
||||
{
|
||||
this.number = number;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for context
|
||||
*******************************************************************************/
|
||||
public String getContext()
|
||||
{
|
||||
return (this.context);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for context
|
||||
*******************************************************************************/
|
||||
public void setContext(String context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for context
|
||||
*******************************************************************************/
|
||||
public BigNumberValues withContext(String context)
|
||||
{
|
||||
this.context = context;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.divider;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseSlots;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseStyles;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.base.BaseValues;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class DividerBlockData extends AbstractBlockWidgetData<DividerBlockData, BaseValues, BaseSlots, BaseStyles>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getBlockTypeName()
|
||||
{
|
||||
return "DIVIDER";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.numbericonbadge;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class NumberIconBadgeBlockData extends AbstractBlockWidgetData<NumberIconBadgeBlockData, NumberIconBadgeValues, NumberIconBadgeSlots, NumberIconBadgeStyles>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getBlockTypeName()
|
||||
{
|
||||
return "NUMBER_ICON_BADGE";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.numbericonbadge;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockSlotsInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum NumberIconBadgeSlots implements BlockSlotsInterface
|
||||
{
|
||||
NUMBER,
|
||||
ICON
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.numbericonbadge;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class NumberIconBadgeStyles implements BlockStylesInterface
|
||||
{
|
||||
private String color;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for color
|
||||
*******************************************************************************/
|
||||
public String getColor()
|
||||
{
|
||||
return (this.color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for color
|
||||
*******************************************************************************/
|
||||
public void setColor(String color)
|
||||
{
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for color
|
||||
*******************************************************************************/
|
||||
public NumberIconBadgeStyles withColor(String color)
|
||||
{
|
||||
this.color = color;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.numbericonbadge;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class NumberIconBadgeValues implements BlockValuesInterface
|
||||
{
|
||||
private Serializable number;
|
||||
private String iconName;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for number
|
||||
*******************************************************************************/
|
||||
public Serializable getNumber()
|
||||
{
|
||||
return (this.number);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for number
|
||||
*******************************************************************************/
|
||||
public void setNumber(Serializable number)
|
||||
{
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for number
|
||||
*******************************************************************************/
|
||||
public NumberIconBadgeValues withNumber(Serializable number)
|
||||
{
|
||||
this.number = number;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for iconName
|
||||
*******************************************************************************/
|
||||
public String getIconName()
|
||||
{
|
||||
return (this.iconName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for iconName
|
||||
*******************************************************************************/
|
||||
public void setIconName(String iconName)
|
||||
{
|
||||
this.iconName = iconName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for iconName
|
||||
*******************************************************************************/
|
||||
public NumberIconBadgeValues withIconName(String iconName)
|
||||
{
|
||||
this.iconName = iconName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.progressbar;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ProgressBarBlockData extends AbstractBlockWidgetData<ProgressBarBlockData, ProgressBarValues, ProgressBarSlots, ProgressBarStyles>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getBlockTypeName()
|
||||
{
|
||||
return "PROGRESS_BAR";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.progressbar;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockSlotsInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum ProgressBarSlots implements BlockSlotsInterface
|
||||
{
|
||||
HEADING,
|
||||
BAR,
|
||||
VALUE
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.progressbar;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ProgressBarStyles implements BlockStylesInterface
|
||||
{
|
||||
private String barColor;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for barColor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getBarColor()
|
||||
{
|
||||
return barColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for barColor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setBarColor(String barColor)
|
||||
{
|
||||
this.barColor = barColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for barColor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ProgressBarStyles withBarColor(String barColor)
|
||||
{
|
||||
this.barColor = barColor;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.progressbar;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ProgressBarValues implements BlockValuesInterface
|
||||
{
|
||||
private String heading;
|
||||
private BigDecimal percent;
|
||||
private Serializable value;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for heading
|
||||
*******************************************************************************/
|
||||
public String getHeading()
|
||||
{
|
||||
return (this.heading);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for heading
|
||||
*******************************************************************************/
|
||||
public void setHeading(String heading)
|
||||
{
|
||||
this.heading = heading;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for heading
|
||||
*******************************************************************************/
|
||||
public ProgressBarValues withHeading(String heading)
|
||||
{
|
||||
this.heading = heading;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for percent
|
||||
*******************************************************************************/
|
||||
public BigDecimal getPercent()
|
||||
{
|
||||
return (this.percent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for percent
|
||||
*******************************************************************************/
|
||||
public void setPercent(BigDecimal percent)
|
||||
{
|
||||
this.percent = percent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for percent
|
||||
*******************************************************************************/
|
||||
public ProgressBarValues withPercent(BigDecimal percent)
|
||||
{
|
||||
this.percent = percent;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for value
|
||||
*******************************************************************************/
|
||||
public Serializable getValue()
|
||||
{
|
||||
return (this.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for value
|
||||
*******************************************************************************/
|
||||
public void setValue(Serializable value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for value
|
||||
*******************************************************************************/
|
||||
public ProgressBarValues withValue(Serializable value)
|
||||
{
|
||||
this.value = value;
|
||||
return (this);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.tablesubrowdetailrow;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TableSubRowDetailRowBlockData extends AbstractBlockWidgetData<TableSubRowDetailRowBlockData, TableSubRowDetailRowValues, TableSubRowDetailRowSlots, TableSubRowDetailRowStyles>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getBlockTypeName()
|
||||
{
|
||||
return "TABLE_SUB_ROW_DETAIL_ROW";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.tablesubrowdetailrow;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockSlotsInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum TableSubRowDetailRowSlots implements BlockSlotsInterface
|
||||
{
|
||||
LABEL,
|
||||
VALUE
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.tablesubrowdetailrow;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TableSubRowDetailRowStyles implements BlockStylesInterface
|
||||
{
|
||||
private String labelColor;
|
||||
private String valueColor;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for labelColor
|
||||
*******************************************************************************/
|
||||
public String getLabelColor()
|
||||
{
|
||||
return (this.labelColor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for labelColor
|
||||
*******************************************************************************/
|
||||
public void setLabelColor(String labelColor)
|
||||
{
|
||||
this.labelColor = labelColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for labelColor
|
||||
*******************************************************************************/
|
||||
public TableSubRowDetailRowStyles withLabelColor(String labelColor)
|
||||
{
|
||||
this.labelColor = labelColor;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for valueColor
|
||||
*******************************************************************************/
|
||||
public String getValueColor()
|
||||
{
|
||||
return (this.valueColor);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for valueColor
|
||||
*******************************************************************************/
|
||||
public void setValueColor(String valueColor)
|
||||
{
|
||||
this.valueColor = valueColor;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for valueColor
|
||||
*******************************************************************************/
|
||||
public TableSubRowDetailRowStyles withValueColor(String valueColor)
|
||||
{
|
||||
this.valueColor = valueColor;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.tablesubrowdetailrow;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TableSubRowDetailRowValues implements BlockValuesInterface
|
||||
{
|
||||
private String label;
|
||||
private Serializable value;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TableSubRowDetailRowValues()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TableSubRowDetailRowValues(String label, Serializable value)
|
||||
{
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for label
|
||||
*******************************************************************************/
|
||||
public void setLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label
|
||||
*******************************************************************************/
|
||||
public TableSubRowDetailRowValues withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for value
|
||||
*******************************************************************************/
|
||||
public Serializable getValue()
|
||||
{
|
||||
return (this.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for value
|
||||
*******************************************************************************/
|
||||
public void setValue(Serializable value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for value
|
||||
*******************************************************************************/
|
||||
public TableSubRowDetailRowValues withValue(Serializable value)
|
||||
{
|
||||
this.value = value;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.text;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TextBlockData extends AbstractBlockWidgetData<TextBlockData, TextValues, TextSlots, TextStyles>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getBlockTypeName()
|
||||
{
|
||||
return "TEXT";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.text;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockSlotsInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum TextSlots implements BlockSlotsInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.text;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TextStyles implements BlockStylesInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.text;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TextValues implements BlockValuesInterface
|
||||
{
|
||||
private String text;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TextValues()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TextValues(String text)
|
||||
{
|
||||
setText(text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for text
|
||||
*******************************************************************************/
|
||||
public String getText()
|
||||
{
|
||||
return (this.text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for text
|
||||
*******************************************************************************/
|
||||
public void setText(String text)
|
||||
{
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for text
|
||||
*******************************************************************************/
|
||||
public TextValues withText(String text)
|
||||
{
|
||||
this.text = text;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.upordownnumber;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.AbstractBlockWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class UpOrDownNumberBlockData extends AbstractBlockWidgetData<UpOrDownNumberBlockData, UpOrDownNumberValues, UpOrDownNumberSlots, UpOrDownNumberStyles>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getBlockTypeName()
|
||||
{
|
||||
return "UP_OR_DOWN_NUMBER";
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.upordownnumber;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockSlotsInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum UpOrDownNumberSlots implements BlockSlotsInterface
|
||||
{
|
||||
NUMBER,
|
||||
CONTEXT
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.upordownnumber;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockStylesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class UpOrDownNumberStyles implements BlockStylesInterface
|
||||
{
|
||||
private String colorOverride;
|
||||
private boolean isStacked = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for colorOverride
|
||||
*******************************************************************************/
|
||||
public String getColorOverride()
|
||||
{
|
||||
return (this.colorOverride);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for colorOverride
|
||||
*******************************************************************************/
|
||||
public void setColorOverride(String colorOverride)
|
||||
{
|
||||
this.colorOverride = colorOverride;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for colorOverride
|
||||
*******************************************************************************/
|
||||
public UpOrDownNumberStyles withColorOverride(String colorOverride)
|
||||
{
|
||||
this.colorOverride = colorOverride;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isStacked
|
||||
*******************************************************************************/
|
||||
public boolean getIsStacked()
|
||||
{
|
||||
return (this.isStacked);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isStacked
|
||||
*******************************************************************************/
|
||||
public void setIsStacked(boolean isStacked)
|
||||
{
|
||||
this.isStacked = isStacked;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isStacked
|
||||
*******************************************************************************/
|
||||
public UpOrDownNumberStyles withIsStacked(boolean isStacked)
|
||||
{
|
||||
this.isStacked = isStacked;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.dashboard.widgets.blocks.upordownnumber;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.blocks.BlockValuesInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class UpOrDownNumberValues implements BlockValuesInterface
|
||||
{
|
||||
private boolean isUp = false;
|
||||
private boolean isGood = false;
|
||||
private Serializable number;
|
||||
private String context;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isUp
|
||||
*******************************************************************************/
|
||||
public boolean getIsUp()
|
||||
{
|
||||
return (this.isUp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isUp
|
||||
*******************************************************************************/
|
||||
public void setIsUp(boolean isUp)
|
||||
{
|
||||
this.isUp = isUp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isUp
|
||||
*******************************************************************************/
|
||||
public UpOrDownNumberValues withIsUp(boolean isUp)
|
||||
{
|
||||
this.isUp = isUp;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for isGood
|
||||
*******************************************************************************/
|
||||
public boolean getIsGood()
|
||||
{
|
||||
return (this.isGood);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for isGood
|
||||
*******************************************************************************/
|
||||
public void setIsGood(boolean isGood)
|
||||
{
|
||||
this.isGood = isGood;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for isGood
|
||||
*******************************************************************************/
|
||||
public UpOrDownNumberValues withIsGood(boolean isGood)
|
||||
{
|
||||
this.isGood = isGood;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for number
|
||||
*******************************************************************************/
|
||||
public Serializable getNumber()
|
||||
{
|
||||
return (this.number);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for number
|
||||
*******************************************************************************/
|
||||
public void setNumber(Serializable number)
|
||||
{
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for number
|
||||
*******************************************************************************/
|
||||
public UpOrDownNumberValues withNumber(Serializable number)
|
||||
{
|
||||
this.number = number;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for context
|
||||
*******************************************************************************/
|
||||
public String getContext()
|
||||
{
|
||||
return (this.context);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for context
|
||||
*******************************************************************************/
|
||||
public void setContext(String context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for context
|
||||
*******************************************************************************/
|
||||
public UpOrDownNumberValues withContext(String context)
|
||||
{
|
||||
this.context = context;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -63,6 +63,11 @@ public @interface QField
|
||||
*******************************************************************************/
|
||||
boolean isHidden() default false;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
String defaultValue() default "";
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -32,6 +32,7 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -167,6 +168,11 @@ public class QRecord implements Serializable
|
||||
ArrayList<?> cloneList = new ArrayList<>(arrayList);
|
||||
clone.put(entry.getKey(), (V) cloneList);
|
||||
}
|
||||
else if(entry.getValue() instanceof LinkedList<?> linkedList)
|
||||
{
|
||||
LinkedList<?> cloneList = new LinkedList<>(linkedList);
|
||||
clone.put(entry.getKey(), (V) cloneList);
|
||||
}
|
||||
else if(entry.getValue() instanceof LinkedHashMap<?, ?> linkedHashMap)
|
||||
{
|
||||
LinkedHashMap<?, ?> cloneMap = new LinkedHashMap<>(linkedHashMap);
|
||||
|
@ -29,6 +29,7 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import com.google.common.reflect.ClassPath;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
@ -50,13 +51,22 @@ public class MetaDataProducerHelper
|
||||
**
|
||||
** Note - they'll be sorted by the sortOrder they provide.
|
||||
*******************************************************************************/
|
||||
public static void processAllMetaDataProducersInPackage(QInstance instance, String packageName) throws IOException
|
||||
public static void processAllMetaDataProducersInPackage(QInstance instance, String packageName) throws QException
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// find all the meta data producer classes in (and under) the package //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
List<Class<?>> classesInPackage = getClassesInPackage(packageName);
|
||||
List<MetaDataProducerInterface<?>> producers = new ArrayList<>();
|
||||
List<Class<?>> classesInPackage;
|
||||
try
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// find all the meta data producer classes in (and under) the package //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
classesInPackage = getClassesInPackage(packageName);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Error getting classes in package [" + packageName + "]", e));
|
||||
}
|
||||
List<MetaDataProducerInterface<?>> producers = new ArrayList<>();
|
||||
|
||||
for(Class<?> aClass : classesInPackage)
|
||||
{
|
||||
try
|
||||
|
@ -1052,10 +1052,16 @@ public class QInstance
|
||||
for(QSecurityKeyType securityKeyType : CollectionUtils.nonNullMap(getSecurityKeyTypes()).values())
|
||||
{
|
||||
rs.add(securityKeyType.getName());
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
|
||||
{
|
||||
rs.add(securityKeyType.getAllAccessKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
|
||||
{
|
||||
rs.add(securityKeyType.getNullValueBehaviorKeyName());
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonFilter;
|
||||
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.TopLevelMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -43,6 +44,7 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface
|
||||
@JsonFilter("secretsFilter")
|
||||
private Map<String, String> values;
|
||||
|
||||
private QCodeReference customizer;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -192,4 +194,35 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface
|
||||
qInstance.setAuthentication(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for customizer
|
||||
*******************************************************************************/
|
||||
public QCodeReference getCustomizer()
|
||||
{
|
||||
return (this.customizer);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for customizer
|
||||
*******************************************************************************/
|
||||
public void setCustomizer(QCodeReference customizer)
|
||||
{
|
||||
this.customizer = customizer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for customizer
|
||||
*******************************************************************************/
|
||||
public QAuthenticationMetaData withCustomizer(QCodeReference customizer)
|
||||
{
|
||||
this.customizer = customizer;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
|
||||
@ -55,10 +57,12 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
|
||||
private boolean storeDropdownSelections;
|
||||
|
||||
private boolean showReloadButton = true;
|
||||
private boolean showExportButton = true;
|
||||
private boolean showExportButton = false;
|
||||
|
||||
protected Map<String, QIcon> icons;
|
||||
|
||||
protected Map<String, QHelpContent> helpContent;
|
||||
|
||||
protected Map<String, Serializable> defaultValues = new LinkedHashMap<>();
|
||||
|
||||
|
||||
@ -217,6 +221,17 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
|
||||
public void setType(String type)
|
||||
{
|
||||
this.type = type;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// originally, showExportButton defaulted to true, and only a few frontend components knew how to render it. //
|
||||
// but, with the advent of csvData that any widget type can export, then the generic frontend widget code //
|
||||
// became aware of the export button, so we wanted to flip the default for showExportButton to false, but //
|
||||
// still have it by-default be true for these 2 types //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(WidgetType.TABLE.getType().equals(type) || WidgetType.CHILD_RECORD_LIST.getType().equals(type))
|
||||
{
|
||||
setShowExportButton(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -227,7 +242,7 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
|
||||
*******************************************************************************/
|
||||
public QWidgetMetaData withType(String type)
|
||||
{
|
||||
this.type = type;
|
||||
setType(type);
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -675,4 +690,35 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for helpContent
|
||||
*******************************************************************************/
|
||||
public Map<String, QHelpContent> getHelpContent()
|
||||
{
|
||||
return (this.helpContent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for helpContent
|
||||
*******************************************************************************/
|
||||
public void setHelpContent(Map<String, QHelpContent> helpContent)
|
||||
{
|
||||
this.helpContent = helpContent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for helpContent
|
||||
*******************************************************************************/
|
||||
public QWidgetMetaData withHelpContent(Map<String, QHelpContent> helpContent)
|
||||
{
|
||||
this.helpContent = helpContent;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -25,9 +25,11 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
|
||||
@ -38,6 +40,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRule
|
||||
*******************************************************************************/
|
||||
public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules, TopLevelMetaDataInterface
|
||||
{
|
||||
QLogger LOG = QLogger.getLogger(QWidgetMetaDataInterface.class);
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
@ -228,6 +232,23 @@ public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules, T
|
||||
return (null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default Map<String, QHelpContent> getHelpContent()
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void setHelpContent(Map<String, QHelpContent> helpContent)
|
||||
{
|
||||
LOG.debug("Setting help content in a widgetMetaData type that doesn't support it (because it didn't override the getter/setter)");
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -51,6 +51,7 @@ public class HtmlWrapper implements Serializable
|
||||
public static final String STYLE_INDENT_2 = "padding-left: 2rem; ";
|
||||
public static final String STYLE_FLOAT_RIGHT = "float: right; ";
|
||||
public static final String STYLE_RED = "color: red; ";
|
||||
public static final String STYLE_YELLOW = "color: #bfb743; ";
|
||||
|
||||
|
||||
|
||||
|
@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -224,6 +225,11 @@ public class QFieldMetaData implements Cloneable
|
||||
{
|
||||
withBehavior(fieldAnnotation.valueTooLongBehavior());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(fieldAnnotation.defaultValue()))
|
||||
{
|
||||
ValueUtils.getValueAsFieldType(this.type, fieldAnnotation.defaultValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(QException qe)
|
||||
|
@ -26,11 +26,13 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
|
||||
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.QSupplementalAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
@ -51,6 +53,7 @@ public class QFrontendAppMetaData
|
||||
|
||||
private List<QAppSection> sections;
|
||||
|
||||
private Map<String, QSupplementalAppMetaData> supplementalAppMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -92,6 +95,31 @@ public class QFrontendAppMetaData
|
||||
{
|
||||
this.sections = filteredSections;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// include supplemental meta data, based on if it's meant for full or partial frontend meta-data requests //
|
||||
// todo - take includeFullMetaData as a param? //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
boolean includeFullMetaData = true;
|
||||
for(QSupplementalAppMetaData supplementalAppMetaData : CollectionUtils.nonNullMap(appMetaData.getSupplementalMetaData()).values())
|
||||
{
|
||||
boolean include;
|
||||
if(includeFullMetaData)
|
||||
{
|
||||
include = supplementalAppMetaData.includeInFullFrontendMetaData();
|
||||
}
|
||||
else
|
||||
{
|
||||
include = supplementalAppMetaData.includeInPartialFrontendMetaData();
|
||||
}
|
||||
|
||||
if(include)
|
||||
{
|
||||
this.supplementalAppMetaData = Objects.requireNonNullElseGet(this.supplementalAppMetaData, HashMap::new);
|
||||
this.supplementalAppMetaData.put(supplementalAppMetaData.getType(), supplementalAppMetaData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -196,4 +224,15 @@ public class QFrontendAppMetaData
|
||||
{
|
||||
return sections;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for supplementalAppMetaData
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Map<String, QSupplementalAppMetaData> getSupplementalAppMetaData()
|
||||
{
|
||||
return supplementalAppMetaData;
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.QHelpContent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
|
||||
|
||||
@ -57,7 +58,8 @@ public class QFrontendWidgetMetaData
|
||||
private boolean showReloadButton = false;
|
||||
private boolean showExportButton = false;
|
||||
|
||||
protected Map<String, QIcon> icons;
|
||||
protected Map<String, QIcon> icons;
|
||||
protected Map<String, QHelpContent> helpContent;
|
||||
|
||||
private final boolean hasPermission;
|
||||
|
||||
@ -92,6 +94,8 @@ public class QFrontendWidgetMetaData
|
||||
this.icons = qWidgetMetaData.getIcons();
|
||||
}
|
||||
|
||||
this.helpContent = widgetMetaData.getHelpContent();
|
||||
|
||||
hasPermission = PermissionsHelper.hasWidgetPermission(actionInput, name);
|
||||
}
|
||||
|
||||
@ -259,4 +263,15 @@ public class QFrontendWidgetMetaData
|
||||
{
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for helpContent
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Map<String, QHelpContent> getHelpContent()
|
||||
{
|
||||
return helpContent;
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
@ -55,6 +57,8 @@ public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRu
|
||||
private List<String> widgets;
|
||||
private List<QAppSection> sections;
|
||||
|
||||
private Map<String, QSupplementalAppMetaData> supplementalMetaData;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -460,4 +464,50 @@ public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRu
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public Map<String, QSupplementalAppMetaData> getSupplementalMetaData()
|
||||
{
|
||||
return (this.supplementalMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public void setSupplementalMetaData(Map<String, QSupplementalAppMetaData> supplementalMetaData)
|
||||
{
|
||||
this.supplementalMetaData = supplementalMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QAppMetaData withSupplementalMetaData(QSupplementalAppMetaData supplementalMetaData)
|
||||
{
|
||||
if(this.supplementalMetaData == null)
|
||||
{
|
||||
this.supplementalMetaData = new HashMap<>();
|
||||
}
|
||||
this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QAppMetaData withSupplementalMetaData(Map<String, QSupplementalAppMetaData> supplementalMetaData)
|
||||
{
|
||||
this.supplementalMetaData = supplementalMetaData;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. 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.qqq.backend.core.model.metadata.layout;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Base-class for app-level meta-data defined by some supplemental module, etc,
|
||||
** outside of qqq core
|
||||
*******************************************************************************/
|
||||
public abstract class QSupplementalAppMetaData
|
||||
{
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean includeInPartialFrontendMetaData()
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean includeInFullFrontendMetaData()
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for type
|
||||
*******************************************************************************/
|
||||
public abstract String getType();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void enrich(QInstance qInstance, QTableMetaData table)
|
||||
{
|
||||
////////////////////////
|
||||
// noop in base class //
|
||||
////////////////////////
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void validate(QInstance qInstance, QTableMetaData tableMetaData, QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
////////////////////////
|
||||
// noop in base class //
|
||||
////////////////////////
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.model.metadata.security;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock.NullValueBehavior;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility for working with security key, nullValueBehaviors.
|
||||
*******************************************************************************/
|
||||
public class NullValueBehaviorUtil
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(NullValueBehaviorUtil.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Look at a RecordSecurityLock, but also the active session - and if the session
|
||||
** has a null-value-behavior key for the lock's key-type, then allow that behavior
|
||||
** to override the lock's default.
|
||||
*******************************************************************************/
|
||||
public static NullValueBehavior getEffectiveNullValueBehavior(RecordSecurityLock recordSecurityLock)
|
||||
{
|
||||
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
|
||||
if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
|
||||
{
|
||||
List<Serializable> nullValueSessionValueList = QContext.getQSession().getSecurityKeyValues(securityKeyType.getNullValueBehaviorKeyName());
|
||||
if(CollectionUtils.nullSafeHasContents(nullValueSessionValueList))
|
||||
{
|
||||
NullValueBehavior nullValueBehavior = NullValueBehavior.tryToGetFromString(ValueUtils.getValueAsString(nullValueSessionValueList.get(0)));
|
||||
if(nullValueBehavior != null)
|
||||
{
|
||||
return nullValueBehavior;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Unexpected value in nullValueBehavior security key. Will use recordSecurityLock's nullValueBehavior",
|
||||
logPair("nullValueBehaviorKeyName", securityKeyType.getNullValueBehaviorKeyName()),
|
||||
logPair("value", nullValueSessionValueList.get(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (recordSecurityLock.getNullValueBehavior());
|
||||
}
|
||||
|
||||
}
|
@ -34,6 +34,7 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface
|
||||
{
|
||||
private String name;
|
||||
private String allAccessKeyName;
|
||||
private String nullValueBehaviorKeyName;
|
||||
private String possibleValueSourceName;
|
||||
|
||||
|
||||
@ -149,4 +150,35 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface
|
||||
qInstance.addSecurityKeyType(this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for nullValueBehaviorKeyName
|
||||
*******************************************************************************/
|
||||
public String getNullValueBehaviorKeyName()
|
||||
{
|
||||
return (this.nullValueBehaviorKeyName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for nullValueBehaviorKeyName
|
||||
*******************************************************************************/
|
||||
public void setNullValueBehaviorKeyName(String nullValueBehaviorKeyName)
|
||||
{
|
||||
this.nullValueBehaviorKeyName = nullValueBehaviorKeyName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for nullValueBehaviorKeyName
|
||||
*******************************************************************************/
|
||||
public QSecurityKeyType withNullValueBehaviorKeyName(String nullValueBehaviorKeyName)
|
||||
{
|
||||
this.nullValueBehaviorKeyName = nullValueBehaviorKeyName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -22,7 +22,9 @@
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.security;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -67,7 +69,34 @@ public class RecordSecurityLock
|
||||
{
|
||||
ALLOW,
|
||||
ALLOW_WRITE_ONLY, // not common - but see Audit, where you can do a thing that inserts them into a generic table, even though you can't later read them yourself...
|
||||
DENY
|
||||
DENY;
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// for use in tryToGetFromString, where we'll lowercase the input //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
private static final Map<String, NullValueBehavior> stringMapping = new HashMap<>();
|
||||
|
||||
static
|
||||
{
|
||||
stringMapping.put("allow", ALLOW);
|
||||
stringMapping.put("allow_write_only", ALLOW_WRITE_ONLY);
|
||||
stringMapping.put("allowwriteonly", ALLOW_WRITE_ONLY);
|
||||
stringMapping.put("deny", DENY);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static NullValueBehavior tryToGetFromString(String string)
|
||||
{
|
||||
if(string == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
return stringMapping.get(string.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.tables;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
@ -69,4 +70,16 @@ public abstract class QSupplementalTableMetaData
|
||||
// noop in base class //
|
||||
////////////////////////
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void validate(QInstance qInstance, QTableMetaData tableMetaData, QInstanceValidator qInstanceValidator)
|
||||
{
|
||||
////////////////////////
|
||||
// noop in base class //
|
||||
////////////////////////
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.savedfilters;
|
||||
package com.kingsrook.qqq.backend.core.model.savedviews;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
@ -32,9 +32,9 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
/*******************************************************************************
|
||||
** Entity bean for the saved filter table
|
||||
*******************************************************************************/
|
||||
public class SavedFilter extends QRecordEntity
|
||||
public class SavedView extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "savedFilter";
|
||||
public static final String TABLE_NAME = "savedView";
|
||||
|
||||
@QField(isEditable = false)
|
||||
private Integer id;
|
||||
@ -55,7 +55,7 @@ public class SavedFilter extends QRecordEntity
|
||||
private String userId;
|
||||
|
||||
@QField(isEditable = false)
|
||||
private String filterJson;
|
||||
private String viewJson;
|
||||
|
||||
|
||||
|
||||
@ -63,7 +63,7 @@ public class SavedFilter extends QRecordEntity
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SavedFilter()
|
||||
public SavedView()
|
||||
{
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ public class SavedFilter extends QRecordEntity
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SavedFilter(QRecord qRecord) throws QException
|
||||
public SavedView(QRecord qRecord) throws QException
|
||||
{
|
||||
populateFromQRecord(qRecord);
|
||||
}
|
||||
@ -172,7 +172,7 @@ public class SavedFilter extends QRecordEntity
|
||||
** Fluent setter for label
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SavedFilter withLabel(String label)
|
||||
public SavedView withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
@ -206,7 +206,7 @@ public class SavedFilter extends QRecordEntity
|
||||
** Fluent setter for tableName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SavedFilter withTableName(String tableName)
|
||||
public SavedView withTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
return (this);
|
||||
@ -240,7 +240,7 @@ public class SavedFilter extends QRecordEntity
|
||||
** Fluent setter for userId
|
||||
**
|
||||
*******************************************************************************/
|
||||
public SavedFilter withUserId(String userId)
|
||||
public SavedView withUserId(String userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
return (this);
|
||||
@ -249,34 +249,31 @@ public class SavedFilter extends QRecordEntity
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filterJson
|
||||
**
|
||||
** Getter for viewJson
|
||||
*******************************************************************************/
|
||||
public String getFilterJson()
|
||||
public String getViewJson()
|
||||
{
|
||||
return filterJson;
|
||||
return (this.viewJson);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for filterJson
|
||||
**
|
||||
** Setter for viewJson
|
||||
*******************************************************************************/
|
||||
public void setFilterJson(String filterJson)
|
||||
public void setViewJson(String viewJson)
|
||||
{
|
||||
this.filterJson = filterJson;
|
||||
this.viewJson = viewJson;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for filterJson
|
||||
**
|
||||
** Fluent setter for viewJson
|
||||
*******************************************************************************/
|
||||
public SavedFilter withFilterJson(String filterJson)
|
||||
public SavedView withViewJson(String viewJson)
|
||||
{
|
||||
this.filterJson = filterJson;
|
||||
this.viewJson = viewJson;
|
||||
return (this);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,25 +19,31 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.savedfilters;
|
||||
package com.kingsrook.qqq.backend.core.model.savedviews;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
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.processes.implementations.savedfilters.DeleteSavedFilterProcess;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.QuerySavedFilterProcess;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.savedfilters.StoreSavedFilterProcess;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.DeleteSavedViewProcess;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.QuerySavedViewProcess;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.savedviews.StoreSavedViewProcess;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class SavedFiltersMetaDataProvider
|
||||
public class SavedViewsMetaDataProvider
|
||||
{
|
||||
|
||||
|
||||
@ -46,11 +52,11 @@ public class SavedFiltersMetaDataProvider
|
||||
*******************************************************************************/
|
||||
public void defineAll(QInstance instance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
|
||||
{
|
||||
instance.addTable(defineSavedFilterTable(backendName, backendDetailEnricher));
|
||||
instance.addPossibleValueSource(defineSavedFilterPossibleValueSource());
|
||||
instance.addProcess(QuerySavedFilterProcess.getProcessMetaData());
|
||||
instance.addProcess(StoreSavedFilterProcess.getProcessMetaData());
|
||||
instance.addProcess(DeleteSavedFilterProcess.getProcessMetaData());
|
||||
instance.addTable(defineSavedViewTable(backendName, backendDetailEnricher));
|
||||
instance.addPossibleValueSource(defineSavedViewPossibleValueSource());
|
||||
instance.addProcess(QuerySavedViewProcess.getProcessMetaData());
|
||||
instance.addProcess(StoreSavedViewProcess.getProcessMetaData());
|
||||
instance.addProcess(DeleteSavedViewProcess.getProcessMetaData());
|
||||
}
|
||||
|
||||
|
||||
@ -58,16 +64,21 @@ public class SavedFiltersMetaDataProvider
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QTableMetaData defineSavedFilterTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
|
||||
public QTableMetaData defineSavedViewTable(String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
|
||||
{
|
||||
QTableMetaData table = new QTableMetaData()
|
||||
.withName(SavedFilter.TABLE_NAME)
|
||||
.withLabel("Saved Filter")
|
||||
.withName(SavedView.TABLE_NAME)
|
||||
.withLabel("Saved View")
|
||||
.withRecordLabelFormat("%s")
|
||||
.withRecordLabelFields("label")
|
||||
.withBackendName(backendName)
|
||||
.withPrimaryKeyField("id")
|
||||
.withFieldsFromEntity(SavedFilter.class);
|
||||
.withFieldsFromEntity(SavedView.class)
|
||||
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "label")))
|
||||
.withSection(new QFieldSection("data", new QIcon().withName("text_snippet"), Tier.T2, List.of("userId", "tableName", "viewJson")))
|
||||
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
|
||||
|
||||
table.getField("viewJson").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("json")));
|
||||
|
||||
if(backendDetailEnricher != null)
|
||||
{
|
||||
@ -82,12 +93,12 @@ public class SavedFiltersMetaDataProvider
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QPossibleValueSource defineSavedFilterPossibleValueSource()
|
||||
private QPossibleValueSource defineSavedViewPossibleValueSource()
|
||||
{
|
||||
return new QPossibleValueSource()
|
||||
.withName(SavedFilter.TABLE_NAME)
|
||||
.withName(SavedView.TABLE_NAME)
|
||||
.withType(QPossibleValueSourceType.TABLE)
|
||||
.withTableName(SavedFilter.TABLE_NAME)
|
||||
.withTableName(SavedView.TABLE_NAME)
|
||||
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY)
|
||||
.withOrderByField("label");
|
||||
}
|
@ -48,6 +48,7 @@ public class QSession implements Serializable
|
||||
private String uuid;
|
||||
|
||||
private Set<String> permissions;
|
||||
|
||||
private Map<String, List<Serializable>> securityKeyValues;
|
||||
private Map<String, Serializable> backendVariants;
|
||||
|
||||
@ -142,6 +143,19 @@ public class QSession implements Serializable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void removeValue(String key)
|
||||
{
|
||||
if(values != null)
|
||||
{
|
||||
values.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -324,15 +338,10 @@ public class QSession implements Serializable
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for securityKeyValues - add a list of values for 1 key
|
||||
** Fluent setter for securityKeyValues - add 1 value for 1 key.
|
||||
*******************************************************************************/
|
||||
public QSession withSecurityKeyValues(String keyName, List<Serializable> values)
|
||||
public QSession withSecurityKeyValue(String keyName, Serializable value)
|
||||
{
|
||||
if(values == null)
|
||||
{
|
||||
return (this);
|
||||
}
|
||||
|
||||
if(securityKeyValues == null)
|
||||
{
|
||||
securityKeyValues = new HashMap<>();
|
||||
@ -342,12 +351,15 @@ public class QSession implements Serializable
|
||||
|
||||
try
|
||||
{
|
||||
securityKeyValues.get(keyName).addAll(values);
|
||||
securityKeyValues.get(keyName).add(value);
|
||||
}
|
||||
catch(UnsupportedOperationException uoe)
|
||||
{
|
||||
/////////////////////
|
||||
// grr, List.of... //
|
||||
/////////////////////
|
||||
securityKeyValues.put(keyName, new ArrayList<>(securityKeyValues.get(keyName)));
|
||||
securityKeyValues.get(keyName).addAll(values);
|
||||
securityKeyValues.get(keyName).add(value);
|
||||
}
|
||||
|
||||
return (this);
|
||||
@ -355,16 +367,6 @@ public class QSession implements Serializable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for securityKeyValues - add 1 value for 1 key.
|
||||
*******************************************************************************/
|
||||
public QSession withSecurityKeyValue(String keyName, Serializable value)
|
||||
{
|
||||
return (withSecurityKeyValues(keyName, List.of(value)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Clear the map of security key values in the session.
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.modules.authentication;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for customizing behavior of an Authentication module.
|
||||
*******************************************************************************/
|
||||
public interface QAuthenticationModuleCustomizerInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void addSecurityKeyValueToSession(QSession session, String keyName, Serializable value)
|
||||
{
|
||||
session.withSecurityKeyValue(keyName, value);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void customizeSession(QInstance qInstance, QSession qSession, Map<String, Object> context)
|
||||
{
|
||||
//////////
|
||||
// noop //
|
||||
//////////
|
||||
}
|
||||
|
||||
}
|
@ -47,6 +47,7 @@ import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.exceptions.TokenExpiredException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.auth0.net.Response;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
@ -71,6 +72,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0Authent
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession;
|
||||
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
|
||||
@ -80,6 +82,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
@ -129,6 +132,19 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
static final String EXPIRED_TOKEN_ERROR = "Token has expired";
|
||||
static final String INVALID_TOKEN_ERROR = "An invalid token was provided";
|
||||
|
||||
private Auth0AuthenticationMetaData metaData;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// do not use this var directly - rather - always call the getCustomizer method //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
private QAuthenticationModuleCustomizerInterface _customizer = null;
|
||||
private boolean customizerHasBeenRequested = false;
|
||||
|
||||
private static boolean mayMemoize = true;
|
||||
|
||||
private static final Memoization<String, String> getAccessTokenFromSessionUUIDMemoization = new Memoization<String, String>()
|
||||
.withTimeout(Duration.of(1, ChronoUnit.MINUTES))
|
||||
.withMaxSize(1000);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this is how we allow the actions within this class to work without themselves having a logged-in user. //
|
||||
@ -165,9 +181,12 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
@Override
|
||||
public QSession createSession(QInstance qInstance, Map<String, String> context) throws QAuthenticationException
|
||||
{
|
||||
QInstance contextInstanceBefore = QContext.getQInstance();
|
||||
|
||||
try
|
||||
{
|
||||
Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
|
||||
QContext.setQInstance(qInstance);
|
||||
this.metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
|
||||
|
||||
String accessToken = null;
|
||||
if(CollectionUtils.containsKeyWithNonNullValue(context, SESSION_UUID_KEY))
|
||||
@ -252,16 +271,6 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
}
|
||||
}
|
||||
|
||||
/* todo confirm this is deprecated
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check to see if the session id is a UUID, if so, that means we need to look up the 'actual' token in the access_token table //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(accessToken != null && StringUtils.isUUID(accessToken))
|
||||
{
|
||||
accessToken = lookupActualAccessToken(metaData, accessToken);
|
||||
}
|
||||
*/
|
||||
|
||||
///////////////////////////////////////////
|
||||
// if token wasn't found by now, give up //
|
||||
///////////////////////////////////////////
|
||||
@ -311,6 +320,10 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
LOG.error(message, e);
|
||||
throw (new QAuthenticationException(message));
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.setQInstance(contextInstanceBefore);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -346,26 +359,36 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
*******************************************************************************/
|
||||
private QSession buildAndValidateSession(QInstance qInstance, String accessToken) throws JwkException
|
||||
{
|
||||
QSession qSession = buildQSessionFromToken(accessToken, qInstance);
|
||||
if(isSessionValid(qInstance, qSession))
|
||||
QSession beforeSession = QContext.getQSession();
|
||||
|
||||
try
|
||||
{
|
||||
QContext.setQSession(getChickenAndEggSession());
|
||||
QSession qSession = buildQSessionFromToken(accessToken, qInstance);
|
||||
if(isSessionValid(qInstance, qSession))
|
||||
{
|
||||
return (qSession);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we make it here it means we have never validated this token or it has been a long //
|
||||
// enough duration so we need to re-verify the token //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
qSession = revalidateTokenAndBuildSession(qInstance, accessToken);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// put now into state so we don't check until next interval passes //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
StateProviderInterface spi = getStateProvider();
|
||||
SimpleStateKey<String> key = new SimpleStateKey<>(qSession.getIdReference());
|
||||
spi.put(key, Instant.now());
|
||||
|
||||
return (qSession);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we make it here it means we have never validated this token or it has been a long //
|
||||
// enough duration so we need to re-verify the token //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
qSession = revalidateTokenAndBuildSession(qInstance, accessToken);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// put now into state so we don't check until next interval passes //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
StateProviderInterface spi = getStateProvider();
|
||||
SimpleStateKey<String> key = new SimpleStateKey<>(qSession.getIdReference());
|
||||
spi.put(key, Instant.now());
|
||||
|
||||
return (qSession);
|
||||
finally
|
||||
{
|
||||
QContext.setQSession(beforeSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -509,7 +532,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
*******************************************************************************/
|
||||
private void validateToken(QInstance qInstance, String tokenString) throws JwkException
|
||||
{
|
||||
Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
|
||||
this.metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
|
||||
|
||||
DecodedJWT idToken = JWT.decode(tokenString);
|
||||
JwkProvider provider = new UrlJwkProvider(metaData.getBaseUrl());
|
||||
@ -567,10 +590,10 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
qSession.setIdReference(accessToken);
|
||||
qSession.setUser(qUser);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// put the user id reference in security key value for usierId key //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
qSession.withSecurityKeyValue("userId", qUser.getIdReference());
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// put the user id reference in security key value for userId key //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
addSecurityKeyValueToSession(qSession, "userId", qUser.getIdReference());
|
||||
|
||||
/////////////////////////////////////////////////
|
||||
// set permissions in the session from the JWT //
|
||||
@ -581,12 +604,43 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
// set security keys in the session from the JWT //
|
||||
///////////////////////////////////////////////////
|
||||
setSecurityKeysInSessionFromJwtPayload(qInstance, payload, qSession);
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// allow customizer to do custom things here, if so desired //
|
||||
//////////////////////////////////////////////////////////////
|
||||
if(getCustomizer() != null)
|
||||
{
|
||||
getCustomizer().customizeSession(qInstance, qSession, Map.of("jwtPayloadJsonObject", payload));
|
||||
}
|
||||
|
||||
return (qSession);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addSecurityKeyValueToSession(QSession qSession, String key, String value)
|
||||
{
|
||||
if(getCustomizer() == null)
|
||||
{
|
||||
///////////////////////////////////////////////////
|
||||
// if there's no customizer, do the direct thing //
|
||||
///////////////////////////////////////////////////
|
||||
qSession.withSecurityKeyValue(key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////
|
||||
// else have the customizer add the value to the session //
|
||||
///////////////////////////////////////////////////////////
|
||||
getCustomizer().addSecurityKeyValueToSession(qSession, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -616,7 +670,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static void setSecurityKeysInSessionFromJwtPayload(QInstance qInstance, JSONObject payload, QSession qSession)
|
||||
void setSecurityKeysInSessionFromJwtPayload(QInstance qInstance, JSONObject payload, QSession qSession)
|
||||
{
|
||||
for(String payloadKey : List.of("com.kingsrook.qqq.app_metadata", "com.kingsrook.qqq.client_metadata"))
|
||||
{
|
||||
@ -668,7 +722,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void setSecurityKeyValuesFromToken(Set<String> allowedSecurityKeyNames, QSession qSession, String securityKeyName, JSONObject securityKeyValues, String jsonKey)
|
||||
private void setSecurityKeyValuesFromToken(Set<String> allowedSecurityKeyNames, QSession qSession, String securityKeyName, JSONObject securityKeyValues, String jsonKey)
|
||||
{
|
||||
if(!allowedSecurityKeyNames.contains(securityKeyName))
|
||||
{
|
||||
@ -686,7 +740,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
Object optValue = valueArray.opt(i);
|
||||
if(optValue != null)
|
||||
{
|
||||
qSession.withSecurityKeyValue(securityKeyName, ValueUtils.getValueAsString(optValue));
|
||||
addSecurityKeyValueToSession(qSession, securityKeyName, ValueUtils.getValueAsString(optValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -697,7 +751,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
{
|
||||
for(String v : values.split(","))
|
||||
{
|
||||
qSession.withSecurityKeyValue(securityKeyName, v);
|
||||
addSecurityKeyValueToSession(qSession, securityKeyName, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -815,6 +869,25 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getAccessTokenFromSessionUUID(Auth0AuthenticationMetaData metaData, String sessionUUID) throws QAuthenticationException
|
||||
{
|
||||
if(mayMemoize)
|
||||
{
|
||||
return getAccessTokenFromSessionUUIDMemoization.getResultThrowing(sessionUUID, (String x) ->
|
||||
doGetAccessTokenFromSessionUUID(sessionUUID)
|
||||
).orElse(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (doGetAccessTokenFromSessionUUID(sessionUUID));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String doGetAccessTokenFromSessionUUID(String sessionUUID) throws QAuthenticationException
|
||||
{
|
||||
String accessToken = null;
|
||||
QSession beforeSession = QContext.getQSession();
|
||||
@ -982,4 +1055,39 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QAuthenticationModuleCustomizerInterface getCustomizer()
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!customizerHasBeenRequested)
|
||||
{
|
||||
customizerHasBeenRequested = true;
|
||||
|
||||
if(this.metaData == null)
|
||||
{
|
||||
this.metaData = (Auth0AuthenticationMetaData) QContext.getQInstance().getAuthentication();
|
||||
}
|
||||
|
||||
if(this.metaData.getCustomizer() != null)
|
||||
{
|
||||
this._customizer = QCodeLoader.getAdHoc(QAuthenticationModuleCustomizerInterface.class, this.metaData.getCustomizer());
|
||||
}
|
||||
}
|
||||
|
||||
return (this._customizer);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
////////////////////////
|
||||
// should this throw? //
|
||||
////////////////////////
|
||||
LOG.warn("Error getting customizer.", e);
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,10 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@ -35,6 +39,7 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.DateTimeGroupBy;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -66,6 +71,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -577,7 +583,11 @@ public class MemoryRecordStore
|
||||
for(GroupBy groupBy : groupBys)
|
||||
{
|
||||
Serializable groupByValue = record.getValue(groupBy.getFieldName());
|
||||
if(groupBy.getType() != null)
|
||||
if(StringUtils.hasContent(groupBy.getFormatString()))
|
||||
{
|
||||
groupByValue = applyFormatString(groupByValue, groupBy);
|
||||
}
|
||||
else if(groupBy.getType() != null)
|
||||
{
|
||||
groupByValue = ValueUtils.getValueAsFieldType(groupBy.getType(), groupByValue);
|
||||
}
|
||||
@ -629,7 +639,9 @@ public class MemoryRecordStore
|
||||
/////////////////////
|
||||
if(aggregateInput.getFilter() != null && CollectionUtils.nullSafeHasContents(aggregateInput.getFilter().getOrderBys()))
|
||||
{
|
||||
Comparator<AggregateResult> comparator = null;
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// lambda to compare 2 serializables, as we'll assume (& cast) them to Comparables //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
Comparator<Serializable> serializableComparator = (Serializable a, Serializable b) ->
|
||||
{
|
||||
if(a == null && b == null)
|
||||
@ -647,9 +659,15 @@ public class MemoryRecordStore
|
||||
return ((Comparable) a).compareTo(b);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// reverse of the lambda above (we had some errors calling .reversed() on the comparator we were building, so this seemed simpler & worked) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Comparator<Serializable> reverseSerializableComparator = (Serializable a, Serializable b) -> -serializableComparator.compare(a, b);
|
||||
|
||||
////////////////////////////////////////////////
|
||||
// build a comparator out of all the orderBys //
|
||||
////////////////////////////////////////////////
|
||||
Comparator<AggregateResult> comparator = null;
|
||||
for(QFilterOrderBy orderBy : aggregateInput.getFilter().getOrderBys())
|
||||
{
|
||||
Function<AggregateResult, Serializable> keyExtractor = aggregateResult ->
|
||||
@ -670,16 +688,11 @@ public class MemoryRecordStore
|
||||
|
||||
if(comparator == null)
|
||||
{
|
||||
comparator = Comparator.comparing(keyExtractor, serializableComparator);
|
||||
comparator = Comparator.comparing(keyExtractor, orderBy.getIsAscending() ? serializableComparator : reverseSerializableComparator);
|
||||
}
|
||||
else
|
||||
{
|
||||
comparator = comparator.thenComparing(keyExtractor, serializableComparator);
|
||||
}
|
||||
|
||||
if(!orderBy.getIsAscending())
|
||||
{
|
||||
comparator = comparator.reversed();
|
||||
comparator = comparator.thenComparing(keyExtractor, orderBy.getIsAscending() ? serializableComparator : reverseSerializableComparator);
|
||||
}
|
||||
}
|
||||
|
||||
@ -696,6 +709,57 @@ public class MemoryRecordStore
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Serializable applyFormatString(Serializable value, GroupBy groupBy) throws QException
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
String formatString = groupBy.getFormatString();
|
||||
|
||||
try
|
||||
{
|
||||
if(formatString.startsWith("DATE_FORMAT"))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// one known-use case we have here looks like this: //
|
||||
// DATE_FORMAT(CONVERT_TZ(%s, 'UTC', 'UTC'), '%%Y-%%m-%%dT%%H') //
|
||||
// ... for now, let's just try to support the formatting bit at the end... //
|
||||
// todo - support the CONVERT_TZ bit too! //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
String sqlDateTimeFormat = formatString.replaceFirst(".*'%%", "%%").replaceFirst("'.*", "");
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeGroupBy.sqlDateFormatToSelectedDateTimeFormatter(sqlDateTimeFormat);
|
||||
if(dateTimeFormatter == null)
|
||||
{
|
||||
throw (new QException("Unsupported sql dateTime format string [" + sqlDateTimeFormat + "] for MemoryRecordStore"));
|
||||
}
|
||||
|
||||
String valueAsString = ValueUtils.getValueAsString(value);
|
||||
Instant valueAsInstant = ValueUtils.getValueAsInstant(valueAsString);
|
||||
ZonedDateTime zonedDateTime = valueAsInstant.atZone(ZoneId.systemDefault());
|
||||
return (dateTimeFormatter.format(zonedDateTime));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("Unsupported group-by format string [" + formatString + "] for MemoryRecordStore"));
|
||||
}
|
||||
}
|
||||
catch(QException qe)
|
||||
{
|
||||
throw (qe);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QException("Error applying format string [" + formatString + "] to group by value [" + value + "]", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -22,13 +22,10 @@
|
||||
package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.UpdateInterface;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -45,17 +42,6 @@ public class MemoryUpdateAction extends AbstractMemoryAction implements UpdateIn
|
||||
{
|
||||
try
|
||||
{
|
||||
QTableMetaData table = updateInput.getTable();
|
||||
Instant now = Instant.now();
|
||||
|
||||
for(QRecord record : updateInput.getRecords())
|
||||
{
|
||||
///////////////////////////////////////////
|
||||
// todo .. better (not hard-coded names) //
|
||||
///////////////////////////////////////////
|
||||
setValueIfTableHasField(record, table, "modifyDate", now, false);
|
||||
}
|
||||
|
||||
UpdateOutput updateOutput = new UpdateOutput();
|
||||
updateOutput.setRecords(MemoryRecordStore.getInstance().update(updateInput, true));
|
||||
return (updateOutput);
|
||||
|
@ -0,0 +1,298 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.processes.implementations.automation;
|
||||
|
||||
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
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.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.NowWithOffset;
|
||||
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.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.HtmlWrapper;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetHtmlLine;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DynamicDefaultValueBehavior;
|
||||
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.processes.NoCodeWidgetFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MultiLevelMapHelper;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process to find records with a bad automation status, and repair them.
|
||||
**
|
||||
** Bad status are defined as:
|
||||
** - failed insert or updates.
|
||||
** - running insert or updates for more than X minutes (see input field value).
|
||||
**
|
||||
** Repair in this case means resetting their status to the corresponding (e.g.,
|
||||
** insert/update) pending status.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class HealBadRecordAutomationStatusesProcessStep implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "HealBadRecordAutomationStatusesProcess";
|
||||
|
||||
private static final QLogger LOG = QLogger.getLogger(HealBadRecordAutomationStatusesProcessStep.class);
|
||||
|
||||
private static final Map<Integer, Integer> statusUpdateMap = Map.of(
|
||||
AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId(), AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(),
|
||||
AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId(), AutomationStatus.PENDING_INSERT_AUTOMATIONS.getId(),
|
||||
AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId(),
|
||||
AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS.getId()
|
||||
);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
QProcessMetaData processMetaData = new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withStepList(List.of(
|
||||
new QFrontendStepMetaData()
|
||||
.withName("input")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
|
||||
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME))
|
||||
.withFormField(new QFieldMetaData("minutesOldLimit", QFieldType.INTEGER).withDefaultValue(60)),
|
||||
new QBackendStepMetaData()
|
||||
.withName("run")
|
||||
.withCode(new QCodeReference(getClass())),
|
||||
new QFrontendStepMetaData()
|
||||
.withName("output")
|
||||
|
||||
.withComponent(new NoCodeWidgetFrontendComponentMetaData()
|
||||
.withOutput(new WidgetHtmlLine()
|
||||
.withCondition(new QFilterCriteria("warningCount", QCriteriaOperator.GREATER_THAN, 0))
|
||||
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_YELLOW))
|
||||
.withVelocityTemplate("<b>Warning:</b>"))
|
||||
.withOutput(new WidgetHtmlLine()
|
||||
.withCondition(new QFilterCriteria("warningCount", QCriteriaOperator.GREATER_THAN, 0))
|
||||
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_INDENT_1))
|
||||
.withWrapper(HtmlWrapper.divWithStyles(HtmlWrapper.STYLE_YELLOW))
|
||||
.withVelocityTemplate("""
|
||||
<ul>
|
||||
#foreach($string in $warnings)
|
||||
<li>$string</li>
|
||||
#end
|
||||
</ul>
|
||||
""")))
|
||||
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
|
||||
.withViewField(new QFieldMetaData("totalRecordsUpdated", QFieldType.INTEGER) /* todo - didn't display commas... .withDisplayFormat(DisplayFormat.COMMAS) */)
|
||||
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.RECORD_LIST))
|
||||
.withRecordListField(new QFieldMetaData("tableName", QFieldType.STRING))
|
||||
.withRecordListField(new QFieldMetaData("badStatus", QFieldType.STRING))
|
||||
.withRecordListField(new QFieldMetaData("count", QFieldType.INTEGER).withDisplayFormat(DisplayFormat.COMMAS) /* todo - didn't display commas... */)
|
||||
|
||||
));
|
||||
|
||||
return (processMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
int recordsUpdated = 0;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// if a table name is given, validate it, and run for just that table //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
ArrayList<String> warnings = new ArrayList<>();
|
||||
if(StringUtils.hasContent(tableName))
|
||||
{
|
||||
if(!QContext.getQInstance().getTables().containsKey(tableName))
|
||||
{
|
||||
throw (new QException("Unrecognized table name: " + tableName));
|
||||
}
|
||||
|
||||
recordsUpdated += processTable(tableName, runBackendStepInput, runBackendStepOutput, warnings);
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// else, try to run for all tables that have an automation status field //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
for(QTableMetaData table : QContext.getQInstance().getTables().values())
|
||||
{
|
||||
recordsUpdated += processTable(table.getName(), runBackendStepInput, runBackendStepOutput, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
runBackendStepOutput.addValue("totalRecordsUpdated", recordsUpdated);
|
||||
runBackendStepOutput.addValue("warnings", warnings);
|
||||
runBackendStepOutput.addValue("warningCount", warnings.size());
|
||||
|
||||
if(CollectionUtils.nullSafeIsEmpty(runBackendStepOutput.getRecords()))
|
||||
{
|
||||
runBackendStepOutput.addRecord(new QRecord()
|
||||
.withValue("tableName", "--")
|
||||
.withValue("badStatus", "--")
|
||||
.withValue("count", "0"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private int processTable(String tableName, RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, List<String> warnings)
|
||||
{
|
||||
try
|
||||
{
|
||||
Integer minutesOldLimit = Objects.requireNonNullElse(runBackendStepInput.getValueInteger("minutesOldLimit"), 60);
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// only process tables w/ automation details w/ a status tracking field //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
if(table != null && table.getAutomationDetails() != null && table.getAutomationDetails().getStatusTracking() != null && StringUtils.hasContent(table.getAutomationDetails().getStatusTracking().getFieldName()))
|
||||
{
|
||||
String automationStatusFieldName = table.getAutomationDetails().getStatusTracking().getFieldName();
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// find the modify-date field on the table //
|
||||
/////////////////////////////////////////////
|
||||
String modifyDateFieldName = null;
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
if(DynamicDefaultValueBehavior.MODIFY_DATE.equals(field.getBehaviorOnlyIfSet(DynamicDefaultValueBehavior.class)))
|
||||
{
|
||||
modifyDateFieldName = field.getName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(modifyDateFieldName == null)
|
||||
{
|
||||
warnings.add("Could not find a Modify Date field on table: " + tableName);
|
||||
LOG.info("Couldn't find a MODIFY_DATE field on table", logPair("tableName", tableName));
|
||||
return 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// query for records either FAILED, or RUNNING w/ modify date too old //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR)
|
||||
.withSubFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.FAILED_INSERT_AUTOMATIONS.getId(), AutomationStatus.FAILED_UPDATE_AUTOMATIONS.getId())))
|
||||
.withSubFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria(automationStatusFieldName, QCriteriaOperator.IN, AutomationStatus.RUNNING_INSERT_AUTOMATIONS.getId(), AutomationStatus.RUNNING_UPDATE_AUTOMATIONS.getId()))
|
||||
.withCriteria(new QFilterCriteria(modifyDateFieldName, QCriteriaOperator.LESS_THAN, NowWithOffset.minus(minutesOldLimit, ChronoUnit.MINUTES))))
|
||||
);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach record found, add it to list of records to be updated - mapping status to appropriate pending status //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||
Map<String, Integer> countByStatus = new HashMap<>();
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
Integer badAutomationStatusId = record.getValueInteger(automationStatusFieldName);
|
||||
Integer updateStatus = statusUpdateMap.get(badAutomationStatusId);
|
||||
if(updateStatus != null)
|
||||
{
|
||||
AutomationStatus badStatus = AutomationStatus.getById(badAutomationStatusId);
|
||||
if(badStatus != null)
|
||||
{
|
||||
MultiLevelMapHelper.getOrPutAndIncrement(countByStatus, badStatus.getLabel());
|
||||
}
|
||||
|
||||
recordsToUpdate.add(new QRecord()
|
||||
.withValue(table.getPrimaryKeyField(), record.getValue(table.getPrimaryKeyField()))
|
||||
.withValue(automationStatusFieldName, updateStatus));
|
||||
}
|
||||
}
|
||||
|
||||
if(!recordsToUpdate.isEmpty())
|
||||
{
|
||||
LOG.info("Healing bad record automation statuses", logPair("tableName", tableName), logPair("count", recordsToUpdate.size()));
|
||||
new UpdateAction().execute(new UpdateInput(tableName).withRecords(recordsToUpdate).withOmitTriggeringAutomations(true));
|
||||
}
|
||||
|
||||
for(Map.Entry<String, Integer> entry : countByStatus.entrySet())
|
||||
{
|
||||
runBackendStepOutput.addRecord(new QRecord()
|
||||
.withValue("tableName", tableName)
|
||||
.withValue("badStatus", entry.getKey())
|
||||
.withValue("count", entry.getValue()));
|
||||
}
|
||||
|
||||
return (recordsToUpdate.size());
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
warnings.add("Error processing table: " + tableName + ": " + ExceptionUtils.getTopAndBottomMessages(e));
|
||||
LOG.warn("Error processing table for bad automation statuses", e, logPair("tableName, name"));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.qqq.backend.core.processes.implementations.automation;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.polling.PollingAutomationPerTableRunner;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.MetaDataProducerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
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.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process to manually run table automations, for a table.
|
||||
**
|
||||
** Useful, maybe, for an e2e test. Or, if you don't want jobs to be running,
|
||||
** but want to run automations by-hand, for some reason.
|
||||
**
|
||||
** In the future, this class could take a param to only do inserts or updates.
|
||||
**
|
||||
** Also, right now, only records that are Pending automations will be run -
|
||||
** again, that could be changed, presumably (take a list of records, always run, etc...)
|
||||
*******************************************************************************/
|
||||
public class RunTableAutomationsProcessStep implements BackendStep, MetaDataProducerInterface<QProcessMetaData>
|
||||
{
|
||||
public static final String NAME = "RunTableAutomationsProcess";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QProcessMetaData produce(QInstance qInstance) throws QException
|
||||
{
|
||||
QProcessMetaData processMetaData = new QProcessMetaData()
|
||||
.withName(NAME)
|
||||
.withStepList(List.of(
|
||||
new QFrontendStepMetaData()
|
||||
.withName("input")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
|
||||
.withFormField(new QFieldMetaData("tableName", QFieldType.STRING).withIsRequired(true).withPossibleValueSourceName(TablesPossibleValueSourceMetaDataProvider.NAME))
|
||||
.withFormField(new QFieldMetaData("automationProviderName", QFieldType.STRING)),
|
||||
new QBackendStepMetaData()
|
||||
.withName("run")
|
||||
.withCode(new QCodeReference(getClass())),
|
||||
new QFrontendStepMetaData()
|
||||
.withName("output")
|
||||
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.VIEW_FORM))
|
||||
.withViewField(new QFieldMetaData("ok", QFieldType.STRING))
|
||||
));
|
||||
|
||||
return (processMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// get tableName param (since this process is not table-specific) //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
if(!StringUtils.hasContent(tableName))
|
||||
{
|
||||
throw (new QException("Missing required input value: tableName"));
|
||||
}
|
||||
|
||||
if(!QContext.getQInstance().getTables().containsKey(tableName))
|
||||
{
|
||||
throw (new QException("Unrecognized table name: " + tableName));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get the automation provider name to use - either as the only-one-in-instance, or via param //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String automationProviderName = runBackendStepInput.getValueString("automationProviderName");
|
||||
if(!StringUtils.hasContent(automationProviderName))
|
||||
{
|
||||
Map<String, QAutomationProviderMetaData> automationProviders = CollectionUtils.nonNullMap(qInstance.getAutomationProviders());
|
||||
if(automationProviders.size() == 1)
|
||||
{
|
||||
automationProviderName = automationProviders.keySet().iterator().next();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("Missing required input value: automationProviderName (and there is not exactly 1 in the active instance)"));
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// run automations for the requested table //
|
||||
/////////////////////////////////////////////
|
||||
List<PollingAutomationPerTableRunner.TableActionsInterface> tableActions = PollingAutomationPerTableRunner.getTableActions(qInstance, automationProviderName);
|
||||
for(PollingAutomationPerTableRunner.TableActionsInterface tableAction : tableActions)
|
||||
{
|
||||
if(tableName.equals(tableAction.tableName()))
|
||||
{
|
||||
PollingAutomationPerTableRunner pollingAutomationPerTableRunner = new PollingAutomationPerTableRunner(qInstance, automationProviderName, () -> QContext.getQSession(), tableAction);
|
||||
pollingAutomationPerTableRunner.processTableInsertOrUpdate(qInstance.getTable(tableAction.tableName()), tableAction.status());
|
||||
}
|
||||
}
|
||||
|
||||
runBackendStepOutput.addValue("ok", "true");
|
||||
}
|
||||
|
||||
}
|
@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.processes.implementations.columnstats;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@ -173,11 +175,10 @@ public class ColumnStatsStep implements BackendStep
|
||||
Aggregate aggregate = new Aggregate(table.getPrimaryKeyField(), AggregateOperator.COUNT).withFieldType(QFieldType.DECIMAL);
|
||||
GroupBy groupBy = new GroupBy(field.getType(), fieldName);
|
||||
|
||||
// todo - something here about "by-date, not time"
|
||||
// todo - something here about an input param to specify how you want dates & date-times grouped
|
||||
if(field.getType().equals(QFieldType.DATE_TIME))
|
||||
{
|
||||
// groupBy = new GroupBy(field.getType(), fieldName, "DATE(%s)");
|
||||
String sqlExpression = DateTimeGroupBy.HOUR.getSqlExpression();
|
||||
String sqlExpression = DateTimeGroupBy.HOUR.getSqlExpression(ZoneId.systemDefault());
|
||||
groupBy = new GroupBy(QFieldType.STRING, fieldName, sqlExpression);
|
||||
}
|
||||
|
||||
@ -230,6 +231,12 @@ public class ColumnStatsStep implements BackendStep
|
||||
for(AggregateResult result : aggregateOutput.getResults())
|
||||
{
|
||||
Serializable value = result.getGroupByValue(groupBy);
|
||||
|
||||
if(field.getType().equals(QFieldType.DATE_TIME) && value != null)
|
||||
{
|
||||
value = Instant.parse(value + ":00:00Z");
|
||||
}
|
||||
|
||||
Integer count = ValueUtils.getValueAsInteger(result.getAggregateValue(aggregate));
|
||||
valueCounts.add(new QRecord().withValue(fieldName, value).withValue("count", count));
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters;
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
@ -34,15 +34,15 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process used by the delete filter dialog
|
||||
** Process used by the delete view dialog
|
||||
*******************************************************************************/
|
||||
public class DeleteSavedFilterProcess implements BackendStep
|
||||
public class DeleteSavedViewProcess implements BackendStep
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(DeleteSavedFilterProcess.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(DeleteSavedViewProcess.class);
|
||||
|
||||
|
||||
|
||||
@ -52,10 +52,10 @@ public class DeleteSavedFilterProcess implements BackendStep
|
||||
public static QProcessMetaData getProcessMetaData()
|
||||
{
|
||||
return (new QProcessMetaData()
|
||||
.withName("deleteSavedFilter")
|
||||
.withName("deleteSavedView")
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withCode(new QCodeReference(DeleteSavedFilterProcess.class))
|
||||
.withCode(new QCodeReference(DeleteSavedViewProcess.class))
|
||||
.withName("delete")
|
||||
)));
|
||||
}
|
||||
@ -72,16 +72,16 @@ public class DeleteSavedFilterProcess implements BackendStep
|
||||
|
||||
try
|
||||
{
|
||||
Integer savedFilterId = runBackendStepInput.getValueInteger("id");
|
||||
Integer savedViewId = runBackendStepInput.getValueInteger("id");
|
||||
|
||||
DeleteInput input = new DeleteInput();
|
||||
input.setTableName(SavedFilter.TABLE_NAME);
|
||||
input.setPrimaryKeys(List.of(savedFilterId));
|
||||
input.setTableName(SavedView.TABLE_NAME);
|
||||
input.setPrimaryKeys(List.of(savedViewId));
|
||||
new DeleteAction().execute(input);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error deleting saved filter", e);
|
||||
LOG.warn("Error deleting saved view", e);
|
||||
throw (e);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters;
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
@ -43,15 +43,15 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process used by the saved filter dialogs
|
||||
** Process used by the saved view dialogs
|
||||
*******************************************************************************/
|
||||
public class QuerySavedFilterProcess implements BackendStep
|
||||
public class QuerySavedViewProcess implements BackendStep
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QuerySavedFilterProcess.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(QuerySavedViewProcess.class);
|
||||
|
||||
|
||||
|
||||
@ -61,10 +61,10 @@ public class QuerySavedFilterProcess implements BackendStep
|
||||
public static QProcessMetaData getProcessMetaData()
|
||||
{
|
||||
return (new QProcessMetaData()
|
||||
.withName("querySavedFilter")
|
||||
.withName("querySavedView")
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withCode(new QCodeReference(QuerySavedFilterProcess.class))
|
||||
.withCode(new QCodeReference(QuerySavedViewProcess.class))
|
||||
.withName("query")
|
||||
)));
|
||||
}
|
||||
@ -81,36 +81,36 @@ public class QuerySavedFilterProcess implements BackendStep
|
||||
|
||||
try
|
||||
{
|
||||
Integer savedFilterId = runBackendStepInput.getValueInteger("id");
|
||||
if(savedFilterId != null)
|
||||
Integer savedViewId = runBackendStepInput.getValueInteger("id");
|
||||
if(savedViewId != null)
|
||||
{
|
||||
GetInput input = new GetInput();
|
||||
input.setTableName(SavedFilter.TABLE_NAME);
|
||||
input.setPrimaryKey(savedFilterId);
|
||||
input.setTableName(SavedView.TABLE_NAME);
|
||||
input.setPrimaryKey(savedViewId);
|
||||
|
||||
GetOutput output = new GetAction().execute(input);
|
||||
runBackendStepOutput.addRecord(output.getRecord());
|
||||
runBackendStepOutput.addValue("savedFilter", output.getRecord());
|
||||
runBackendStepOutput.addValue("savedFilterList", (Serializable) List.of(output.getRecord()));
|
||||
runBackendStepOutput.addValue("savedView", output.getRecord());
|
||||
runBackendStepOutput.addValue("savedViewList", (Serializable) List.of(output.getRecord()));
|
||||
}
|
||||
else
|
||||
{
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
|
||||
QueryInput input = new QueryInput();
|
||||
input.setTableName(SavedFilter.TABLE_NAME);
|
||||
input.setTableName(SavedView.TABLE_NAME);
|
||||
input.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName))
|
||||
.withOrderBy(new QFilterOrderBy("label")));
|
||||
|
||||
QueryOutput output = new QueryAction().execute(input);
|
||||
runBackendStepOutput.setRecords(output.getRecords());
|
||||
runBackendStepOutput.addValue("savedFilterList", (Serializable) output.getRecords());
|
||||
runBackendStepOutput.addValue("savedViewList", (Serializable) output.getRecords());
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error deleting saved filter", e);
|
||||
LOG.warn("Error querying for saved views", e);
|
||||
throw (e);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 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/
|
||||
@ -19,37 +19,45 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.savedfilters;
|
||||
package com.kingsrook.qqq.backend.core.processes.implementations.savedviews;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.savedviews.SavedView;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process used by the saved filter dialog
|
||||
** Process used by the saved view dialog
|
||||
*******************************************************************************/
|
||||
public class StoreSavedFilterProcess implements BackendStep
|
||||
public class StoreSavedViewProcess implements BackendStep
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(StoreSavedFilterProcess.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(StoreSavedViewProcess.class);
|
||||
|
||||
|
||||
|
||||
@ -59,10 +67,10 @@ public class StoreSavedFilterProcess implements BackendStep
|
||||
public static QProcessMetaData getProcessMetaData()
|
||||
{
|
||||
return (new QProcessMetaData()
|
||||
.withName("storeSavedFilter")
|
||||
.withName("storeSavedView")
|
||||
.withStepList(List.of(
|
||||
new QBackendStepMetaData()
|
||||
.withCode(new QCodeReference(StoreSavedFilterProcess.class))
|
||||
.withCode(new QCodeReference(StoreSavedViewProcess.class))
|
||||
.withName("store")
|
||||
)));
|
||||
}
|
||||
@ -79,39 +87,73 @@ public class StoreSavedFilterProcess implements BackendStep
|
||||
|
||||
try
|
||||
{
|
||||
String userId = QContext.getQSession().getUser().getIdReference();
|
||||
String tableName = runBackendStepInput.getValueString("tableName");
|
||||
String label = runBackendStepInput.getValueString("label");
|
||||
|
||||
QRecord qRecord = new QRecord()
|
||||
.withValue("id", runBackendStepInput.getValueInteger("id"))
|
||||
.withValue("label", runBackendStepInput.getValueString("label"))
|
||||
.withValue("tableName", runBackendStepInput.getValueString("tableName"))
|
||||
.withValue("filterJson", runBackendStepInput.getValueString("filterJson"))
|
||||
.withValue("userId", runBackendStepInput.getSession().getUser().getIdReference());
|
||||
.withValue("viewJson", runBackendStepInput.getValueString("viewJson"))
|
||||
.withValue("label", label)
|
||||
.withValue("tableName", tableName)
|
||||
.withValue("userId", userId);
|
||||
|
||||
List<QRecord> savedFilterList = new ArrayList<>();
|
||||
List<QRecord> savedViewList;
|
||||
if(qRecord.getValueInteger("id") == null)
|
||||
{
|
||||
checkForDuplicates(userId, tableName, label, null);
|
||||
|
||||
InsertInput input = new InsertInput();
|
||||
input.setTableName(SavedFilter.TABLE_NAME);
|
||||
input.setTableName(SavedView.TABLE_NAME);
|
||||
input.setRecords(List.of(qRecord));
|
||||
|
||||
InsertOutput output = new InsertAction().execute(input);
|
||||
savedFilterList = output.getRecords();
|
||||
savedViewList = output.getRecords();
|
||||
}
|
||||
else
|
||||
{
|
||||
checkForDuplicates(userId, tableName, label, qRecord.getValueInteger("id"));
|
||||
|
||||
UpdateInput input = new UpdateInput();
|
||||
input.setTableName(SavedFilter.TABLE_NAME);
|
||||
input.setTableName(SavedView.TABLE_NAME);
|
||||
input.setRecords(List.of(qRecord));
|
||||
|
||||
UpdateOutput output = new UpdateAction().execute(input);
|
||||
savedFilterList = output.getRecords();
|
||||
savedViewList = output.getRecords();
|
||||
}
|
||||
|
||||
runBackendStepOutput.addValue("savedFilterList", (Serializable) savedFilterList);
|
||||
runBackendStepOutput.addValue("savedViewList", (Serializable) savedViewList);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error storing data saved filter", e);
|
||||
LOG.warn("Error storing saved view", e);
|
||||
throw (e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void checkForDuplicates(String userId, String tableName, String label, Integer id) throws QException
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(SavedView.TABLE_NAME);
|
||||
queryInput.setFilter(new QQueryFilter(
|
||||
new QFilterCriteria("userId", QCriteriaOperator.EQUALS, userId),
|
||||
new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName),
|
||||
new QFilterCriteria("label", QCriteriaOperator.EQUALS, label)));
|
||||
|
||||
if(id != null)
|
||||
{
|
||||
queryInput.getFilter().addCriteria(new QFilterCriteria("id", QCriteriaOperator.NOT_EQUALS, id));
|
||||
}
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
|
||||
{
|
||||
throw (new QUserFacingException("You already have a saved view on this table with this name."));
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,6 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.AuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.RunAdHocRecordScriptAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
@ -133,11 +132,6 @@ public class RunRecordScriptLoadStep extends AbstractLoadStep implements Process
|
||||
throw (new QException("Could not find script by id: " + scriptId));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set an "audit context" - so any DML executed during the script will include the note of what script was running. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
runBackendStepInput.addValue(DMLAuditAction.AUDIT_CONTEXT_FIELD_NAME, "via Script \"" + script.getValue("name") + "\"");
|
||||
|
||||
String tableName = script.getValueString("tableName");
|
||||
|
||||
RunAdHocRecordScriptInput input = new RunAdHocRecordScriptInput();
|
||||
|
@ -252,6 +252,10 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
|
||||
{
|
||||
initializeRecordLookupHelper(runBackendStepInput, runBackendStepInput.getRecords());
|
||||
}
|
||||
else
|
||||
{
|
||||
reinitializeRecordLookupHelper(runBackendStepInput, runBackendStepInput.getRecords());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// query to see if we already have those records in the destination (to determine insert/update) //
|
||||
@ -468,6 +472,18 @@ public abstract class AbstractTableSyncTransformStep extends AbstractTransformSt
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** for pages after the first, possibly load more records in the lookup helper.
|
||||
*******************************************************************************/
|
||||
protected void reinitializeRecordLookupHelper(RunBackendStepInput runBackendStepInput, List<QRecord> sourceRecordList) throws QException
|
||||
{
|
||||
////////////////////////
|
||||
// noop in base class //
|
||||
////////////////////////
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Let the subclass "easily" add an audit to be inserted on the Execute step.
|
||||
*******************************************************************************/
|
||||
|
@ -164,4 +164,23 @@ public class ExceptionUtils
|
||||
|
||||
return (StringUtils.join("; ", messages));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the messages from the top & bottoms (root) of an exception.
|
||||
**
|
||||
** If there's no root, just return the top (e.g., parameter)'s message.
|
||||
** If they are both found, put ": " between them.
|
||||
*******************************************************************************/
|
||||
public static String getTopAndBottomMessages(Exception e)
|
||||
{
|
||||
String rs = e.getMessage();
|
||||
Throwable rootException = getRootException(e);
|
||||
if(rootException != e)
|
||||
{
|
||||
rs += ": " + rootException.getMessage();
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user