inizializzazione progetto stub java 8 con react 18

This commit is contained in:
Alessandro Seravalli 2025-08-12 12:52:22 +02:00
commit b55e8a2bb5
273 changed files with 22381 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/.metadata/
RemoteSystemsTempFiles
.project
.settings
.classpath

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
java 8.0.342+7

12
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Attach debbugger",
"request": "attach",
"hostName": "localhost",
"port": 8787
}
]
}

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"java.project.sourcePaths": [
//"C:/Dev2012/source/projects/tech/armtest"
]
}

35
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,35 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start stub-gui",
"type": "shell",
"command": "cd stub-gui; npm run watch",
"group": "build",
"problemMatcher": [],
},
{
"label": "Monitor WAR",
"type": "shell",
"command": "cd scripts/jboss; ./monitor-war.bat",
"group": "build",
"problemMatcher": [],
},
{
"label": "Start JBoss",
"type": "shell",
"windows": {
"command": "cmd.exe",
"args": ["/c", "cd ${cwd}\\scripts\\jboss && start start-jboss.bat"]
},
"group": "build"
},
{
"label": "Maven Build",
"type": "shell",
"command": "cd stub-pom; mvn clean install -U -DskipTests",
"group": "build",
"problemMatcher": [],
},
]
}

13
README.md Normal file
View File

@ -0,0 +1,13 @@
# stub
Stub per applicativo JBoss Java con React.
## JUNIT
Utilizziamo arm-test: https://gitea.armundia.com/tech/armtest
## H2
aprire dbeaver e connettersi a (path è il percorso fisico al file):
jdbc:h2:file:{path};IFEXISTS=TRUE;DATABASE_TO_UPPER=FALSE;CASE_INSENSITIVE_IDENTIFIERS=TRUE

188
README_armtest.md Normal file
View File

@ -0,0 +1,188 @@
# ARMTEST
Funzionalità per il run di test su progetti Armundia.
## Basic usage
Dipendenza da utilizzare:
```xml
<dependency>
<groupId>com.armundia.tech</groupId>
<artifactId>armtest</artifactId>
<version>1.0.2</version>
</dependency>
```
Utilizzo per una classe di test (con o senza `@Testable`) e metodi `@Test`:
```java
class MioPrimoTest extends ArmUnitTest {
@Inject
private CdiTestService service;
@Test
void test01_base() {
String info = service.readInfo();
assertNotNull(info, "info must be not null");
}
}
```
da notare l'uso di una classe base `ArmUnitTest` per i test.
Questo permetterà di avere:
* supporto completo CDI (auto discovery anche su più jar dei bean)
* unico limite: i bean devono essere esplicitamente indicati con un'annotazione `@Dependent`, `@RequestScoped`, `@SessionScoped` o `@ApplicationScoped`
* supporto `@PersistenceContext` (normalmente disponibile solo in contesto Enterprise)
## supporto scenari DB via file H2
```java
@Scenario(name = "one", order = 1)
class ScenarioOne extends ArmUnitTest {
@Inject
private OneService service;
@Test
void test01_query() {
List<ApDom> dom = service.getApDom("CMA_ASSET_CLASS_INFO");
assertTrue(!dom.isEmpty(), "dominio CMA_ASSET_CLASS_INFO vuoto/non trovato");
}
}
```
Dove il servizio `OneService` è, ad esempio, con accesso al db tramite `EntityManager`:
```java
@Dependent
public class OneService {
@PersistenceContext
private EntityManager em;
public List<ApDom> getApDom(String cod) {
TypedQuery<ApDom> query = em.createQuery("SELECT a FROM ApDom a WHERE a.id.codCampo = :cod", ApDom.class);
query.setParameter("cod", cod);
return query.getResultList();
}
}
```
userà il database per lo scenario **`one`** che corrisponde al database, H2, in:
`resources\META-INF\scenarios`
`one.mv.db`
a cui corrisponde il file:
`one.trace.db`
Oltre questa modalità con i file `.db`, è supportato il recupero di tali file da un file `.zip` (per ridurre lo spazio utilizzato).
Altre modalità supportate:
**script aggiuntivo** a partire da un database esistente:
```java
@Scenario(name = "one", scripts = "META-INF/scenarios/add-datas.sql")
```
**script db completo** a partire da un database vuoto (in memoria):
```java
@Scenario(name = "one", asResource=false, scripts = "META-INF/scenarios/from-scratch.sql")
```
**script db multipli** a partire da un database vuoto in memoria (base non esiste come risorsa):
```java
@Scenario(name = "base", scripts = "META-INF/scenarios/from-scratch.sql,META-INF/scenarios/more.sql")
```
## Auto test per tutti i Pojo di un package (!)
```java
List<String> errors = ArmTest.instance().testPojosInPackage("com.armundia.armtest.testpojo");
// eventualmente aggiungere un controllo, ma già la riga precedente
// aumenta la coverage richiamando tanti get/set
assertEquals(0, errors.size(), "expected no errors");
```
Oppure:
````java
List<String> errors = ArmTest.instance().testPojos(
null, // nessuna annotation in particolare
"com.armundia.armtest.testpojo", "com.armundia.armtest.testpojo");
// eventualmente aggiungere un controllo, ma già la riga precedente
// aumenta la coverage richiamando tanti get/set
assertEquals(0, errors.size(), "expected no errors");
````
## MAVEN run test commands
da bash shell:
```bash
mvn test
```
con report coverage:
```bash
mvn test jacoco:report
```
## Future features
* verifica supporto Rest JAX-RS
## Release commands
Follow the steps to release a maven package into Nexus Repository
Assure that the `pom.xml` has the `<scm>` tag that points to the git developer connection and manage takes the HEAD commit of your repository.
```xml
<scm>
<developerConnection>scm:git:{git-http-connection}</developerConnection>
<tag>HEAD</tag>
</scm>
```
`mvn release:clean`
`mvn release:prepare`
After, if it goes all well, **push** the commits and the tag created by the execution and run:
`mvn release:perform`
If something about `mvn release:perform` goes wrong and we want to revert all the `release` stuff, we need to remove the commit and tag created by the execution and run:
`mvn release:rollback`
## Release
Qui sono indicate le release finora effettuate.
### Next Release
#### 1.0.2
Major release con:
* supporto CDI/Junit 5
* supporto scenari di test db, basati su h2
#### 0.0.1
* Released the package... TODO

View File

@ -0,0 +1,32 @@
DELETE FROM SE_AUTHORIZATION WHERE AUTHID = 'ADMIN.APP';
INSERT INTO SE_AUTHORIZATION (AUTHID, AUTHDESCR, APPLICATION)
VALUES (N'ADMIN.APP', N'Authorization to navigate admin application', NULL);
DELETE FROM SE_AUTHORIZATION WHERE AUTHID = 'APPLICATION';
INSERT INTO SE_AUTHORIZATION (AUTHID, AUTHDESCR, APPLICATION)
VALUES (N'APPLICATION', N'Authorization to navigate the application', NULL);
DELETE FROM SE_ROLES WHERE ROLEID = 'ADMIN';
INSERT INTO SE_ROLES (ROLEID, ROLEDESC, ID_USER_UPD, DATE_UPD, ID_USER_CRE, DATE_CRE, APPLICATION)
VALUES (N'ADMIN', N'Admin role', NULL, NULL, NULL, NULL, NULL);
DELETE FROM SE_ROLES WHERE ROLEID = 'USER';
INSERT INTO SE_ROLES (ROLEID, ROLEDESC, ID_USER_UPD, DATE_UPD, ID_USER_CRE, DATE_CRE, APPLICATION)
VALUES (N'USER', N'User role', NULL, NULL, NULL, NULL, NULL);
DELETE FROM SE_ROLES_AUTH WHERE ROLEID = 'ADMIN';
INSERT INTO SE_ROLES_AUTH (ROLEID, AUTHID, DTAUTH, ID_USER_UPD, DATE_UPD)
VALUES (N'ADMIN', N'ADMIN.APP', NULL, NULL, NULL);
INSERT INTO SE_ROLES_AUTH (ROLEID, AUTHID, DTAUTH, ID_USER_UPD, DATE_UPD)
VALUES (N'ADMIN', N'APPLICATION', NULL, NULL, NULL);
DELETE FROM SE_ROLES_AUTH WHERE ROLEID = 'USER';
INSERT INTO SE_ROLES_AUTH (ROLEID, AUTHID, DTAUTH, ID_USER_UPD, DATE_UPD)
VALUES (N'USER', N'APPLICATION', NULL, NULL, NULL);
DELETE FROM SE_USERS WHERE USERID = 'ZUXT2151';
INSERT INTO SE_USERS (USERID, NAME, LASTNAME, TITLE, EMAIL, PHONE, PASSWORD, FIRSTNAME, DTPASSWORD, LASTPASSWORD, COD_TYPEENT, USER_ACR, COD_LANG, COD_FIL, STATUS, EXT_KEY, COD_BANK, ID_USER_UPD, DATE_UPD, IS_FAKE, DATE_DELETED, DATE_LAST_LOGIN, TIPO_ACCOUNT, COD_COMPAGNIA, ID_PERSON, COD_PORT, IP_ADDRESS, PASS_TEMP, DATE_CRE, DATE_PREVIOUS_LOGIN, CLIENT_VERSION, DELETED, CONSECUTIVE_FAILURE, LOGIN_FAILURE, MASKED_MODE, LOGIN_SUCCESS, CODICE_INTERNO, IS_ENABLED, DATE_VERIFIED_EMAIL, DATE_VERIFIED_PHONE, EMAIL_TO_VERIFY, PHONE_TO_VERIFY, TOKEN_VERIFY_EMAIL, TOKEN_VERIFY_PHONE, DATE_EXPIRE_TOKEN_EMAIL, DATE_EXPIRE_TOKEN_PHONE, DATE_DISABLED, DATE_BLOCK, USERID_BO, COD_FISCALE, N_ISCRIZIONE_RUI, DT_ISCRIZIONE_RUI, DT_CANCELLAZIONE_RUI, COD_CDR, COD_REG, LICENSE, ACTIVE_JSESSIONID)
VALUES (N'ZUXT2151', NULL, N'Rosati', NULL, N'r.rosati@armundia.com', NULL, N'M/HBXZF8u1P6XWm3Lw+EQxrXD+fxZQjDPCbkTJPADYc=', NULL, '2024-01-01 00:00:00', NULL, NULL, NULL, N'IT', NULL, NULL, NULL, NULL, N'c.rosati', '2024-01-01 00:00:00', NULL, NULL, '2024-01-01 00:00:00', NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2024-01-01 00:00:00', NULL, NULL, 0, 4, NULL, 4893, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, N'@', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
DELETE FROM SE_USERS_ROLES WHERE USERID = 'ZUXT2151';
INSERT INTO SE_USERS_ROLES (USERID, ROLEID, DTROLE, ENABLED, ID_USER_UPD, DATE_UPD)
VALUES (N'ZUXT2151', N'ADMIN', '2024-01-01 00:00:00', 1, NULL, NULL);
INSERT INTO SE_USERS_ROLES (USERID, ROLEID, DTROLE, ENABLED, ID_USER_UPD, DATE_UPD)
VALUES (N'ZUXT2151', N'USER', '2024-01-01 00:00:00', 1, NULL, NULL);

View File

@ -0,0 +1,207 @@
-------------- arm-core-utility ----------------
-- AP_DOM
CREATE TABLE IF NOT EXISTs AP_DOM (
COD_CAMPO varchar(30) NOT NULL,
VAL_CAMPO varchar(100) NOT NULL,
DES_DOM varchar(4000) NULL,
COD_ML varchar(100) NULL,
PROD varchar(1) NULL,
TRASC_VAL varchar(4000) NULL,
ORD_VIS numeric(3,0) NULL,
TRASC_VAL_1 varchar(4000) NULL,
STATUS varchar(1) NULL,
INFO varchar(4000) NULL,
DATA_AGG TIMESTAMP NULL,
HIDE numeric(1,0) DEFAULT 0 NULL,
TRASC_VAL_2 varchar(250) NULL,
USERID_UPD varchar(30) NULL,
CONSTRAINT PK_AP_DOM PRIMARY KEY (COD_CAMPO, VAL_CAMPO)
);
-- AP_LANG
CREATE TABLE IF NOT EXISTS AP_LANG (
CODE varchar(200) NULL,
LANG varchar(2) NOT NULL,
DESCR varchar(4000) NULL,
DATA_AGG TIMESTAMP NULL,
ORIGIN varchar(10) NULL,
DESCR_MCHAR CLOB NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS PK_AP_LANG ON AP_LANG (CODE ASC, LANG ASC);
-- AP_OPZIONI
CREATE TABLE IF NOT EXISTS AP_OPZIONI (
USERID varchar(30) NOT NULL,
SEZIONE varchar(15) NOT NULL,
OPZIONE varchar(50) NOT NULL,
VALORE CLOB NULL,
DESCRIZIONE varchar(255) NULL,
TIPO varchar(1) DEFAULT 'S' NULL,
OVERRIDE varchar(1) DEFAULT 'N' NULL,
DATA_AGG TIMESTAMP NULL,
USERID_UPD varchar(30) NULL,
CONSTRAINT AP_OPZIONI_PK PRIMARY KEY (USERID, SEZIONE, OPZIONE)
);
-------------- arm-core-users ----------------
--SE_UO
CREATE TABLE IF NOT EXISTS SE_UO (
ID_UO BIGINT NOT NULL,
COD_UO varchar(30) NULL,
DESC_UO varchar(255) NULL,
ID_USER_MANAGER varchar(20) NULL,
ID_UO_FATHER BIGINT NULL,
NOTE varchar(500) NULL,
COD_ML_NOTE varchar(65) NULL,
DATE_UPDATE TIMESTAMP NULL,
ID_USER_UPD varchar(30) NULL,
DELETED TINYINT NULL,
COD_ML_DESC varchar(65) NULL,
GOAL_CONTRIBUTOR TINYINT NULL,
CENTRAL_OFFICE TINYINT NULL,
PROSPECT_MANAGER TINYINT NULL,
COD_FIL varchar(10) NULL,
EMAIL_UO varchar(256) NULL,
CONSTRAINT SE_UO_PK PRIMARY KEY (ID_UO)
);
--SE_USERS
CREATE TABLE IF NOT EXISTS SE_USERS (
USERID varchar(50) NOT NULL,
NAME varchar(50) NULL,
LASTNAME varchar(70) NULL,
TITLE varchar(10) NULL,
EMAIL varchar(290) NULL,
PHONE varchar(50) NULL,
PASSWORD varchar(255) NULL,
FIRSTNAME varchar(255) NULL,
DTPASSWORD TIMESTAMP NULL,
LASTPASSWORD varchar(255) NULL,
COD_TYPEENT varchar(10) NULL,
USER_ACR varchar(10) NULL,
COD_LANG varchar(10) NULL,
COD_FIL varchar(10) NULL,
STATUS varchar(1) NULL,
EXT_KEY varchar(20) NULL,
COD_BANK varchar(15) NULL,
ID_USER_UPD varchar(20) NULL,
DATE_UPD TIMESTAMP NULL,
IS_FAKE TINYINT NULL,
DATE_DELETED TIMESTAMP NULL,
DATE_LAST_LOGIN TIMESTAMP NULL,
TIPO_ACCOUNT varchar(20) NULL,
COD_COMPAGNIA varchar(30) NULL,
ID_PERSON BIGINT NULL,
COD_PORT varchar(16) NULL,
IP_ADDRESS varchar(100) NULL,
PASS_TEMP varchar(255) NULL,
DATE_CRE TIMESTAMP NULL,
DATE_PREVIOUS_LOGIN TIMESTAMP NULL,
CLIENT_VERSION varchar(50) NULL,
DELETED TINYINT NULL,
CONSECUTIVE_FAILURE BIGINT NULL,
LOGIN_FAILURE BIGINT NULL,
MASKED_MODE TINYINT NULL,
LOGIN_SUCCESS BIGINT NULL,
CODICE_INTERNO BIGINT NULL,
IS_ENABLED TINYINT NULL,
DATE_VERIFIED_EMAIL TIMESTAMP NULL,
DATE_VERIFIED_PHONE TIMESTAMP NULL,
EMAIL_TO_VERIFY varchar(50) NULL,
PHONE_TO_VERIFY varchar(50) NULL,
TOKEN_VERIFY_EMAIL varchar(10) NULL,
TOKEN_VERIFY_PHONE varchar(10) NULL,
DATE_EXPIRE_TOKEN_EMAIL TIMESTAMP NULL,
DATE_EXPIRE_TOKEN_PHONE TIMESTAMP NULL,
DATE_DISABLED TIMESTAMP NULL,
DATE_BLOCK TIMESTAMP NULL,
USERID_BO varchar(50) NULL,
COD_FISCALE varchar(30) NULL,
N_ISCRIZIONE_RUI varchar(30) NULL,
DT_ISCRIZIONE_RUI TIMESTAMP NULL,
DT_CANCELLAZIONE_RUI TIMESTAMP NULL,
COD_CDR varchar(30) NULL,
COD_REG varchar(10) NULL,
LICENSE TINYINT NULL,
ACTIVE_JSESSIONID varchar(255) NULL,
CONSTRAINT SE_USERS_PK PRIMARY KEY (USERID)
);
--SE_USERS_ACCESS
CREATE TABLE IF NOT EXISTS SE_USERS_ACCESS (
ID BIGINT AUTO_INCREMENT NOT NULL,
DATA_LOGIN TIMESTAMP NULL,
DATA_LOGOUT TIMESTAMP NULL,
USER_ID varchar(20) NULL,
IP_ADDRESS varchar(100) NULL,
USER_AGENT varchar(200) NULL,
SESSION_ID varchar(40) NULL,
RESULT_MESSAGE varchar(150) NULL,
RESULT_STATUS TINYINT NULL,
FROM_AVATAR TINYINT NULL,
CLIENT_VERSION varchar(50) NULL,
APPLICATION_NAME varchar(255) NULL,
CONSTRAINT SE_USERS_ACCESS_PK PRIMARY KEY (ID)
);
--SE_USERS_ROLES
CREATE TABLE IF NOT EXISTS SE_USERS_ROLES (
USERID varchar(20) NOT NULL,
ROLEID varchar(30) NOT NULL,
DTROLE TIMESTAMP NULL,
ENABLED TINYINT NULL,
ID_USER_UPD varchar(20) NULL,
DATE_UPD TIMESTAMP NULL,
CONSTRAINT SE_USERS_ROLES_PK PRIMARY KEY (USERID, ROLEID)
);
--SE_USERS_UO
CREATE TABLE IF NOT EXISTS SE_USERS_UO (
ID_USER varchar(20) NOT NULL,
ID_UO BIGINT NOT NULL,
MAIN TINYINT NULL,
DATE_ACT TIMESTAMP NULL,
CONSTRAINT SE_USERS_UO_PK PRIMARY KEY (ID_USER, ID_UO)
);
-- SE_AUTHORIZATION
CREATE TABLE IF NOT EXISTS SE_AUTHORIZATION (
AUTHID varchar(50) NOT NULL,
AUTHDESCR varchar(255) NULL,
APPLICATION varchar(3) NULL,
CONSTRAINT SE_AUTHORIZATION_PK PRIMARY KEY (AUTHID)
);
-- SE_HISTORY_USERS_UO
CREATE TABLE IF NOT EXISTS SE_HISTORY_USERS_UO (
ID_USER varchar(20) NULL,
ID_UO BIGINT NULL,
DATE_FROM TIMESTAMP NULL,
DATE_TO TIMESTAMP NULL,
DATE_ACT TIMESTAMP NULL
);
-- SE_ROLES
CREATE TABLE IF NOT EXISTS SE_ROLES (
ROLEID varchar(30) NOT NULL,
ROLEDESC varchar(255) NULL,
ID_USER_UPD varchar(20) NULL,
DATE_UPD TIMESTAMP NULL,
ID_USER_CRE varchar(20) NULL,
DATE_CRE TIMESTAMP NULL,
APPLICATION varchar(3) NULL,
CONSTRAINT SE_ROLES_PK PRIMARY KEY (ROLEID)
);
-- SE_ROLES_AUTH
CREATE TABLE IF NOT EXISTS SE_ROLES_AUTH (
ROLEID varchar(30) NOT NULL,
AUTHID varchar(50) NOT NULL,
DTAUTH TIMESTAMP NULL,
ID_USER_UPD varchar(20) NULL,
DATE_UPD TIMESTAMP NULL,
CONSTRAINT SE_ROLES_AUTH_PK PRIMARY KEY (ROLEID, AUTHID)
);

8
scripts/jboss/.env Normal file
View File

@ -0,0 +1,8 @@
# in seconds
UPDATE_TIME=5
# path to your jboss-eap-7.4
JBOSS_HOME=C:/Dev2012/servers/jboss-eap-7.4
# path to your jdk1.8.0_333
JAVA_HOME=C:/Program Files/Java/jdk1.8.0_333

View File

@ -0,0 +1,64 @@
@echo off
setlocal enabledelayedexpansion
REM Carica le variabili d'ambiente dal file .env
if exist .env (
for /f "usebackq tokens=*" %%i in (.env) do (
set %%i
)
) else (
echo .env file not found. Please create one with UPDATE_TIME and JBOSS_HOME variables.
exit /b 1
)
REM Controlla se le variabili d'ambiente richieste sono impostate
if not defined JBOSS_HOME (
echo JBOSS_HOME is not set. Please check your .env file.
exit /b 1
)
if not defined UPDATE_TIME (
echo UPDATE_TIME is not set. Please check your .env file.
exit /b 1
)
rem war folder
set "SOURCE_DIR=..\..\stub-war\target"
rem jboss deployment
set "DEST_DIR=%JBOSS_HOME%\standalone\deployments"
rem Imposta UPDATE_TIME come intero
set /a "UPDATE_TIME=%UPDATE_TIME%"
rem war to monitor
set "FILE_TO_MONITOR=stub-war.war"
echo Controllo esistenza delle directory...
if not defined DEST_DIR (
echo DEST_DIR non esiste: %DEST_DIR%
exit /b 1
)
if not defined SOURCE_DIR (
echo %~dp0
echo SOURCE_DIR non esiste: %SOURCE_DIR%
exit /b 1
)
rem Ottieni la data di modifica iniziale del file
set LAST_COPY_TIME=""
:loop
rem Ottieni la data di modifica attuale del file
for %%F in ("%SOURCE_DIR%\%FILE_TO_MONITOR%") do set CURRENT_LAST_EDIT_FILE_TIME=%%~tF
rem Confronta le date di modifica
if not "!CURRENT_LAST_EDIT_FILE_TIME!"=="!LAST_COPY_TIME!" (
echo CURRENT_LAST_EDIT_FILE_TIME = !CURRENT_LAST_EDIT_FILE_TIME!
rem Sposta il file se è stato modificato
copy "%SOURCE_DIR%\%FILE_TO_MONITOR%" "%DEST_DIR%\"
rem Aggiorna la data di modifica
set LAST_COPY_TIME=!CURRENT_LAST_EDIT_FILE_TIME!
echo LAST_COPY_TIME = !LAST_COPY_TIME!
)
timeout /t %UPDATE_TIME% >nul
goto loop

View File

@ -0,0 +1,48 @@
#!/bin/bash
if [ -f .env ]; then
source .env
else
echo ".env file not found. Please create one with JBOSS_HOME and JAVA_HOME variables."
exit 1
fi
# war folder
SOURCE_DIR="../../stub-war/target"
# jboss deployment
DEST_DIR="$JBOSS_HOME/standalone/deployments"
UPDATE_TIME=$(($UPDATE_TIME))
# war to monitor
FILE_TO_MONITOR="stub-war.war"
echo "Controllo esistenza delle directory..."
if [ ! -d "$DEST_DIR" ]; then
echo "DEST_DIR non esiste: $DEST_DIR"
exit 1
fi
if [ ! -d "$SOURCE_DIR" ]; then
echo "SOURCE_DIR non esiste: $SOURCE_DIR"
exit 1
fi
# Ottieni la data di modifica iniziale del file
LAST_MODIFIED=$(stat -c %Y "$SOURCE_DIR/$FILE_TO_MONITOR")
while true; do
# Ottieni la data di modifica attuale del file
CURRENT_MODIFIED=$(stat -c %Y "$SOURCE_DIR/$FILE_TO_MONITOR")
# Confronta le date di modifica
if [ "$CURRENT_MODIFIED" -ne "$LAST_MODIFIED" ]; then
# Sposta il file se è stato modificato
cp "$SOURCE_DIR/$FILE_TO_MONITOR" "$DEST_DIR/"
echo "File copiato in $DEST_DIR/"
# Aggiorna la data di modifica
LAST_MODIFIED=$CURRENT_MODIFIED
fi
sleep $UPDATE_TIME
done

View File

@ -0,0 +1,37 @@
@echo off
setlocal enabledelayedexpansion
REM Carica le variabili d'ambiente dal file .env
if exist .env (
for /f "usebackq tokens=*" %%i in (.env) do (
set %%i
)
) else (
echo .env file not found. Please create one with JBOSS_HOME and JAVA_HOME variables.
exit /b 1
)
REM Controlla se le variabili d'ambiente richieste sono impostate
if not defined JBOSS_HOME (
echo JBOSS_HOME is not set. Please check your .env file.
exit /b 1
)
if not defined JAVA_HOME (
echo JAVA_HOME is not set. Please check your .env file.
exit /b 1
)
REM Imposta la variabile di ambiente per Java
set "PATH=%JAVA_HOME%\bin;%PATH%"
echo JAVA_HOME = %JAVA_HOME%
echo JBOSS_HOME = %JBOSS_HOME%
REM Controlla se JBoss è già in esecuzione
tasklist | findstr /I "standalone.bat" >nul
if %errorlevel%==0 (
echo JBoss è già in esecuzione
) else (
echo Avvio di JBoss con standalone-full-stub.xml
start /B cmd /c "%JBOSS_HOME%\bin\standalone.bat" -Djboss.server.config.dir=../../stub-war/standalone -c standalone-full.xml
)

View File

@ -0,0 +1,29 @@
#!/bin/bash
# Load environment variables from .env file
if [ -f .env ]; then
source .env
else
echo ".env file not found. Please create one with JBOSS_HOME and JAVA_HOME variables."
exit 1
fi
# Check if required environment variables are set
if [ -z "$JBOSS_HOME" ] || [ -z "$JAVA_HOME" ]; then
echo "JBOSS_HOME or JAVA_HOME is not set. Please check your .env file."
exit 1
fi
# Imposta la variabile di ambiente per Java
export PATH="$JAVA_HOME/bin:$PATH"
echo JAVA_HOME = $JAVA_HOME
echo JBOSS_HOME = $JBOSS_HOME
# Controlla se JBoss è già in esecuzione
if grep -f "$JBOSS_HOME" > /dev/null; then
echo "JBoss è già in esecuzione"
else
echo "Avvio di JBoss con standalone-full-stub.xml"
nohup "$JBOSS_HOME/bin/standalone.sh" -c ../../stub-war/standalone/standalone-full.xml > /dev/null 2>&1 &
fi

View File

@ -0,0 +1,4 @@
# UPDATE_TIME=5 # in seconds
# JBOSS_HOME="C:/Dev2012/servers/jboss-eap-7.4" # path to your jboss-eap-7.4
# JAVA_HOME="C:/Program Files/Java/jdk1.8.0_333" # path to your jdk1.8.0_333

View File

@ -0,0 +1,4 @@
@echo off
cd /d "%~dp0..\stub-pom\"
mvn clean install -Pproduction
pause

14
scripts/run.bat Normal file
View File

@ -0,0 +1,14 @@
@echo off
cd /d "%~dp0\jboss"
REM Esegui il primo script in una nuova finestra
start cmd /k "monitor-war.bat"
REM Attendi 5 secondi
timeout /t 5 /nobreak
REM Esegui il secondo script in una nuova finestra
start cmd /k "start-jboss.bat"
pause

5
stub-gui/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules
release
package-lock.json
node
/target/

3
stub-gui/README.md Normal file
View File

@ -0,0 +1,3 @@
# stub-gui
Armundia gui stub application with integrated module bundler.

View File

@ -0,0 +1,10 @@
THEME="adv360v2"
REST_SERVER="localhost"
REST_BASE_URL="http://localhost:8080/stub-war/rest/"
SESSION_EXPIRED_TIMEOUT=180000
KEEP_ALIVE_MINUTES=5
AUTHENTICATION_METHOD="internal"
DEBUG_VIEW=true

View File

@ -0,0 +1,10 @@
THEME="adv360v2"
REST_SERVER="./"
REST_BASE_URL="../rest/"
SESSION_EXPIRED_TIMEOUT=180000
KEEP_ALIVE_MINUTES=3
AUTHENTICATION_METHOD="internal"
DEBUG_VIEW=false

5
stub-gui/gulpfile.js Normal file
View File

@ -0,0 +1,5 @@
const gulp = require("gulp");
const { release } = require("arm-gulp-builders");
gulp.task("build", gulp.series(...release.series.build));
gulp.task("watch", gulp.series(...release.series.watch));

37
stub-gui/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"version": "0.0.1",
"name": "stub-gui",
"description": "Stub - base application",
"devDependencies": {
"arm-gulp-builders": "3.0.0-13"
},
"dependencies": {
"@amcharts/amcharts5": "5.4.11",
"@redux-devtools/extension": "3.3.0",
"arm-common": "1.9.0",
"arm-core-layouts": "1.5.0",
"arm-core-querybuilder": "3.12.3",
"arm-file-uploader": "3.3.3",
"arm-gui": "4.17.2",
"arm-gui-grid-wj": "1.15.0",
"arm-router": "1.10.0",
"arm-splitter": "0.0.4",
"arm-themes": "2.11.0",
"arm-visual-component": "3.2.0",
"immutability-helper": "3.1.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-redux": "9.1.2",
"react-router-dom": "5.3.4",
"redux": "5.0.1",
"redux-thunk": "3.1.0"
},
"scripts": {
"watch": "npm i && gulp watch",
"build": "npm i && gulp build --env=production",
"build-prod": "npm i && gulp build --env=production --minified",
"watch-m": "npm i && gulp watch --max-old-space-size=4096",
"build-m": "npm i && gulp build --env=production --max-old-space-size=4096",
"build-prod-m": "npm i && gulp build --env=production --minified --max-old-space-size=4096"
}
}

119
stub-gui/pom.xml Normal file
View File

@ -0,0 +1,119 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.armundia</groupId>
<artifactId>stub-pom</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../stub-pom/pom.xml</relativePath>
</parent>
<artifactId>stub-gui</artifactId>
<properties>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<sonar.exclusions>node_modules/**,release/**, node/**, source/libs/**, target/**</sonar.exclusions>
<sonar.sources>source</sonar.sources>
</properties>
<profiles>
<profile>
<id>production</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<!-- Gestisce la compilazione del front
- Scarica ed installa le versioni corrette di node ed npm (nella cartella node)
- lancia il comando npm per il build
-->
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.11.3</version>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v16.13.2</nodeVersion>
<npmVersion>8.4.0</npmVersion>
</configuration>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build-prod-m</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<!-- Gestisco la copia dei file compilati all'interno del progetto web in modo che
il jar generato abbia tutto il necessario -->
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>install</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/../stub-war/src/main/webapp/app</outputDirectory>
<resources>
<resource>
<directory>release</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<!-- Evitare la generazione del jar visto che siamo in un progetto gui -->
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
<configuration>
<finalName>unwanted</finalName>
<classifier>unwanted</classifier>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<!-- Evitare lo step di install ...
non è un progetto pom vero ma un progetto GUI,
ci pensa il copy-resources
a spostare il compilato all'interno del progetto web -->
<artifactId>maven-install-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>default-install</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>@@NAME@@</title>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes, shrink-to-fit=no">
<link rel="icon" type="image/png" href="./images/favicon/favicon-96x96.png" sizes="96x96" />
<link rel="stylesheet" href="libs/fontawesome-free-5.15.3/css/all.css"/>
<script type="module" src="libs/amcharts-5.5.2/index.js"></script>
<script type="module" src="libs/amcharts-5.5.2/xy.js"></script>
<script type="module" src="libs/amcharts-5.5.2/percent.js"></script>
<script type="module" src="libs/amcharts-5.5.2/themes/Animated.js"></script>
<script type="module" src="libs/amcharts-5.5.2/locales/it_IT.js"></script>
</head>
<body>
<div id="main-react-app"></div>
</body>
</html>

View File

@ -0,0 +1,38 @@
import Rest from './webapi/Rest';
class KeepAliveWorkerManager {
worker = null;
keepAliveResultHandler = null;
init = (keepAliveResultHandler, minutes = 5, url = 'session/keep-alive') => {
if (window.Worker) {
this.keepAliveResultHandler = keepAliveResultHandler;
this.worker = new Worker('libs/KeepAliveWorker.js');
this.worker.postMessage({ type: "KEEP_ALIVE_TEST" });
const fullUrl = Rest.createRestLink(url);
this.worker.postMessage({
type: "START_KEEP_ALIVE",
data: {
url: fullUrl,
minutes
}
});
this.worker.onmessage = this.onmessage.bind(this);
}
}
onmessage = (e) => {
const { type, data } = e.data;
console.log('Message received from [KeepAliveWorker]', e.data);
if (this.keepAliveResultHandler && type === 'KEEP_ALIVE_RESULT') {
this.keepAliveResultHandler(data);
}
}
}
export default new KeepAliveWorkerManager();

View File

@ -0,0 +1,63 @@
import "@babel/polyfill";
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { AppConstants } from './constants/AppConstants.js';
import { init } from "./init.js";
import { store } from './reducers/store.js';
import MasterPageBase from './ui/MasterPageBase.jsx';
function MainReactApp() {
useEffect(() => {
console.log("\tAppConstants.DEBUG_VIEW: " + AppConstants.DEBUG_VIEW);
console.log("\tprocess.env.NODE_ENV: " + process.env.NODE_ENV);
console.log("\tReact.version: " + React.version);
}, []);
return (
<ErrorBoundary>
<div className={AppConstants.THEME}>
<MasterPageBase />
</div>
</ErrorBoundary>
);
}
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
// logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
console.warn(`Start application: ${AppConstants.APP_NAME}`);
init();
const reactRoot = document.getElementById('main-react-app');
const root = ReactDOM.createRoot(reactRoot);
root.render(
<Provider store={store} >
<MainReactApp />
</Provider>
);

View File

@ -0,0 +1,100 @@
const EnumSessionWorker = Object.freeze({
MESSAGE: 'SessionWorker.MESSAGE',
START: 'SessionWorker.START',
CLEAR: 'SessionWorker.CLEAR',
TIMEOUT: 'SessionWorker.TIMEOUT',
});
class SessionWorkerManager {
/**
* worker
* @type {(Worker|null)}
*/
activeWorker = null;
/** Milliseconds to expire session */
sessionExpiredTimeout = 60000 * 30;
setSessionExpiredTimeout(sessionExpiredTimeout){
this.sessionExpiredTimeout = sessionExpiredTimeout;
}
/**
* function execute on session timeout
* @type {Function}
*/
expireFunction = null;
setExpireFunction(expireFunction){
this.expireFunction = expireFunction;
}
name(message){
return `[SessionWorkerManager]: ${message}`;
}
/**
* Initialize the worker and set the expireFunction
* @param {Function} expireFunction function execute on session timeout
*/
start() {
if (window.Worker) {
console.log(this.name('start Worker conversation'));
this.activeWorker = new Worker('sessionWorker.js');
this.activeWorker.onmessage = this._messageRecieverHandler.bind(this);
this._message("SONO WORKER... SESSION WORKER");
this._sendData(EnumSessionWorker.START, this.sessionExpiredTimeout);
}
}
clear() {
if (this.activeWorker) this._sendData(EnumSessionWorker.CLEAR);
}
end() {
if (this.activeWorker) {
this.activeWorker.terminate();
delete this.activeWorker;
}
}
/**
* Message from Worker
* @param {MessageEvent} ev message event
*/
_messageRecieverHandler(ev){
const { type, data } = ev.data;
switch (type) {
case EnumSessionWorker.TIMEOUT:
console.log(this.name('session timeout'));
if (this.expireFunction) this.expireFunction();
this.end();
break;
default:
console.log(this.name('message recieved'), { type, data });
}
}
/**
* Send string message to active worker
* @param {string} message
*/
_message(message) {
this._sendData(EnumSessionWorker.MESSAGE, message);
}
/**
* Send message to active worker
* @param {string} type
* @param {*} data
*/
_sendData(type, data){
if (this.activeWorker) {
console.log(this.name('send data'), {type, data})
this.activeWorker.postMessage({ type, data });
}
}
}
export default new SessionWorkerManager();

View File

@ -0,0 +1,83 @@
import { i18n } from 'arm-common';
import { NotificationManager } from 'arm-core-layouts';
import { AppConstants } from '../constants/AppConstants.js';
import { DOMAINS_PRELOGIN } from '../constants/Domini.js';
import OPZIONI from '../constants/Opzioni.js';
import AccountActions, { EnumAccountActions } from '../ui/account/AccountActions.js';
import Rest from '../webapi/Rest.js';
import BaseActions from './BaseActions.js';
export const EnumAppActions = Object.freeze({
WINDOW_WIDTH_CHANGED: 'AppActions.WINDOW_WIDTH_CHANGED',
NOTIFY_MESSAGE_ADD: 'AppActions.NOTIFY_MESSAGE_ADD',
NOTIFY_MESSAGE_DELETE: 'AppActions.NOTIFY_MESSAGE_DELETE',
NOTIFY_MESSAGE_CLEAR: 'AppActions.NOTIFY_MESSAGE_CLEAR',
LOADER_SHOW: 'AppActions.LOADER_SHOW',
LOADER_HIDE: 'AppActions.LOADER_HIDE',
LOADER_USER_SHOW: 'AppActions.LOADER_USER_SHOW',
LOADER_USER_HIDE: 'AppActions.LOADER_USER_HIDE',
LOAD_OPZIONI: 'AppActions.LOAD_OPZIONI',
LOAD_DOMINI: 'AppActions.LOAD_DOMINI_APP',
LOAD_I18N: 'AppActions.LOAD_I18N',
LOCATE_CHANGE: 'AppActions.LOCATE_CHANGE',
CHANGE_URL: 'AppActions.CHANGE_URL',
LOADING_DELAY_STREAMS: 'AppActions.app_actions.LOADING_DELAY_STREAMS',
});
export const EnumNotifyType = Object.freeze({
Success: 'SUCCESS',
Info: 'INFO',
Warning: 'WARNING',
Danger: 'DANGER'
});
class AppActions extends BaseActions {
getReducer(getState) {
return getState().app;
}
preLoginLoadUtilities() {
return function (dispatch) {
const utilities = {
codiciOpzioni: OPZIONI,
codiciDomini: DOMAINS_PRELOGIN
};
return dispatch(AccountActions.loadUtilities(utilities));
}
}
addNotify(tipo, messaggio) {
switch (tipo) {
case (EnumNotifyType.Info):
NotificationManager.info(messaggio);
break;
case (EnumNotifyType.Success):
NotificationManager.success(messaggio);
break;
case (EnumNotifyType.Warning):
NotificationManager.warning(messaggio, { timeOut: 10000 });
break;
case (EnumNotifyType.Danger):
NotificationManager.error(messaggio, { timeOut: 0 });
break;
}
}
deleteNotify(id) {
return { type: EnumAppActions.NOTIFY_MESSAGE_DELETE, messageId: id };
}
clearNotify() {
return { type: EnumAppActions.NOTIFY_MESSAGE_CLEAR }
}
}
export default new AppActions();

View File

@ -0,0 +1,191 @@
import update from 'immutability-helper';
import { AppConstants } from '../constants/AppConstants.js';
import { EnumAccountActions } from '../ui/account/AccountActions.js';
import { EnumAppActions } from './AppActions.js';
const initialState = {
locate: AppConstants.DEFAULT_LANG,
listOfNotify: null,
jsessionId: null,
environment: null,
isSSO: false,
serverid: null,
loadingStack: 0,
loadingState: 'hide',
authorizations: {},
domini: {},
dominiLoaded: false,
i18nlabels: {},
i18nlabelsLoaded: true, // TODO riportare a false e nel loading ri-set to true
opzioni: {},
opzioniLoaded: false,
loggedOut: false,
versionInformation: null,
debugObj: null,
showPopUp: false,
alog: null,
showAlogPopup: false,
ritardoCaricamentoFlussi: false
};
const app = (state, action) => {
if (typeof state === 'undefined') {
return initialState
}
let notifyCollection = null;
switch (action.type) {
case EnumAccountActions.LOGIN_SUCCESS:
case EnumAccountActions.SSO:
let authorizations = action.authorizations;
let jsessionId = action.jsessionId;
let environment = null;
let isSSO = false;
if (action.type === EnumAccountActions.SSO) {
isSSO = action.datiUtente.userId !== null && action.datiUtente.userId !== undefined;
}
if (action.datiUtente) {
authorizations = action.datiUtente.authorizations;
jsessionId = action.datiUtente.jsessionId;
environment = action.datiUtente.environment;
}
if (action.user) {
authorizations = action.user.authorizations;
jsessionId = action.user.jsessionId;
}
let authMap = {};
if (authorizations) {
authorizations.forEach(element => {
authMap[element] = element;
});
}
return update(state, {
authorizations: { $set: authMap },
jsessionId: { $set: jsessionId },
environment: { $set: environment },
isSSO: { $set: isSSO },
listOfNotify: { $set: null }
});
case EnumAccountActions.LOGOUT:
return update(state, {
loggedOut: { $set: true },
datiUtente: { $set: {} }
});
case EnumAppActions.NOTIFY_MESSAGE_ADD:
if (state.listOfNotify === null) {
notifyCollection = new Map();
} else {
//devo clonare la lista altrimenti non vengono intercettati i cambiamenti
notifyCollection = new Map(state.listOfNotify);
}
if (action.messageId === null || action.messageId === undefined) {
action.messageId = new Date().getTime();
}
notifyCollection.set(Number(action.messageId), {
message: action.message,
messageType: action.messageType,
messageId: action.messageId
});
return update(state, { listOfNotify: { $set: notifyCollection } });
case EnumAppActions.NOTIFY_MESSAGE_DELETE:
//devo clonare la lista altrimenti non vengono intercettati i cambiamenti
notifyCollection = new Map(state.listOfNotify);
notifyCollection.delete(action.messageId);
return update(state, { listOfNotify: { $set: notifyCollection } });
case EnumAppActions.NOTIFY_MESSAGE_CLEAR:
return update(state, { listOfNotify: { $set: null } });
case EnumAppActions.LOAD_DOMINI:
const domini = update(state.domini, { $merge: action.domini });
return update(state, {
domini: { $set: domini },
dominiLoaded: { $set: true }
});
case EnumAppActions.LOAD_OPZIONI:
const opzioni = update(state.opzioni, { $merge: action.opzioni });
return update(state, {
opzioni: { $set: opzioni },
opzioniLoaded: { $set: true }
});
case EnumAppActions.LOAD_I18N:
return update(state, {
i18nlabels: { $merge: action.i18nlabels }
});
case EnumAppActions.WINDOW_WIDTH_CHANGED:
return update(state, { deviceSize: { $set: action.deviceSize } });
case EnumAppActions.LOADER_SHOW:
if (state.loadingState === 'show') {
return update(state, { loadingStack: { $set: state.loadingStack + 1 } });
}
return update(state, {
loadingState: { $set: "show" },
loadingStack: { $set: 1 }
});
case EnumAppActions.LOADER_HIDE:
if (state.loadingState === 'hide') {
return update(state, {
loadingState: { $set: "hide" },
loadingStack: { $set: 0 }
});
}
if (state.loadingStack > 1) {
return update(state, {
loadingStack: { $set: state.loadingStack - 1 }
});
}
return update(state, {
loadingState: { $set: "hide" },
loadingStack: { $set: 0 }
});
case EnumAppActions.LOCATE_CHANGE:
return update(state, { locate: { $set: action.locate } });
case EnumAppActions.LOADING_DELAY_STREAMS:
return update(state, { ritardoCaricamentoFlussi: { $set: action.ritardoCaricamentoFlussi } });
default:
return state;
}
}
export default app;

View File

@ -0,0 +1,41 @@
import { AppConstants } from '../constants/AppConstants.js';
import { EnumAppActions } from './AppActions.js';
class BaseActions {
constructor() {}
get AppConstants() {
return AppConstants;
}
getErrorMessage(jqxhr, textStatus, error) {
let message = null;
console.log("Request Failed: " + textStatus + ", " + error + ' ' + jqxhr.responseText);
if (jqxhr.readyState === 4) {
// HTTP error (can be checked by XMLHttpRequest.status and XMLHttpRequest.statusText)
message = textStatus + ", " + error + ' ' + jqxhr.responseText;
}
else if (jqxhr.readyState === 0) {
// Network error (i.e. connection refused, access denied due to CORS, etc.)
message = textStatus + ", network connection";
}
else {
// something weird is happening
message = textStatus + ", " + error + ' ' + jqxhr.responseText;
}
return message;
}
showLoader() {
return { type: EnumAppActions.LOADER_SHOW }
}
hideLoader() {
return { type: EnumAppActions.LOADER_HIDE }
}
}
export default BaseActions;

View File

@ -0,0 +1,18 @@
export const AppConstants = Object.freeze({
APP_NAME: process.env.DESCRIPTION, //package.json description
APP_CODE: process.env.NAME, // package.json name
VERSION: process.env.VERSION,
REST_SERVER: process.env.REST_SERVER,
REST_BASE_URL: process.env.REST_BASE_URL,
SESSION_EXPIRED_TIMEOUT: process.env.SESSION_EXPIRED_TIMEOUT,
KEEP_ALIVE_MINUTES: process.env.KEEP_ALIVE_MINUTES,
DEFAULT_LANG: process.env.DEFAULT_LANG || 'IT',
I18N_CODES: [''],
AUTHENTICATION_METHOD: process.env.AUTHENTICATION_METHOD,
DEBUG_VIEW: process.env.DEBUG_VIEW,
THEME: process.env.THEME
});

View File

@ -0,0 +1,7 @@
/** @type {string[]} */
const DOMAINS = [];
/** @type {string[]} */
const DOMAINS_PRELOGIN = [];
export { DOMAINS_PRELOGIN };
export default DOMAINS;

View File

@ -0,0 +1,10 @@
/**
* Array of options.
*
* @type {Array<{ opzione: string, sezione: string }>}
*/
const OPZIONI = [
{ opzione: 'DIVRIF', sezione: 'GLOBALE' }
];
export default OPZIONI;

View File

@ -0,0 +1,14 @@
import { armundiaInit } from "arm-common";
import { setLicenseKey } from "arm-gui-grid-wj";
//components initialization
import { store } from './reducers/store';
export function init(){
//connects redux store to arm-common
armundiaInit(store, null);
//set wijmo license key
setLicenseKey('armundia-20240430-all,925232318584228#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#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');
}

View File

@ -0,0 +1,15 @@
import { QbLinkReducer, QbReducer } from "arm-core-querybuilder";
import { combineReducers } from "redux";
import app from "../app/AppReducer.js";
import account from "../ui/account/AccountReducer.js";
const appReducers = combineReducers({
app,
account,
QbReducer,
QbLinkReducer,
});
export default appReducers;

View File

@ -0,0 +1,15 @@
import { legacy_createStore as createStore, applyMiddleware } from 'redux';
import { thunk} from 'redux-thunk';
import rootReducer from '.';
import { composeWithDevTools } from '@redux-devtools/extension';
const composeEnhancers = composeWithDevTools({
// Specify name here, actionsBlacklist, actionsCreators and other options if needed
});
export const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunk))
);

View File

@ -0,0 +1,61 @@
import { ApplicationContainer, MasterPageContainer, PageBody, PageContainer, PageHeader, PageHeaderContainer, TopBarRouting } from "arm-core-layouts";
import React, { PureComponent } from 'react';
export function getMasterPageMenuInfo() {
return [
{ Component: RouteLabelPrinter, pathKey: 'section-1', label: 'Section 1', visible: true, auth: true },
{
label: 'Section 2', routesInfo: [
{ Component: RouteLabelPrinter, pathKey: 'section-2.0', label: 'Section 2.0', visible: true, auth: true },
{ Component: RouteLabelPrinter, pathKey: 'section-2.1', label: 'Section 2.1', visible: true, auth: true }
]
}
];
}
export default class MasterPage extends PureComponent {
state = {
menuInfo: getMasterPageMenuInfo(),
userMenu: [
{
label: 'user',
className: 'hoverless' //hoverless: unclickable
},
// {
// label: i18n('USER_INFO.LOG_OUT', 'Disconnettiti'),
// onClick: () => logoutOnClick && logoutOnClick().then(() => history.replace('logout'))
// }
]
}
render() {
const { menuInfo, userMenu } = this.state;
return (
<ApplicationContainer>
<TopBarRouting
menuInfo={menuInfo}
userMenu={userMenu}
/>
</ApplicationContainer>
);
}
}
function RouteLabelPrinter({ routeLabel }) {
return (
<MasterPageContainer>
<PageContainer>
<PageHeaderContainer>
<PageHeader
title={routeLabel}
/>
</PageHeaderContainer>
<PageBody>
<h1>{routeLabel}</h1>
</PageBody>
</PageContainer>
</MasterPageContainer>
)
}

View File

@ -0,0 +1,53 @@
import { NotificationContainer } from "arm-core-layouts";
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { HashRouter, Route, Switch } from "react-router-dom";
import AppActions from "../app/AppActions.js";
import { AppConstants } from "../constants/AppConstants.js";
import MasterPage from "./MasterPage.jsx";
import NotFound from "./NotFoundPage.jsx";
import PrivateRoute from "./PrivateRoute.jsx";
import Standalone from "./Standalone.jsx";
import AliveStatusMessage from "./account/AliveStatusMessage.jsx";
import Login from "./account/Login.jsx";
import SessionExpired from "./account/SessionExpired.jsx";
import Loading from "./shared/Loading.jsx";
import Notify from "./shared/Notify.jsx";
import TestMasterPage from "./test/TestMasterPage.jsx";
export default function MasterPageBase(props){
const { preLoginLoadUtilities } = props;
useEffect(() => {
preLoginLoadUtilities();
}, []);
return (
<HashRouter>
<Switch>
<Route path="/">
<Loading />
<Notify />
<NotificationContainer />
<SessionExpired/>
<AliveStatusMessage/>
<Switch>
<Route path="/login" component={Login} />
{AppConstants.DEBUG_VIEW && <Route path="/test" component={TestMasterPage} />}
<Route path="/standalone" component={Standalone} />
<PrivateRoute path="/" component={MasterPage} />
<Route component={NotFound} />
</Switch>
</Route>
</Switch>
</HashRouter>
)
}
const mapDispatchToProps = (dispatch) => ({
preLoginLoadUtilities: () => dispatch(AppActions.preLoginLoadUtilities())
})
MasterPageBase = connect(null, mapDispatchToProps)(MasterPageBase);

View File

@ -0,0 +1,36 @@
import React from 'react';
import agui from 'arm-gui';
class NotFound extends React.Component {
constructor(props) {
super(props);
}
render() {
let url = this.props.history.location.pathname;
console.log("[NotFoundPage - new React]PAGE NOT FOUND WITH URL:", url, this.context.router);
return (
<agui.aCard>
{/* <agui.aCardHeader className="stand-alone">
<h1>Page not found</h1>
</agui.aCardHeader>
<agui.aCardBody>
<div >
<p>The requested URL <b>{'#'}{url}</b> was not found.</p>
<p>Please check misspelling and try again.</p>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</agui.aCardBody> */}
</agui.aCard>
);
}
}
export default NotFound;

View File

@ -0,0 +1,25 @@
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
export default function PrivateRoute(props) {
const { path, utente, component: Component } = props;
return (
<Route path={path} render={(properties) => (
utente
? <Component {...properties} />
: <Redirect to={{
pathname: '/login',
state: { from: properties.location }
}} />
)} />
);
}
const mapStateToProps = (state) => ({
utente: state.account.utente
});
PrivateRoute = connect(mapStateToProps)(PrivateRoute);

View File

@ -0,0 +1,111 @@
import { ApplicationContainer } from 'arm-core-layouts';
import { Routing } from 'arm-router';
import React, { PureComponent, useCallback, useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Prompt, Route, Switch, useHistory } from 'react-router-dom';
import AccountActions from './account/AccountActions.js';
import { getMasterPageMenuInfo } from './MasterPage.jsx';
export default class Standalone extends PureComponent {
state = { to: '' }
componentDidMount() {
this.initStandalone();
AccountActions.getSession().then(this.checkIsLogged);
}
initStandalone() {
const { history } = this.props; //route props
this.setState(() => ({ to: this.getBasePath() }));
history.push('/standalone');
}
getBasePath(){
const { pathname } = this.props.location; //route props
const crumbs = pathname.split('/');
crumbs.splice(0, 1); //rimuovo lo spazio creato dal primo slash
if (pathname.includes('pk')){
return `/${crumbs[0]}/${crumbs[1]}/${crumbs[2]}/${crumbs[3]}/${crumbs[4]}`;
}
return `/${crumbs[0]}/${crumbs[1]}`;
}
checkIsLogged = (utente) => {
const {
history, match, //route props
postLogin //dispatch functions
} = this.props;
const { path } = match;
const { to } = this.state;
if (!utente) history.push(`${path}/login`);
else {
postLogin(utente).then(() => history.push(to));
}
}
getPaths() {
const { utente } = this.props;
if (!utente) return null;
return <Routing routesInfo={getMasterPageMenuInfo()}/>
}
render() {
const { utente, opzioni } = this.props;
const { path } = this.props.match;
const { to, section } = this.state;
return (
<>
<ApplicationContainer>
<Switch>
<Route path={`${path}/login`} render={(props) => <Login {...props} to={to} />} />
{this.getPaths()}
{utente && <Route component={NotFound} />}
</Switch>
</ApplicationContainer>
<PromptStandalone />
</>
);
}
}
const mapStateToProps = (state) => ({
utente: state.account.utente,
opzioni: state.app.opzioni
});
const mapDispatchToProps = (dispatch) => ({
postLogin: (resolve) => dispatch(AccountActions.postLogin(resolve))
});
Standalone = connect(mapStateToProps, mapDispatchToProps)(Standalone);
function PromptStandalone(){
const history = useHistory();
const [ nextLocation, setNextLocation ] = useState('');
useEffect(() => {
if (nextLocation) history.push(nextLocation);
if (nextLocation !== '') setNextLocation('');
}, [nextLocation]);
const checkIsInStandalone = useCallback((location) => {
const pathname = location.pathname;
if (pathname.includes('standalone')) return true;
const newLocation = `/standalone${pathname}`;
console.log('[PromptStandalone]: converting location', location, 'to', newLocation);
setNextLocation(newLocation);
return false;
}, [history]);
return <Prompt when={true} message={checkIsInStandalone} />
}

View File

@ -0,0 +1,141 @@

import { AppConstants } from '../../constants/AppConstants.js';
import SessionWorkerManager from '../../SessionWorkerManager.js';
import AppActions, { EnumAppActions } from '../../app/AppActions.js';
import BaseActions from '../../app/BaseActions.js';
import DOMAINS from '../../constants/Domini.js';
import OPZIONI from '../../constants/Opzioni.js';
import Rest from '../../webapi/Rest.js';
import { NotificationManager } from 'arm-core-layouts';
import { i18n } from 'arm-common';
import KeepAliveWorkerManager from '../../KeepAliveWorkerManager.js';
export const EnumAccountActions = Object.freeze({
LOGIN_SUCCESS: 'EnumAccountActions.LOGIN_SUCCESS',
LOGIN_FAILED: 'EnumAccountActions.LOGIN_FAILED',
LOGOUT: 'EnumAccountActions.LOGOUT',
SSO: 'EnumAccountActions.SSO',
UPDATE_FORM: 'EnumAccountActions.UPDATE_FORM',
ALIVE_STATUS: 'EnumAccountActions.ALIVE_STATUS',
SESSION_EXPIRED: 'account_actions.SESSION_EXPIRED',
});
const BASE_PATH = 'authentication';
class AccountActions extends BaseActions {
getReducer(state) {
if (typeof state === 'function') state = state();
return state.account;
}
createPath(path) {
return `${BASE_PATH}/${path}`;
}
login() {
return async (dispatch, getState) => {
const { userid, password } = this.getReducer(getState);
try {
const utente = await Rest.postForm(this.createPath('login'), { userid, password, clientVersion: AppConstants.VERSION }, dispatch);
await dispatch(this.postLogin(utente));
const username = utente.name || utente.lastName || ' ';
NotificationManager.success(i18n("APP.ACCOUNT.WELCOME", "Benvenuto #username#!", "username", username));
return utente;
} catch (error) {
console.warn("[AccountActions.js]: Authentication failed", error);
dispatch({ type: EnumAccountActions.LOGIN_FAILED });
}
return Promise.reject();
}
}
postLogin(resolve) {
return async (dispatch) => {
console.log("[AccountActions.js]: Authentication success");
dispatch({ type: EnumAccountActions.LOGIN_SUCCESS, user: resolve });
console.log("[AccountActions.js]: user language", resolve.codLang);
KeepAliveWorkerManager.init(
(aliveStatus) => dispatch(this.setAliveStatus(aliveStatus)),
AppConstants.KEEP_ALIVE_MINUTES
);
await dispatch(this.loadUtilities());
return resolve;
}
}
setAliveStatus(aliveStatus) {
return {
type: EnumAccountActions.ALIVE_STATUS,
aliveStatus: aliveStatus
}
}
logout() {
return dispatch => Rest.post("authentication/logout", null, dispatch).then(
resolve => {
dispatch({ type: EnumAccountActions.LOGOUT });
return resolve;
},
reject => reject
);
}
loadUtilities(utilities) {
console.log("[AccountActions.js]: get OPTIONS, DOMAINS and AP_LANG");
if (!utilities) {
// Utilities di default.
utilities = {
codiciOpzioni: OPZIONI,
codiciDomini: DOMAINS,
codiciApLang: AppConstants.I18N_CODES
};
}
return (dispatch) => {
return Rest.post("utilities/load", utilities, dispatch, null).then(
resolve => {
dispatch({
type: EnumAppActions.LOAD_OPZIONI,
opzioni: resolve.opzioni || [],
});
dispatch({
type: EnumAppActions.LOAD_DOMINI,
domini: resolve.domini || []
});
dispatch({
type: EnumAppActions.LOAD_I18N,
i18nlabels: resolve.apLang
});
return resolve;
}
)
}
}
updateForm(value, key) {
return {
type: EnumAccountActions.UPDATE_FORM,
value, key
}
}
logoutExpiredSession() {
return { type: EnumAccountActions.SESSION_EXPIRED }
}
}
export default AccountActions = new AccountActions;

View File

@ -0,0 +1,51 @@
import update from "immutability-helper";
import { EnumAccountActions } from "./AccountActions.js";
const initialState = {
utente: null,
userid: null,
password: null,
aliveStatus: {
application: true,
session: true
},
sessionExpired: false
}
function account(state, action) {
if (typeof state === "undefined") {
return initialState
}
switch (action.type) {
case EnumAccountActions.LOGIN_SUCCESS:
console.log("Welcome " + action.user.userId);
return update(state, { utente: { $set: action.user } });
case EnumAccountActions.SSO:
return update(state, {
userid: { $set: action.datiUtente.userId }
});
case EnumAccountActions.LOGIN_FAILED:
case EnumAccountActions.LOGOUT:
return update(state, { $set: initialState });
case EnumAccountActions.UPDATE_FORM:
return update(state, { [action.key]: { $set: action.value } });
case EnumAccountActions.SESSION_EXPIRED:
return update(state, { sessionExpired: { $set: true } });
case EnumAccountActions.ALIVE_STATUS:
return update(state, { aliveStatus: { $set: action.aliveStatus } });
default:
return state;
}
}
export default account;

View File

@ -0,0 +1,80 @@
import { i18n } from 'arm-common';
import agui from 'arm-gui';
import React, { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
const selectorName = 'stub-gui-alive-status-message';
export default function AliveStatusMessage(props) {
const { aliveStatus } = props;
const reload = useCallback(() => {
location.reload();
}, [history])
const showMessage = !aliveStatus.application || !aliveStatus.session;
const text = useMemo(() => {
if (!aliveStatus.session) {
return {
header: i18n(`CMA.ALIVE_STATUS.SESSION_EXPIRED`, 'Sessione scaduta'),
messages: [
i18n(`CMA.ALIVE_STATUS.SESSION_EXPIRED.MSG.USER_DISCONNECTED`, 'Utente disconnesso.'),
i18n(`CMA.ALIVE_STATUS.SESSION_EXPIRED.MSG`, 'Per riconnettersi premere il tasto "Ricarica" oppure ricaricare la pagina.')
]
};
}
return {
header: i18n(`CMA.ALIVE_STATUS.APPLICATION_DOWN`, 'Applicazione non attiva'),
messages: [
i18n(`CMA.ALIVE_STATUS.APPLICATION_DOWN.NOT_AVAILABLE`, 'Applicazione momentaneamente non disponibile.'),
i18n(`CMA.ALIVE_STATUS.APPLICATION_DOWN.MSG`, 'La preghiamo di contattare l\'amministratore, oppure ricaricare la pagina.')
]
};
}, [aliveStatus]);
return (
<agui.PopupDialog
show={showMessage}
className={selectorName}
addBackDrop
onClose={reload}
>
<agui.PopupDialog.Header>
{text.header}
</agui.PopupDialog.Header>
<agui.PopupDialog.Body>
<p>
{text.messages.map((message, index) => (
index === text.messages.length - 1 ?
<React.Fragment key={`${selectorName}-${index}`}>{message}</React.Fragment> :
<React.Fragment key={`${selectorName}-${index}`}>
{message}
<br />
</React.Fragment>
))}
</p>
</agui.PopupDialog.Body>
<agui.PopupDialog.Footer>
<div style={{ marginLeft: 'auto' }} />
<agui.aButton
bs='primary'
onClick={reload}
>
{i18n(`CMA.SESSION_EXPIRED.RELOAD`, 'Ricarica')}
</agui.aButton>
</agui.PopupDialog.Footer>
</agui.PopupDialog>
);
}
const mapStateToProps = (state) => ({
aliveStatus: state.account.aliveStatus
})
AliveStatusMessage = connect(mapStateToProps)(AliveStatusMessage);

View File

@ -0,0 +1,5 @@
@aliveStatusMessagePrefix: cma-alive-status-message;
.@{aliveStatusMessagePrefix} {
width: min(100%, 40rem);
}

View File

@ -0,0 +1,108 @@
import { i18n } from 'arm-common';
import { HBox, LoginContainer, VBox } from 'arm-core-layouts';
import agui, { CoolValidator } from "arm-gui";
import React from "react";
import { connect } from "react-redux";
import { AppConstants } from "../../constants/AppConstants.js";
import AccountActions from "./AccountActions.js";
export default class Login extends React.Component {
state = {
useridRule: {
required: i18n("APP.LOGIN.USER", "Nome utente")
},
passwordRule: {
required: i18n("APP.LOGIN.PSW", "Password")
},
otherErrors: null
}
login = async () => {
const { login } = this.props;
try {
await login();
this.goThrough();
} catch (e) {}
}
goThrough = () => {
const { history, to } = this.props;
history.push({ pathname: to || "/", state: { showCollapse: true } });
}
goToTest = async () => {
const { history } = this.props;
history.push({ pathname: "/test" });
}
onEnter = (e) => e.key === 'Enter' && this.login()
render() {
const { userid, password, updateForm } = this.props;
const { useridRule, passwordRule, otherErrors } = this.state;
return (
<LoginContainer header footer={`V. ${AppConstants.VERSION}`} >
<CoolValidator otherErrors={otherErrors} >
<VBox>
<agui.CoolTextInput
onKeyUp={this.onEnter}
required
id="userid"
autocomplete="on"
label={i18n("APP.LOGIN.USER", "Nome utente")}
placeholder={i18n("APP.LOGIN.USER.PLACEHOLDER", "Inserisci il nome utente")}
value={userid}
validationRule={useridRule}
onChange={updateForm}
/>
<agui.CoolTextInput
onKeyUp={this.onEnter}
required
id="password"
type="password"
autocomplete="on"
label={i18n("APP.LOGIN.PSW", "Password")}
placeholder={i18n("APP.LOGIN.PSW.PLACEHOLDER", "Inserisci la password")}
value={password}
validationRule={passwordRule}
onChange={updateForm}
/>
<HBox style={{ justifyContent: 'flex-end' }}>
<agui.aButton
bs="secondary"
visible={!!AppConstants.DEBUG_VIEW}
onClick={this.goToTest}
icon='fas fa-bug'
/>
<agui.aButton
bs="primary"
type="submit"
onClick={this.login}
> {i18n("APP.LOGIN.LOGIN", "Accedi")} </agui.aButton>
</HBox>
</VBox>
</CoolValidator>
</LoginContainer>
)
}
};
const mapStateToProps = (state) => ({
userid: state.account.userid,
password: state.account.password,
});
const mapDispatchToProps = (dispatch) => ({
updateForm: (value, field) => dispatch(AccountActions.updateForm(value, field)),
login: () => dispatch(AccountActions.login()),
});
Login = connect(mapStateToProps, mapDispatchToProps)(Login);

View File

@ -0,0 +1,50 @@
import { i18n } from "arm-common";
import agui from "arm-gui";
import React from "react";
import { connect } from "react-redux";
import { AppConstants } from "../../constants/AppConstants.js";
import AccountActions from "./AccountActions.js";
export default class Logout extends React.PureComponent {
componentDidMount() {
this.props.logout();
}
back = () => {
this.props.history.push("login");
location.reload();
}
render() {
return (
<LoginContainer header footer={`V. ${AppConstants.VERSIONE}`}>
<agui.aCard>
<agui.aCardHeader>
{i18n("V2.LOGOUT.CARD_TIT.USER_LOGGED_OUT", "Utente Disconnesso")}
</agui.aCardHeader>
<agui.aCardBody>
<p>{i18n("V2.LOGOUT.MESS_LOG.SESSION_HAS_BEEN_CLOSED.", "Session has been closed.")}</p>
<p>{i18n("V2.LOGOUT.MESS_LOG.TO_WORK_ON_THE_APPLICATION", "To work on the application click on the \"home\" button")}</p>
</agui.aCardBody>
<agui.aCardFooter>
<agui.aButton
bs="primary"
type="submit"
onClick={this.back}
> {i18n("V2.LOGOUT.BUTTON_TIT_LOG.HOME", "HOME")} </agui.aButton>
</agui.aCardFooter>
</agui.aCard>
</LoginContainer>
);
}
}
const mapDispatchToProps = (dispatch) => ({
logout: () => dispatch(AccountActions.logout()),
});
Logout = connect(null, mapDispatchToProps)(Logout);

View File

@ -0,0 +1,52 @@
import { i18n } from 'arm-common';
import agui from 'arm-gui';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
const getI18n = (code, desc, ...args) => i18n(`ADV.SESSION_EXPIRED.${code}`, desc, ...args)
export default function SessionExpired(props) {
const { sessionExpired } = props;
const reload = useCallback(() => {
location.reload();
}, [history])
return (
<agui.PopupDialog
show={sessionExpired}
addBackDrop
onClose={reload}
>
<agui.PopupDialog.Header>
{getI18n('EXPIRED_SESSION', 'Sessione scaduta')}
</agui.PopupDialog.Header>
<agui.PopupDialog.Body>
<p>
{getI18n('USER_DISCONNECTED', 'Utente disconnesso.')}
<br />
{getI18n('EXPIRED_SESSION_MESSAGE', 'Per riconnettersi premere il tasto "Ricarica" oppure ricaricare la pagina.')}
</p>
</agui.PopupDialog.Body>
<agui.PopupDialog.Footer>
<div style={{ marginLeft: 'auto' }}/>
<agui.aButton
bs='primary'
onClick={reload}
>
{getI18n('RELOAD', 'Ricarica')}
</agui.aButton>
</agui.PopupDialog.Footer>
</agui.PopupDialog>
);
}
const mapStateToProps = (state) => ({
sessionExpired: state.account.sessionExpired
})
SessionExpired = connect(mapStateToProps)(SessionExpired);

View File

@ -0,0 +1,40 @@
import * as React from 'react';
import AppConstants from '../constants/AppConstants';
class Settings extends React.Component {
constructor(props) {
super(props);
this.componentName = "[Settings.jsx] ";
}
render() {
console.log(this.componentName + 'render');
let properties = Object.getOwnPropertyNames(AppConstants);
let rows = properties.map(function (key) {
return (
<tr key={key}>
<td>{key}</td>
<td>{AppConstants[key]}</td>
</tr>
);
});
return (
<div>
<h1>Settings</h1>
<table className="table table-bordered table-striped">
<tbody>
{rows}
</tbody>
</table>
</div>
);
}
}
export default Settings;

View File

@ -0,0 +1,30 @@
import { DateFormatter } from "arm-common";
export function scrollToId(id, yOffset) {
const element = document.querySelector("#" + id);
if (element) {
//const yOffset = -75;
const y = element.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({ top: y, behavior: 'smooth' });
} else {
setTimeout(() => this.scrollToId(id, yOffset), 100);
}
}
export function objectIsEmpty(obj) {
for (let property in obj) return false;
return true;
}
export function addTsToFileName(fileName){
const dateString = DateFormatter.formatDate(new Date(), '_YYYYMMDD-HHmmss')
const ind = fileName.indexOf('.');
if (ind === -1) {
fileName = fileName + dateString;
} else {
const preName = fileName.substring(0, ind);
const postName = fileName.substring(ind, fileName.length);
fileName = preName + dateString + postName;
}
return fileName;
}

View File

@ -0,0 +1,36 @@
import * as React from 'react';
import { connect } from 'react-redux';
function Spinner(props){
return (
<svg className="spinner" viewBox="0 0 50 50">
<circle className="path" cx="25" cy="25" r="20" fill="none" strokeWidth="5"></circle>
</svg>
)
}
module.exports.Spinner = Spinner;
class Loading extends React.Component {
constructor(props) {
super(props);
this.componentName = "[Loading.jsx] ";
}
render() {
console.log(this.componentName + 'render');
return (
<div className={ "loading " + this.props.loadingState }>
<Spinner/>
</div>
);
}
}
function mapStateToProps(state) {
return {
loadingState: state.app.loadingState,
}
};
export default connect(mapStateToProps, null)(Loading);

View File

@ -0,0 +1,53 @@
.loading {
position: fixed;
background: rgba(255, 255, 255, 0.6);
top: 0;
width: 100vw;
height: 100%;
z-index: -1000;
opacity: 0;
transition: opacity 0.3s ease-in-out;
&.show {
opacity: 1;
z-index: 10000;
}
display: flex;
justify-content: center;
align-items: center;
}
.spinner {
animation: rotate 2s linear infinite;
z-index: 2;
width: 50px;
height: 50px;
& .path {
stroke: #3e76c8;
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}

View File

@ -0,0 +1,65 @@
import React from 'react';
import { connect } from 'react-redux';
import AppActions from '../../app/AppActions.js';
class Notify extends React.Component {
constructor(props) {
super(props);
this.componentLog = (text, elem) => console.log("[Notify.jsx]: " + text, elem ? elem : null);
this.closeNotifies = this.closeNotifies.bind(this);
}
closeNotifies(){
let item = null;
for (let key of this.props.listOfNotify.keys()) {
item = this.props.listOfNotify.get(key)
this.props.closeNotify(item.messageId);
}
}
render() {
this.componentLog("render", this);
const { listOfNotify, closeNotify } = this.props;
let myClass = "alert hidden";
let message = "";
let onClick = null;
if (listOfNotify && listOfNotify !== null && listOfNotify.size > 0){
let item = null;
message = [];
for (let key of listOfNotify.keys()) {
item = listOfNotify.get(key)
message.push(<div key={key} className={"alert-" + item.messageType.toLowerCase()}>{item.message}</div>);
}
myClass = "alert";
onClick = this.closeNotifies;
}
return (
<div className="notify">
<div className={myClass} role="alert">
<button type="button" className="close" onClick={onClick}><i className="fa fa-times"/></button>
{message}
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
listOfNotify: state.app.listOfNotify
};
}
function mapDispatchToProps(dispatch) {
return {
closeNotify: (index) => dispatch(AppActions.deleteNotify(index))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Notify);

View File

@ -0,0 +1,36 @@
/* Usa alert e close di bootstrap*/
.notify {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100000;
div.alert {
opacity: 1;
max-height: 100%;
transition: all 1s ease-in-out;
background-color: #fff;
padding: 0;
&> * {
transition: padding 0.5s ease-in-out;
padding:.75rem 1.25rem;
}
&.hidden {
&> * { padding: 0; }
filter: alpha(opacity=0);
font-size: 0;
line-height: 0;
opacity: 0;
max-height: 0;
padding: 0;
margin: 0;
border: 0;
button {
font-size: 0;
}
}
}
}

View File

@ -0,0 +1,100 @@
import React, { useEffect, useRef, useState } from 'react';
export const am5ClassName = 'armu_ai_chatbot-am5';
export function useAm5() {
const [am5, setAm5] = useState(null);
useEffect(() => {
function check() {
if (window.am5) {
window.am5.ready(() => setAm5(window.am5));
} else setTimeout(check, 100);
}
check();
}, []);
return am5;
}
let am5Colors = null;
export function useAm5Colors() {
const am5 = useAm5();
if (!am5Colors && am5) {
am5Colors = colors.map((color) => am5.color(color));
}
return am5Colors;
}
const roots = {};
export function getRoot(id) {
let root = roots[id];
if (root) root.dispose();
root = am5.Root.new(id);
roots[id] = root;
root.setThemes([
am5themes_Animated.new(root)
]);
root.locale = am5locales_it_IT;
root.numberFormatter.set("numberFormat", "#,###.00");
return root;
}
export const colors = [
'#4CC9C3',
'#3D9AA0',
'#1B3353',
'#637081',
'#A790A5',
'#EBE1E2',
'#EEE8DC',
'#D4B483'
];
export function ChartContainer(props) {
const { id, className } = props;
const chartRef = useRef();
const [style, setStyle] = useState(null);
useEffect(() => {
if (chartRef.current) {
setStyle({
backgroundColor: findFirstBackgroundColor(chartRef.current),
...props.style
});
}
}, [chartRef, props.style]);
return (
<div id={id} style={style} className={`${am5ClassName} ${className}`.trimEnd()} ref={chartRef} />
)
}
ChartContainer.defaultProps = {
id: '',
className: '',
style: {}
}
function findFirstBackgroundColor(element) {
if (element) {
const backgroundColor = getComputedStyle(element)?.backgroundColor;
if (backgroundColor !== "rgba(0, 0, 0, 0)") {
return backgroundColor;
}
if (element.parentElement) {
return findFirstBackgroundColor(element.parentElement);
}
}
return null;
}

View File

@ -0,0 +1,19 @@
@am5Prefix: armu_ai_chatbot-am5;
.@{am5Prefix} {
position: relative;
flex-grow: 1;
height: 100%;
min-width: 17rem;
min-height: 17rem;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 5rem;
height: 1.8rem;
background-color: inherit;
}
}

View File

@ -0,0 +1,184 @@
import { Utils } from 'arm-common';
import p from 'prop-types';
import React, { useLayoutEffect, useMemo, useState } from 'react';
import { ChartContainer, useAm5, useAm5Colors, getRoot } from '../Am5.jsx';
import { AutoSize } from 'arm-core-layouts';
const classNamePrefix = 'armu_ai_chatbot-bar-chart';
export default function BarChart(props) {
const { id, xAxis, yAxis, series, data } = props;
const [chartId] = useState(id || Utils.uuid(classNamePrefix));
const am5 = useAm5();
const am5Colors = useAm5Colors();
const allValues = useMemo(() => {
if (data?.length) {
if (series?.length) {
const values = [];
for (let item of data) {
for (let serie of series) {
if (!isNaN(item[serie.yAxis])) {
values.push(item[serie.yAxis]);
}
}
}
return values;
} else {
return data.map(d => d[yAxis]);
}
}
return [];
}, [data, series, yAxis]);
const minValue = Math.min(...allValues);
useLayoutEffect(() => {
if (am5 && data) {
const root = getRoot(chartId);
const chart = root.container.children.push(
am5xy.XYChart.new(root, {
layout: root.verticalLayout
})
);
const yAxisAm5 = chart.yAxes.push(
am5xy.CategoryAxis.new(root, {
categoryField: xAxis,
renderer: am5xy.AxisRendererY.new(root, {
inversed: true,
cellStartLocation: 0.1,
cellEndLocation: 0.9
})
})
);
yAxisAm5.data.setAll(data);
const xAxisAm5 = chart.xAxes.push(
am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererX.new(root, {
strokeOpacity: 0.1
}),
min: minValue < 0 ? minValue : 0,
})
);
chart.set("colors", am5.ColorSet.new(root, {
colors: am5Colors
}));
if (series?.length){
const legend = chart.children.push(am5.Legend.new(root, {
centerX: am5.p50,
x: am5.p50,
}));
series.forEach(addSeries.bind({ root, chart, xAxisAm5, yAxisAm5, xAxis, data }));
legend.data.setAll(chart.series.values);
} else if (yAxis){
addSeries.call({ root, chart, xAxisAm5, yAxisAm5, xAxis, data }, { yAxis });
}
const cursor = chart.set("cursor", am5xy.XYCursor.new(root, {
behavior: "zoomY"
}));
cursor.lineY.set("forceHidden", true);
cursor.lineX.set("forceHidden", true);
}
}, [am5, allValues]);
const height = useMemo(() => calcHeight(data, series), [data, series]);
return (
<ChartContainer id={chartId} style={{ height }} className={classNamePrefix} />
);
}
BarChart.defaultProps = {
id: '',
xAxis: '',
yAxis: '',
series: [],
data: null
}
BarChart.propTypes = {
id: p.string,
xAxis: p.string.isRequired,
yAxis: p.string,
series: p.arrayOf(p.shape({
name: p.string,
yAxis: p.string
})),
data: p.arrayOf(p.object).isRequired
}
function addSeries(seriesConfig) {
const { root, chart, xAxisAm5, yAxisAm5, xAxis, data } = this;
const { name, yAxis } = seriesConfig;
const series = chart.series.push(
am5xy.ColumnSeries.new(root, {
name,
minBulletDistance: 10,
xAxis: xAxisAm5,
yAxis: yAxisAm5,
valueXField: yAxis,
categoryYField: xAxis,
sequencedInterpolation: true,
tooltip: am5.Tooltip.new(root, {
pointerOrientation: "horizontal",
labelText: "[bold]{name}[/]\n{categoryY}: {valueX}"
})
})
);
series.columns.template.setAll({
height: am5.p100,
strokeOpacity: 0
});
series.bullets.push(function () {
return am5.Bullet.new(root, {
locationX: 1,
locationY: 0.5,
sprite: am5.Label.new(root, {
centerY: am5.p50,
text: "{valueX}",
populateText: true
})
});
});
series.bullets.push(function () {
return am5.Bullet.new(root, {
locationX: 1,
locationY: 0.5,
sprite: am5.Label.new(root, {
centerX: am5.p100,
centerY: am5.p50,
text: "{name}",
fill: am5.color(0xffffff),
populateText: true
})
});
});
series.data.setAll(data);
series.columns.template.setAll({ cornerRadiusBR: 5, cornerRadiusTR: 5, strokeOpacity: 0 });
series.appear();
}
function calcHeight(data, series) {
const lineHeight = AutoSize.convertRemToPixels(3);
const barHeight = AutoSize.convertRemToPixels(4);
return (lineHeight * (data.length + 1)) + (barHeight * ((series?.length || 0) + 1)) + AutoSize.convertRemToPixels(5);
}

View File

@ -0,0 +1,5 @@
@barChartPrefix: armu_ai_chatbot-bar-chart;
.@{barChartPrefix} {
height: unset;
}

View File

@ -0,0 +1,126 @@
import { Utils } from 'arm-common';
import p from 'prop-types';
import React, { useLayoutEffect, useState } from 'react';
import { ChartContainer, getRoot, useAm5, useAm5Colors } from '../Am5.jsx';
const classNamePrefix = 'armu_ai_chatbot-line-chart';
export default function LineChart(props) {
const { id, xAxis, yAxis, series, data } = props;
const [chartId] = useState(id || Utils.uuid(classNamePrefix));
const am5 = useAm5();
const am5Colors = useAm5Colors();
useLayoutEffect(() => {
if (am5 && data) {
const root = getRoot(chartId);
const chart = root.container.children.push(
am5xy.XYChart.new(root, {
panX: true,
panY: true,
layout: root.verticalLayout
})
);
const cursor = chart.set("cursor", am5xy.XYCursor.new(root, {
behavior: "none"
}));
cursor.lineY.set("visible", false);
const xAxisAm5 = chart.xAxes.push(
am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererX.new(root, {
minGridDistance: 50
}),
tooltip: am5.Tooltip.new(root, {})
})
);
const yAxisAm5 = chart.yAxes.push(
am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererY.new(root, {}),
})
);
chart.set("colors", am5.ColorSet.new(root, {
colors: am5Colors
}));
if (series?.length){
const legend = chart.children.push(am5.Legend.new(root, {
centerX: am5.p50,
x: am5.p50,
}));
series.forEach(addSeries.bind({ root, chart, legend, xAxisAm5, yAxisAm5, xAxis, data }));
legend.data.setAll(chart.series.values);
} else if (yAxis){
addSeries.call({ root, chart, xAxisAm5, yAxisAm5, xAxis, data }, { yAxis });
}
}
}, [am5, data]);
return (
<ChartContainer id={chartId} className={classNamePrefix} />
);
}
LineChart.defaultProps = {
id: '',
xAxis: '',
yAxis: '',
series: [],
data: null
}
LineChart.propTypes = {
id: p.string,
xAxis: p.string.isRequired,
yAxis: p.string,
series: p.arrayOf(p.shape({
name: p.string,
yAxis: p.string
})).isRequired,
data: p.arrayOf(p.object).isRequired
}
function addSeries(seriesConfig) {
const { root, chart, xAxisAm5, yAxisAm5, xAxis, data } = this;
const { name, yAxis } = seriesConfig;
const tooltip = am5.Tooltip.new(root, {
labelText: "{valueY}",
});
const series = chart.series.push(
am5xy.LineSeries.new(root, {
name,
minBulletDistance: 10,
xAxis: xAxisAm5,
yAxis: yAxisAm5,
valueXField: xAxis,
valueYField: yAxis,
tooltip
})
);
series.bullets.push(function () {
return am5.Bullet.new(root, {
sprite: am5.Circle.new(root, {
radius: 6,
fill: series.get("fill"),
stroke: root.interfaceColors.get("background"),
strokeWidth: 2,
})
});
});
series.data.setAll(data);
}

View File

@ -0,0 +1,73 @@
import { Utils } from 'arm-common';
import p from 'prop-types';
import React, { useLayoutEffect, useMemo, useState } from 'react';
import { HBox } from 'arm-core-layouts';
import { ChartContainer, getRoot, useAm5, useAm5Colors } from '../Am5.jsx';
import PieChartLegend from './PieChartLegend.jsx';
const classNamePrefix = 'armu_ai_chatbot-pie-chart';
export default function PieChart(props) {
const { id, xAxis, yAxis } = props;
const [chartId] = useState(id || Utils.uuid(classNamePrefix));
const am5 = useAm5();
const am5Colors = useAm5Colors();
const data = useMemo(() => {
if (props.data?.length){
const data = [...props.data];
data.sort((a, b) => b[yAxis] - a[yAxis]);
return data;
}
return [];
}, [props.data])
useLayoutEffect(() => {
if (am5 && data) {
const root = getRoot(chartId);
const chart = root.container.children.push(am5percent.PieChart.new(root, {
layout: root.verticalLayout,
innerRadius: am5.percent(85)
}));
const series = chart.series.push(am5percent.PieSeries.new(root, {
valueField: yAxis,
categoryField: xAxis
}));
series.labels.template.set("visible", false);
series.ticks.template.set("visible", false);
series.set("colors", am5.ColorSet.new(root, {
colors: am5Colors
}));
series.data.setAll(data);
}
}, [am5, data]);
return (
<HBox className={classNamePrefix}>
<ChartContainer id={chartId} className={`${classNamePrefix}-chart`}/>
<PieChartLegend enableTotal xAxis={xAxis} yAxis={yAxis} data={data} />
</HBox>
);
}
PieChart.defaultProps = {
id: '',
xAxis: '',
yAxis: '',
data: null
}
PieChart.propTypes = {
id: p.string,
xAxis: p.string.isRequired,
yAxis: p.string.isRequired,
data: p.arrayOf(p.object).isRequired
}

View File

@ -0,0 +1,6 @@
@pieChartPrefix: armu_ai_chatbot-pie-chart;
.@{pieChartPrefix} {
align-items: center;
height: 100%;
}

View File

@ -0,0 +1,59 @@
import { NumberFormatter } from 'arm-common';
import { HBox, VBox } from 'arm-core-layouts';
import p from 'prop-types';
import React, { useMemo } from 'react';
import { colors } from '../Am5.jsx';
const classNamePrefix = 'armu_ai_chatbot-pie-chart-legend';
export default function PieChartLegend(props) {
const { data, xAxis, yAxis, enableTotal } = props;
const total = useMemo(() => {
if (enableTotal && data?.length) {
return data.reduce((total, item) => total + (item[yAxis] || 0), 0);
}
return null;
},[data, enableTotal]);
if (!data || !data?.length) return null;
return (
<VBox className={`${classNamePrefix}`}>
{data.map((item, index) => (
<HBox key={`${classNamePrefix}-${item[xAxis]}-${item[yAxis]}`} className={`${classNamePrefix}-item`}>
<span style={{ backgroundColor: colors[index] }} className={`${classNamePrefix}-color-ref`}/>
<span>{item[xAxis]}</span>
<span className={`${classNamePrefix}-value`}>{NumberFormatter.percent(item[yAxis])}</span>
</HBox>
))}
{total && (
<>
<span className={`${classNamePrefix}-divisor`}/>
<HBox className={`${classNamePrefix}-item`}>
<span className={`${classNamePrefix}-color-ref`}/>
<span>Total</span>
<span className={`${classNamePrefix}-value`}>{NumberFormatter.percent(total)}</span>
</HBox>
</>
)}
</VBox>
);
}
PieChartLegend.defaultProps = {
className: '',
children: null
}
PieChartLegend.propTypes = {
className: p.string,
children: p.node
}

View File

@ -0,0 +1,36 @@
@pieChartLegendPrefix: armu_ai_chatbot-pie-chart-legend;
.@{pieChartLegendPrefix} {
width: auto;
flex-grow: 2;
max-height: 100%;
min-width: 20rem;
overflow-y: auto;
&-item {
align-items: center;
font-weight: 500;
}
&-color-ref {
margin-left: 1rem;
width: 2px;
height: 17px;
top: 6px;
left: 14px;
border-radius: 4px;
}
&-value {
flex-grow: 1;
text-align: right;
padding-right: 1rem;
}
&-divisor {
height: 1px;
width: 100%;
background-color: #BFBFBF;
}
}

View File

@ -0,0 +1,182 @@
import { DateFormatter, Utils } from 'arm-common';
import p from 'prop-types';
import React, { useLayoutEffect, useMemo, useState } from 'react';
import { ChartContainer, useAm5, useAm5Colors, getRoot } from '../Am5.jsx';
import { AutoSize } from 'arm-core-layouts';
const classNamePrefix = 'armu_ai_chatbot-stacked-chart';
/**
* Generates a stacked chart using the provided data and configuration.
*
* @param {Object} props - The configuration object for the chart.
* @param {string} props.id - The unique identifier for the chart.
* @param {string} props.xAxis - The field to use for the x-axis.
* @param {string} props.yAxis - The field to use for the y-axis.
* @param {Array} props.series - The series configuration for the chart.
* @param {Array} props.data - The data to be displayed on the chart.
* @return {ReactElement} The rendered stacked chart component.
*/
export default function StackedChart(props) {
const { id, xAxis, series, isDateXAxis } = props;
const [chartId] = useState(id || Utils.uuid(classNamePrefix));
const am5 = useAm5();
const am5Colors = useAm5Colors();
const data = useMemo(() => {
if (isDateXAxis) {
return props.data.map((d) => ({...d, [xAxis]: DateFormatter.formatDate(d[xAxis], "DD/MM/YYYY") }))
}
return props.data;
}, [props.data])
useLayoutEffect(() => {
if (am5 && data) {
const root = getRoot(chartId);
const chart = root.container.children.push(
am5xy.XYChart.new(root, {
layout: root.verticalLayout
})
);
const xRenderer = am5xy.AxisRendererX.new(root, {});
const xAxisAm5 = chart.xAxes.push(
am5xy.CategoryAxis.new(root, {
categoryField: xAxis,
renderer: xRenderer,
tooltip: am5.Tooltip.new(root, {})
})
);
xRenderer.grid.template.setAll({
location: 1
})
xAxisAm5.data.setAll(data);
const yAxisAm5 = chart.yAxes.push(
am5xy.ValueAxis.new(root, {
min: 0,
max: 100,
renderer: am5xy.AxisRendererY.new(root, {
strokeOpacity: 0.1
})
})
);
chart.set("colors", am5.ColorSet.new(root, {
colors: am5Colors
}));
if (series?.length) {
const legend = chart.children.push(am5.Legend.new(root, {
centerX: am5.p50,
x: am5.p50,
}));
series.forEach(addSeries.bind({ root, chart, xAxisAm5, yAxisAm5, xAxis, data }));
legend.data.setAll(chart.series.values);
}
}
}, [am5, data]);
return (
<ChartContainer id={chartId} className={classNamePrefix} />
);
}
StackedChart.defaultProps = {
id: '',
xAxis: '',
series: [],
data: null
}
StackedChart.propTypes = {
id: p.string,
xAxis: p.string.isRequired,
series: p.arrayOf(p.shape({
name: p.string,
yAxis: p.string
})).isRequired,
data: p.arrayOf(p.object).isRequired
}
function addSeries(seriesConfig) {
const { root, chart, xAxisAm5, yAxisAm5, xAxis, data } = this;
const { name, yAxis } = seriesConfig;
const series = chart.series.push(
am5xy.ColumnSeries.new(root, {
name,
stacked: true,
xAxis: xAxisAm5,
yAxis: yAxisAm5,
categoryXField: xAxis,
valueYField: yAxis
})
);
series.columns.template.setAll({
tooltipText: "[bold]{name}[/]: {valueY}",
tooltipY: am5.percent(10)
});
series.bullets.push(function () {
return am5.Bullet.new(root, {
sprite: am5.Label.new(root, {
text: "{valueY}",
fill: root.interfaceColors.get("alternativeText"),
centerY: am5.p50,
centerX: am5.p50,
populateText: true
})
});
});
series.data.setAll(data);
addRoundedCorners(series, chart, xAxisAm5);
series.appear();
}
//TODO doesnt work
function addRoundedCorners(series, chart, xAxisAm5) {
series.columns.template.adapters.add("cornerRadiusTL", function (radius, target) {
return isTop.call({ chart, series, xAxisAm5 }, target.dataItem) ? 5 : 0;
});
series.columns.template.adapters.add("cornerRadiusTR", function (radius, target) {
return isTop.call({ chart, series, xAxisAm5 }, target.dataItem) ? 5 : 0;
});
}
function getCategoryDataItems(dataItem) {
const index = this.xAxisAm5.categoryToIndex(dataItem.get("categoryX"));
const items = [];
this.chart.series.each((series) => {
if (series.get("visible")) {
const item = this.series.dataItems[index];
if (item.get("valueY") >= 0) {
items.push(item);
}
else {
items.unshift(item);
}
}
});
return items;
}
function isTop(dataItem) {
const items = getCategoryDataItems.call(this, dataItem);
if (dataItem.get("valueY") >= 0) {
return items.indexOf(dataItem) == (items.length - 1);
}
else {
return items.indexOf(dataItem) == 0;
}
}

View File

@ -0,0 +1,163 @@
import { Utils } from 'arm-common';
import p from 'prop-types';
import React, { useLayoutEffect, useMemo, useState } from 'react';
import { ChartContainer, getRoot, useAm5, useAm5Colors } from '../Am5.jsx';
const classNamePrefix = 'armu_ai_chatbot-time-chart';
export default function TimeChart(props) {
const { id, xAxis, yAxis, series, data } = props;
const [chartId] = useState(id || Utils.uuid(classNamePrefix));
const am5 = useAm5();
const am5Colors = useAm5Colors();
const allValues = useMemo(() => {
if (data?.length) {
if (series?.length) {
const values = [];
for (let item of data) {
for (let serie of series) {
if (!isNaN(item[serie.yAxis])) {
values.push(item[serie.yAxis]);
}
}
}
return values;
} else {
return data.map(d => d[yAxis]);
}
}
return [];
}, [data, series, yAxis]);
const minValue = Math.min(...allValues);
const maxValue = Math.max(...allValues);
const largerValue = Math.max(maxValue, Math.abs(minValue));
useLayoutEffect(() => {
if (am5 && data) {
const root = getRoot(chartId);
const chart = root.container.children.push(
am5xy.XYChart.new(root, {
panX: true,
panY: true,
wheelX: "panX",
wheelY: "zoomX",
pinchZoomX: true,
layout: root.verticalLayout
})
);
const cursor = chart.set("cursor", am5xy.XYCursor.new(root, {
behavior: "none"
}));
cursor.lineY.set("visible", false);
const xAxisAm5 = chart.xAxes.push(
am5xy.DateAxis.new(root, {
maxDeviation: 0.2,
baseInterval: {
timeUnit: "day",
count: 1
},
renderer: am5xy.AxisRendererX.new(root, {}),
tooltip: am5.Tooltip.new(root, {})
})
);
const yAxisAm5 = chart.yAxes.push(
am5xy.ValueAxis.new(root, {
renderer: am5xy.AxisRendererY.new(root, {
pan: "zoom"
}),
min: minValue - (largerValue * 0.1),
max: maxValue + (largerValue * 0.1),
strictMinMax: true
})
);
chart.set("colors", am5.ColorSet.new(root, {
colors: am5Colors
}));
if (series?.length) {
const legend = chart.children.push(am5.Legend.new(root, {
centerX: am5.p50,
x: am5.p50,
}));
series.forEach(addSeries.bind({ root, chart, xAxisAm5, yAxisAm5, xAxis, data }));
legend.data.setAll(chart.series.values);
} else if (yAxis) {
addSeries.call({ root, chart, xAxisAm5, yAxisAm5, xAxis, data }, { yAxis });
}
chart.set("scrollbarX", am5.Scrollbar.new(root, {
orientation: "horizontal"
}));
}
}, [am5, allValues]);
return (
<ChartContainer id={chartId} className={classNamePrefix} />
);
}
TimeChart.defaultProps = {
id: '',
xAxis: '',
yAxis: '',
series: [],
data: null
}
TimeChart.propTypes = {
id: p.string,
xAxis: p.string.isRequired,
yAxis: p.string,
series: p.arrayOf(p.shape({
name: p.string,
yAxis: p.string
})),
data: p.arrayOf(p.object).isRequired
}
function addSeries(seriesConfig) {
const { root, chart, xAxisAm5, yAxisAm5, xAxis, data } = this;
const { name, yAxis } = seriesConfig;
const tooltip = am5.Tooltip.new(root, {
labelText: "{valueY}",
});
const series = chart.series.push(
am5xy.LineSeries.new(root, {
name,
minBulletDistance: 10,
xAxis: xAxisAm5,
yAxis: yAxisAm5,
valueXField: xAxis,
valueYField: yAxis,
tooltip
})
);
series.bullets.push(function () {
return am5.Bullet.new(root, {
sprite: am5.Circle.new(root, {
radius: 6,
fill: series.get("fill"),
stroke: root.interfaceColors.get("background"),
strokeWidth: 2,
})
});
});
series.data.setAll(data);
}

View File

@ -0,0 +1,6 @@
@timeChartPrefix: armu_ai_chatbot-time-chart;
.@{timeChartPrefix} {
min-width: 5rem;
min-height: 24rem;
}

View File

@ -0,0 +1,26 @@
import { i18n } from 'arm-common';
import { ApplicationContainer, MasterPageContainer, PageBody, PageContainer, PageHeader, PageHeaderContainer, TopBarRouting } from "arm-core-layouts";
import ArmGuiDemo from 'arm-gui/dist/demo/js/ui/Demo.jsx';
import React, { PureComponent } from 'react';
const selectorName = 'stub-test-master-page';
export default class TestMasterPage extends PureComponent {
constructor(props) {
super(props);
this.state = {
menuInfo: [
{ pathKey: 'arm-gui', pageTitle: i18n('STUB.AGUI', 'arm-gui'), Component: ArmGuiDemo },
]
}
}
render() {
return (
<ApplicationContainer>
<TopBarRouting menuInfo={this.state.menuInfo} />
</ApplicationContainer>
);
}
}

View File

@ -0,0 +1,133 @@
import { WebRest } from 'arm-common';
import { setRestContainer as setQBRestContainer } from 'arm-core-querybuilder';
import { setRestContainer as setSPRestContainer } from 'arm-gui-grid-wj';
import SessionWorkerManager from '../SessionWorkerManager.js';
import AppActions, { EnumNotifyType } from '../app/AppActions.js';
import { AppConstants } from '../constants/AppConstants.js';
import { store } from '../reducers/store.js';
import AccountActions from '../ui/account/AccountActions.js';
/**
* @param {number} minutes
* @returns {number} minutes in milliseconds
*/
function getMinutesInMilliseconds(minutes){
return 1000 * 60 * minutes;
}
function onSessionExpired() {
const dispatch = store.dispatch;
dispatch(AccountActions.logoutExpiredSession());
}
/**
* classe Rest basata su WebRest
*/
class Rest extends WebRest {
constructor() {
/** @type {import('arm-common').SessionConfigOptions} */
const sessionConf = {};
if (window.Worker){
SessionWorkerManager.setSessionExpiredTimeout(getMinutesInMilliseconds(AppConstants.SESSION_EXPIRED_TIMEOUT));
SessionWorkerManager.setExpireFunction(onSessionExpired);
sessionConf.externalSessionTimeoutHandler = SessionWorkerManager.clear.bind(SessionWorkerManager);
} else {
sessionConf.sessionExpiredHandlerTimeout = AppConstants.SESSION_EXPIRED_TIMEOUT;
sessionConf.sessionExpiredHandler = onSessionExpired;
}
super(
AppConstants.REST_SERVER, //
AppConstants.REST_BASE_URL, //
["account/login", "authentication/loginbo"], // to skip
// recover jsessionId
() => {
let app = store.getState().app;
return app.jsessionId;
},
// loading, show loader
(dispatch) => dispatch(AppActions.showLoader()),
// end load, hide loader
(dispatch) => dispatch(AppActions.hideLoader()),
// handle error message
(dispatch, message, statusCode, options, stacktrace) => AppActions.addNotify(EnumNotifyType.Danger, message),
// do logout
(dispatch) => dispatch({ type: 'LOGOUT' }),
// path iniziale per chiamate stubs
"./stubs/",
// recover serverId
() => {
let app = store.getState().app;
return app.serverid;
},
sessionConf, // Sessionconf
"cid", // default JBOSS string identifier for conversation id
{
useRequestId: true,
base64: true,
zip: true,
minKbToZip: 500
} // attiva encoding in base64 del body delle post.
)
setQBRestContainer(this);
setSPRestContainer(this);
}
qs(fields) {
let q = "";
let sep = "?";
Object.keys(fields || {}).forEach((k) => {
let v = fields[k];
if (v === null || v === undefined) return;
v = encodeURIComponent(v);
q += sep + k + "=" + v;
sep = "&";
});
return q;
}
parseQueryString(queryString) {
const queryStringTruncated = queryString.substring(queryString.indexOf('?') + 1);
const unparsedParams = queryStringTruncated.split('&');
const params = {};
unparsedParams.forEach((param) => {
const [key, value] = param.split('=');
params[key] = decodeURIComponent(value);
});
return params;
}
saveByteArray(fileName, resultByte, mimeType) {
if (!resultByte) return;
const bytes = new Uint8Array(resultByte); // pass your byte response to this constructor
const blob = new Blob([bytes], { type: mimeType });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = fileName;
link.click();
link.remove();
}
base64ToBlob(base64, type = "application/octet-stream") {
const binStr = atob(base64);
const len = binStr.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
}
return new Blob([arr], { type: type });
}
}
export default new Rest;

View File

@ -0,0 +1,54 @@
@import (less) "~arm-gui/dist/less/base.less";
@import (less) "~arm-file-uploader/dist/less/base.less";
@import (less) "~arm-gui-grid-wj/dist/less/base.less";
@import (less) "~arm-core-layouts/dist/less/base.less";
@import (less) "~arm-core-querybuilder/dist/less/base.less";
@import (less) "~arm-splitter/dist/less/base.less";
@import (less) "~arm-router/dist/less/base.less";
@import (less) "~arm-visual-component/dist/less/base.less";
@import (less) "~arm-themes/dist/@@THEME@@/base.less";
@fonts-path: './webfonts';
//#region Bootstrap generics
*,
::after,
::before {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
body {
margin: 0;
font-size: 1rem;
line-height: 1.5;
}
a {
text-decoration: none;
}
a:not([href]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]) {
color: inherit;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 500;
line-height: 1.2;
margin: 0;
}
//#endregion Bootstrap generics

View File

@ -0,0 +1,34 @@
function getCurrentTime(){
return Intl.DateTimeFormat('it-IT', {hour: '2-digit', minute: '2-digit', second: '2-digit'}).format(new Date());
}
function mt(m){
return `${getCurrentTime()} - [KeepAliveWorker]: ${m}`;
}
onmessage = function (ev) {
const { type, data } = ev.data;
if (type === 'START_KEEP_ALIVE') {
const { url, minutes } = data;
async function fetchKeepAlive() {
console.log(mt(`calling ${url}`));
fetch(url).then(
(response) => response.json().then(
(json) => postMessage({
type: 'KEEP_ALIVE_RESULT',
data: json
})
)
);
}
console.log(mt('starting keep alive'), data);
fetchKeepAlive();
setInterval(fetchKeepAlive, minutes * 60000);
} else {
postMessage({ type, data: 'OK' });
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunk_am5=self.webpackChunk_am5||[]).push([[6933],{3012:function(e,n,u){u.r(n),u.d(n,{am5locales_en:function(){return o}});const o=u(3100).Z}},function(e){var n=(3012,e(e.s=3012)),u=window;for(var o in n)u[o]=n[o];n.__esModule&&Object.defineProperty(u,"__esModule",{value:!0})}]);
//# sourceMappingURL=en.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"locales/en.js","mappings":"qJACO,MAAMA,E,QAAgB,C","sources":["webpack://@amcharts/amcharts5/./tmp/webpack/locales/en.js"],"sourcesContent":["import m from \"./../../../dist/es2015/locales/en.js\";\nexport const am5locales_en = m;"],"names":["am5locales_en"],"sourceRoot":""}

View File

@ -0,0 +1,2 @@
"use strict";(self.webpackChunk_am5=self.webpackChunk_am5||[]).push([[2012],{2570:function(e,a,o){o.r(a),o.d(a,{am5locales_en_CA:function(){return t}});const t={_decimalSeparator:".",_thousandSeparator:",",_percentPrefix:null,_percentSuffix:"%",_date_millisecond:"mm:ss SSS",_date_millisecond_full:"HH:mm:ss SSS",_date_second:"hh:mm:ss a",_date_second_full:"hh:mm:ss a",_date_minute:"hh:mm a",_date_minute_full:"hh:mm a - MMM dd, yyyy",_date_hour:"hh:mm a",_date_hour_full:"hh:mm a - MMM dd, yyyy",_date_day:"MMM dd",_date_day_full:"MMM dd, yyyy",_date_week:"ww",_date_week_full:"MMM dd, yyyy",_date_month:"MMM",_date_month_full:"MMM, yyyy",_date_year:"yyyy",_duration_millisecond:"SSS",_duration_second:"ss",_duration_minute:"mm",_duration_hour:"hh",_duration_day:"dd",_duration_week:"ww",_duration_month:"MM",_duration_year:"yyyy",_era_ad:"AD",_era_bc:"BC",A:"",P:"",AM:"",PM:"","A.M.":"","P.M.":"",January:"",February:"",March:"",April:"",May:"",June:"",July:"",August:"",September:"",October:"",November:"",December:"",Jan:"",Feb:"",Mar:"",Apr:"","May(short)":"May",Jun:"",Jul:"",Aug:"",Sep:"",Oct:"",Nov:"",Dec:"",Sunday:"",Monday:"",Tuesday:"",Wednesday:"",Thursday:"",Friday:"",Saturday:"",Sun:"",Mon:"",Tue:"",Wed:"",Thu:"",Fri:"",Sat:"",_dateOrd:function(e){let a="th";if(e<11||e>13)switch(e%10){case 1:a="st";break;case 2:a="nd";break;case 3:a="rd"}return a},Play:"",Stop:"","Zoom Out":"",Legend:"","Press ENTER to toggle":"",Loading:"",Home:"",Chart:"","Serial chart":"","X/Y chart":"","Pie chart":"","Gauge chart":"","Radar chart":"","Sankey diagram":"","Chord diagram":"","Flow diagram":"","TreeMap chart":"",Series:"","Candlestick Series":"","Column Series":"","Line Series":"","Pie Slice Series":"","X/Y Series":"",Map:"","Press ENTER to zoom in":"","Press ENTER to zoom out":"","Use arrow keys to zoom in and out":"","Use plus and minus keys on your keyboard to zoom in and out":"",Export:"",Image:"",Data:"",Print:"","Press ENTER to open":"","Press ENTER to print.":"","Press ENTER to export as %1.":"","(Press ESC to close this message)":"","Image Export Complete":"","Export operation took longer than expected. Something might have gone wrong.":"","Saved from":"",PNG:"",JPG:"",GIF:"",SVG:"",PDF:"",JSON:"",CSV:"",XLSX:"",HTML:"","Use TAB to select grip buttons or left and right arrows to change selection":"","Use left and right arrows to move selection":"","Use left and right arrows to move left selection":"","Use left and right arrows to move right selection":"","Use TAB select grip buttons or up and down arrows to change selection":"","Use up and down arrows to move selection":"","Use up and down arrows to move lower selection":"","Use up and down arrows to move upper selection":"","From %1 to %2":"","From %1":"","To %1":"","No parser available for file: %1":"","Error parsing file: %1":"","Unable to load file: %1":"","Invalid date":""}}},function(e){var a=(2570,e(e.s=2570)),o=window;for(var t in a)o[t]=a[t];a.__esModule&&Object.defineProperty(o,"__esModule",{value:!0})}]);
//# sourceMappingURL=en_CA.js.map

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More