diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ca9b96403403231ff452d8fd94b9fe9207c104be..90a63db2bdc441892231c4fe8d228dba7477e08a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ stages: - test - sonar - build + - mend test: stage: test @@ -27,7 +28,7 @@ sonar: build_and_push_latest_image: stage: build - image: korvoj/docker-git:26.1.3 + image: korvoj/docker-git:26.1.4 only: - develop variables: @@ -54,7 +55,7 @@ build_and_push_alfa_image: build_and_push_release_image: stage: build - image: korvoj/docker-git:26.1.3 + image: korvoj/docker-git:26.1.4 only: - tags script: @@ -64,4 +65,23 @@ build_and_push_release_image: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REPOSITORY docker build -t $DOCKER_REPOSITORY_LOCAL:$IMAGE_TAG . docker push $DOCKER_REPOSITORY_LOCAL:$IMAGE_TAG - docker logout $DOCKER_REPOSITORY \ No newline at end of file + docker logout $DOCKER_REPOSITORY + +mend: + stage: mend + image: openjdk:17-jdk-slim + only: + - /^release/ + variables: + PRODUCT_NAME: "nmaas" + PROJECT_NAME: "nmaas-portal" + script: + - | + export PRODUCT_VERSION=$(echo $CI_COMMIT_BRANCH | cut -c 9-) + export PROJECT_VERSION=$PRODUCT_VERSION + apt-get update && apt-get install -y curl nodejs npm + npm install -g @angular/cli + npm ci + chmod +x ./gradlew + curl -LJO https://github.com/whitesource/unified-agent-distribution/releases/latest/download/wss-unified-agent.jar + java -jar wss-unified-agent.jar -userKey ${MEND_USER_KEY} -apiKey ${MEND_API_KEY} -projectVersion ${PROJECT_VERSION} -project ${PROJECT_NAME} -productVersion ${PRODUCT_VERSION} -product ${PRODUCT_NAME} -c ./ws/ws.config -d ./ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4b9e926d9c2c34de45e0f75578bdc449abae29ec..5adcf622271c5f6fa7efa21a5687cb066cc76ab8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN npm install -g @angular/cli@17 RUN npm i --force RUN ng build --base-href / --configuration production -FROM nginx:1.27-alpine +FROM nginx:1.28-alpine MAINTAINER nmaas@lists.geant.org ARG webdir=/usr/share/nginx/html diff --git a/angular.json b/angular.json index 7293e9a6ec3bd6115a05c4cf4d8e668648648f8c..8492ebcbc23ad486ee46289147b0c724f361f397 100644 --- a/angular.json +++ b/angular.json @@ -96,7 +96,8 @@ "tsConfig": "tsconfig.spec.json", "codeCoverage": true, "codeCoverageExclude": [ - "src/app/test/**" + "src/app/test/**", + "**/*.spec.ts" ], "scripts": [ "node_modules/jquery/dist/jquery.min.js", diff --git a/build.gradle b/build.gradle index a443e5382e46befdc5870955a7650591f7e79e96..8ac5811585e0a6ccca6fa68e856bc18098b97d06 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,10 @@ import org.gradle.internal.os.OperatingSystem; plugins { id "com.moowork.node" version "1.3.1" - id "org.sonarqube" version "3.2.0" + id "org.sonarqube" version "6.1.0.5360" } -version = '1.7.0-SNAPSHOT' +version = '1.7.0' task buildGUI(type: Exec) { println 'Building using Angular CLI' @@ -58,7 +58,7 @@ sonarqube { properties { property "sonar.sources", "src" - property "sonar.exclusions", "**/node_modules/**,**/*.spec.ts,**/bootstrap.js,**/src/app/service/**,**/src/app/model/**" + property "sonar.exclusions", ["**/node_modules/**","**/*.spec.ts","**/bootstrap.js","**/src/app/service/**","**/src/app/model/**"] property "sonar.tests", "src" property "sonar.test.inclusions", "**/*.spec.ts" property "sonar.typescript.lcov.reportPaths", "coverage/lcov.info" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9ca236b7bcce1835850cad748b58cb9719f26655..94336fcae912db8a11d55634156fa011f4686124 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5b8cb7fbc8711fc2ef9087feb9161670a7d3ac0d..5c82cb03242098aebc387e4b8faf85068bea988c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Jun 18 14:34:36 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip diff --git a/gradlew b/gradlew index 4453ccea33d960069d9137ee65f6b21fc65e7e92..cccdd3d517fc5249beaefa600691cf150f2fa3e6 100644 --- a/gradlew +++ b/gradlew @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -155,7 +155,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/gradlew.bat b/gradlew.bat index f9553162f122c71b34635112e717c3e733b5b212..e95643d6a2ca62258464e83c72f5156dc941c609 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,84 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/karma.conf.js b/karma.conf.js index ba90f3360f44634aff4d5256a95ca96c8e7742a9..a385b0a180d5158703d862e2913e52303d341452 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -35,7 +35,9 @@ module.exports = function (config) { coverageReporter: { dir: require('path').join(__dirname, 'coverage'), subdir: '.', - exclude: ['**/*.spec.ts'], + exclude: ['**/*.spec.ts', + '**/*.service.ts' + ], reporters: [ {type: 'html', subdir: '.'}, {type: 'lcovonly', subdir: '.', file: 'lcov.info'}, @@ -43,7 +45,7 @@ module.exports = function (config) { check: { global: { statements: 40, - branches: 20, + branches: 19, functions: 30, lines: 30, }, diff --git a/package-lock.json b/package-lock.json index 9587e3edb93829b96b09f645a89e4907917c705d..f61f5e9255306db709c48725cefda03a03fd3821 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nmaas-portal", - "version": "1.7.0", + "version": "1.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nmaas-portal", - "version": "1.7.0", + "version": "1.7.1", "license": "Apache 2.0", "dependencies": { "@angular/animations": "17.3.12", @@ -23,24 +23,25 @@ "@formio/angular": "7.0.0", "@formio/js": "^5.0.0", "@ngx-translate/core": "^14.0.0", - "@ngx-translate/http-loader": "^7.0.0", + "@ngx-translate/http-loader": "^16.0.0", "@types/semver": "^7.3.8", "@types/zxcvbn": "^4.4.1", "@zxcvbn-ts/core": "^3.0.0", "@zxcvbn-ts/language-en": "^3.0.0", - "ajv": "^6.12.6", + "ajv": "^8.0.0", "angular-password-strength-meter": "^11.0.0", "bootstrap": "^3.4.1", + "chart.js": "^4.4.8", "document-register-element": "^1.14.10", "jquery": "^3.6.0", "lodash": "^4.17.21", "ng-event-source": "^1.0.14", - "ng-recaptcha": "^13.0.0", + "ng-recaptcha": "^13.2.1", "ng-terminal": "^6.3.0", "ngx-pagination": "^6.0.3", "ngx-webstorage-service": "^5.0.0", "primeflex": "^3.3.1", - "primeicons": "^6.0.1", + "primeicons": "^7.0.0", "primeng": "^17.18.15", "rxjs": "^7.8.1", "semver": "^7.5.4", @@ -53,14 +54,14 @@ "@angular-devkit/build-angular": "^17.3.11", "@angular/cli": "^17.3.11", "@angular/compiler-cli": "^17.3.12", - "@types/jasmine": "~3.6.0", + "@types/jasmine": "~5.1.0", "@types/node": "^12.20.13", "codelyzer": "^6.0.0", "intl": "^1.2.5", "jasmine-core": "~4.6.0", "jasmine-spec-reporter": "~5.0.0", "karma": "^6.3.14", - "karma-chrome-launcher": "~3.1.0", + "karma-chrome-launcher": "~3.2.0", "karma-cli": "^1.0.1", "karma-coverage": "^2.0.3", "karma-jasmine": "~4.0.0", @@ -89,12 +90,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1703.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.11.tgz", - "integrity": "sha512-YNasVZk4rYdcM6M+KRH8PUBhVyJfqzUYLpO98GgRokW+taIDgifckSlmfDZzQRbw45qiwei1IKCLqcpC8nM5Tw==", + "version": "0.1703.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.16.tgz", + "integrity": "sha512-wuqKu20ekgzzikUTZD28dS72F6vjniZuiQ7RgAYhykmsU0z0br2tksHQvjD/auzVArtgQir1+V9wp6BN4dSdNQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.3.11", + "@angular-devkit/core": "17.3.16", "rxjs": "7.8.1" }, "engines": { @@ -103,27 +104,36 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/build-angular": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.11.tgz", - "integrity": "sha512-lHX5V2dSts328yvo/9E2u9QMGcvJhbEKKDDp9dBecwvIG9s+4lTOJgi9DPUE7W+AtmPcmbbhwC2JRQ/SLQhAoA==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.16.tgz", + "integrity": "sha512-5JiR1NK3MOwzipAn4UmvJ8yQa6NaBtHBWbLrY0Ps6a21kHWn42C+dpvVdlXN/ZZSpEll/nzA+b77zA3kDrGlKw==", "dev": true, "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1703.11", - "@angular-devkit/build-webpack": "0.1703.11", - "@angular-devkit/core": "17.3.11", - "@babel/core": "7.24.0", - "@babel/generator": "7.23.6", - "@babel/helper-annotate-as-pure": "7.22.5", - "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.9", - "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.24.0", - "@babel/preset-env": "7.24.0", - "@babel/runtime": "7.24.0", + "@angular-devkit/architect": "0.1703.16", + "@angular-devkit/build-webpack": "0.1703.16", + "@angular-devkit/core": "17.3.16", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.3.11", + "@ngtools/webpack": "17.3.16", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.18", @@ -164,8 +174,7 @@ "terser": "5.29.1", "tree-kill": "1.2.2", "tslib": "2.6.2", - "undici": "6.11.1", - "vite": "5.1.8", + "vite": "~5.4.17", "watchpack": "2.4.0", "webpack": "5.94.0", "webpack-dev-middleware": "6.1.2", @@ -233,21 +242,21 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -272,9 +281,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -288,9 +297,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -304,9 +313,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -320,9 +329,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -336,9 +345,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -352,9 +361,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -368,9 +377,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -384,9 +393,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -400,9 +409,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -416,9 +425,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -432,9 +441,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -448,9 +457,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -464,9 +473,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -480,9 +489,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -496,9 +505,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -512,9 +521,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -528,9 +537,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -544,9 +553,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -560,9 +569,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -576,9 +585,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -592,9 +601,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -608,9 +617,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -624,9 +633,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -640,14 +649,14 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/@types/node": { - "version": "22.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", - "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "dev": true, "optional": true, "peer": true, "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@angular-devkit/build-angular/node_modules/@vitejs/plugin-basic-ssl": { @@ -662,12 +671,34 @@ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" } }, + "node_modules/@angular-devkit/build-angular/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@angular-devkit/build-angular/node_modules/picomatch": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", @@ -680,6 +711,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -720,14 +760,14 @@ "dev": true }, "node_modules/@angular-devkit/build-angular/node_modules/vite": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.8.tgz", - "integrity": "sha512-mB8ToUuSmzODSpENgvpFk2fTiU/YQ1tmcVJJ4WZbq4fPdGJkFNVcmVL5k7iDug6xzWjjuGDKAuSievIsD6H7Xw==", + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", + "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -746,6 +786,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -763,6 +804,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -775,9 +819,9 @@ } }, "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -787,29 +831,57 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, "node_modules/@angular-devkit/build-angular/node_modules/watchpack": { @@ -899,12 +971,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1703.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.11.tgz", - "integrity": "sha512-qbCiiHuoVkD7CtLyWoRi/Vzz6nrEztpF5XIyWUcQu67An1VlxbMTE4yoSQiURjCQMnB/JvS1GPVed7wOq3SJ/w==", + "version": "0.1703.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.16.tgz", + "integrity": "sha512-ybZr+2F4siu0MztyhSkzN3lIMF0YFeyMaoTygWKjMeGMrkdj4IOAHiR+le2dQ+W4RhwEwQwkV2lvyDJzbivWhg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1703.11", + "@angular-devkit/architect": "0.1703.16", "rxjs": "7.8.1" }, "engines": { @@ -917,10 +989,19 @@ "webpack-dev-server": "^4.0.0" } }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.16.tgz", + "integrity": "sha512-3Dhb/pE3c6P9bDfYLhYb0ArTFYmYSx5QgEUVMuowXJtP/3EyU7lWB2kcuiBZgScxrhRLOiMGMPgcF9jVmvovug==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -960,12 +1041,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@angular-devkit/core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/@angular-devkit/core/node_modules/picomatch": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", @@ -978,13 +1053,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/schematics": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", - "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.16.tgz", + "integrity": "sha512-EcKBdQ02RIwYLHrExOvtrj8FXtTT/Z0IQe8maUy+YkOWjJHsjpdRBOwi3JOICyjnkopOtkkHy/bxu5VKh6rL7A==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.3.11", + "@angular-devkit/core": "17.3.16", "jsonc-parser": "3.2.1", "magic-string": "0.30.8", "ora": "5.4.1", @@ -996,6 +1080,15 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular/animations": { "version": "17.3.12", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz", @@ -1027,15 +1120,15 @@ } }, "node_modules/@angular/cli": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.11.tgz", - "integrity": "sha512-8R9LwAGL8hGAWJ4mNG9ZPUrBUzIdmst0Ldua6RJJ+PrqgjX+8IbO+lNnfrOY/XY+Z3LXbCEJflL26f9czCvTPQ==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.16.tgz", + "integrity": "sha512-cG/+aAW7z/o8Tl75U6d+pa+zygV9cvdeY/vb6ve16o4MS6Ifwbls6L51gekYGdWpLl1QnWHLbZFz7+mOa7Um2w==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1703.11", - "@angular-devkit/core": "17.3.11", - "@angular-devkit/schematics": "17.3.11", - "@schematics/angular": "17.3.11", + "@angular-devkit/architect": "0.1703.16", + "@angular-devkit/core": "17.3.16", + "@angular-devkit/schematics": "17.3.16", + "@schematics/angular": "17.3.16", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.2", @@ -1347,9 +1440,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", - "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1385,40 +1478,12 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/@babel/core/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1429,27 +1494,28 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "dev": true, "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1516,18 +1582,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1554,18 +1608,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1576,9 +1618,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1591,18 +1633,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", @@ -1684,18 +1714,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-replace-supers": { "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", @@ -1727,12 +1745,12 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1780,25 +1798,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", - "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "dependencies": { - "@babel/types": "^7.26.5" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1807,6 +1825,37 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", @@ -1867,69 +1916,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", @@ -1947,137 +1933,11 @@ }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2118,15 +1978,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" }, "engines": { "node": ">=6.9.0" @@ -2136,14 +1995,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2234,18 +2093,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", @@ -2308,6 +2155,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", @@ -2354,12 +2217,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", + "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "engines": { @@ -2685,18 +2548,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", @@ -2728,6 +2579,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", @@ -2744,16 +2611,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", - "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "engines": { @@ -2819,12 +2686,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", + "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -2834,12 +2701,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", - "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz", + "integrity": "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -2912,90 +2779,79 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", - "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.24.0", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "engines": { @@ -3029,9 +2885,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3040,30 +2896,30 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", - "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.5", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.5", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -3072,13 +2928,13 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "dev": true, "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -3087,22 +2943,10 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@babel/types": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", - "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -3519,9 +3363,9 @@ } }, "node_modules/@formio/bootstrap": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@formio/bootstrap/-/bootstrap-3.0.0.tgz", - "integrity": "sha512-9a2JMY/qU/UUQ3BvlxrZfAjy/v5unwkXXBdy98+MiUbmQVaLi8ITIsA/YeYkRKFQW8V6BSbOoTlJhmVvxdvPnA==" + "version": "3.0.2-rc.1", + "resolved": "https://registry.npmjs.org/@formio/bootstrap/-/bootstrap-3.0.2-rc.1.tgz", + "integrity": "sha512-CfwnU04l8yMc0kr1j2Y7mj193Z5BfqoNiCGBsTz1bDIr/tnaQkfEOZ21zJ15KVpCXVGc6SjW7QQ1JY8UVsE0lQ==" }, "node_modules/@formio/bootstrap3": { "version": "2.12.4-rc.1", @@ -3543,9 +3387,9 @@ } }, "node_modules/@formio/core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@formio/core/-/core-2.3.0.tgz", - "integrity": "sha512-jAlNm0vaSZQ9RSxt79PqeHdXYv/1i+zNfasVWbH1xRJSicJK0JcdodrBaDw8kKnEh0UmflW6RyIiaMyDbww4Ug==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@formio/core/-/core-2.3.3.tgz", + "integrity": "sha512-Tgv5FEwEf9xQtl/cqviaWyBCFcwGCb1pVXwTjIH7KxtWMwsyXtbxGotjfFyZY9unOw6Snk2ik/sln80yQe5y9A==", "dependencies": { "@types/json-logic-js": "^2.0.7", "browser-cookies": "^1.2.0", @@ -3567,14 +3411,14 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, "node_modules/@formio/js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@formio/js/-/js-5.0.0.tgz", - "integrity": "sha512-Ly3f7q2wo5oFjlECKFtkIMy+Ah9HyAkf6ob70OrF/6MooSPF3hEu9euXouEWOoNdI+Tokr690/GdM2sDZtYzww==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@formio/js/-/js-5.0.2.tgz", + "integrity": "sha512-zAuCUuxBPCJ7Nmt8+xPE5XyLkVg550a//I9ke8bhI7EGlRkN6xF1Ve7Nomsi8ruohiw2BVcMH+5SS3LXWUm0uQ==", "dependencies": { - "@formio/bootstrap": "3.0.0", + "@formio/bootstrap": "3.0.2-rc.1", "@formio/choices.js": "^10.2.1", - "@formio/core": "2.3.0", - "@formio/text-mask-addons": "^3.8.0-formio.3", + "@formio/core": "2.3.3", + "@formio/text-mask-addons": "3.8.0-formio.4", "@formio/vanilla-text-mask": "^5.1.1-formio.1", "abortcontroller-polyfill": "^1.7.5", "autocompleter": "^8.0.4", @@ -3614,9 +3458,9 @@ "integrity": "sha512-q334YswwucSBphN5djVkEt3beVhHotrCtPGNIXmyilw9UnXV9Cb+gNAZ2yhZSfiBSzP6rxHLLT2gpr57xgbcwQ==" }, "node_modules/@formio/js/node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", + "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", "funding": [ { "type": "github", @@ -3853,6 +3697,12 @@ "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3872,9 +3722,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.11.tgz", - "integrity": "sha512-SfTCbplt4y6ak5cf2IfqdoVOsnoNdh/j6Vu+wb8WWABKwZ5yfr2S/Gk6ithSKcdIZhAF8DNBOoyk1EJuf8Xkfg==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.16.tgz", + "integrity": "sha512-Wxtiut1o9rj3+HumyXoYWg4iCuPDEt4nf1Y9bRCo3y5evEKp0ZTO7IFPpZLaZ0JGGrpROiQErjvHdDQBOuXwWQ==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -3900,16 +3750,15 @@ } }, "node_modules/@ngx-translate/http-loader": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-7.0.0.tgz", - "integrity": "sha512-j+NpXXlcGVdyUNyY/qsJrqqeAdJdizCd+GKh3usXExSqy1aE9866jlAIL+xrfDU4w+LiMoma5pgE4emvFebZmA==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-16.0.1.tgz", + "integrity": "sha512-xJEOUpvs6Zfc8G4cmQmegFOEpfYSoplTHHoisPNrATXjRBjpaKsBaPOXlZsuFUW2XV00s16gIyI4+9z1XkO5bw==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": ">=13.0.0", - "@ngx-translate/core": ">=14.0.0", - "rxjs": "^6.5.3 || ^7.4.0" + "@angular/common": ">=16", + "@angular/core": ">=16" } }, "node_modules/@nodelib/fs.scandir": { @@ -4506,13 +4355,13 @@ ] }, "node_modules/@schematics/angular": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.11.tgz", - "integrity": "sha512-tvJpTgYC+hCnTyLszYRUZVyNTpPd+C44gh5CPTcG3qkqStzXQwynQAf6X/DjtwXbUiPQF0XfF0+0R489GpdZPA==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.16.tgz", + "integrity": "sha512-Ts/cAZmxlIL+AOLbmBylCMjXdHeqWZE2IIYmP5334tQNERSharOlKbLIz5PeESmBDGpH9KRa0wSMK5KXI5ixnQ==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.3.11", - "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/core": "17.3.16", + "@angular-devkit/schematics": "17.3.16", "jsonc-parser": "3.2.1" }, "engines": { @@ -4797,9 +4646,9 @@ } }, "node_modules/@types/jasmine": { - "version": "3.6.11", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz", - "integrity": "sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.7.tgz", + "integrity": "sha512-DVOfk9FaClQfNFpSfaML15jjB5cjffDMvjtph525sroR5BEAW2uKnTOYUTqTFuZFjNvH0T5XMIydvIctnUKufw==", "dev": true }, "node_modules/@types/json-logic-js": { @@ -4864,9 +4713,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==" + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==" }, "node_modules/@types/send": { "version": "0.17.4", @@ -4943,9 +4792,9 @@ } }, "node_modules/@types/zxcvbn": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.1.tgz", - "integrity": "sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==" + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.5.tgz", + "integrity": "sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==" }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", @@ -5125,17 +4974,17 @@ "dev": true }, "node_modules/@zxcvbn-ts/core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@zxcvbn-ts/core/-/core-3.0.0.tgz", - "integrity": "sha512-AuuYc66+5csSLd/mJ1CXuTUsjLdYw4X0zh8WvDCLztTOye0b+HrKCu8WzAEmq6Ja1u+cSbCNrKLW7m4ZCeg0FA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@zxcvbn-ts/core/-/core-3.0.4.tgz", + "integrity": "sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==", "dependencies": { "fastest-levenshtein": "1.0.16" } }, "node_modules/@zxcvbn-ts/language-en": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@zxcvbn-ts/language-en/-/language-en-3.0.0.tgz", - "integrity": "sha512-JJ0H3ovVIO4EEpYXk5qq6NbxyZ8b3/qkpwEFIOu0m8bb54vXtRy2EbdfzUm0XKdtw9gfx15MaHAn0pWv0hTzYg==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@zxcvbn-ts/language-en/-/language-en-3.0.2.tgz", + "integrity": "sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==" }, "node_modules/abbrev": { "version": "2.0.0", @@ -5244,14 +5093,14 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -5275,28 +5124,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -5622,31 +5449,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.12", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", @@ -5671,57 +5473,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", - "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", - "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.5.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -6271,6 +6041,17 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/chart.js": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -6823,12 +6604,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", - "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", + "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", "dev": true, "dependencies": { - "browserslist": "^4.24.3" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -8228,13 +8009,13 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-uri": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", - "dev": true, "funding": [ { "type": "github", @@ -8816,6 +8597,28 @@ "node": ">=6" } }, + "node_modules/har-validator/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/har-validator/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -9725,14 +9528,15 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "dependencies": { - "@babel/core": "^7.7.5", + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" }, "engines": { @@ -9784,9 +9588,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "dependencies": { "debug": "^4.1.1", @@ -9794,7 +9598,7 @@ "source-map": "^0.6.1" }, "engines": { - "node": ">=8" + "node": ">=10" } }, "node_modules/istanbul-lib-source-maps/node_modules/source-map": { @@ -9807,9 +9611,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -9849,9 +9653,9 @@ } }, "node_modules/jasmine-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", - "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", "dev": true }, "node_modules/jasmine-spec-reporter": { @@ -9938,9 +9742,9 @@ } }, "node_modules/jquery": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", - "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, "node_modules/js-tokens": { "version": "4.0.0", @@ -9968,15 +9772,15 @@ "dev": true }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-logic-js": { @@ -9997,9 +9801,9 @@ "dev": true }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -10105,9 +9909,9 @@ "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" }, "node_modules/karma": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz", - "integrity": "sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "dependencies": { "@colors/colors": "1.5.0", @@ -10129,7 +9933,7 @@ "qjobs": "^1.2.0", "range-parser": "^1.2.1", "rimraf": "^3.0.2", - "socket.io": "^4.4.1", + "socket.io": "^4.7.2", "source-map": "^0.6.1", "tmp": "^0.2.1", "ua-parser-js": "^0.7.30", @@ -10143,9 +9947,9 @@ } }, "node_modules/karma-chrome-launcher": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", - "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", "dev": true, "dependencies": { "which": "^1.2.1" @@ -10167,16 +9971,16 @@ } }, "node_modules/karma-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.0.3.tgz", - "integrity": "sha512-atDvLQqvPcLxhED0cmXYdsPMCQuh6Asa9FMZW1bhNqlVEhJoB9qyZ2BY1gu7D/rr5GLGb5QzYO4siQskxaWP/g==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", "dev": true, "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", "minimatch": "^3.0.4" }, "engines": { @@ -10184,9 +9988,9 @@ } }, "node_modules/karma-jasmine": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz", - "integrity": "sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz", + "integrity": "sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==", "dev": true, "dependencies": { "jasmine-core": "^3.6.0" @@ -10252,51 +10056,19 @@ }, "node_modules/karma-webpack/node_modules/minimatch": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/karma/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/karma/node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/karma/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -10319,15 +10091,12 @@ } }, "node_modules/karma/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/kind-of": { @@ -10655,6 +10424,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -12418,9 +12188,9 @@ "integrity": "sha512-zaOq3YvcOYytbAmKv3zYc+0VNS9Wg5d37dfxZnveKBFPr7vEIwfV5ydrpiouTft8MVW6qNjfkaQphHSnvgQbpQ==" }, "node_modules/primeicons": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-6.0.1.tgz", - "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==" }, "node_modules/primeng": { "version": "17.18.15", @@ -12776,6 +12546,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "engines": { "node": ">=6" } @@ -13237,7 +13008,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -13449,9 +13219,9 @@ } }, "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dependencies": { "tslib": "^2.1.0" } @@ -13596,22 +13366,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", @@ -13624,12 +13378,6 @@ "ajv": "^8.8.2" } }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -13690,12 +13438,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "bin": { "semver": "bin/semver.js" }, @@ -15088,19 +14833,10 @@ "node": "*" } }, - "node_modules/undici": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", - "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", - "dev": true, - "engines": { - "node": ">=18.0" - } - }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "optional": true, "peer": true @@ -15197,6 +14933,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -15670,6 +15407,28 @@ } } }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -15900,7 +15659,8 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/yargs": { "version": "16.2.0", @@ -15991,36 +15751,47 @@ } }, "@angular-devkit/architect": { - "version": "0.1703.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.11.tgz", - "integrity": "sha512-YNasVZk4rYdcM6M+KRH8PUBhVyJfqzUYLpO98GgRokW+taIDgifckSlmfDZzQRbw45qiwei1IKCLqcpC8nM5Tw==", + "version": "0.1703.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.16.tgz", + "integrity": "sha512-wuqKu20ekgzzikUTZD28dS72F6vjniZuiQ7RgAYhykmsU0z0br2tksHQvjD/auzVArtgQir1+V9wp6BN4dSdNQ==", "dev": true, "requires": { - "@angular-devkit/core": "17.3.11", + "@angular-devkit/core": "17.3.16", "rxjs": "7.8.1" + }, + "dependencies": { + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, "@angular-devkit/build-angular": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.11.tgz", - "integrity": "sha512-lHX5V2dSts328yvo/9E2u9QMGcvJhbEKKDDp9dBecwvIG9s+4lTOJgi9DPUE7W+AtmPcmbbhwC2JRQ/SLQhAoA==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.16.tgz", + "integrity": "sha512-5JiR1NK3MOwzipAn4UmvJ8yQa6NaBtHBWbLrY0Ps6a21kHWn42C+dpvVdlXN/ZZSpEll/nzA+b77zA3kDrGlKw==", "dev": true, "requires": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1703.11", - "@angular-devkit/build-webpack": "0.1703.11", - "@angular-devkit/core": "17.3.11", - "@babel/core": "7.24.0", - "@babel/generator": "7.23.6", - "@babel/helper-annotate-as-pure": "7.22.5", - "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.9", - "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.24.0", - "@babel/preset-env": "7.24.0", - "@babel/runtime": "7.24.0", + "@angular-devkit/architect": "0.1703.16", + "@angular-devkit/build-webpack": "0.1703.16", + "@angular-devkit/core": "17.3.16", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.3.11", + "@ngtools/webpack": "17.3.16", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.18", @@ -16062,8 +15833,7 @@ "terser": "5.29.1", "tree-kill": "1.2.2", "tslib": "2.6.2", - "undici": "6.11.1", - "vite": "5.1.8", + "vite": "~5.4.17", "watchpack": "2.4.0", "webpack": "5.94.0", "webpack-dev-middleware": "6.1.2", @@ -16073,21 +15843,21 @@ }, "dependencies": { "@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -16104,175 +15874,175 @@ } }, "@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "dev": true, "optional": true }, "@types/node": { - "version": "22.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", - "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "dev": true, "optional": true, "peer": true, "requires": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "@vitejs/plugin-basic-ssl": { @@ -16282,18 +16052,45 @@ "dev": true, "requires": {} }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "picomatch": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", "dev": true }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, "schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -16321,46 +16118,57 @@ "dev": true }, "vite": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.8.tgz", - "integrity": "sha512-mB8ToUuSmzODSpENgvpFk2fTiU/YQ1tmcVJJ4WZbq4fPdGJkFNVcmVL5k7iDug6xzWjjuGDKAuSievIsD6H7Xw==", + "version": "5.4.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", + "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", "dev": true, "requires": { - "esbuild": "^0.19.3", + "esbuild": "^0.21.3", "fsevents": "~2.3.3", - "postcss": "^8.4.35", - "rollup": "^4.2.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "dependencies": { "esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "requires": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" } } } @@ -16432,19 +16240,30 @@ } }, "@angular-devkit/build-webpack": { - "version": "0.1703.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.11.tgz", - "integrity": "sha512-qbCiiHuoVkD7CtLyWoRi/Vzz6nrEztpF5XIyWUcQu67An1VlxbMTE4yoSQiURjCQMnB/JvS1GPVed7wOq3SJ/w==", + "version": "0.1703.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.16.tgz", + "integrity": "sha512-ybZr+2F4siu0MztyhSkzN3lIMF0YFeyMaoTygWKjMeGMrkdj4IOAHiR+le2dQ+W4RhwEwQwkV2lvyDJzbivWhg==", "dev": true, "requires": { - "@angular-devkit/architect": "0.1703.11", + "@angular-devkit/architect": "0.1703.16", "rxjs": "7.8.1" + }, + "dependencies": { + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, "@angular-devkit/core": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", - "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.16.tgz", + "integrity": "sha512-3Dhb/pE3c6P9bDfYLhYb0ArTFYmYSx5QgEUVMuowXJtP/3EyU7lWB2kcuiBZgScxrhRLOiMGMPgcF9jVmvovug==", "dev": true, "requires": { "ajv": "8.12.0", @@ -16467,31 +16286,45 @@ "uri-js": "^4.2.2" } }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "picomatch": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", "dev": true + }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } } } }, "@angular-devkit/schematics": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", - "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.16.tgz", + "integrity": "sha512-EcKBdQ02RIwYLHrExOvtrj8FXtTT/Z0IQe8maUy+YkOWjJHsjpdRBOwi3JOICyjnkopOtkkHy/bxu5VKh6rL7A==", "dev": true, "requires": { - "@angular-devkit/core": "17.3.11", + "@angular-devkit/core": "17.3.16", "jsonc-parser": "3.2.1", "magic-string": "0.30.8", "ora": "5.4.1", "rxjs": "7.8.1" + }, + "dependencies": { + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, "@angular/animations": { @@ -16512,15 +16345,15 @@ } }, "@angular/cli": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.11.tgz", - "integrity": "sha512-8R9LwAGL8hGAWJ4mNG9ZPUrBUzIdmst0Ldua6RJJ+PrqgjX+8IbO+lNnfrOY/XY+Z3LXbCEJflL26f9czCvTPQ==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.16.tgz", + "integrity": "sha512-cG/+aAW7z/o8Tl75U6d+pa+zygV9cvdeY/vb6ve16o4MS6Ifwbls6L51gekYGdWpLl1QnWHLbZFz7+mOa7Um2w==", "dev": true, "requires": { - "@angular-devkit/architect": "0.1703.11", - "@angular-devkit/core": "17.3.11", - "@angular-devkit/schematics": "17.3.11", - "@schematics/angular": "17.3.11", + "@angular-devkit/architect": "0.1703.16", + "@angular-devkit/core": "17.3.16", + "@angular-devkit/schematics": "17.3.16", + "@schematics/angular": "17.3.16", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.2", @@ -16714,9 +16547,9 @@ } }, "@babel/compat-data": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", - "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true }, "@babel/core": { @@ -16742,31 +16575,12 @@ "semver": "^6.3.1" }, "dependencies": { - "@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", - "dev": true, - "requires": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - } - }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true - }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -16776,24 +16590,25 @@ } }, "@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "dev": true, "requires": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" } }, "@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.25.9" } }, "@babel/helper-compilation-targets": { @@ -16847,15 +16662,6 @@ "semver": "^6.3.1" }, "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "requires": { - "@babel/types": "^7.25.9" - } - }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -16875,15 +16681,6 @@ "semver": "^6.3.1" }, "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "requires": { - "@babel/types": "^7.25.9" - } - }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -16893,9 +16690,9 @@ } }, "@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", "dev": true, "requires": { "@babel/helper-compilation-targets": "^7.22.6", @@ -16905,15 +16702,6 @@ "resolve": "^1.14.2" } }, - "@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "requires": { - "@babel/types": "^7.24.7" - } - }, "@babel/helper-member-expression-to-functions": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", @@ -16969,17 +16757,6 @@ "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-wrap-function": "^7.25.9", "@babel/traverse": "^7.25.9" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "requires": { - "@babel/types": "^7.25.9" - } - } } }, "@babel/helper-replace-supers": { @@ -17004,12 +16781,12 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-string-parser": { @@ -17042,212 +16819,96 @@ } }, "@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "requires": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" } }, "@babel/parser": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", - "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", - "dev": true, - "requires": { - "@babel/types": "^7.26.5" - } - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.25.9" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/types": "^7.27.0" } }, - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" } }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "requires": {} - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.25.9" - } - }, - "@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.25.9" } }, - "@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.25.9" } }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" } }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.8.0" + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" } }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } + "requires": {} }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.9" } }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "@babel/helper-plugin-utils": "^7.25.9" } }, "@babel/plugin-syntax-unicode-sets-regex": { @@ -17270,26 +16931,25 @@ } }, "@babel/plugin-transform-async-generator-functions": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", - "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" } }, "@babel/plugin-transform-block-scoped-functions": { @@ -17342,17 +17002,6 @@ "@babel/helper-replace-supers": "^7.25.9", "@babel/traverse": "^7.25.9", "globals": "^11.1.0" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "requires": { - "@babel/types": "^7.25.9" - } - } } }, "@babel/plugin-transform-computed-properties": { @@ -17393,6 +17042,16 @@ "@babel/helper-plugin-utils": "^7.25.9" } }, + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, "@babel/plugin-transform-dynamic-import": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", @@ -17421,12 +17080,12 @@ } }, "@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", + "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" } }, @@ -17624,17 +17283,6 @@ "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", - "dev": true, - "requires": { - "@babel/types": "^7.25.9" - } - } } }, "@babel/plugin-transform-property-literals": { @@ -17656,6 +17304,16 @@ "regenerator-transform": "^0.15.2" } }, + "@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, "@babel/plugin-transform-reserved-words": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", @@ -17666,16 +17324,16 @@ } }, "@babel/plugin-transform-runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", - "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.24.0", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "dependencies": { @@ -17716,21 +17374,21 @@ } }, "@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", + "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", - "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz", + "integrity": "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" } }, "@babel/plugin-transform-unicode-escapes": { @@ -17773,90 +17431,79 @@ } }, "@babel/preset-env": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", - "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.24.0", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.9", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.8", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.9", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.24.0", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.8", - "babel-plugin-polyfill-corejs3": "^0.9.0", - "babel-plugin-polyfill-regenerator": "^0.5.5", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "dependencies": { @@ -17880,64 +17527,58 @@ } }, "@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "requires": { "regenerator-runtime": "^0.14.0" } }, "@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "requires": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" } }, "@babel/traverse": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", - "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", "dev": true, "requires": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.5", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.5", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" }, "dependencies": { "@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", "dev": true, "requires": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } - }, - "jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true } } }, "@babel/types": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", - "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.25.9", @@ -18126,9 +17767,9 @@ } }, "@formio/bootstrap": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@formio/bootstrap/-/bootstrap-3.0.0.tgz", - "integrity": "sha512-9a2JMY/qU/UUQ3BvlxrZfAjy/v5unwkXXBdy98+MiUbmQVaLi8ITIsA/YeYkRKFQW8V6BSbOoTlJhmVvxdvPnA==" + "version": "3.0.2-rc.1", + "resolved": "https://registry.npmjs.org/@formio/bootstrap/-/bootstrap-3.0.2-rc.1.tgz", + "integrity": "sha512-CfwnU04l8yMc0kr1j2Y7mj193Z5BfqoNiCGBsTz1bDIr/tnaQkfEOZ21zJ15KVpCXVGc6SjW7QQ1JY8UVsE0lQ==" }, "@formio/bootstrap3": { "version": "2.12.4-rc.1", @@ -18150,9 +17791,9 @@ } }, "@formio/core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@formio/core/-/core-2.3.0.tgz", - "integrity": "sha512-jAlNm0vaSZQ9RSxt79PqeHdXYv/1i+zNfasVWbH1xRJSicJK0JcdodrBaDw8kKnEh0UmflW6RyIiaMyDbww4Ug==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@formio/core/-/core-2.3.3.tgz", + "integrity": "sha512-Tgv5FEwEf9xQtl/cqviaWyBCFcwGCb1pVXwTjIH7KxtWMwsyXtbxGotjfFyZY9unOw6Snk2ik/sln80yQe5y9A==", "requires": { "@types/json-logic-js": "^2.0.7", "browser-cookies": "^1.2.0", @@ -18176,14 +17817,14 @@ } }, "@formio/js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@formio/js/-/js-5.0.0.tgz", - "integrity": "sha512-Ly3f7q2wo5oFjlECKFtkIMy+Ah9HyAkf6ob70OrF/6MooSPF3hEu9euXouEWOoNdI+Tokr690/GdM2sDZtYzww==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@formio/js/-/js-5.0.2.tgz", + "integrity": "sha512-zAuCUuxBPCJ7Nmt8+xPE5XyLkVg550a//I9ke8bhI7EGlRkN6xF1Ve7Nomsi8ruohiw2BVcMH+5SS3LXWUm0uQ==", "requires": { - "@formio/bootstrap": "3.0.0", + "@formio/bootstrap": "3.0.2-rc.1", "@formio/choices.js": "^10.2.1", - "@formio/core": "2.3.0", - "@formio/text-mask-addons": "^3.8.0-formio.3", + "@formio/core": "2.3.3", + "@formio/text-mask-addons": "3.8.0-formio.4", "@formio/vanilla-text-mask": "^5.1.1-formio.1", "abortcontroller-polyfill": "^1.7.5", "autocompleter": "^8.0.4", @@ -18223,9 +17864,9 @@ "integrity": "sha512-q334YswwucSBphN5djVkEt3beVhHotrCtPGNIXmyilw9UnXV9Cb+gNAZ2yhZSfiBSzP6rxHLLT2gpr57xgbcwQ==" }, "bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", + "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", "requires": {} }, "compare-versions": { @@ -18401,6 +18042,11 @@ "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, + "@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -18417,9 +18063,9 @@ } }, "@ngtools/webpack": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.11.tgz", - "integrity": "sha512-SfTCbplt4y6ak5cf2IfqdoVOsnoNdh/j6Vu+wb8WWABKwZ5yfr2S/Gk6ithSKcdIZhAF8DNBOoyk1EJuf8Xkfg==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.16.tgz", + "integrity": "sha512-Wxtiut1o9rj3+HumyXoYWg4iCuPDEt4nf1Y9bRCo3y5evEKp0ZTO7IFPpZLaZ0JGGrpROiQErjvHdDQBOuXwWQ==", "dev": true, "requires": {} }, @@ -18432,9 +18078,9 @@ } }, "@ngx-translate/http-loader": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-7.0.0.tgz", - "integrity": "sha512-j+NpXXlcGVdyUNyY/qsJrqqeAdJdizCd+GKh3usXExSqy1aE9866jlAIL+xrfDU4w+LiMoma5pgE4emvFebZmA==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-16.0.1.tgz", + "integrity": "sha512-xJEOUpvs6Zfc8G4cmQmegFOEpfYSoplTHHoisPNrATXjRBjpaKsBaPOXlZsuFUW2XV00s16gIyI4+9z1XkO5bw==", "requires": { "tslib": "^2.3.0" } @@ -18832,13 +18478,13 @@ "optional": true }, "@schematics/angular": { - "version": "17.3.11", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.11.tgz", - "integrity": "sha512-tvJpTgYC+hCnTyLszYRUZVyNTpPd+C44gh5CPTcG3qkqStzXQwynQAf6X/DjtwXbUiPQF0XfF0+0R489GpdZPA==", + "version": "17.3.16", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.16.tgz", + "integrity": "sha512-Ts/cAZmxlIL+AOLbmBylCMjXdHeqWZE2IIYmP5334tQNERSharOlKbLIz5PeESmBDGpH9KRa0wSMK5KXI5ixnQ==", "dev": true, "requires": { - "@angular-devkit/core": "17.3.11", - "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/core": "17.3.16", + "@angular-devkit/schematics": "17.3.16", "jsonc-parser": "3.2.1" } }, @@ -19091,9 +18737,9 @@ } }, "@types/jasmine": { - "version": "3.6.11", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz", - "integrity": "sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.7.tgz", + "integrity": "sha512-DVOfk9FaClQfNFpSfaML15jjB5cjffDMvjtph525sroR5BEAW2uKnTOYUTqTFuZFjNvH0T5XMIydvIctnUKufw==", "dev": true }, "@types/json-logic-js": { @@ -19158,9 +18804,9 @@ "dev": true }, "@types/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==" + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==" }, "@types/send": { "version": "0.17.4", @@ -19237,9 +18883,9 @@ } }, "@types/zxcvbn": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.1.tgz", - "integrity": "sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==" + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.5.tgz", + "integrity": "sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==" }, "@webassemblyjs/ast": { "version": "1.14.1", @@ -19417,17 +19063,17 @@ "dev": true }, "@zxcvbn-ts/core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@zxcvbn-ts/core/-/core-3.0.0.tgz", - "integrity": "sha512-AuuYc66+5csSLd/mJ1CXuTUsjLdYw4X0zh8WvDCLztTOye0b+HrKCu8WzAEmq6Ja1u+cSbCNrKLW7m4ZCeg0FA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@zxcvbn-ts/core/-/core-3.0.4.tgz", + "integrity": "sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==", "requires": { "fastest-levenshtein": "1.0.16" } }, "@zxcvbn-ts/language-en": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@zxcvbn-ts/language-en/-/language-en-3.0.0.tgz", - "integrity": "sha512-JJ0H3ovVIO4EEpYXk5qq6NbxyZ8b3/qkpwEFIOu0m8bb54vXtRy2EbdfzUm0XKdtw9gfx15MaHAn0pWv0hTzYg==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@zxcvbn-ts/language-en/-/language-en-3.0.2.tgz", + "integrity": "sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==" }, "abbrev": { "version": "2.0.0", @@ -19509,14 +19155,14 @@ } }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" } }, "ajv-formats": { @@ -19526,26 +19172,6 @@ "dev": true, "requires": { "ajv": "^8.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } } }, "ajv-keywords": { @@ -19787,27 +19413,6 @@ "@istanbuljs/schema": "^0.1.2", "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" - }, - "dependencies": { - "istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - } - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } } }, "babel-plugin-polyfill-corejs2": { @@ -19830,52 +19435,22 @@ } }, "babel-plugin-polyfill-corejs3": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", - "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.5.0", - "core-js-compat": "^3.34.0" - }, - "dependencies": { - "@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - } - } + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", - "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", + "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.5.0" - }, - "dependencies": { - "@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - } - } + "@babel/helper-define-polyfill-provider": "^0.6.4" } }, "balanced-match": { @@ -20278,6 +19853,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -20705,12 +20288,12 @@ "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==" }, "core-js-compat": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", - "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", + "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", "dev": true, "requires": { - "browserslist": "^4.24.3" + "browserslist": "^4.24.4" } }, "core-util-is": { @@ -21790,13 +21373,13 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "fast-uri": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", - "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", - "dev": true + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==" }, "fastest-levenshtein": { "version": "1.0.16", @@ -22219,6 +21802,26 @@ "requires": { "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } } }, "has-ansi": { @@ -22892,14 +22495,15 @@ "dev": true }, "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "requires": { - "@babel/core": "^7.7.5", + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" }, "dependencies": { @@ -22940,9 +22544,9 @@ } }, "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "requires": { "debug": "^4.1.1", @@ -22959,9 +22563,9 @@ } }, "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -22998,9 +22602,9 @@ } }, "jasmine-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", - "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", "dev": true }, "jasmine-spec-reporter": { @@ -23062,9 +22666,9 @@ "dev": true }, "jquery": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", - "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, "js-tokens": { "version": "4.0.0", @@ -23089,9 +22693,9 @@ "dev": true }, "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true }, "json-logic-js": { @@ -23112,9 +22716,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-stringify-safe": { "version": "5.0.1", @@ -23210,9 +22814,9 @@ "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" }, "karma": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz", - "integrity": "sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA==", + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "requires": { "@colors/colors": "1.5.0", @@ -23234,38 +22838,13 @@ "qjobs": "^1.2.0", "range-parser": "^1.2.1", "rimraf": "^3.0.2", - "socket.io": "^4.4.1", + "socket.io": "^4.7.2", "source-map": "^0.6.1", "tmp": "^0.2.1", "ua-parser-js": "^0.7.30", "yargs": "^16.1.1" }, "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, "mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -23282,20 +22861,17 @@ "dev": true }, "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true } } }, "karma-chrome-launcher": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", - "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", "dev": true, "requires": { "which": "^1.2.1" @@ -23311,23 +22887,23 @@ } }, "karma-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.0.3.tgz", - "integrity": "sha512-atDvLQqvPcLxhED0cmXYdsPMCQuh6Asa9FMZW1bhNqlVEhJoB9qyZ2BY1gu7D/rr5GLGb5QzYO4siQskxaWP/g==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", "dev": true, "requires": { - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", "minimatch": "^3.0.4" } }, "karma-jasmine": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.1.tgz", - "integrity": "sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz", + "integrity": "sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==", "dev": true, "requires": { "jasmine-core": "^3.6.0" @@ -23627,6 +23203,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" } @@ -24891,9 +24468,9 @@ "integrity": "sha512-zaOq3YvcOYytbAmKv3zYc+0VNS9Wg5d37dfxZnveKBFPr7vEIwfV5ydrpiouTft8MVW6qNjfkaQphHSnvgQbpQ==" }, "primeicons": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-6.0.1.tgz", - "integrity": "sha512-KDeO94CbWI4pKsPnYpA1FPjo79EsY9I+M8ywoPBSf9XMXoe/0crjbUK7jcQEDHuc0ZMRIZsxH3TYLv4TUtHmAA==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==" }, "primeng": { "version": "17.18.15", @@ -25175,7 +24752,8 @@ "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true }, "puppeteer": { "version": "1.20.0", @@ -25537,8 +25115,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "require-main-filename": { "version": "2.0.0", @@ -25689,9 +25266,9 @@ } }, "rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "requires": { "tslib": "^2.1.0" } @@ -25785,18 +25362,6 @@ "ajv-keywords": "^5.1.0" }, "dependencies": { - "ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - } - }, "ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", @@ -25805,12 +25370,6 @@ "requires": { "fast-deep-equal": "^3.1.3" } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true } } }, @@ -25863,12 +25422,9 @@ } }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" }, "semver-dsl": { "version": "1.0.1", @@ -26929,16 +26485,10 @@ "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==", "dev": true }, - "undici": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", - "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", - "dev": true - }, "undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "optional": true, "peer": true @@ -26997,6 +26547,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "requires": { "punycode": "^2.1.0" } @@ -27231,6 +26782,24 @@ "webpack-sources": "^3.2.3" }, "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -27508,7 +27077,8 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "yargs": { "version": "16.2.0", diff --git a/package.json b/package.json index 6c76618594cd9546a117d73864d93c6fccfd124d..6b64b51760053ce0aef49fe87fe6bab107b9d996 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nmaas-portal", - "version": "1.7.0", + "version": "1.7.1", "license": "Apache 2.0", "angular-cli": {}, "scripts": { @@ -28,24 +28,25 @@ "@formio/angular": "7.0.0", "@formio/js": "^5.0.0", "@ngx-translate/core": "^14.0.0", - "@ngx-translate/http-loader": "^7.0.0", + "@ngx-translate/http-loader": "^16.0.0", "@types/semver": "^7.3.8", "@types/zxcvbn": "^4.4.1", "@zxcvbn-ts/core": "^3.0.0", "@zxcvbn-ts/language-en": "^3.0.0", - "ajv": "^6.12.6", + "ajv": "^8.0.0", "angular-password-strength-meter": "^11.0.0", "bootstrap": "^3.4.1", + "chart.js": "^4.4.8", "document-register-element": "^1.14.10", "jquery": "^3.6.0", "lodash": "^4.17.21", "ng-event-source": "^1.0.14", - "ng-recaptcha": "^13.0.0", + "ng-recaptcha": "^13.2.1", "ng-terminal": "^6.3.0", "ngx-pagination": "^6.0.3", "ngx-webstorage-service": "^5.0.0", "primeflex": "^3.3.1", - "primeicons": "^6.0.1", + "primeicons": "^7.0.0", "primeng": "^17.18.15", "rxjs": "^7.8.1", "semver": "^7.5.4", @@ -58,14 +59,14 @@ "@angular-devkit/build-angular": "^17.3.11", "@angular/cli": "^17.3.11", "@angular/compiler-cli": "^17.3.12", - "@types/jasmine": "~3.6.0", + "@types/jasmine": "~5.1.0", "@types/node": "^12.20.13", "codelyzer": "^6.0.0", "intl": "^1.2.5", "jasmine-core": "~4.6.0", "jasmine-spec-reporter": "~5.0.0", "karma": "^6.3.14", - "karma-chrome-launcher": "~3.1.0", + "karma-chrome-launcher": "~3.2.0", "karma-cli": "^1.0.1", "karma-coverage": "^2.0.3", "karma-jasmine": "~4.0.0", diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000000000000000000000000000000000000..fd6683814e88163cfbff70a4a77a9a2d2f88e454 --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "separateMinorPatch": true +} diff --git a/src/app/app.component.css b/src/app/app.component.css index 48e17db39d06b20dd9d84fc11e0a2a89c75b53dc..035dee4e90e9ec4bc20dd27e9817e86ba1045d3d 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -14,34 +14,46 @@ body{ flex-direction: row; } +.flex-container-column { + flex: 1; + height:100vh; + display: flex; + flex-direction: column; +} + .flex-spacer { - /*flex: 1 0 auto;*/ - /* width: 100%; - height: 100%; */ + flex: 1 0 auto; + height: 100% } .logged-out-layout { display: flex; flex-direction: column; min-height: 100vh; - } - .logged-in-layout { +} +.logged-in-layout { display: flex; flex-direction: row; min-height: 100vh; - } - .content-area { +} +.content-area { flex: 1; padding: 1rem; - } + display: flex; + flex-direction: column; + min-height: calc(100vh - 150px); +} - .side-menu { - width: var(--left-panel-width); - background-color: var(--menu-color); - color: var(--l-text-color); - height: 100vh ; - position: fixed; - top: 0; - left: 0; - padding: 1rem; - } +.router-outlet { + flex: 1; /* Wypełnia dostępną przestrzeń */ +} + +.side-menu { + /*background-color: var(--menu-color);*/ + /*color: var(--l-text-color);*/ + /*height: 100vh ;*/ + /*position: fixed;*/ + /*top: 0;*/ + /*left: 0;*/ + /*padding: 1rem;*/ +} diff --git a/src/app/app.component.html b/src/app/app.component.html index 1717bc47861bbb0dc21e1b0b92f916fe445f7d05..479de55ee239bb09c3c58c025f65c0eef14ed746 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,8 +1,8 @@ <div *ngIf="!isLoggedIn" class="flex-container"> <!--- --> <app-navbar ></app-navbar> <router-outlet></router-outlet> - <!-- <div class="flex-spacer"></div> - <nmaas-footer></nmaas-footer> --> + <div class="flex-spacer"></div> + <nmaas-footer></nmaas-footer> </div> @@ -10,15 +10,16 @@ <div class="side-menu"> <app-left-menu [style]="{'display': 'flex', 'height': '100%'}"></app-left-menu> </div> - <div class="flex-spacer"> - - </div> - <div class="content-area" [style]="{'margin-left': 'var(--left-panel-width)'}"> - <router-outlet></router-outlet> + <div class="flex flex-column" [style]="{'margin-left': 'var(--left-panel-width)', 'width': 'calc(100vw - var(--left-panel-width))'}"> + <div class="content-area"> + <router-outlet></router-outlet> + </div> + + <nmaas-footer></nmaas-footer> </div> - <!-- <div class="flex-spacer"></div> - <nmaas-footer></nmaas-footer> --> + + </div> <app-toast-container aria-live="polite" aria-atomic="true"></app-toast-container> diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 9ed907e1a0d0bc44573f38ccdf7dc472151253d7..67ac91ebe50e5c5f3f2cfe3b28052b2120ca3e51 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,12 +1,10 @@ /* tslint:disable:no-unused-variable */ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { AppComponent } from './app.component'; -import { RouterTestingModule} from '@angular/router/testing'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {AppComponent} from './app.component'; +import {RouterTestingModule} from '@angular/router/testing'; import {AppConfigService, ConfigurationService} from './service'; -import {HttpClient, HttpHandler} from '@angular/common/http'; -import {TranslateService, TranslateModule, TranslateLoader, MissingTranslationHandler} from '@ngx-translate/core'; -import {TranslateFakeLoader} from '@ngx-translate/core'; +import {MissingTranslationHandler, TranslateFakeLoader, TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core'; import {Observable, of} from 'rxjs'; import {Configuration} from './model/configuration'; import {CustomMissingTranslationService} from './i18n/custommissingtranslation.service'; @@ -18,82 +16,111 @@ import { LeftMenuComponent } from './shared/left-menu/left-menu.component'; import { ToastContainerComponent } from './shared/toast-container/toast-container.component'; import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; import { MessageService } from 'primeng/api'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; class MockConfigurationService { - protected uri: string; + protected uri: string; - constructor() { - this.uri = 'http://localhost/api'; - } + constructor() { + this.uri = 'http://localhost/api'; + } - public getApiUrl(): string { - return 'http://localhost/api'; - } + public getApiUrl(): string { + return 'http://localhost/api'; + } - public getConfiguration(): Observable<Configuration> { - return of<Configuration>(); - } + public getConfiguration(): Observable<Configuration> { + return of<Configuration>(); + } - public updateConfiguration(configuration: Configuration): Observable<any> { - return of<Configuration>(); - } + public updateConfiguration(configuration: Configuration): Observable<any> { + return of<Configuration>(); + } } +class MockAppConfigService { + config: any; + + constructor() { } + + public load() { + } + + public getApiUrl(): string { + return ''; + } + + public getNmaasGlobalDomainId(): number { + return 0; + } + + public getHttpTimeout(): number { + return 10000; + } + + public getShowGitInfo(): boolean { + return false; + } + + public getShowChangelog(): boolean { + return false; + } + } + class MockServiceUnavailableService { - public isServiceAvailable: boolean; + public isServiceAvailable: boolean; - constructor() { - this.isServiceAvailable = true; - } + constructor() { + this.isServiceAvailable = true; + } } describe('App: NmaasPortal', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - AppComponent, - LeftMenuComponent, - ToastContainerComponent - ], - imports: [ - RouterTestingModule, - TranslateModule.forRoot({ - missingTranslationHandler: {provide: MissingTranslationHandler, useClass: CustomMissingTranslationService}, - loader: { - provide: TranslateLoader, - useClass: TranslateFakeLoader - } - }), - JwtModule.forRoot({ - config: { - tokenGetter: () => { - return ''; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent, + LeftMenuComponent, + ToastContainerComponent + ], + imports: [ + HttpClientTestingModule, + RouterTestingModule, + TranslateModule.forRoot({ + missingTranslationHandler: {provide: MissingTranslationHandler, useClass: CustomMissingTranslationService}, + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader } - } - }), - SharedModule - ], - providers: [ - {provide: AppConfigService, useClass: MockConfigurationService}, - HttpClient, - HttpHandler, - ConfigurationService, - TranslateService, - AuthService, - JwtHelperService, + }), + JwtModule.forRoot({ + config: { + tokenGetter: () => { + return ''; + } + } + }), + SharedModule + ], + providers: [ + {provide: AppConfigService, useClass: MockAppConfigService}, + {provide: ConfigurationService, useClass: MockConfigurationService}, + TranslateService, + AuthService, + JwtHelperService, MessageService, - {provide: ServiceUnavailableService, useClass: MockServiceUnavailableService} + {provide: ServiceUnavailableService, useClass: MockServiceUnavailableService} ], schemas: [ CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA - ] + ] + }); }); - }); - it('should create the app', waitForAsync(() => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - })); + it('should create the app', waitForAsync(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); }); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3a3ca3e05f970ad267c7e7ca5f10de457a406718..be39d339e116fb2a4cf03099c6c8666ea24ace06 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -32,6 +32,8 @@ import { MessageService } from 'primeng/api'; import { ToastModule } from 'primeng/toast'; import {SplitButtonModule} from 'primeng/splitbutton'; import {MenuModule} from 'primeng/menu'; +import { AdminLeftMenuComponent } from './shared/admin-left-menu/admin-left-menu.component'; +import {AccordionModule} from 'primeng/accordion'; export function appConfigFactory(config: AppConfigService) { return function create() { @@ -59,7 +61,8 @@ export const jwtOptionsFactory = (appConfig: AppConfigService) => ({ declarations: [ AppComponent, LeftMenuComponent, - ToastContainerComponent + ToastContainerComponent, + AdminLeftMenuComponent ], imports: [ BrowserModule, @@ -90,6 +93,7 @@ export const jwtOptionsFactory = (appConfig: AppConfigService) => ({ ToastModule, SplitButtonModule, MenuModule, + AccordionModule, ], providers: [ AuthGuard, diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 610c10ccf5c6a61d2610e4be2b3fdefe9e51fcde..31a968fec3b241c1d54e3257898a92a6e781fe71 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -6,6 +6,7 @@ import { WelcomeRoutes } from './welcome/welcome.routes'; import {ServiceUnavailableRoutes} from './service-unavailable/service-unavailable.routes'; import {PageNotFoundComponent} from './shared/page-not-found/page-not-found.component'; import {LoginSuccessComponent} from './auth/login-success/login-success.component'; +import {LinkAccountComponent} from './welcome/link-account/link-account.component'; const appRoutes: Routes = [ ...WelcomeRoutes, @@ -13,6 +14,7 @@ const appRoutes: Routes = [ ...ServiceUnavailableRoutes, { path: 'notfound', component: PageNotFoundComponent }, { path: 'login-success', component: LoginSuccessComponent }, + { path: 'login-linking', component: LinkAccountComponent}, { path: '**', redirectTo: '/welcome' }, ]; diff --git a/src/app/appmarket/admin/clusters/clusters.module.ts b/src/app/appmarket/admin/clusters/clusters.module.ts index c5137d01d8dd76c55e3ca77fda8f4fe5178ea2ce..fc03ca25144830a738d453913d47046effd941cb 100644 --- a/src/app/appmarket/admin/clusters/clusters.module.ts +++ b/src/app/appmarket/admin/clusters/clusters.module.ts @@ -7,10 +7,20 @@ import {NgModule} from "@angular/core"; import {SharedModule} from "../../../shared/shared.module"; import {ClusterDetailsComponent} from "./details/clusterdetails.component"; import {ClusterService} from "../../../service/cluster.service"; +import { ClusterManagerDetailsComponent } from "../../../shared/admin/clusters/managerdetails/managerdetails.component"; +import { ClusterManagerComponent } from "../../../shared/admin/clusters/manager/manager.component"; +import { TableModule } from 'primeng/table'; +import { FileUploadModule } from 'primeng/fileupload'; +import { TranslateModule } from "@ngx-translate/core"; +import { TooltipModule } from 'primeng/tooltip'; + + @NgModule({ declarations: [ ClusterDetailsComponent, + ClusterManagerDetailsComponent, + ClusterManagerComponent ], imports: [ CommonModule, @@ -19,9 +29,16 @@ import {ClusterService} from "../../../service/cluster.service"; SharedModule, AuthModule, PipesModule, + TableModule, + FileUploadModule, + TranslateModule.forChild(), + TooltipModule ], exports: [ - ClusterDetailsComponent + ClusterDetailsComponent, + ClusterManagerDetailsComponent, + ClusterManagerComponent + ], providers: [ ClusterService, diff --git a/src/app/appmarket/admin/clusters/clusters.routes.ts b/src/app/appmarket/admin/clusters/clusters.routes.ts index 4bfe2d9af597abd5daf68e0e66f31d816c92083e..9d6894ae8926e9cc19faf0e48cce1bc1d9a4e20b 100644 --- a/src/app/appmarket/admin/clusters/clusters.routes.ts +++ b/src/app/appmarket/admin/clusters/clusters.routes.ts @@ -3,10 +3,16 @@ import {ClusterDetailsComponent} from './index'; import {AuthGuard} from '../../../auth/auth.guard'; import {RoleGuard} from '../../../auth/role.guard'; import {ComponentMode} from '../../../shared/common/componentmode'; +import { ClusterManagerComponent } from '../../../shared/admin/clusters/manager/manager.component'; +import { ClusterManagerDetailsComponent } from '../../../shared/admin/clusters/managerdetails/managerdetails.component'; export const ClustersRoutes: Route[] = [ - { path: 'admin/clusters', component: ClusterDetailsComponent, canActivate: [AuthGuard, RoleGuard], + { path: 'clusters', component: ClusterDetailsComponent, canActivate: [AuthGuard, RoleGuard], data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']}}, - { path: 'admin/clusters/view', component: ClusterDetailsComponent, canActivate: [AuthGuard, RoleGuard], - data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']}} + { path: 'clusters/view', component: ClusterDetailsComponent, canActivate: [AuthGuard, RoleGuard], + data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']}}, + { path: 'manage/clusters', component: ClusterManagerComponent, canActivate: [AuthGuard, RoleGuard], + data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']}}, + { path: 'manage/clusters/:id', component: ClusterManagerDetailsComponent, canActivate: [AuthGuard, RoleGuard], + data: {mode: ComponentMode.EDIT, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']}}, ]; diff --git a/src/app/appmarket/admin/clusters/details/clusterdetails.component.html b/src/app/appmarket/admin/clusters/details/clusterdetails.component.html index b5730cb4b9e56571b219092a7e57e10fe0a5bc8e..464ced99150a4f51d3e6e2fb7f4aece24ad04137 100644 --- a/src/app/appmarket/admin/clusters/details/clusterdetails.component.html +++ b/src/app/appmarket/admin/clusters/details/clusterdetails.component.html @@ -1 +1 @@ -<nmaas-clusterdetails class="col-sm-12 col-sm-12 col-md-12" [cluster]="cluster" [error]="error" [mode]="getCurrentMode()" [allowedModes]="[ComponentMode.VIEW, ComponentMode.CREATE]" (onSave)="onSave($event)" (onDelete)="onDelete($event)"></nmaas-clusterdetails> +<nmaas-clusterdetails class="" [cluster]="cluster" [error]="error" [mode]="getCurrentMode()" [allowedModes]="[ComponentMode.VIEW, ComponentMode.CREATE]"></nmaas-clusterdetails> diff --git a/src/app/appmarket/admin/clusters/details/clusterdetails.component.ts b/src/app/appmarket/admin/clusters/details/clusterdetails.component.ts index 248a6943e8c8662468fd62f1dad8c0833908ba1d..d9990a60a4ed7bf7effaebbf62dce13b1bcd63c4 100644 --- a/src/app/appmarket/admin/clusters/details/clusterdetails.component.ts +++ b/src/app/appmarket/admin/clusters/details/clusterdetails.component.ts @@ -29,21 +29,4 @@ export class ClusterDetailsComponent extends BaseComponent implements OnInit { }); } - public onSave($event) { - const upCluster: Cluster = $event; - if (!upCluster) { - return; - } - if (this.isInMode(ComponentMode.CREATE)) { - this.clusterService.add(upCluster) - .subscribe(() => this.router.navigateByUrl('/admin/clusters'), err => this.error = err.message); - } else { - this.clusterService.update(upCluster) - .subscribe(() => this.router.navigateByUrl('/admin/clusters'), err => this.error = err.message); - } - } - - public onDelete($event): void { - this.clusterService.remove($event).subscribe(() => this.router.navigate(['/admin/clusters/'])); - } } diff --git a/src/app/appmarket/admin/configuration/configuration.routes.ts b/src/app/appmarket/admin/configuration/configuration.routes.ts index 3d232bc59f5f9a1407e2e5237807268e7d989421..adde5168ce4badc1f068931ff7978c8122ecdbbf 100644 --- a/src/app/appmarket/admin/configuration/configuration.routes.ts +++ b/src/app/appmarket/admin/configuration/configuration.routes.ts @@ -4,6 +4,6 @@ import {RoleGuard} from "../../../auth/role.guard"; import {ConfigurationDetailsComponent} from "./index"; export const ConfigurationRoutes: Route[] = [ - {path: 'admin/configuration', component: ConfigurationDetailsComponent, canActivate: [AuthGuard, RoleGuard], + {path: 'configuration', component: ConfigurationDetailsComponent, canActivate: [AuthGuard, RoleGuard], data:{roles: ['ROLE_SYSTEM_ADMIN']} } ]; diff --git a/src/app/appmarket/admin/configuration/details/configurationdetails.component.html b/src/app/appmarket/admin/configuration/details/configurationdetails.component.html index de8a5d141eabd06407384500438ac27cf15ed1a3..120f90dc214c1069294c29a1ea17b387dc260d4a 100644 --- a/src/app/appmarket/admin/configuration/details/configurationdetails.component.html +++ b/src/app/appmarket/admin/configuration/details/configurationdetails.component.html @@ -1,6 +1,6 @@ -<div class="col-sm-12 col-sm-12 col-md-12" > - <div class="panel panel-default"> - <div class="panel-heading">{{ 'PORTAL_CONFIGURATION.TITLE' | translate }}</div> +<div class="" > + <div class="background-section"> + <h4 style="font-size:15px; font-weight: bold">{{ 'PORTAL_CONFIGURATION.TITLE' | translate }}</h4> <div class="panel-body"> <form (submit)="save()" class="form-horizontal" #configurationForm="ngForm" *ngIf="this.configuration"> <div class="form-group"> @@ -87,59 +87,85 @@ [(ngModel)]="this.configuration.registrationDomainSelectionEnabled"> </div> </div> - <div class="form-group"> - <label for="bulkDomainsAllowForSsoAccounts" - class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DOMAINS_ALLOW_FOR_SSO_ACCOUNTS' | translate}}</label> + <label for="deploymentPrefix" + class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.DEPLOYMENT_PREFIX' | translate}}</label> <div class="col-sm-9 pd-top-7"> - <input type="checkbox" id="bulkDomainsAllowForSsoAccounts" name="bulkDomainsAllowForSsoAccounts" - [(ngModel)]="this.configuration.bulkDomainsAllowForSsoAccounts" - [checked]="this.configuration.bulkDomainsAllowForSsoAccounts === true"> + <div class="input-width"> + <input class="form-control" type="text" id="deploymentPrefix" name="deploymentPrefix" + [(ngModel)]="this.configuration.deploymentPrefix"> + </div> + </div> </div> - <div class="form-group"> - <label for="bulkDomainsSendEmailForNewAccounts" - class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DOMAINS_SEND_EMAIL_FOR_NEW_ACCOUNTS' | translate}}</label> - <div class="col-sm-9 pd-top-7"> - <input type="checkbox" id="bulkDomainsSendEmailForNewAccounts" name="bulkDomainsSendEmailForNewAccounts" - [(ngModel)]="this.configuration.bulkDomainsSendEmailForNewAccounts" - [checked]="this.configuration.bulkDomainsSendEmailForNewAccounts === true"> + <div class="panel-default panel-heading">Bulk settings</div> + <div class="panel-body"> + + <div class="form-group"> + <label for="bulkDomainsAllowForSsoAccounts" + class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DOMAINS_ALLOW_FOR_SSO_ACCOUNTS' | translate}}</label> + <div class="col-sm-9 pd-top-7"> + <input type="checkbox" id="bulkDomainsAllowForSsoAccounts" name="bulkDomainsAllowForSsoAccounts" + [(ngModel)]="this.configuration.bulkDomainsAllowForSsoAccounts" + [checked]="this.configuration.bulkDomainsAllowForSsoAccounts === true"> + </div> </div> - </div> - <div class="form-group"> - <label for="bulkDeploymentJobCron" - class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DEPLOYMENT_CRON' | translate}}</label> - <div class="col-sm-9 pd-top-7 "> - <div class="input-width"> - <input class="form-control" type="text" id="bulkDeploymentJobCron" name="bulkDeploymentJobCron" - [(ngModel)]="this.configuration.bulkDeploymentJobCron"> + <div class="form-group"> + <label for="bulkDomainsSendEmailForNewAccounts" + class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DOMAINS_SEND_EMAIL_FOR_NEW_ACCOUNTS' | translate}}</label> + <div class="col-sm-9 pd-top-7"> + <input type="checkbox" id="bulkDomainsSendEmailForNewAccounts" name="bulkDomainsSendEmailForNewAccounts" + [(ngModel)]="this.configuration.bulkDomainsSendEmailForNewAccounts" + [checked]="this.configuration.bulkDomainsSendEmailForNewAccounts === true"> </div> - </div> - </div> - <div class="form-group"> - <label for="parallelDeploymentsLimit" - class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DEPLOYMENT_LIMIT' | translate}}</label> - <div class="col-sm-9 pd-top-7"> - <div class="input-width"> - <input class="form-control" type="number" id="parallelDeploymentsLimit" name="parallelDeploymentsLimit" - [(ngModel)]="this.configuration.parallelDeploymentsLimit"> + <div class="form-group"> + <label for="bulkDeploymentJobCron" + class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DEPLOYMENT_CRON' | translate}}</label> + <div class="col-sm-9 pd-top-7 "> + <div class="input-width"> + <input class="form-control" type="text" id="bulkDeploymentJobCron" name="bulkDeploymentJobCron" + [(ngModel)]="this.configuration.bulkDeploymentJobCron"> + </div> + </div> - </div> - </div> - <div class="form-group"> - <label for="bulkDeploymentQueueRefresh" - class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DEPLOYMENT_QUEUE_REFRESH' | translate}}</label> - <div class="col-sm-9 pd-top-7"> - <div class="input-width"> - <input class="form-control" type="number" id="bulkDeploymentQueueRefresh" name="bulkDeploymentQueueRefresh" - [(ngModel)]="this.configuration.bulkDeploymentQueueRefresh"> + + <div class="form-group"> + <label for="parallelDeploymentsLimit" + class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DEPLOYMENT_LIMIT' | translate}}</label> + <div class="col-sm-9 pd-top-7"> + <div class="input-width"> + <input class="form-control" type="number" id="parallelDeploymentsLimit" name="parallelDeploymentsLimit" + [(ngModel)]="this.configuration.parallelDeploymentsLimit"> + </div> + + </div> + </div> + <div class="form-group"> + <label for="bulkDeploymentQueueRefresh" + class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DEPLOYMENT_QUEUE_REFRESH' | translate}}</label> + <div class="col-sm-9 pd-top-7"> + <div class="input-width"> + <input class="form-control" type="number" id="bulkDeploymentQueueRefresh" name="bulkDeploymentQueueRefresh" + [(ngModel)]="this.configuration.bulkDeploymentQueueRefresh"> + </div> + + </div> + </div> + <div class="form-group"> + <label for="bulkDeploymentTimeThreshold" + class="col-sm-3 control-label">{{'PORTAL_CONFIGURATION.BULK_DEPLOYMENT_THRESHOLD' | translate}}</label> + <div class="col-sm-9 pd-top-7"> + <div class="input-width"> + <input class="form-control" type="number" id="bulkDeploymentTimeThreshold" name="bulkDeploymentTimeThreshold" + [(ngModel)]="this.configuration.bulkDeploymentTimeThreshold"> + </div> + </div> - </div> </div> <div class="flex justify-content-end"> diff --git a/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.css b/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.css index 76a9184a9116d73518d2bd3b9db0239df54da9cb..ef5401480eb1663ac2c9dec13c5a1014d6ea0a61 100644 --- a/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.css +++ b/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.css @@ -13,6 +13,6 @@ tr.clickable { cursor: pointer; } -.dropdown:hover .dropdown-menu { - display: block; -} +/*.dropdown:hover .dropdown-menu {*/ +/* display: block;*/ +/*}*/ diff --git a/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.html b/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.html index fb9fcc5dce55c757cd218532db961cea45ee2c53..12bab7e9b17986da97dca30fc7dfda12de7c6acc 100644 --- a/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.html +++ b/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.html @@ -1,40 +1,40 @@ -<div class="col-sm-12 col-sm-offset-1 col-sm-10 col-md-offset-1 col-md-10"> - <h3>{{'LANGUAGE_MANAGEMENT.TITLE' | translate }}</h3> - <hr> - <table class="table table-hover table-condensed" aria-describedby="Language list table"> - <thead> - <tr> - <th scope="col"> </th> - <th scope="col">{{'LANGUAGE_MANAGEMENT.LANGUAGE' | translate }}</th> - <th scope="col"> </th> - </tr> - </thead> +<div class=""> + <h4 class="header">{{'LANGUAGE_MANAGEMENT.TITLE' | translate }}</h4> + <div class="background-section"> + <p-table [value]="languages" class="p-datatable-hover p-datatable-sm" [responsive]="true"> + <ng-template pTemplate="header"> + <tr> + <th></th> + <th>{{'LANGUAGE_MANAGEMENT.LANGUAGE' | translate }}</th> + <th></th> + </tr> + </ng-template> - <tbody> - <ng-template ngFor let-lang [ngForOf]="languages" let-i="index"> - <tr class="clickable" [routerLink]="['/admin/languages/' + lang.language]"> - <td><img alt="language" src="assets/images/country/{{lang.language}}_circle.png" style="height: 22px;"></td> - <td>{{'LANGUAGE.' + lang.language.toUpperCase() + '_LABEL' | translate}}</td> + <ng-template pTemplate="body" let-lang> + <tr class="clickable" > + <td [routerLink]="['/admin/languages/' + lang.language]"> + <img alt="language" src="assets/images/country/{{lang.language}}_circle.png" style="height: 22px;"> + </td> + <td [routerLink]="['/admin/languages/' + lang.language]">{{'LANGUAGE.' + lang.language.toUpperCase() + '_LABEL' | translate}}</td> <td class="text-right"> <span class="dropdown"> - <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" - data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> + <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> + <em class="pi pi-cog" style="font-size: 1.8rem; color: var(--l-text-color)"></em> </a> <ul class="dropdown-menu pull-right-drop"> <li> <a [routerLink]="['/admin/languages/' + lang.language]"> - {{ 'LANGUAGE_MANAGEMENT.EDIT_BUTTON' | translate }} + {{ 'LANGUAGE_MANAGEMENT.EDIT_BUTTON' | translate }} </a> </li> <li> <a *ngIf="lang.enabled" (click)="changeLanguageState(lang)"> - {{ 'LANGUAGE_MANAGEMENT.LANGUAGE_DISABLED' | translate }} + {{ 'LANGUAGE_MANAGEMENT.LANGUAGE_DISABLED' | translate }} </a> </li> <li> <a *ngIf="!lang.enabled" (click)="changeLanguageState(lang)"> - {{ 'LANGUAGE_MANAGEMENT.LANGUAGE_ENABLED' | translate }} + {{ 'LANGUAGE_MANAGEMENT.LANGUAGE_ENABLED' | translate }} </a> </li> </ul> @@ -42,8 +42,9 @@ </td> </tr> </ng-template> - </tbody> - </table> + </p-table> + </div> + </div> <nmaas-modal (click)="modal.hide()"> diff --git a/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.spec.ts b/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.spec.ts index d3b5af459a7e540f00cb8a5af733f244aad46240..c444bc324042ff3be92876785d98b41622fad058 100644 --- a/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.spec.ts +++ b/src/app/appmarket/admin/languagemanagement/languagelist/languagelist.component.spec.ts @@ -8,6 +8,7 @@ import {InternationalizationService} from '../../../../service/internationalizat import {AppConfigService} from '../../../../service'; import {of} from 'rxjs'; import {ModalComponent} from '../../../../shared/modal'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; describe('LanguagelistComponent', () => { let component: LanguageListComponent; @@ -35,7 +36,8 @@ describe('LanguagelistComponent', () => { } }, {provide: AppConfigService, useValue: {}} - ] + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) .compileComponents(); })); diff --git a/src/app/appmarket/admin/languagemanagement/languagemanagement.module.ts b/src/app/appmarket/admin/languagemanagement/languagemanagement.module.ts index 1fb084a507599dfa164511b46d80f94853a41fee..801e94f4dbc50cfa8eece97b8abea1c35f436ee2 100644 --- a/src/app/appmarket/admin/languagemanagement/languagemanagement.module.ts +++ b/src/app/appmarket/admin/languagemanagement/languagemanagement.module.ts @@ -8,17 +8,19 @@ import {TranslateModule} from '@ngx-translate/core'; import {SharedModule} from '../../../shared'; import {FormsModule} from '@angular/forms'; import {InputSwitchModule} from 'primeng/inputswitch'; +import {TableModule} from 'primeng/table'; @NgModule({ declarations: [LanguageListComponent, LanguageDetailsComponent], - imports: [ - CommonModule, - FormsModule, - InputSwitchModule, - RouterModule, - SharedModule, - TranslateModule.forChild() - ], + imports: [ + CommonModule, + FormsModule, + InputSwitchModule, + RouterModule, + SharedModule, + TranslateModule.forChild(), + TableModule + ], providers: [InternationalizationService] }) export class LanguageManagementModule { } diff --git a/src/app/appmarket/admin/languagemanagement/languagemanagement.routes.ts b/src/app/appmarket/admin/languagemanagement/languagemanagement.routes.ts index dcc2b8c840b7a220ef228fa3c9b5105fde2f714d..6586b88743e1b3164e96e5ba749cd6ca2ef045ce 100644 --- a/src/app/appmarket/admin/languagemanagement/languagemanagement.routes.ts +++ b/src/app/appmarket/admin/languagemanagement/languagemanagement.routes.ts @@ -5,6 +5,6 @@ import {AuthGuard} from "../../../auth/auth.guard"; import {RoleGuard} from "../../../auth/role.guard"; export const LanguageManagementRoutes: Route[] = [ - {path: 'admin/languages', component: LanguageListComponent, canActivate: [AuthGuard, RoleGuard], data:{ roles: 'ROLE_SYSTEM_ADMIN'}}, - {path: 'admin/languages/:id', component: LanguageDetailsComponent, canActivate: [AuthGuard, RoleGuard], data: { roles: 'ROLE_SYSTEM_ADMIN'}} + {path: 'languages', component: LanguageListComponent, canActivate: [AuthGuard, RoleGuard], data:{ roles: 'ROLE_SYSTEM_ADMIN'}}, + {path: 'languages/:id', component: LanguageDetailsComponent, canActivate: [AuthGuard, RoleGuard], data: { roles: 'ROLE_SYSTEM_ADMIN'}} ]; \ No newline at end of file diff --git a/src/app/appmarket/admin/monitor/list/monitor-list.component.html b/src/app/appmarket/admin/monitor/list/monitor-list.component.html index d762b99e4772dd1b0b8e7f24401d5914d3dd2f1f..cdb2ce67972016f5020c03ec64f06851d24309e5 100644 --- a/src/app/appmarket/admin/monitor/list/monitor-list.component.html +++ b/src/app/appmarket/admin/monitor/list/monitor-list.component.html @@ -1,36 +1,36 @@ -<div class="col-sm-12 col-sm-12 col-md-12"> - <h3>{{ 'MONITOR.TITLE' | translate }}</h3> - <br> - <table class="table table-hover table-condensed" aria-describedby="Service status table"> - <thead> - <tr> - <th scope="col">{{ 'MONITOR.SERVICE_NAME' | translate }}</th> - <th scope="col">{{ 'MONITOR.LAST_CHECK' | translate }}</th> - <th scope="col">{{ 'MONITOR.LAST_SUCCESS' | translate }}</th> - <th scope="col">{{ 'MONITOR.CHECK_INTERVAL' | translate }}</th> - <th scope="col"> </th> - </tr> - </thead> +<div class=""> + <h4 class="header">{{ 'MONITOR.TITLE' | translate }}</h4> + <div class="background-section"> + <table class="table table-hover table-condensed" aria-describedby="Service status table"> + <thead> + <tr> + <th scope="col">{{ 'MONITOR.SERVICE_NAME' | translate }}</th> + <th scope="col">{{ 'MONITOR.LAST_CHECK' | translate }}</th> + <th scope="col">{{ 'MONITOR.LAST_SUCCESS' | translate }}</th> + <th scope="col">{{ 'MONITOR.CHECK_INTERVAL' | translate }}</th> + <th scope="col"> </th> + </tr> + </thead> - <tbody> - <ng-template ngFor let-entry [ngForOf]="monitorEntries"> - <tr class="clickable" [routerLink]="['view/', entry.serviceName.toString()]" - [ngClass]="{'entry-unknown': entry.status === null || entry.status === undefined, + <tbody> + <ng-template ngFor let-entry [ngForOf]="monitorEntries"> + <tr class="clickable" [routerLink]="['view/', entry.serviceName.toString()]" + [ngClass]="{'entry-unknown': entry.status === null || entry.status === undefined, 'entry-deactivated': !entry.active, 'entry-success': entry.status.toString() === 'SUCCESS', 'entry-failure': entry.status.toString() === 'FAILURE'}"> - <td>{{services[entry.serviceName]}}</td> - <td> - <span *ngIf="entry.lastCheck">{{entry.lastCheck | date:'medium'}}</span> - <span *ngIf="!entry.lastCheck">---</span> - </td> - <td> - <span *ngIf="entry.lastSuccess">{{entry.lastSuccess | date:'medium'}}</span> - <span *ngIf="!entry.lastSuccess">---</span> - </td> - <td>{{getIntervalCheck(entry.checkInterval, entry.timeFormat)}}</td> - <td> + <td>{{services[entry.serviceName]}}</td> + <td> + <span *ngIf="entry.lastCheck">{{entry.lastCheck | date:'medium'}}</span> + <span *ngIf="!entry.lastCheck">---</span> + </td> + <td> + <span *ngIf="entry.lastSuccess">{{entry.lastSuccess | date:'medium'}}</span> + <span *ngIf="!entry.lastSuccess">---</span> + </td> + <td>{{getIntervalCheck(entry.checkInterval, entry.timeFormat)}}</td> + <td> <span class="dropdown"> <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" @@ -51,9 +51,10 @@ </li> </ul> </span> - </td> - </tr> - </ng-template> - </tbody> - </table> + </td> + </tr> + </ng-template> + </tbody> + </table> + </div> </div> diff --git a/src/app/appmarket/admin/monitor/monitor.routes.ts b/src/app/appmarket/admin/monitor/monitor.routes.ts index b91a355cc48de6fb9712241163d8601e10e42283..2ae94cfbf60f50dc7f18bd5232772a60edd764dc 100644 --- a/src/app/appmarket/admin/monitor/monitor.routes.ts +++ b/src/app/appmarket/admin/monitor/monitor.routes.ts @@ -6,11 +6,11 @@ import {MonitorListComponent} from './list/monitor-list.component'; import {ComponentMode} from '../../../shared'; export const MonitorRoutes: Route[] = [ - {path: 'admin/monitor', component: MonitorListComponent, canActivate: [AuthGuard, RoleGuard], + {path: 'monitor', component: MonitorListComponent, canActivate: [AuthGuard, RoleGuard], data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']}}, - {path: 'admin/monitor/edit/:name', component: MonitorDetailsComponent, canActivate: [AuthGuard, RoleGuard], + {path: 'monitor/edit/:name', component: MonitorDetailsComponent, canActivate: [AuthGuard, RoleGuard], data: {mode: ComponentMode.EDIT, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']}}, - {path: 'admin/monitor/view/:name', component: MonitorDetailsComponent, canActivate: [AuthGuard, RoleGuard], + {path: 'monitor/view/:name', component: MonitorDetailsComponent, canActivate: [AuthGuard, RoleGuard], data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']}} ]; diff --git a/src/app/appmarket/appdetails/appdetails.component.html b/src/app/appmarket/appdetails/appdetails.component.html index 7db442fe15ab9b6a711759732ed1056378587362..7caaea2e74a14291fecff80651685535df415117 100644 --- a/src/app/appmarket/appdetails/appdetails.component.html +++ b/src/app/appmarket/appdetails/appdetails.component.html @@ -10,14 +10,17 @@ <h3 style="margin:0 20px;">{{app?.name}}</h3> </div> <div class="" *ngIf="app && domain" style="display:flex; align-items: center"> - <div *ngIf="!subscribed && isSubscriptionAllowed()" class="btn-group pull-right" + <div *ngIf="!subscribed && isSubscriptionAllowed()" class="pull-right" pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_SUBSCRIBE' | translate}}" tooltipPosition="bottom" [showDelay]="50" [tooltipDisabled]="defaultTooltipDisabled"> - <button class="btn btn-primary" [disabled]="!active || !isApplicationEnabledInDomain()" (click)="subscribe()">{{'APPLICATIONS.SUBSCRIBE_BUTTON' | translate}}</button> + <button class="btn btn-primary m-1" [disabled]="!active || !isApplicationEnabledInDomain()" (click)="subscribe()">{{'APPLICATIONS.SUBSCRIBE_BUTTON' | translate}}</button> + <button class="btn btn-primary m-1"[routerLink]="['/admin/apps/bulks']">Bulk deployments</button> </div> + <div *ngIf="subscribed" class=" pull-right" > <div class="btn no-padding" pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_DEPLOY' | translate}}" tooltipPosition="bottom" [showDelay]="50" [tooltipDisabled]="defaultTooltipDisabled"> <button *ngIf="isSubscriptionAllowed()" class="btn btn-danger m-1" (click)="unsubscribe()">{{'APPLICATIONS.UNSUBSCRIBE_BUTTON' | translate}}</button> </div> + <button class="btn btn-primary m-1" [routerLink]="['/admin/apps/bulks']">Bulk deployments</button> <button *ngIf="isSubscriptionAllowed()" class="btn btn-primary m-1" [disabled]="!isApplicationEnabledInDomain()" (click)="appInstallModal.show()">{{'APPLICATIONS.DEPLOY_BUTTON' | translate}}</button> </div> @@ -58,7 +61,7 @@ <div class="" *ngIf="versionVisible"> <div class="" *ngIf="activeVersions"> <a *ngFor="let version of activeVersions" class="tag-button"> - v.{{version}} + v{{version}} </a> </div> </div> @@ -81,7 +84,7 @@ </div> <div style="padding-top:50px"> <p style="font-weight: bold" *ngIf="numberOfScreenshots > 0">{{'SCREENSHOTS.HEADER' | translate}}</p> - <screenshots (numberOfScreenshots)="screenshots($event)" [pathUrl]="'/apps/' + appId + '/screenshots'"></screenshots> + <screenshots style="display: flex" (numberOfScreenshots)="screenshots($event)" [pathUrl]="'/apps/' + appId + '/screenshots'"></screenshots> </div> </div> diff --git a/src/app/appmarket/appdetails/appdetails.component.ts b/src/app/appmarket/appdetails/appdetails.component.ts index e5518794a0850eb436fcafbe67025c1e78c33d75..cb156314c0447fa5f6e4530551266420d105368f 100644 --- a/src/app/appmarket/appdetails/appdetails.component.ts +++ b/src/app/appmarket/appdetails/appdetails.component.ts @@ -146,7 +146,8 @@ export class AppDetailsComponent implements OnInit { } return this.authService.hasRole(Role[Role.ROLE_SYSTEM_ADMIN]) - || this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_DOMAIN_ADMIN]); + || this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_DOMAIN_ADMIN]) + || this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_GROUP_DOMAIN_ADMIN]); } public isApplicationEnabledInDomain(): boolean { diff --git a/src/app/appmarket/appinstance/appinstance.module.ts b/src/app/appmarket/appinstance/appinstance.module.ts index 4fe735db92c8eeb7995b5811b5bad2d6d9adc515..bc7d02664d02586552884f6722dc9727463396a2 100644 --- a/src/app/appmarket/appinstance/appinstance.module.ts +++ b/src/app/appmarket/appinstance/appinstance.module.ts @@ -38,6 +38,7 @@ import {FormioAppConfig, FormioModule} from '@formio/angular'; import {SelectButtonModule} from 'primeng/selectbutton'; import {CheckboxModule} from 'primeng/checkbox'; import {TableModule} from 'primeng/table'; +import {ProgressBarModule} from 'primeng/progressbar'; @NgModule({ declarations: [ @@ -78,6 +79,7 @@ import {TableModule} from 'primeng/table'; CheckboxModule, SelectButtonModule, TableModule, + ProgressBarModule, ], exports: [ AppInstanceComponent, diff --git a/src/app/appmarket/appinstance/appinstance/appinstance.component.css b/src/app/appmarket/appinstance/appinstance/appinstance.component.css index 96f3d93cb7b3cb432509395a983316d14c8ba532..3fe8d07992c63f0ea74cc5d8d25653db29998897 100644 --- a/src/app/appmarket/appinstance/appinstance/appinstance.component.css +++ b/src/app/appmarket/appinstance/appinstance/appinstance.component.css @@ -23,22 +23,26 @@ .other-states{ padding:15px; - margin-bottom:20px; + margin:50px 0; border-bottom: 1px solid lightgray; border-top: 1px solid lightgray; text-align: center; color: #337ab7; + background-image: none; + width:100%; + background: transparent; } .alert-danger{ padding:15px; - margin-bottom:20px; + margin:50px 0; text-align: center; border-bottom: 1px solid #a94442; border-top: 1px solid #a94442; border-radius: 0; - background-color: #FFFFFF; background-image: none; + width:100%; + background: transparent; } .h-100 { @@ -81,3 +85,6 @@ :host ::ng-deep.p-dropdown-panel .p-dropdown-items { max-height: 150px; } +:host ::ng-deep .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover{ + background: var(--user-button-background-hover) +} diff --git a/src/app/appmarket/appinstance/appinstance/appinstance.component.html b/src/app/appmarket/appinstance/appinstance/appinstance.component.html index 330151db9ebe9fe0c3741dec40f339e289153372..9ad4da9d993d742d8c3d061ce577ee84d3739b34 100644 --- a/src/app/appmarket/appinstance/appinstance/appinstance.component.html +++ b/src/app/appmarket/appinstance/appinstance/appinstance.component.html @@ -1,73 +1,61 @@ -<div class="container" *ngIf="appInstance"> - <div class="row"> - <!-- App logo --> - <div class="col-xs-4 col-sm-3 col-md-3 col-lg-2"> - <div class="thumbnail" *ngIf="app"> - <img alt="App logo" - [src]="(appImagesService.getAppLogoUrl(app?.applicationBase.id) | secure) || 'assets/images/app-logo-example.png'"/> - </div> - <div class="thumbnail" *ngIf="!app"> - <img alt="App logo" src="assets/images/app-logo-example.png"/> - </div> - </div> - <!-- App information--> - <div class="col-xs-8 col-sm-9 col-md-9 col-lg-10"> - <div class="row flex-stretch"> - <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6"> - <h2 style="margin-bottom: 4px;">{{appInstance?.name}} ({{app?.applicationBase.name}})</h2> - <rate *ngIf="app?.applicationBase.id" [showVotes]="true" [short]="true" - [pathUrl]="getPathUrl(app?.applicationBase.id)"></rate> - <div class="text-muted" style="font-size: small;"> - {{app?.application.version ? 'v.' + app?.application.version : 'None'}} - | - <span pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_NOT_AVAILABLE' | translate}}" - tooltipPosition="bottom" [showDelay]="50" - [tooltipDisabled]="!!app?.applicationBase.licenseUrl"> - <a class="{{app?.applicationBase.licenseUrl ? '' : 'disabled-url'}}" - [href]="app?.applicationBase.licenseUrl" - target="_blank">{{app?.applicationBase.license || 'License'}}</a> - </span> - | - <span pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_NOT_AVAILABLE' | translate}}" - tooltipPosition="bottom" [showDelay]="50" - [tooltipDisabled]="!!app?.applicationBase.wwwUrl"> - <a class="{{app?.applicationBase.wwwUrl ? '' : 'disabled-url'}}" - [href]="app?.applicationBase.wwwUrl">WWW</a> - </span> - | - <span pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_NOT_AVAILABLE' | translate}}" - tooltipPosition="bottom" [showDelay]="50" - [tooltipDisabled]="!!app?.applicationBase.sourceUrl"> - <a class="{{app?.applicationBase.sourceUrl ? '' : 'disabled-url'}}" - [href]="app?.applicationBase.sourceUrl" - target="_blank">{{'APP_INSTANCE.SOURCE' | translate}}</a> - </span> - | - <span pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_NOT_AVAILABLE' | translate}}" - tooltipPosition="bottom" [showDelay]="50" - [tooltipDisabled]="!!app?.applicationBase.issuesUrl"> - <a class="{{app?.applicationBase.issuesUrl ? '' : 'disabled-url'}}" - [href]="app?.applicationBase.issuesUrl" - target="_blank">{{'APP_INSTANCE.ISSUES' | translate}}</a> - </span> - </div> +<div class="" *ngIf="appInstance"> + <div class="background-section"> + <div style=" display: flex; flex-direction: row; justify-content: space-between; padding-bottom: 50px"> + <div style="display: flex; align-items: center;"> + <div class="" *ngIf="app"> + <img alt="App logo" + [src]="(appImagesService.getAppLogoUrl(app?.applicationBase.id) | secure) || 'assets/images/app-logo-example.png'" height="90px"/> </div> - <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6 container-bottom-right"> - <h3><em style="color: #337ab7;">{{ translateState(appInstanceStatus?.state) }}</em></h3> + <div class="" *ngIf="!app"> + <img alt="App logo" src="assets/images/app-logo-example.png" height="90px"/> </div> + <h2 style="margin:0 20px;">{{appInstance?.name}} ({{app?.applicationBase.name}})</h2> </div> - <hr> - <div class="row"> - <!-- Tags --> - <div class="col-xs-12 col-sm-6 col-md-6 col-lg-6" *ngIf="app?.applicationBase.tags"> + <h3><em style="color: #337ab7;">{{ translateState(appInstanceStatus?.state) }}</em></h3> + </div> + <div style=" display: flex; flex-direction: row; justify-content: space-between;"> + <div class="" style="display: flex;flex-direction: column;"> + <div class="text-muted mt-2" style="font-size: small;"> + {{app?.application.version ? 'v.' + app?.application.version : 'None'}} + | + <span pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_NOT_AVAILABLE' | translate}}" + tooltipPosition="bottom" [showDelay]="50" + [tooltipDisabled]="!!app?.applicationBase.licenseUrl"> + <a class="{{app?.applicationBase.licenseUrl ? '' : 'disabled-url'}}" + [href]="app?.applicationBase.licenseUrl" + target="_blank">{{app?.applicationBase.license || 'License'}}</a> + </span> + | + <span pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_NOT_AVAILABLE' | translate}}" + tooltipPosition="bottom" [showDelay]="50" + [tooltipDisabled]="!!app?.applicationBase.wwwUrl"> + <a class="{{app?.applicationBase.wwwUrl ? '' : 'disabled-url'}}" + [href]="app?.applicationBase.wwwUrl">WWW</a> + </span> + | + <span pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_NOT_AVAILABLE' | translate}}" + tooltipPosition="bottom" [showDelay]="50" + [tooltipDisabled]="!!app?.applicationBase.sourceUrl"> + <a class="{{app?.applicationBase.sourceUrl ? '' : 'disabled-url'}}" + [href]="app?.applicationBase.sourceUrl" + target="_blank">{{'APP_INSTANCE.SOURCE' | translate}}</a> + </span> + | + <span pTooltip="{{'APPLICATIONS.TOOLTIP_MESSAGE_NOT_AVAILABLE' | translate}}" + tooltipPosition="bottom" [showDelay]="50" + [tooltipDisabled]="!!app?.applicationBase.issuesUrl"> + <a class="{{app?.applicationBase.issuesUrl ? '' : 'disabled-url'}}" + [href]="app?.applicationBase.issuesUrl" + target="_blank">{{'APP_INSTANCE.ISSUES' | translate}}</a> + </span> + </div> + <div class="" *ngIf="app?.applicationBase.tags"> <a *ngFor="let tag of app.applicationBase.tags" class="tag-button"> {{tag.name | titlecase}} </a> </div> - - <!-- Deployment buttons --> - - <!-- if application is still being deployed --> + </div> + <div> <div class=" pull-right" *ngIf="getStateAsEnum(appInstanceStatus?.state) != AppInstanceState.FAILURE && getStateAsEnum(appInstanceStatus?.state) != AppInstanceState.RUNNING @@ -86,7 +74,7 @@ <div *ngIf="appInstance && ( getStateAsEnum(appInstanceStatus?.state) == AppInstanceState.RUNNING || getStateAsEnum(appInstanceStatus?.state) == AppInstanceState.FAILURE)" - class="col-xs-12 col-sm-6 col-md-6 col-lg-6"> + class="col-xs-12 col-sm-12 col-md-12 col-lg-12"> <div class="btn-group pull-right"> <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> @@ -95,7 +83,7 @@ <ul class="dropdown-menu"> <ng-container *ngIf="getStateAsEnum(appInstanceStatus?.state) === AppInstanceState.RUNNING"> <li> - <a role="button" (click)="this.accessMethodsModal.show()"> + <a role="button" (click)="openAccessMethodsModal()"> {{'APP_INSTANCE.APP_ACCESS_METHODS' | translate}} </a> </li> @@ -162,9 +150,8 @@ {{'APP_INSTANCE.VIEW_LOGS' | translate}} </a> </li> - <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN']" - style="background-color: #ff0000;"> - <a role="button" (click)="this.undeployModal.show()" style="color: #ffffff;"> + <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN']"> + <a role="button" (click)="this.undeployModal.show()" style="color: var(--danger-button-color)"> {{'APP_INSTANCE.UNDEPLOY_BUTTON' | translate}} </a> </li> @@ -187,7 +174,7 @@ </a> </li> <li> - <a role="button" style="color: red" + <a role="button" style="color: var(--danger-button-color)" *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN']" (click)="removeConfirm.show()">{{'APP_INSTANCE.REMOVE_BUTTON' | translate}} </a> @@ -199,6 +186,83 @@ </div> </div> </div> + <div class="background-section"> + <!-- Installation step description text --> + <div id="app-progress-info" style="font-size: 16px;" class=""> + <div id="app-progress-info-sub alert" + [ngClass]="{ + 'alert-danger': getStateAsEnum(appInstanceStatus?.state) === AppInstanceState.FAILURE || getStateAsEnum(appInstanceStatus?.state) === AppInstanceState.UNKNOWN, + 'other-states': getStateAsEnum(appInstanceStatus?.state) !== AppInstanceState.FAILURE && getStateAsEnum(appInstanceStatus?.state) !== AppInstanceState.UNKNOWN }" + class=""> + <div *ngIf="getStateAsEnum(appInstanceStatus?.state) === AppInstanceState.FAILURE" class="info-container"> + <span class="glyphicon glyphicon-exclamation-sign info-icon" aria-hidden="true"></span> + <span [innerHTML]="'APP_INSTANCE.INSTALLATION_PROGRESS.DEFAULT_ERROR_MESSAGE' | translate"></span> + </div> + <div *ngIf="getStateAsEnum(appInstanceStatus?.state) !== AppInstanceState.FAILURE && getStateAsEnum(appInstanceStatus?.state) !== AppInstanceState.UNKNOWN" + class="info-container"> + <span class="glyphicon glyphicon-info-sign info-icon" aria-hidden="true"></span> + <span [innerHTML]="'APP_INSTANCE.USER_FRIENDLY.' + getStateAsString(appInstanceStatus?.state) | translate"></span> + </div> + </div> + </div> + + <!-- Progress bar --> + <div class=''> + <h3 style="margin-bottom: 30px">{{'APP_INSTANCE.INSTALLATION_PROGRESS.HEADER' | translate}}</h3> + + + <div id="app-prop" class="col-xs-12 col-sm-12 col-md-12 col-lg-12" style="overflow-x: auto;"> + <nmaas-appinstanceprogress [stages]='getStages()' + [activeState]="getStateAsEnum(appInstanceStatus?.state)"></nmaas-appinstanceprogress> + </div> + </div> + + + <!-- Show additional information checkbox --> + <div class="" > + <label style="margin-top: 40px"> + {{ 'APP_INSTANCE.ADDITIONAL_INFO' | translate}} + <input type="checkbox" [(ngModel)]="showAppInstanceHistory" (change)="showHistory()"> + </label> + </div> + + <!-- App Instance State History table --> + <div class="" *ngIf="this.showAppInstanceHistory && this.appInstanceStateHistory"> + <h3>{{'APP_INSTANCE.DEPLOYMENT_HISTORY.HEADER' | translate}}</h3> + <hr> + <table class="table table-hover table-condensed" aria-describedby="App instance deployment history table"> + <thead> + <tr> + <th scope="col">{{'APP_INSTANCE.DEPLOYMENT_HISTORY.TIMESTAMP' | translate}}</th> + <th scope="col">{{'APP_INSTANCE.DEPLOYMENT_HISTORY.STATE_TRANSITIONS' | translate}}</th> + <th scope="col"> </th> + </tr> + </thead> + <tbody> + <ng-template ngFor let-history + [ngForOf]="this.appInstanceStateHistory | paginate: { itemsPerPage: maxItemsOnPage, currentPage: pageNumber, id: p_first }" + let-isLast="last"> + <tr> + <td>{{history.timestamp | localDate:'medium' }}</td> + <td *ngIf="history.previousState !== null"> + {{history.previousState | translate }} + <span class="glyphicon glyphicon-arrow-right" + style="padding-left: 5px;padding-right: 5px"></span> + {{history.currentState | translate }} + </td> + <td *ngIf="history.previousState === null">{{history.currentState | translate }}</td> + </tr> + </ng-template> + </tbody> + </table> + <pagination-controls class="text-right" (pageChange)="pageNumber = $event" id="{{ p_first }}" + previousLabel="{{ 'PAGINATION.PREVIOUS' | translate }}" + nextLabel="{{ 'PAGINATION.NEXT' | translate }}" + screenReaderPaginationLabel="{{ 'PAGINATION.SCREEN_READER.PAGINATION' | translate }}" + screenReaderPageLabel="{{ 'PAGINATION.SCREEN_READER.PAGE' | translate }}" + screenReaderCurrentLabel="{{ 'PAGINATION.SCREEN_READER.CURRENT' | translate }}"></pagination-controls> + </div> + </div> <!-- Undeploy modal--> <nmaas-modal styleModal="warning" #undeployModal> @@ -211,10 +275,10 @@ </div> </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-danger" - (click)="this.undeploy()">{{'UNDEPLOY_MODAL.YES_BUTTON' | translate}}</button> <button type="button" class="btn btn-primary" (click)="this.undeployModal.hide()">{{'UNDEPLOY_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-danger" + (click)="this.undeploy()">{{'UNDEPLOY_MODAL.YES_BUTTON' | translate}}</button> </div> </nmaas-modal> @@ -238,11 +302,11 @@ </div> </div> <div class="nmaas-modal-footer"> + <button type="button" class="btn btn-secondary" + (click)="this.manualUpdateModal.hide()">{{'UNDEPLOY_MODAL.CANCEL_BUTTON' | translate}}</button> <button type="button" class="btn btn-primary " [disabled]="appVersions.length === 0 || selectedVersion === ''" (click)="this.manualUpdateVersion()">{{'APP_INSTANCES.MANUAL_UPDATE.YES_BUTTON' | translate}}</button> - <button type="button" class="btn btn-secondary" - (click)="this.manualUpdateModal.hide()">{{'UNDEPLOY_MODAL.CANCEL_BUTTON' | translate}}</button> </div> </nmaas-modal> @@ -301,82 +365,6 @@ </div> </nmaas-modal> - <!-- Installation step description text --> - <div id="app-progress-info" style="font-size: 16px;" class="col-xs-12"> - <div id="app-progress-info-sub alert" - [ngClass]="{ - 'alert-danger': getStateAsEnum(appInstanceStatus?.state) === AppInstanceState.FAILURE || getStateAsEnum(appInstanceStatus?.state) === AppInstanceState.UNKNOWN, - 'other-states': getStateAsEnum(appInstanceStatus?.state) !== AppInstanceState.FAILURE && getStateAsEnum(appInstanceStatus?.state) !== AppInstanceState.UNKNOWN }" - class="col-xs-offset-1 col-xs-10"> - <div *ngIf="getStateAsEnum(appInstanceStatus?.state) === AppInstanceState.FAILURE" class="info-container"> - <span class="glyphicon glyphicon-exclamation-sign info-icon" aria-hidden="true"></span> - <span [innerHTML]="'APP_INSTANCE.INSTALLATION_PROGRESS.DEFAULT_ERROR_MESSAGE' | translate"></span> - </div> - <div *ngIf="getStateAsEnum(appInstanceStatus?.state) !== AppInstanceState.FAILURE && getStateAsEnum(appInstanceStatus?.state) !== AppInstanceState.UNKNOWN" - class="info-container"> - <span class="glyphicon glyphicon-info-sign info-icon" aria-hidden="true"></span> - <span [innerHTML]="'APP_INSTANCE.USER_FRIENDLY.' + getStateAsString(appInstanceStatus?.state) | translate"></span> - </div> - </div> - </div> - - <!-- Progress bar --> - <div class='row'> - <h3>{{'APP_INSTANCE.INSTALLATION_PROGRESS.HEADER' | translate}}</h3> - <hr> - - <div id="app-prop" class="col-xs-12 col-sm-12 col-md-12 col-lg-12" style="overflow-x: auto;"> - <nmaas-appinstanceprogress [stages]='getStages()' - [activeState]="getStateAsEnum(appInstanceStatus?.state)"></nmaas-appinstanceprogress> - </div> - </div> - <hr> - - <!-- Show additional information checkbox --> - <div class="row"> - <label> - {{ 'APP_INSTANCE.ADDITIONAL_INFO' | translate}} - <input type="checkbox" [(ngModel)]="showAppInstanceHistory" (change)="showHistory()"> - </label> - </div> - - <!-- App Instance State History table --> - <div class="row" *ngIf="this.showAppInstanceHistory && this.appInstanceStateHistory"> - <h3>{{'APP_INSTANCE.DEPLOYMENT_HISTORY.HEADER' | translate}}</h3> - <hr> - <table class="table table-hover table-condensed" aria-describedby="App instance deployment history table"> - <thead> - <tr> - <th scope="col">{{'APP_INSTANCE.DEPLOYMENT_HISTORY.TIMESTAMP' | translate}}</th> - <th scope="col">{{'APP_INSTANCE.DEPLOYMENT_HISTORY.STATE_TRANSITIONS' | translate}}</th> - <th scope="col"> </th> - </tr> - </thead> - <tbody> - <ng-template ngFor let-history - [ngForOf]="this.appInstanceStateHistory | paginate: { itemsPerPage: maxItemsOnPage, currentPage: pageNumber, id: p_first }" - let-isLast="last"> - <tr> - <td>{{history.timestamp | localDate:'medium' }}</td> - <td *ngIf="history.previousState !== null"> - {{history.previousState | translate }} - <span class="glyphicon glyphicon-arrow-right" - style="padding-left: 5px;padding-right: 5px"></span> - {{history.currentState | translate }} - </td> - <td *ngIf="history.previousState === null">{{history.currentState | translate }}</td> - </tr> - </ng-template> - </tbody> - </table> - <pagination-controls class="text-right" (pageChange)="pageNumber = $event" id="{{ p_first }}" - previousLabel="{{ 'PAGINATION.PREVIOUS' | translate }}" - nextLabel="{{ 'PAGINATION.NEXT' | translate }}" - screenReaderPaginationLabel="{{ 'PAGINATION.SCREEN_READER.PAGINATION' | translate }}" - screenReaderPageLabel="{{ 'PAGINATION.SCREEN_READER.PAGE' | translate }}" - screenReaderCurrentLabel="{{ 'PAGINATION.SCREEN_READER.CURRENT' | translate }}"></pagination-controls> - </div> - <!-- apply config modal --> <nmaas-modal styleModal="info" #applyConfig> <div class="nmaas-modal-header"> @@ -414,10 +402,11 @@ {{'REDEPLOY_MODAL.BODY' | translate}} </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" - (click)="redeploy(); redeployConfirm.hide()">{{'REDEPLOY_MODAL.OK_BUTTON' | translate}}</button> <button type="button" class="btn btn-secondary" (click)="redeployConfirm.hide()">{{'REDEPLOY_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-primary" + (click)="redeploy(); redeployConfirm.hide()">{{'REDEPLOY_MODAL.OK_BUTTON' | translate}}</button> + </div> </nmaas-modal> @@ -430,10 +419,10 @@ {{'REMOVE_MODAL.BODY' | translate}} </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" - (click)="removalFailedInstance(); removeConfirm.hide()">{{'REMOVE_MODAL.OK_BUTTON' | translate}}</button> <button type="button" class="btn btn-secondary" (click)="removeConfirm.hide()">{{'REMOVE_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-primary" + (click)="removalFailedInstance(); removeConfirm.hide()">{{'REMOVE_MODAL.OK_BUTTON' | translate}}</button> </div> </nmaas-modal> @@ -446,10 +435,10 @@ <div [innerHTML]="'ENABLE_AUTO_UPGRADES_MODAL.BODY' | translate"></div> </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" - (click)="enableAutoUpgrades(); enableAutoUpgradesConfirm.hide()">{{'ENABLE_AUTO_UPGRADES_MODAL.OK_BUTTON' | translate}}</button> <button type="button" class="btn btn-secondary" (click)="enableAutoUpgradesConfirm.hide()">{{'ENABLE_AUTO_UPGRADES_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-primary" + (click)="enableAutoUpgrades(); enableAutoUpgradesConfirm.hide()">{{'ENABLE_AUTO_UPGRADES_MODAL.OK_BUTTON' | translate}}</button> </div> </nmaas-modal> @@ -462,10 +451,10 @@ <div [innerHTML]="'DISABLE_AUTO_UPGRADES_MODAL.BODY' | translate"></div> </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" - (click)="disableAutoUpgrades(); disableAutoUpgradesConfirm.hide()">{{'DISABLE_AUTO_UPGRADES_MODAL.OK_BUTTON' | translate}}</button> <button type="button" class="btn btn-secondary" (click)="disableAutoUpgradesConfirm.hide()">{{'DISABLE_AUTO_UPGRADES_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-primary" + (click)="disableAutoUpgrades(); disableAutoUpgradesConfirm.hide()">{{'DISABLE_AUTO_UPGRADES_MODAL.OK_BUTTON' | translate}}</button> </div> </nmaas-modal> diff --git a/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts b/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts index 449459e4f4b881d437f763ae6433b6b45ad4b7b7..89a7c18a4588f435c824b0041fc9a844f1874499 100644 --- a/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts +++ b/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts @@ -159,6 +159,7 @@ describe('Component: AppInstance', () => { domainDcnDetails: null, domainTechDetails: null, groups: [], + clusters: [], applicationStatePerDomain: [ { applicationBaseId: 2, @@ -186,6 +187,7 @@ describe('Component: AppInstance', () => { createdAt: new Date(), descriptiveDeploymentId: 'test-oxidized-48', domainId: 4, + domainName: "Test Domain", id: 1, internalId: 'eccbaf70-7fdd-401a-bb3e-b8659bcfbdff', name: 'oxi-virt-1', diff --git a/src/app/appmarket/appinstance/appinstance/appinstance.component.ts b/src/app/appmarket/appinstance/appinstance/appinstance.component.ts index c4ce87cd4c01e91f4f95949c30428da0c5ec570d..543a563ff26a4089231b8e225fbcc4512120f17c 100644 --- a/src/app/appmarket/appinstance/appinstance/appinstance.component.ts +++ b/src/app/appmarket/appinstance/appinstance/appinstance.component.ts @@ -164,8 +164,6 @@ export class AppInstanceComponent implements OnInit, OnDestroy { this.configurationTemplate = this.getTemplate(appInstance.configWizardTemplate.template); this.app = appInstance.application; - this.updateAppInstancePodNames(); - this.submission.data.configuration = JSON.parse(appInstance.configuration); if (this.appInstance.configUpdateWizardTemplate != null) { @@ -645,4 +643,13 @@ export class AppInstanceComponent implements OnInit, OnDestroy { } + public openAccessMethodsModal(): void { + this.appInstanceService.getDeploymentParameters(this.appInstanceId).subscribe( + deployParams => { + this.deployParametersSubject.next(deployParams) + this.accessMethodsModal.show() ; + }) + + } + } diff --git a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.css b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.css index 0253fa70fbc1fcf20b1873fbd2d8360ddaa3309a..a1f3548446b32f144eaa14a1c003ce1a2b358506 100644 --- a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.css +++ b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.css @@ -25,7 +25,7 @@ height: 20px; } -tr.clickable { +.clickable { cursor: pointer; } @@ -39,52 +39,7 @@ label{ margin-bottom: 0; font-weight: unset; } -:host ::ng-deep .p-datatable .p-datatable-thead > tr > th{ - border: 1px solid #E0E2E5; - border-width: 0 0 1px 0; -} -:host ::ng-deep .p-datatable .p-datatable-tbody > tr > td { - text-align: left; - border: 1px solid #E0E2E5; - border-width: 0 0 1px 0; - padding: 1rem 1rem; -} -:host ::ng-deep .p-datatable .p-paginator-bottom{ - height: 40px; - background: transparent; - border: none; - margin-top:10px; -} -:host ::ng-deep .p-datatable .p-datatable-tbody > tr{ - background: transparent; -} - -:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page{ - transition: unset; - border-radius: 50%; - min-width:3.5rem; - height:3.5rem; - margin:0 5px; - font-size: 14px; -} -:host ::ng-deep .p-paginator-element{ - border-radius:50%; - margin:0 5px; - min-width:3.5rem; - height:3.5rem; - font-size: 14px; -} -:host ::ng-deep .p-paginator .p-dropdown{ - height:3rem; -} -:host ::ng-deep .p-paginator-icon{ - height: 1.5rem; - width: 1.5rem; -} -:host ::ng-deep .p-paginator .p-dropdown .p-dropdown-label{ - padding-right: 10px; -} ::ng-deep.p-selectbutton .p-button.p-highlight{ background: var(--primary-button-color) !important; border-color: var(--primary-button-color); @@ -100,3 +55,36 @@ label{ border-color: var(--primary-button-color); color: var(--background); } +:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page.p-highlight{ + background: var(--user-button-background-hover); +} +:host ::ng-deep .p-button.p-button-icon-only{ + width: unset; +} +::ng-deep .running .p-progressbar .p-progressbar-value { + background: #EBBD59; +} +.running{ + color: #EBBD59; + font-weight: bold; +} +::ng-deep .failure .p-progressbar .p-progressbar-value{ + background: #AA0404; +} +.failure{ + color: #AA0404; + font-weight: bold; +} +::ng-deep .done .p-progressbar .p-progressbar-value{ + background: #136214; +} +.done{ + color: #136214; + font-weight: bold; +} +::ng-deep .p-progressbar .p-progressbar-label{ + display: none; +} +p-selectbutton{ + width: max-content; +} diff --git a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.html b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.html index 2713e4d1baf36a9ca2ba56cc5eee3e7a089dcf68..7af02f7f4a03f7036cea696cf1beb791583dcfca 100644 --- a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.html +++ b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.html @@ -1,28 +1,37 @@ -<div style="display: flex; align-items: center; justify-content: space-between; margin-top:20px"> +<div style="display: flex; align-items: flex-start; justify-content: space-between; margin-top:20px"> <div style="display:flex; align-items: center;"> - <input pInputText name="search" id="search" placeholder="Search" type="text" [(ngModel)]="searchValue"> - <div style="margin-left:20px"> + <span class="p-input-icon-right"> + <i class="pi pi-search" style="font-size: 13px; top: 16px; margin-right: 5px;"></i> + <input pInputText class="form-control" name="search" id="search" placeholder="Search" type="text" [(ngModel)]="searchValue"> + </span> + <div style="display: flex; margin: 0 20px"> + <p-checkbox id="show_my" inputId="show_my" binary="true" [(ngModel)]="showAll" (onChange)="onSelectionChange()" [ngModelOptions]="{standalone: true}" ngDefaultControl></p-checkbox> + <label for="show_my" style="text-wrap: nowrap">Show all instances</label> + </div> + <div style="display: flex"> <p-checkbox *domainRoles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR'];domainId:domainId" id="show_visible" inputId="show_visible" binary="true" [(ngModel)]="undeployedVisible" [ngModelOptions]="{standalone: true}"></p-checkbox> - <label for="show_visible" *domainRoles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR'];domainId:domainId">{{'APP_INSTANCES.UNDEPLOYED_VISIBLE' | translate}}</label> + <label for="show_visible" style="text-wrap: nowrap" *domainRoles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR'];domainId:domainId">{{'APP_INSTANCES.UNDEPLOYED_VISIBLE' | translate}}</label> </div> </div> <div class="" style="display: inline-flex; align-items: center"> - <label class="mr-3" for="selectionType">{{ 'APP_INSTANCES.SHOW' | translate }}: </label> <p-selectButton id="selectionType" - [options]="selectionOptions" - [(ngModel)]="listSelection" + [options]="viewOptions" + [(ngModel)]="selectedOption" [ngModelOptions]="{standalone: true}" - (ngModelChange)="onSelectionChange($event)" optionLabel="label" optionValue="value" - ngDefaultControl/> + ngDefaultControl> + <ng-template let-item pTemplate> + <i style="font-size: 14px; padding: 0 10px" [class]="item.icon"></i> + </ng-template> + </p-selectButton> </div> </div> <h4 style="margin-top:40px; font-weight: bold" *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']">{{ 'APP_INSTANCES.DEPLOYED' | translate }}</h4> -<div class="background-section" style="margin-top:30px"> +<div *ngIf="selectedOption === 'list'" class="background-section" style="margin-top:30px"> <p-table [value]="appDeployedInstances | async | searchAppInstance: searchValue" [paginator]="true" [rows]="maxItemsOnPage" @@ -51,7 +60,7 @@ <td>{{ appInstance?.applicationName }}</td> <td>{{ appInstance?.applicationVersion }}</td> <td *ngIf="domainId === undefined || domainId === domainService.getGlobalDomainId()"> - {{ getDomainNameById(appInstance?.domainId) }} + {{ appInstance?.domainName }} </td> <td>{{ appInstance?.owner?.username }}</td> <td>{{ appInstance?.createdAt | localDate:'dd-MM-yyyy HH:mm' }}</td> @@ -66,9 +75,52 @@ </p-table> </div> +<div *ngIf="selectedOption === 'cards'" class="grid col-12"> + <div class="col-lg-4 col-md-6 col-sm-6 col-xs-12" *ngFor="let appInstance of appDeployedInstances | async | searchAppInstance: searchValue"> + <div class="background-section" [ngClass]="{'clickable': !userHasGuestRoleInCurrentDomain()}" + [routerLink]="userHasGuestRoleInCurrentDomain() ? [] : [appInstance.id]" > + <div style="display:flex; justify-content: space-between; margin-bottom: 30px"> + <div> + <p>{{appInstance?.name}}</p> + <p>{{ appInstance?.applicationName }}</p> + </div> + <div> + <div class="" *ngIf="appInstance?.application"> + <img alt="App logo" + [src]="(appImagesService.getAppLogoUrl(appInstance?.application.applicationBase.id) | secure) || 'assets/images/app-logo-example.png'" height="50px"/> + </div> + <div class="" *ngIf="!appInstance?.application"> + <img alt="App logo" src="assets/images/app-logo-example.png" height="50px"/> + </div> + </div> + </div> + <div *ngIf="getStateAsEnum(appInstance?.state) == AppInstanceState.FAILURE || + getStateAsEnum(appInstance?.state) == AppInstanceState.REMOVED"> + <p class="failure">{{ translateState(appInstance?.state) }}</p> + <p-progressBar class="failure" [value]="100" [style]="{'height': '6px'}"></p-progressBar> + </div> + <div *ngIf="getStateAsEnum(appInstance?.state) == AppInstanceState.RUNNING || + getStateAsEnum(appInstance?.state) == AppInstanceState.DEPLOYING || + getStateAsEnum(appInstance?.state) == AppInstanceState.CONFIGURATION_AWAITING || + getStateAsEnum(appInstance?.state) == AppInstanceState.CONNECTING"> + <p class="running">{{ translateState(appInstance?.state) }}</p> + <p-progressBar class="running" mode="indeterminate" [style]="{'height': '6px'}"></p-progressBar> + </div> + <div *ngIf="getStateAsEnum(appInstance?.state) == AppInstanceState.DONE || + getStateAsEnum(appInstance?.state) == AppInstanceState.RUNNING "> + <p class="done">{{ translateState(appInstance?.state) }}</p> + <p-progressBar class="done" [value]="100" [style]="{'height': '6px'}"></p-progressBar> + </div> + + + </div> + </div> + +</div> + <div style="margin-top:40px" *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']"> - <h4 style="margin-top:40px; font-weight: bold" *ngIf="undeployedVisible">{{ 'APP_INSTANCES.UNDEPLOYED' | translate }}</h4> - <div style="margin-top:30px" *ngIf="undeployedVisible" class="background-section"> + <h4 class="header" *ngIf="undeployedVisible">{{ 'APP_INSTANCES.UNDEPLOYED' | translate }}</h4> + <div style="margin-top:30px" *ngIf="undeployedVisible && selectedOption === 'list'" class="background-section"> <p-table [value]="appUndeployedInstances | async | paginate: { itemsPerPage: maxItemsOnPageSec, currentPage: secondPageNumber, id: p_second }" [paginator]="true" [rows]="maxItemsOnPageSec" @@ -91,7 +143,7 @@ <td>{{ appInstance?.name }}</td> <td>{{ appInstance?.applicationName }}</td> <td *ngIf="domainId === undefined || domainId === domainService.getGlobalDomainId()"> - {{ getDomainNameById(appInstance?.domainId) }} + {{ appInstance?.domainName }} </td> <td>{{ appInstance?.owner?.username }}</td> <td>{{ appInstance?.createdAt | localDate:'dd-MM-yyyy HH:mm' }}</td> diff --git a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.ts b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.ts index bf81b9835a99a41308b8073d3f2849cbed6d0073..6c09b32f86857764f1631aa41a5580bd094e2449 100644 --- a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.ts +++ b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.ts @@ -1,7 +1,7 @@ import {Component, OnInit} from '@angular/core'; import {AppInstance, AppInstanceState, parseAppInstanceState} from '../../../model'; -import {AppConfigService, AppInstanceService, CustomerSearchCriteria, DomainService} from '../../../service'; +import {AppConfigService, AppImagesService, AppInstanceService, CustomerSearchCriteria, DomainService} from '../../../service'; import {AuthService} from '../../../auth/auth.service'; import {UserDataService} from '../../../service/userdata.service'; import {forkJoin, Observable, of} from 'rxjs'; @@ -23,6 +23,7 @@ export enum AppInstanceListSelection { export class AppInstanceListComponent implements OnInit { public undeployedVisible = false; + public showAll = false; private readonly item_number_key: string = 'item_number_per_page'; private readonly list_selection_key: string = 'list_selection'; @@ -53,12 +54,13 @@ export class AppInstanceListComponent implements OnInit { public domainId = 0; public domains: Domain[] = []; + public viewOptions = [ + {icon: 'pi pi-list', value: 'list'}, + {icon: 'pi pi-th-large', value: 'cards'} + ]; + public selectedOption = 'list'; public searchValue = ''; - public selectionOptions = [ - { label: this.translateEnum(AppInstanceListSelection.ALL), value: AppInstanceListSelection.ALL }, - { label: this.translateEnum(AppInstanceListSelection.MY), value: AppInstanceListSelection.MY }, - ]; constructor(private appInstanceService: AppInstanceService, @@ -67,15 +69,13 @@ export class AppInstanceListComponent implements OnInit { public authService: AuthService, private appConfig: AppConfigService, private translateService: TranslateService, - private sessionService: SessionService) { + private sessionService: SessionService, + public appImagesService: AppImagesService) { } ngOnInit() { this.sessionService.registerCulture(this.translateService.currentLang); - this.domainService.getAll().subscribe(result => { - this.domains.push(...result); - }); const i = sessionStorage.getItem(this.item_number_key); if (i) { this.maxItemsOnPage = +i; @@ -85,6 +85,7 @@ export class AppInstanceListComponent implements OnInit { const ls = AppInstanceListSelection[sessionStorage.getItem(this.list_selection_key)]; if (ls !== undefined) { this.listSelection = ls; + this.showAll = ls === AppInstanceListSelection.ALL; } console.log(this.listSelection); this.userDataService.selectedDomainId.subscribe(domainId => { @@ -97,49 +98,6 @@ export class AppInstanceListComponent implements OnInit { this.update(domainId) }); - forkJoin({ - all: this.translateService.get('ENUM.ALL'), - my: this.translateService.get('ENUM.MY') - }).subscribe(translations => { - this.selectionOptions = [ - { label: translations.all, value: AppInstanceListSelection.ALL }, - { label: translations.my, value: AppInstanceListSelection.MY }, - ]; - }); - - - forkJoin({ - all: this.translateService.get('ENUM.ALL'), - my: this.translateService.get('ENUM.MY') - }).subscribe(translations => { - this.selectionOptions = [ - { label: translations.all, value: AppInstanceListSelection.ALL }, - { label: translations.my, value: AppInstanceListSelection.MY }, - ]; - }); - - } - - public getDomainNameById(id: number): string { - if (this.domains === undefined) { - return 'none'; - } - return this.domains.find(value => value.id === id).name; - } - - public translateEnum(value: AppInstanceListSelection): string { - let outValue = ''; - if (value.toString() === 'ALL') { - this.translateService.get('ENUM.ALL').subscribe((res: string) => { - outValue = res; - }) - } - if (value.toString() === 'MY') { - this.translateService.get('ENUM.MY').subscribe((res: string) => { - outValue = res; - }) - } - return outValue; } public update(domainId: number): void { @@ -165,7 +123,11 @@ export class AppInstanceListComponent implements OnInit { || this.authService.hasDomainRole(app.domainId, 'ROLE_USER'); } - public onSelectionChange(event) { + public onSelectionChange() { + this.listSelection = this.showAll + ? AppInstanceListSelection.ALL + : AppInstanceListSelection.MY; + sessionStorage.setItem(this.list_selection_key, AppInstanceListSelection[this.listSelection]); this.update(this.domainId); } @@ -240,4 +202,8 @@ export class AppInstanceListComponent implements OnInit { public userHasGuestRoleInCurrentDomain(): boolean { return this.authService.hasDomainRole(this.domainId, 'ROLE_GUEST'); } + + public getStateAsEnum(state: string | AppInstanceState): AppInstanceState { + return typeof state === 'string' ? AppInstanceState[state] : state; + } } diff --git a/src/app/appmarket/appinstance/modals/add-members-modal/add-members-modal.component.spec.ts b/src/app/appmarket/appinstance/modals/add-members-modal/add-members-modal.component.spec.ts index 4a613f1394d03e28924f20c069a4f8c3e716cf28..0e2900d4ff0f8e9c11652b992abff6e0be6964e8 100644 --- a/src/app/appmarket/appinstance/modals/add-members-modal/add-members-modal.component.spec.ts +++ b/src/app/appmarket/appinstance/modals/add-members-modal/add-members-modal.component.spec.ts @@ -32,6 +32,7 @@ describe('AddMembersModalComponent', () => { createdAt: new Date(), descriptiveDeploymentId: 'test-oxidized-48', domainId: 4, + domainName: "Test Domain", id: 1, internalId: 'eccbaf70-7fdd-401a-bb3e-b8659bcfbdff', name: 'oxi-virt-1', diff --git a/src/app/appmarket/applist/applist.component.html b/src/app/appmarket/applist/applist.component.html index e39a6a560095b604441ce2ea47ba6c4ed49cbe32..4e46de83f11494f1d5ac48708eb7782aac7f1af4 100644 --- a/src/app/appmarket/applist/applist.component.html +++ b/src/app/appmarket/applist/applist.component.html @@ -1 +1 @@ -<nmaas-applications-view class="col-sm-12 col-sm-12 col-md-12" [domainId]="domainId" [appView]="appsView"></nmaas-applications-view> +<nmaas-applications-view class="col-sm-12 col-sm-12 col-md-12" style="padding:0;" [domainId]="domainId" [appView]="appsView"></nmaas-applications-view> diff --git a/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.css b/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.css new file mode 100644 index 0000000000000000000000000000000000000000..f2c387a9db7ec1e2c9aeefc41a92b4d6336c9fdb --- /dev/null +++ b/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.css @@ -0,0 +1,38 @@ +:host ::ng-deep input[type=file]{ + display:none; +} +:host ::ng-deep .p-button{ + width: unset; + margin-right: 5px; + background: var(--primary-button-color); + color: var(--button-text-color); +} +:host ::ng-deep .p-button:hover{ + background: var(--primary-button-hover); + border:none; +} +:host ::ng-deep .p-button-label{ + font-weight: normal; +} +:host ::ng-deep .p-fileupload .p-fileupload-buttonbar{ + border: none; + background: transparent; + margin-bottom: 10px; + padding: 0; +} +:host ::ng-deep .p-fileupload .p-fileupload-content{ + border: none; + padding: 0; + border-radius: 3px; + +} +:host ::ng-deep .p-fileupload-content .p-progressbar{ + display: none; +} +textarea{ + border-color: #ccc; +} +:host ::ng-deep .p-inputtext:enabled:focus{ + box-shadow: none; + border-color: var(--l-text-color); +} diff --git a/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.html b/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.html index 4fbade572a717bc1ceaa366511bc588551d6f8b7..ebd41c48dae2e11520b48a6727d77f2ad92d16f0 100644 --- a/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.html +++ b/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.html @@ -14,7 +14,7 @@ <div style="margin-bottom: 10px"> {{ 'APPS_MANAGEMENT.ADD_JSON_TEXTAREA'| translate}} </div> - <textarea rows="10" cols="100" pInputTextarea [(ngModel)]="jsonText" (keyup)="this.JsonError = false; this.error = ''"></textarea> + <textarea pInputTextarea rows="10" cols="100" style="min-height: 200px; width: 100%" [autoResize]="true" [(ngModel)]="jsonText" (keyup)="this.JsonError = false; this.error = ''"></textarea> </div> <div class="flex flex-row justify-content-center justify-content-center mt-2"> <button *ngIf="jsonText.length >0" pButton class="btn btn-secondary" diff --git a/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.spec.ts b/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.spec.ts index e7ad001d3a837a971adbc8b9c1dc216762b20d64..061fc3dfa636a27175d86b3c25866756c197e669 100644 --- a/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.spec.ts +++ b/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.spec.ts @@ -1,34 +1,39 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { AppAddJsonAppComponent } from './app-add-json-app.component'; -import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; -import {RouterTestingModule} from '@angular/router/testing'; -import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import { ModalComponent } from '../../../shared'; +import { AppsService } from '../../../service'; +import { Router } from '@angular/router'; +import { of, throwError } from 'rxjs'; import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; describe('AppAddJsonAppComponent', () => { let component: AppAddJsonAppComponent; let fixture: ComponentFixture<AppAddJsonAppComponent>; + let mockAppsService: jasmine.SpyObj<AppsService>; + let mockRouter: jasmine.SpyObj<Router>; beforeEach(async () => { + mockAppsService = jasmine.createSpyObj('AppsService', ['createApplicationDTO']); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + await TestBed.configureTestingModule({ - declarations: [ AppAddJsonAppComponent ], + declarations: [AppAddJsonAppComponent, ModalComponent], imports: [ - HttpClientTestingModule, - RouterTestingModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateFakeLoader - } - }), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader + } + }), ], - schemas: [CUSTOM_ELEMENTS_SCHEMA,NO_ERRORS_SCHEMA], - }) - .compileComponents(); - }); + providers: [ + { provide: AppsService, useValue: mockAppsService }, + { provide: Router, useValue: mockRouter } + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA] + }).compileComponents(); - beforeEach(() => { fixture = TestBed.createComponent(AppAddJsonAppComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -37,4 +42,72 @@ describe('AppAddJsonAppComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should handle valid JSON upload', () => { + const mockFile = new Blob([JSON.stringify({ key: 'value' })], { type: 'application/json' }); + const mockEvent = { files: [mockFile] }; + mockAppsService.createApplicationDTO.and.returnValue(of({ id: '123' })); + + spyOn(component, 'uploadHandler'); + component.onUpload(mockEvent); + + const fileReader = new FileReader(); + fileReader.onload = () => { + expect(component.uploadHandler).toHaveBeenCalledWith({ id: '123' }); + }; + }); + + it('should handle invalid JSON upload', () => { + const mockFile = new Blob(['invalid json'], { type: 'application/json' }); + const mockEvent = { files: [mockFile] }; + + component.onUpload(mockEvent); + + const fileReader = new FileReader(); + fileReader.onload = () => { + expect(component.JsonError).toBeTrue(); + }; + }); + + it('should handle valid JSON text submission', () => { + component.jsonText = JSON.stringify({ key: 'value' }); + mockAppsService.createApplicationDTO.and.returnValue(of({ id: '123' })); + + spyOn(component, 'uploadHandler'); + component.sendJsonText(); + + expect(component.uploadHandler).toHaveBeenCalledWith({ id: '123' }); + }); + + it('should handle invalid JSON text submission', () => { + component.jsonText = 'invalid json'; + + component.sendJsonText(); + + expect(component.JsonError).toBeTrue(); + }); + + it('should handle error during JSON submission', () => { + component.jsonText = JSON.stringify({ key: 'value' }); + mockAppsService.createApplicationDTO.and.returnValue(throwError({ message: 'Error occurred' })); + + component.sendJsonText(); + + expect(component.error).toBe('Error occurred'); + }); + + it('should navigate to app details on successful upload', () => { + const mockResult = { id: '123' }; + + component.uploadHandler(mockResult); + + expect(mockRouter.navigate).toHaveBeenCalledWith(['apps', '123']); + }); + + it('should show modal', () => { + spyOn(component.modal, 'show'); + component.show(); + + expect(component.modal.show).toHaveBeenCalled(); + }); }); diff --git a/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.ts b/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.ts index e0d4bf65b773d08f3427a7c5fcec100a4a99d6d7..10e18dcfc0598fd779fe86dbd52a66e796f8bf94 100644 --- a/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.ts +++ b/src/app/appmarket/appmanagement/app-add-json-app/app-add-json-app.component.ts @@ -6,7 +6,7 @@ import {Router} from '@angular/router'; @Component({ selector: 'app-app-add-json-app', templateUrl: './app-add-json-app.component.html', - styleUrls: [] + styleUrls: ['./app-add-json-app.component.css'] }) export class AppAddJsonAppComponent { diff --git a/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.css b/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.css new file mode 100644 index 0000000000000000000000000000000000000000..f2c387a9db7ec1e2c9aeefc41a92b4d6336c9fdb --- /dev/null +++ b/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.css @@ -0,0 +1,38 @@ +:host ::ng-deep input[type=file]{ + display:none; +} +:host ::ng-deep .p-button{ + width: unset; + margin-right: 5px; + background: var(--primary-button-color); + color: var(--button-text-color); +} +:host ::ng-deep .p-button:hover{ + background: var(--primary-button-hover); + border:none; +} +:host ::ng-deep .p-button-label{ + font-weight: normal; +} +:host ::ng-deep .p-fileupload .p-fileupload-buttonbar{ + border: none; + background: transparent; + margin-bottom: 10px; + padding: 0; +} +:host ::ng-deep .p-fileupload .p-fileupload-content{ + border: none; + padding: 0; + border-radius: 3px; + +} +:host ::ng-deep .p-fileupload-content .p-progressbar{ + display: none; +} +textarea{ + border-color: #ccc; +} +:host ::ng-deep .p-inputtext:enabled:focus{ + box-shadow: none; + border-color: var(--l-text-color); +} diff --git a/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.html b/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.html index b1aebf85fa2ec769679f9d620222cb298756fc8c..658ebdfd794e2511480d2b3a613ca1ad9caf018d 100644 --- a/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.html +++ b/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.html @@ -14,7 +14,7 @@ <div style="margin-bottom: 10px"> {{ 'APPS_MANAGEMENT.ADD_JSON_TEXTAREA'| translate}} </div> - <textarea rows="10" cols="100" pInputTextarea [(ngModel)]="jsonText" (keyup)="this.JsonError = false; this.error = ''"></textarea> + <textarea rows="10" cols="100" pInputTextarea style="min-height: 200px; width: 100%" [autoResize]="true" [(ngModel)]="jsonText" (keyup)="this.JsonError = false; this.error = ''"></textarea> </div> <div class="flex flex-row justify-content-center justify-content-center mt-2"> <button *ngIf="jsonText.length >0" pButton class="btn btn-primary" diff --git a/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.spec.ts b/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.spec.ts index 958fb094e3a8d04e440656c3714c5f944b70a678..be45384e8dd010b6bf124400cb5be08d459cc57f 100644 --- a/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.spec.ts +++ b/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.spec.ts @@ -1,18 +1,26 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { AppAddJsonVersionAppComponent } from './app-add-json-version-app.component'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; -import {RouterTestingModule} from '@angular/router/testing'; -import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import { AppsService } from '../../../service'; +import { Router } from '@angular/router'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { of, throwError } from 'rxjs'; +import { ModalComponent } from '../../../shared'; describe('AppAddJsonVersionAppComponent', () => { let component: AppAddJsonVersionAppComponent; let fixture: ComponentFixture<AppAddJsonVersionAppComponent>; + let mockAppsService: jasmine.SpyObj<AppsService>; + let mockRouter: jasmine.SpyObj<Router>; beforeEach(async () => { + mockAppsService = jasmine.createSpyObj('AppsService', ['createApplication']); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + await TestBed.configureTestingModule({ - declarations: [ AppAddJsonVersionAppComponent ], + declarations: [AppAddJsonVersionAppComponent, ModalComponent], imports: [ HttpClientTestingModule, RouterTestingModule, @@ -23,9 +31,12 @@ describe('AppAddJsonVersionAppComponent', () => { } }), ], + providers: [ + { provide: AppsService, useValue: mockAppsService }, + { provide: Router, useValue: mockRouter } + ], schemas: [NO_ERRORS_SCHEMA], - }) - .compileComponents(); + }).compileComponents(); }); beforeEach(() => { @@ -37,4 +48,55 @@ describe('AppAddJsonVersionAppComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should handle valid JSON version upload', () => { + const mockFile = new Blob([JSON.stringify({ version: '1.0.0' })], { type: 'application/json' }); + const mockEvent = { files: [mockFile] }; + mockAppsService.createApplication.and.returnValue(of({ id: '123' })); + + component.onUpload(mockEvent); + + const fileReader = new FileReader(); + fileReader.onload = () => { + expect(mockAppsService.createApplication).toHaveBeenCalledWith(jasmine.objectContaining({ version: '1.0.0' })); + }; + fileReader.readAsText(mockFile); + }); + + it('should call modal.hide on successful upload', () => { + spyOn(component.modal, 'hide'); + const mockResult = { id: '123' }; + component.handleUpload(mockResult); + + expect(component.modal.hide).toHaveBeenCalled(); + }); + + + it('should handle valid JSON version text submission', () => { + component.jsonText = JSON.stringify({ version: '1.0.0' }); + mockAppsService.createApplication.and.returnValue(of({ id: '123' })); + + spyOn(component, 'handleUpload'); + component.sendJsonText(); + + expect(component.handleUpload).toHaveBeenCalledWith({ id: '123' }); + }); + + it('should handle invalid JSON version text submission', () => { + component.jsonText = 'invalid json'; + + component.sendJsonText(); + + expect(component.JsonError).toBeTrue(); + }); + + it('should handle error during JSON version submission', () => { + component.jsonText = JSON.stringify({ version: '1.0.0' }); + mockAppsService.createApplication.and.returnValue(throwError({ message: 'Error occurred' })); + + component.sendJsonText(); + + expect(component.error).toBe('Error occurred'); + }); + }); diff --git a/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.ts b/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.ts index b72cade07a20a92d867071265e9642d8ce4aa6ee..12c2470341b84d814793fb754f9164ffeb2ca70d 100644 --- a/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.ts +++ b/src/app/appmarket/appmanagement/app-add-json-version-app/app-add-json-version-app.component.ts @@ -5,7 +5,7 @@ import {AppsService} from '../../../service'; @Component({ selector: 'app-app-add-json-version-app', templateUrl: './app-add-json-version-app.component.html', - styleUrls: [] + styleUrls: ['app-add-json-version-app.component.css'] }) export class AppAddJsonVersionAppComponent { @@ -73,7 +73,7 @@ export class AppAddJsonVersionAppComponent { this.modal.show(); } - private handleUpload(result: any) { + public handleUpload(result: any) { console.log('uploaded', result); this.modal.hide(); this.refresh.emit(true); diff --git a/src/app/appmarket/appmanagement/app-change-owner-modal/app-change-owner-modal.component.html b/src/app/appmarket/appmanagement/app-change-owner-modal/app-change-owner-modal.component.html index b9bdad7579139c2b35a093187883df888e18cd3f..8968f1e38a3745a3b76255014caab217d1dd8f06 100644 --- a/src/app/appmarket/appmanagement/app-change-owner-modal/app-change-owner-modal.component.html +++ b/src/app/appmarket/appmanagement/app-change-owner-modal/app-change-owner-modal.component.html @@ -18,12 +18,13 @@ </div> </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" (click)="submit()" [disabled]="selectOwner.invalid"> - {{'APPS_MANAGEMENT.CHANGE_OWNER_BUTTON' | translate}} - </button> <button type="button" class="btn btn-secondary" (click)="modal.hide()"> {{'APP_CHANGE_STATE_MODAL.CANCEL_BUTTON' | translate}} </button> + <button type="button" class="btn btn-primary" (click)="submit()" [disabled]="selectOwner.invalid"> + {{'APPS_MANAGEMENT.CHANGE_OWNER_BUTTON' | translate}} + </button> + </div> </nmaas-modal> diff --git a/src/app/appmarket/appmanagement/app-create-wizard/app-create-wizard.component.html b/src/app/appmarket/appmanagement/app-create-wizard/app-create-wizard.component.html index 72fd4db5ee3d8ae828654970a253fc669cc7b7ca..3771d215733c6678848010cdfda44529fe087dd0 100644 --- a/src/app/appmarket/appmanagement/app-create-wizard/app-create-wizard.component.html +++ b/src/app/appmarket/appmanagement/app-create-wizard/app-create-wizard.component.html @@ -536,6 +536,7 @@ <form-builder *ngIf="formDisplayChange && applicationDTO.application.configWizardTemplate.template" [form]="applicationDTO.application.configWizardTemplate.template" (change)="setConfigTemplate($event)"> </form-builder> + </p-tabPanel> <p-tabPanel header="{{'APPS_WIZARD.RAW_JSON' | translate}}"> diff --git a/src/app/appmarket/appmanagement/app-create-wizard/app-create-wizard.component.ts b/src/app/appmarket/appmanagement/app-create-wizard/app-create-wizard.component.ts index e9e30b6449f6c4154ed1f3e679754a5d17c26636..90f3e447abb68f69f83746904b7a43dc3712885e 100644 --- a/src/app/appmarket/appmanagement/app-create-wizard/app-create-wizard.component.ts +++ b/src/app/appmarket/appmanagement/app-create-wizard/app-create-wizard.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit, ViewChild, ViewEncapsulation} from '@angular/core'; +import {Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation} from '@angular/core'; import {ConfigWizardTemplate} from '../../../model'; import {MenuItem, SelectItem} from 'primeng/api'; import {AppImagesService, AppsService, TagService} from '../../../service'; @@ -29,7 +29,7 @@ import {ApplicationBase} from '../../../model/application-base'; styleUrls: ['./app-create-wizard.component.css'] }) -export class AppCreateWizardComponent extends BaseComponent implements OnInit { +export class AppCreateWizardComponent extends BaseComponent implements OnInit, OnDestroy { @ViewChild(ModalComponent, {static: true}) public modal: ModalComponent; @@ -59,6 +59,9 @@ export class AppCreateWizardComponent extends BaseComponent implements OnInit { public languages: SelectItem[] = []; public formDisplayChange = true; + public template : any; + public translateUpdate: any; + // properties for global parameters deploy validation // in future extensions pack this into single object public deployParamKeyValidator: ValidatorFn = noParameterTypeInControlValueValidator(); @@ -97,6 +100,7 @@ export class AppCreateWizardComponent extends BaseComponent implements OnInit { })); this.getParametersTypes().forEach(val => this.deployParameter.push({label: val.replace('_', ' '), value: val})); this.steps = this.getSteps(); + this.updateStepsTranslation(); this.route.params.subscribe(params => { if (params['id'] == null) { this.createNewWizard(); @@ -120,6 +124,25 @@ export class AppCreateWizardComponent extends BaseComponent implements OnInit { }); } + private updateStepsTranslation() { + this.translateUpdate = setInterval(() => { + if(this.translate.instant('APPS_WIZARD.GENERAL_INFO_STEP') !== null) { + this.steps = this.getSteps(); + this.stopTranslationUpdate(); + } + }, 200); + } + + private stopTranslationUpdate() { + clearInterval(this.translateUpdate); + this.translateUpdate = null; + } + + ngOnDestroy(): void { + clearInterval(this.translateUpdate); + this.translateUpdate = null; + } + public getSteps(): any { if (this.isInMode(ComponentMode.CREATE)) { return [ @@ -206,7 +229,8 @@ export class AppCreateWizardComponent extends BaseComponent implements OnInit { this.configFileTemplates.push(new ConfigFileTemplate()); this.applicationDTO.application.configWizardTemplate = new ConfigWizardTemplate(); this.applicationDTO.application.configWizardTemplate.template = this.configTemplateService.getConfigTemplate(); - } + }; + public nextStep(): void { this.activeStepIndex += 1; @@ -325,10 +349,15 @@ export class AppCreateWizardComponent extends BaseComponent implements OnInit { } public setConfigTemplate(event): void { - if (!this.applicationDTO.application.configWizardTemplate) { - this.applicationDTO.application.configWizardTemplate = new ConfigWizardTemplate(); + console.log(event) + if(event.type === "addComponent" || event.type === "saveComponent") { + console.log(event); + this.template = event.form; + this.applicationDTO.application.configWizardTemplate.template = null; + this.applicationDTO.application.configWizardTemplate.template = Object.assign({}, this.template); + console.log('Wizard saved',this.applicationDTO.application.configWizardTemplate.template) } - this.applicationDTO.application.configWizardTemplate.template = event.form; + } public setUpdateConfigTemplate(event): void { @@ -539,7 +568,7 @@ export class AppCreateWizardComponent extends BaseComponent implements OnInit { if (this.applicationDTO.application.appConfigurationSpec.configFileRepositoryRequired) { this.removeDefaultElement(); } else { - this.addDefaultElement(); + // this.addDefaultElement(); this.removeElementsFromUpdateConfig(); } } diff --git a/src/app/appmarket/appmanagement/app-management-list/appmanagementlist.component.css b/src/app/appmarket/appmanagement/app-management-list/appmanagementlist.component.css index 08d4bc6e817ee8bbdd9dcedfe621a9a439dc6709..83c4455b01ded2b71a8f81afc08872d537108a63 100644 --- a/src/app/appmarket/appmanagement/app-management-list/appmanagementlist.component.css +++ b/src/app/appmarket/appmanagement/app-management-list/appmanagementlist.component.css @@ -13,6 +13,7 @@ margin-right: 5px; } -.dropdown:hover .dropdown-menu { - display: block; -} +/*.dropdown:hover .dropdown-menu {*/ +/* display: block;*/ +/*}*/ + diff --git a/src/app/appmarket/appmanagement/app-management-list/appmanagementlist.component.html b/src/app/appmarket/appmanagement/app-management-list/appmanagementlist.component.html index 19fca710ffd4222b0e182db1df8224394cd6eb49..8c62d32324d1e5e25b5cf52127828ae5af7c8353 100644 --- a/src/app/appmarket/appmanagement/app-management-list/appmanagementlist.component.html +++ b/src/app/appmarket/appmanagement/app-management-list/appmanagementlist.component.html @@ -1,80 +1,99 @@ -<div class="col-sm-12 col-sm-offset-1 col-sm 10 col-md-offset-1 col-md-10"> - <h3>{{'APPS_MANAGEMENT.TITLE'| translate}}</h3> - <div style="display:flex; justify-content: space-between"> +<div class=""> + + <div style="display:flex; "> + <div style="margin-right:20px"> + <span class="p-input-icon-right" style="width: 100%"> + <i class="pi pi-search" style="font-size: 13px; top: 16px; margin-right: 5px;"></i> + <input pInputText id="searchText" type="text" class="form-control" placeholder="Search" (keyup)="searchApp($event.target.value)"> + </span> + </div> <div style="display:flex"> <button [routerLink]="['/admin/apps/create']" class="btn btn-primary">{{ 'APPS_MANAGEMENT.ADD_BUTTON' | translate }}</button> <button (click)="appAddJson.show()" class="btn btn-primary" style="margin-left: 10px">{{ 'APPS_MANAGEMENT.ADD_BUTTON' | translate }} (JSON)</button> </div> - <div style="display: flex"> - <input pInputText id="searchText" type="text" placeholder="Search" (keyup)="searchApp($event.target.value)"> - </div> </div> + <h4 class="header">{{'APPS_MANAGEMENT.TITLE'| translate}}</h4> + <div class="background-section"> + <p-table [value]="apps" class="p-datatable-hover p-datatable-sm" [responsive]="true"> + <ng-template pTemplate="header"> + <tr> + <th></th> + <th pSortableColumn="name"> + {{'APPS_MANAGEMENT.NAME' | translate}} + <p-sortIcon field="name"></p-sortIcon> + </th> + <th pSortableColumn="owner"> + {{'APPS_MANAGEMENT.OWNER' | translate}} + <p-sortIcon field="owner"></p-sortIcon> + </th> + <th *ngIf="isAnySubtableVisible()"> + {{'APPS_MANAGEMENT.VERSION' | translate}} + </th> + <th *ngIf="isAnySubtableVisible()"> + {{'APPS_MANAGEMENT.STATE' | translate}} + </th> + <th></th> + <th></th> + <th></th> + </tr> + </ng-template> - <table class="table table-hover table-condensed" aria-describedby="Apps management table" style="margin-top:15px"> - <thead> - <tr> - <th scope="col"></th> - <th scope="col">{{'APPS_MANAGEMENT.NAME' | translate}}</th> - <th scope="col">{{'APPS_MANAGEMENT.OWNER' | translate}}</th> - <th scope="col" *ngIf="isAnySubtableVisible()">{{'APPS_MANAGEMENT.VERSION' | translate}}</th> - <th scope="col" *ngIf="isAnySubtableVisible()">{{'APPS_MANAGEMENT.STATE' | translate}}</th> - <th scope="col"></th> - </tr> - </thead> - - <tbody> - <ng-template ngFor let-app [ngForOf]="apps" let-i="index"> - <tr class="table-row" (click)="clickTableRow(i)"> - <td style="width: 5%" *ngIf="!versionRowVisible[i]"><span class="glyphicon glyphicon-chevron-right"></span></td> - <td style="width: 5%" *ngIf="versionRowVisible[i]"><span class="glyphicon glyphicon-chevron-down"></span></td> - <td style="width: 25%">{{app?.name}}</td> - <td style="width: 20%">{{app?.owner}}</td> - <td style="width: 15%"></td> - <td style="width: 15%"></td> - <td style="width: 20%" class="text-right"> - <span class="dropdown"> - <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> - </a> - <ul class="dropdown-menu pull-right-drop"> - <li *roles="['ROLE_SYSTEM_ADMIN']"> - <a (click)="appChangeOwnerModal.show(app)" >{{ 'APPS_MANAGEMENT.CHANGE_OWNER_BUTTON' | translate }}</a> - </li> - <li> - <a [routerLink]="['/admin/apps/create/version', app?.name]">{{ 'APPS_MANAGEMENT.ADD_NEW_VERSION_BUTTON' | translate }}</a> - </li> - <li> - <a (click)="appAddJsonVersion.show()" >{{ 'APPS_MANAGEMENT.ADD_NEW_VERSION_BUTTON' | translate }} (JSON)</a> - </li> - <li> - <a (click)="getApplicationInfoJSONWithBase(app?.id)"> {{'APPS_MANAGEMENT.EXPORT_JSON' | translate}}</a> - </li> - <li> - <a [routerLink]="['/admin/apps/edit', app?.id]">{{ 'APPS_MANAGEMENT.EDIT_BUTTON' | translate }}</a> - </li> - <li> - <a (click)="openRemovalModal(app)"> {{ 'APPS_MANAGEMENT.DELETE_BUTTON' | translate}}</a> - </li> - </ul> - </span> - </td> - </tr> - <ng-template ngFor let-version [ngForOf]="app.versions.sort(appVersionCompare)"> - <tr *ngIf="versionRowVisible[i]" class="table-row" > - <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]"></td> - <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]"></td> - <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]"></td> - <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]">{{version.version}}</td> - <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]">{{"ENUM.STATE." + getStateAsString(version.state).toUpperCase() | translate }}</td> + <ng-template pTemplate="body" let-app let-i="rowIndex"> + <tr> + <td *ngIf="!versionRowVisible[i]" (click)="clickTableRow(i)"> + <span class="pi pi-chevron-right"></span> + </td> + <td *ngIf="versionRowVisible[i]" (click)="clickTableRow(i)"> + <span class="pi pi-chevron-down"></span> + </td> + <td (click)="clickTableRow(i)">{{app?.name}}</td> + <td (click)="clickTableRow(i)">{{app?.owner}}</td> + <td></td> + <td></td> <td class="text-right"> - <a [routerLink]="['/admin/apps/view', version?.appVersionId]"> - <em class="far fa-eye icon-black icon-bigger"></em> - </a> <span class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> + <em class="pi pi-cog" style="font-size: 1.8rem; color: var(--l-text-color)"></em> + </a> + <ul class="dropdown-menu pull-right-drop"> + <li *roles="['ROLE_SYSTEM_ADMIN']"> + <a (click)="appChangeOwnerModal.show(app)" >{{ 'APPS_MANAGEMENT.CHANGE_OWNER_BUTTON' | translate }}</a> + </li> + <li> + <a [routerLink]="['/admin/apps/create/version', app?.name]">{{ 'APPS_MANAGEMENT.ADD_NEW_VERSION_BUTTON' | translate }}</a> + </li> + <li> + <a (click)="appAddJsonVersion.show()" >{{ 'APPS_MANAGEMENT.ADD_NEW_VERSION_BUTTON' | translate }} (JSON)</a> + </li> + <li> + <a (click)="getApplicationInfoJSONWithBase(app?.id)"> {{'APPS_MANAGEMENT.EXPORT_JSON' | translate}}</a> + </li> + <li> + <a [routerLink]="['/admin/apps/edit', app?.id]">{{ 'APPS_MANAGEMENT.EDIT_BUTTON' | translate }}</a> + </li> + <li> + <a (click)="openRemovalModal(app)"> {{ 'APPS_MANAGEMENT.DELETE_BUTTON' | translate}}</a> + </li> + </ul> + </span> + </td> + </tr> + <ng-container *ngIf="versionRowVisible[i]"> + <tr *ngFor="let version of app.versions.sort(appVersionCompare)"> + <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]"></td> + <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]"></td> + <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]"></td> + <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]">{{version.version}}</td> + <td [routerLink]="['/admin/apps/edit/version', version?.appVersionId]">{{"ENUM.STATE." + getStateAsString(version.state).toUpperCase() | translate }}</td> + <td class="text-right"> + <a [routerLink]="['/admin/apps/view', version?.appVersionId]"> + <em class="pi pi-eye" style="font-size: 1.8rem; color: var(--l-text-color); margin-right:10px"></em> + </a> + <span class="dropdown"> <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> + <em class="pi pi-cog" style="font-size: 1.8rem; color: var(--l-text-color)"></em> </a> <ul class="dropdown-menu pull-right-drop"> <li *roles="['ROLE_SYSTEM_ADMIN']"> @@ -88,13 +107,12 @@ </li> </ul> </span> - </td> - </tr> + </td> + </tr> + </ng-container> </ng-template> - </ng-template> - </tbody> - </table> - + </p-table> + </div> </div> <app-appchangestatemodal [appName]="selectedAppName" [app]="selectedVersion"></app-appchangestatemodal> diff --git a/src/app/appmarket/appmanagement/app-management.module.ts b/src/app/appmarket/appmanagement/app-management.module.ts index ebaa7d15cc0cb276cc55ef0203201a683c594bdc..fa4470584c3485a0e65ed7be4eba85c84ecc76db 100644 --- a/src/app/appmarket/appmanagement/app-management.module.ts +++ b/src/app/appmarket/appmanagement/app-management.module.ts @@ -34,6 +34,7 @@ import {AppAddJsonAppComponent} from './app-add-json-app/app-add-json-app.compon import {InputTextareaModule} from 'primeng/inputtextarea'; import {AppAddJsonVersionAppComponent} from './app-add-json-version-app/app-add-json-version-app.component'; import {DomainsModule} from '../domains/domains.module'; +import {TableModule} from 'primeng/table'; export function getJsonTemplates(config: ConfigTemplateService) { @@ -83,7 +84,8 @@ export function formioAppConfigFactory(appConfig: AppConfigService) { TooltipModule, DropdownModule, InputTextareaModule, - DomainsModule + DomainsModule, + TableModule ], exports: [], providers: [ diff --git a/src/app/appmarket/appmanagement/app-management.routes.ts b/src/app/appmarket/appmanagement/app-management.routes.ts index 3a38b22f99228ccae091e7ab302d6140d07ace95..8ef778a898b373941e2f6909a1dd2be0100dd534 100644 --- a/src/app/appmarket/appmanagement/app-management.routes.ts +++ b/src/app/appmarket/appmanagement/app-management.routes.ts @@ -15,48 +15,48 @@ import {AppsummaryComponent} from '../bulkDeployment/appDeployment/appsummary/ap export const AppManagementRoutes: Route[] = [ { - path: 'admin/apps', + path: 'apps', component: AppManagementListComponent, canActivate: [AuthGuard, RoleGuard], data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER']} }, { - path: 'admin/apps/create', + path: 'apps/create', component: AppCreateWizardComponent, canActivate: [AuthGuard, RoleGuard], data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER'], mode: ComponentMode.CREATE} }, { - path: 'admin/apps/create/version/:name', + path: 'apps/create/version/:name', component: AppVersionCreateWizardComponent, canActivate: [AuthGuard, RoleGuard], data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER'], mode: ComponentMode.CREATE} }, { - path: 'admin/apps/edit/:id', + path: 'apps/edit/:id', component: AppCreateWizardComponent, canActivate: [AuthGuard, RoleGuard], data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER'], mode: ComponentMode.EDIT} }, { - path: 'admin/apps/edit/version/:id', + path: 'apps/edit/version/:id', component: AppVersionCreateWizardComponent, canActivate: [AuthGuard, RoleGuard], data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER'], mode: ComponentMode.EDIT} }, { - path: 'admin/apps/view/:id', + path: 'apps/view/:id', component: AppPreviewComponent, canActivate: [AuthGuard, RoleGuard], data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER']} }, { - path: 'admin/apps/bulks', + path: 'apps/bulks', component: BulkAppListComponent, canActivate: [AuthGuard, RoleGuard], - data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']} + data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']} }, - { path: 'admin/apps/bulks/new', + { path: 'apps/bulks/new', component: AppnavigatorComponent, children: [ {path: '', redirectTo: 'select', pathMatch: 'full'}, @@ -65,9 +65,9 @@ export const AppManagementRoutes: Route[] = [ {path: 'summary', component: AppsummaryComponent} ]}, { - path: 'admin/apps/bulks/:id', + path: 'apps/bulks/:id', component: BulkViewComponent, canActivate: [AuthGuard, RoleGuard], - data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER' ]} + data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER' ]} }, ]; diff --git a/src/app/appmarket/appmanagement/app-version-create-wizard/app-version-create-wizard.component.ts b/src/app/appmarket/appmanagement/app-version-create-wizard/app-version-create-wizard.component.ts index b964ab330bbac1b623b4390ecba870329d4b1e95..d4f6b6c6b18513c382a8d9605ca5a20a943246a5 100644 --- a/src/app/appmarket/appmanagement/app-version-create-wizard/app-version-create-wizard.component.ts +++ b/src/app/appmarket/appmanagement/app-version-create-wizard/app-version-create-wizard.component.ts @@ -1,25 +1,25 @@ -import {Component, OnInit, ViewChild} from '@angular/core'; -import {BaseComponent} from '../../../shared/common/basecomponent/base.component'; -import {ModalComponent} from '../../../shared'; -import {ConfigWizardTemplate} from '../../../model'; -import {ConfigFileTemplate} from '../../../model/configfiletemplate'; -import {AppImagesService, AppsService} from '../../../service'; -import {ActivatedRoute, Router} from '@angular/router'; -import {ConfigTemplateService} from '../../../service/configtemplate.service'; -import {ParameterType} from '../../../model/parametertype'; -import {KubernetesTemplate} from '../../../model/kubernetes-template'; -import {TranslateService} from '@ngx-translate/core'; -import {DomSanitizer} from '@angular/platform-browser'; -import {ApplicationState} from '../../../model/application-state'; -import {KubernetesChart} from '../../../model/kuberneteschart'; -import {AppStorageVolume} from '../../../model/app-storage-volume'; -import {parseServiceStorageVolumeType, ServiceStorageVolumeType} from '../../../model/service-storage-volume'; -import {AppAccessMethod} from '../../../model/app-access-method'; -import {parseServiceAccessMethodType, ServiceAccessMethodType} from '../../../model/service-access-method'; -import {AbstractControl, ValidatorFn} from '@angular/forms'; -import {MultiSelect} from 'primeng/multiselect'; -import {MenuItem, SelectItem} from 'primeng/api'; -import {ApplicationDTO} from '../../../model/application-dto'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { BaseComponent } from '../../../shared/common/basecomponent/base.component'; +import { ModalComponent } from '../../../shared'; +import { ConfigWizardTemplate } from '../../../model'; +import { ConfigFileTemplate } from '../../../model/configfiletemplate'; +import { AppImagesService, AppsService } from '../../../service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ConfigTemplateService } from '../../../service/configtemplate.service'; +import { ParameterType } from '../../../model/parametertype'; +import { KubernetesTemplate } from '../../../model/kubernetes-template'; +import { TranslateService } from '@ngx-translate/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ApplicationState } from '../../../model/application-state'; +import { KubernetesChart } from '../../../model/kuberneteschart'; +import { AppStorageVolume } from '../../../model/app-storage-volume'; +import { parseServiceStorageVolumeType, ServiceStorageVolumeType } from '../../../model/service-storage-volume'; +import { AppAccessMethod } from '../../../model/app-access-method'; +import { parseServiceAccessMethodType, ServiceAccessMethodType } from '../../../model/service-access-method'; +import { AbstractControl, ValidatorFn } from '@angular/forms'; +import { MultiSelect } from 'primeng/multiselect'; +import { MenuItem, SelectItem } from 'primeng/api'; +import { ApplicationDTO } from '../../../model/application-dto'; import { ApplicationVersion } from '../../../model/application-version'; import * as semver from 'semver'; import { Application } from '../../../model/application'; @@ -34,7 +34,7 @@ export function noParameterTypeInControlValueValidator(): ValidatorFn { } const notValid = labels.filter(val => control.value.includes(val)).length === 0; console.log('checking: ', control.value, 'valid: ', !notValid); - return notValid ? {'noParameterTypeInControlValue': {value: control.value}} : null; + return notValid ? { 'noParameterTypeInControlValue': { value: control.value } } : null; }; } @@ -43,10 +43,10 @@ export function noParameterTypeInControlValueValidator(): ValidatorFn { templateUrl: './app-version-create-wizard.component.html', styleUrls: ['./app-version-create-wizard.component.css'] }) -export class AppVersionCreateWizardComponent extends BaseComponent implements OnInit { +export class AppVersionCreateWizardComponent extends BaseComponent implements OnInit, OnDestroy { - @ViewChild(ModalComponent, {static: true}) + @ViewChild(ModalComponent, { static: true }) public modal: ModalComponent; @ViewChild('tagsMultiSelect') @@ -68,7 +68,9 @@ export class AppVersionCreateWizardComponent extends BaseComponent implements On public logo: any[] = []; public screenshots: any[] = []; public applicationVersions: ApplicationVersion[] = []; - public selectedVersion : any ; + public selectedVersion: any; + public template: any; + public translateUpdate: any; // properties for global parameters deploy validation // in future extensions pack this into single object @@ -83,12 +85,12 @@ export class AppVersionCreateWizardComponent extends BaseComponent implements On }; constructor(public appsService: AppsService, - public route: ActivatedRoute, - public translate: TranslateService, - public dom: DomSanitizer, - public configTemplateService: ConfigTemplateService, - public router: Router, - public appImagesService: AppImagesService) { + public route: ActivatedRoute, + public translate: TranslateService, + public dom: DomSanitizer, + public configTemplateService: ConfigTemplateService, + public router: Router, + public appImagesService: AppImagesService) { super(); } @@ -97,14 +99,21 @@ export class AppVersionCreateWizardComponent extends BaseComponent implements On this.modal.setModalType('success'); this.modal.setStatusOfIcons(false); this.mode = this.getMode(this.route); - this.getParametersTypes().forEach(val => this.deployParameter.push({label: val.replace('_', ' '), value: val})); - this.steps = [ - {label: this.translate.instant('APPS_WIZARD.GENERAL_INFO_STEP')}, - {label: this.translate.instant('APPS_WIZARD.BASIC_APP_INFO_STEP')}, - {label: this.translate.instant('APPS_WIZARD.APP_DEPLOYMENT_SPEC_STEP')}, - {label: this.translate.instant('APPS_WIZARD.CONFIG_TEMPLATES_STEP')}, - {label: this.translate.instant('APPS_WIZARD.SHORT_REVIEW_STEP')} - ]; + this.getParametersTypes().forEach(val => this.deployParameter.push({ label: val.replace('_', ' '), value: val })); + //trick to avoid using this.translate.onChange cuz its not working on current angular without changing language + this.translateUpdate = setInterval(() => { + if(this.translate.instant('APPS_WIZARD.GENERAL_INFO_STEP') !== null) { + this.steps = [ + { label: this.translate.instant('APPS_WIZARD.GENERAL_INFO_STEP') }, + { label: this.translate.instant('APPS_WIZARD.BASIC_APP_INFO_STEP') }, + { label: this.translate.instant('APPS_WIZARD.APP_DEPLOYMENT_SPEC_STEP') }, + { label: this.translate.instant('APPS_WIZARD.CONFIG_TEMPLATES_STEP') }, + { label: this.translate.instant('APPS_WIZARD.SHORT_REVIEW_STEP') } + ]; + this.stopTranslationUpdate(); + } + }, 200); + this.route.params.subscribe(params => { const appName = params['name'] const appId = params['id'] @@ -137,6 +146,16 @@ export class AppVersionCreateWizardComponent extends BaseComponent implements On }); } + private stopTranslationUpdate() { + clearInterval(this.translateUpdate); + this.translateUpdate = null; + } + + ngOnDestroy(): void { + clearInterval(this.translateUpdate); + this.translateUpdate = null; + } + public appVersionCompare(a: ApplicationVersion, b: ApplicationVersion): number { // defaults version that cannot be parsed to `0.0.0` return semver.compare(semver.coerce(b.version) || '0.0.0', semver.coerce(a.version) || '0.0.0') @@ -238,10 +257,13 @@ export class AppVersionCreateWizardComponent extends BaseComponent implements On } public setConfigTemplate(event): void { - if (!this.applicationDTO.application.configWizardTemplate) { - this.applicationDTO.application.configWizardTemplate = new ConfigWizardTemplate(); + if (event.type === "addComponent" || event.type === "saveComponent") { + console.log(event); + this.template = event.form; + this.applicationDTO.application.configWizardTemplate.template = null; + this.applicationDTO.application.configWizardTemplate.template = Object.assign({}, this.template); + console.log('Wizard saved', this.applicationDTO.application.configWizardTemplate.template) } - this.applicationDTO.application.configWizardTemplate.template = event.form; } public setUpdateConfigTemplate(event): void { @@ -494,7 +516,7 @@ export class AppVersionCreateWizardComponent extends BaseComponent implements On } private convertToProperImageFile(file: any) { - const result: any = new File([file], 'uploaded file', {type: file.type}); + const result: any = new File([file], 'uploaded file', { type: file.type }); result.objectURL = this.dom.bypassSecurityTrustUrl(URL.createObjectURL(result)); return result; } @@ -545,7 +567,7 @@ export class AppVersionCreateWizardComponent extends BaseComponent implements On } public onVersionSelect(event: any) { - console.log("Slected version ",event ) + console.log("Slected version ", event) this.appsService.getApplication(event.value.appVersionId).subscribe(data => { console.log(data); this.applicationDTO.application = data; diff --git a/src/app/appmarket/appmanagement/json-edit/json-edit.component.ts b/src/app/appmarket/appmanagement/json-edit/json-edit.component.ts index e92e8eaf211d700684c6b23b62d3b405d147f609..0cbc671cfff9fc0fb5c8262285c57b1f0ae9b77a 100644 --- a/src/app/appmarket/appmanagement/json-edit/json-edit.component.ts +++ b/src/app/appmarket/appmanagement/json-edit/json-edit.component.ts @@ -28,7 +28,6 @@ export class JsonEditComponent { @Input() set object(obj: any) { - console.log('setting value') const contentString = JSON.stringify(obj, null, 2); if (this.content && contentString !== this.content.value) { this.content.setValue(contentString) diff --git a/src/app/appmarket/appmarket.routes.ts b/src/app/appmarket/appmarket.routes.ts index f1c40acf24a4f60353bf6c957a51de17a704ecd1..2d0196a195449dad55a32f1f6ed4a791b6a0b657 100644 --- a/src/app/appmarket/appmarket.routes.ts +++ b/src/app/appmarket/appmarket.routes.ts @@ -13,6 +13,8 @@ import {ConfigurationRoutes} from './admin/configuration/configuration.routes'; import {MonitorRoutes} from './admin/monitor/monitor.routes'; import {AppManagementRoutes} from './appmanagement/app-management.routes'; import {LanguageManagementRoutes} from './admin/languagemanagement/languagemanagement.routes'; +import { AdminLeftMenuComponent } from '../shared/admin-left-menu/admin-left-menu.component'; +import { AdminDashboardComponent } from '../shared/admin-dashboard/admin-dashboard.component'; export const AppMarketRoutes: Route[] = [ { @@ -23,15 +25,32 @@ export const AppMarketRoutes: Route[] = [ children: [ ...AppListRoutes, ...AppInstanceRoutes, + { path: 'apps/:id', component: AppDetailsComponent }, + + ] + }, + { + path: 'admin', + component: AdminLeftMenuComponent, + canActivate: [AuthGuard], + canActivateChild: [AuthGuard], + children: [ + { + path: '', + redirectTo: '/admin/dashboard', + pathMatch: 'full' + }, + { + path: 'dashboard', + component: AdminDashboardComponent + }, ...DomainsRoutes, ...UsersRoutes, ...ClustersRoutes, - ...ConfigurationRoutes, - ...MonitorRoutes, - ...AppManagementRoutes, - ...LanguageManagementRoutes, - { path: 'apps/:id', component: AppDetailsComponent }, - + ...ConfigurationRoutes, + ...MonitorRoutes, + ...AppManagementRoutes, + ...LanguageManagementRoutes, ] } ]; diff --git a/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.css b/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.css new file mode 100644 index 0000000000000000000000000000000000000000..f2c387a9db7ec1e2c9aeefc41a92b4d6336c9fdb --- /dev/null +++ b/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.css @@ -0,0 +1,38 @@ +:host ::ng-deep input[type=file]{ + display:none; +} +:host ::ng-deep .p-button{ + width: unset; + margin-right: 5px; + background: var(--primary-button-color); + color: var(--button-text-color); +} +:host ::ng-deep .p-button:hover{ + background: var(--primary-button-hover); + border:none; +} +:host ::ng-deep .p-button-label{ + font-weight: normal; +} +:host ::ng-deep .p-fileupload .p-fileupload-buttonbar{ + border: none; + background: transparent; + margin-bottom: 10px; + padding: 0; +} +:host ::ng-deep .p-fileupload .p-fileupload-content{ + border: none; + padding: 0; + border-radius: 3px; + +} +:host ::ng-deep .p-fileupload-content .p-progressbar{ + display: none; +} +textarea{ + border-color: #ccc; +} +:host ::ng-deep .p-inputtext:enabled:focus{ + box-shadow: none; + border-color: var(--l-text-color); +} diff --git a/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.html b/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.html index 328d1af22da2d36c22a9624a6d9c57e56fa4e5d3..1385e9861fe9bd8022d8459450797eee0ea952f1 100644 --- a/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.html +++ b/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.html @@ -17,7 +17,7 @@ <div style="margin-top: 1.5rem; margin-bottom: 1rem"> <p>{{'BULK.APP.UPLOAD_TEXT' | translate}}</p> </div> - <textarea pInputTextarea [(ngModel)]="csvText" rows="10" cols="127" (keyup)="changeDetector = true"></textarea> + <textarea pInputTextarea [(ngModel)]="csvText" rows="10" cols="127" [autoResize]="true" (keyup)="changeDetector = true"></textarea> </div> <div *ngIf="errorMessage !== ''" style="margin-top: 1rem; display: flex; justify-content: start; color: indianred"> diff --git a/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.ts b/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.ts index d718907e2389cbb9192bacddda36ad28e8733c32..015af6e6d18fc46553d4898a6e79e531d0f2c34e 100644 --- a/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.ts +++ b/src/app/appmarket/bulkDeployment/appDeployment/appupload/appupload.component.ts @@ -6,7 +6,7 @@ import {AppImagesService} from '../../../../service'; @Component({ selector: 'app-appupload', templateUrl: './appupload.component.html', - styleUrls: [] + styleUrls: ['./appupload.component.css'] }) export class AppuploadComponent implements OnInit { diff --git a/src/app/appmarket/bulkDeployment/appdeployment.service.ts b/src/app/appmarket/bulkDeployment/appdeployment.service.ts index a44c0f9cc361bc8f12b247c5f9a1534fdbfe11f0..f660f0531c4ff76f1c92041d070d6205ced673fe 100644 --- a/src/app/appmarket/bulkDeployment/appdeployment.service.ts +++ b/src/app/appmarket/bulkDeployment/appdeployment.service.ts @@ -79,7 +79,7 @@ export class AppdeploymentService { } public getBulksDomainDeploymentsOwner(): Observable<BulkDeployment[]> { - return this.http.get<BulkDeployment[]>(this.getUrl() + 'domains/vl'); + return this.http.get<BulkDeployment[]>(this.getUrl() + 'domains/group'); } public getBulksAppDeployments(showDeleted: boolean = false): Observable<BulkDeployment[]> { @@ -89,7 +89,7 @@ export class AppdeploymentService { } public getBulksAppDeploymentsOwner(): Observable<BulkDeployment[]> { - return this.http.get<BulkDeployment[]>(this.getUrl() + 'apps/vl'); + return this.http.get<BulkDeployment[]>(this.getUrl() + 'apps/group'); } public getBulkDeployment(id: number): Observable<BulkDeployment> { diff --git a/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.ts b/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.ts index 4a773179c64068601af012a9fe6506a818eecfbe..7b89c6899bce5d8dbe07c971fd8601093a27d2fc 100644 --- a/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.ts +++ b/src/app/appmarket/bulkDeployment/bulk-app-list/bulk-app-list.component.ts @@ -24,7 +24,7 @@ export class BulkAppListComponent implements OnInit { } onRefresh(showDeleted = false) : void { - if (this.authService.getRoles().find(value => value === 'ROLE_VL_MANAGER') !== undefined) { + if (this.authService.getRoles().find(value => value === 'ROLE_GROUP_MANAGER') !== undefined) { this.deployService.getBulksAppDeploymentsOwner().subscribe(data => { data = data.sort((a, b) => new Date(b.creationDate).getTime() - new Date(a.creationDate).getTime()) this.bulks = data diff --git a/src/app/appmarket/bulkDeployment/bulk-domain-list/bulk-domain-list.component.ts b/src/app/appmarket/bulkDeployment/bulk-domain-list/bulk-domain-list.component.ts index 696e86a9f8ccc30a4615d7e13af798cd423e4651..cfd2fe6751732bde31b6ef9e5553bb53b20e1a5f 100644 --- a/src/app/appmarket/bulkDeployment/bulk-domain-list/bulk-domain-list.component.ts +++ b/src/app/appmarket/bulkDeployment/bulk-domain-list/bulk-domain-list.component.ts @@ -20,7 +20,7 @@ export class BulkDomainListComponent implements OnInit { } ngOnInit(): void { - if (this.authService.getRoles().find(value => value === 'ROLE_VL_MANAGER') !== undefined) { + if (this.authService.getRoles().find(value => value === 'ROLE_GROUP_MANAGER') !== undefined) { this.deployService.getBulksDomainDeploymentsOwner().subscribe(data => { data = data.sort((a, b) => new Date(b.creationDate).getTime() - new Date(a.creationDate).getTime()) this.bulks = data diff --git a/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.css b/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.css index 90bb2e521b576a5d1a687e77ce002fc4b7cded50..81faf4286ba49fea43ef151ccf0535977a655763 100644 --- a/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.css +++ b/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.css @@ -8,3 +8,4 @@ margin-left: 5px; margin-right: 5px; } + diff --git a/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.html b/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.html index 06d42a13307598d26cd863a2d5d086d6b4284510..77fa7865ee58f68e0d6d01784560d9c485f3aa1a 100644 --- a/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.html +++ b/src/app/appmarket/bulkDeployment/bulk-list/bulk-list.component.html @@ -1,156 +1,272 @@ -<div class="col-sm-12 col-sm-offset-1 col-sm 10 col-md-offset-1 col-md-10"> - <h3>{{header | translate}}</h3> - <div class="" style="display: flex; justify-content: space-between; margin-top: 10px;"> - <div *ngIf="mode=== bulkTypeDomain"> +<div class=""> + <div style="display:flex; "> + <div style="margin-right:20px"> + <span class="p-input-icon-right" style="width: 100%"> + <i class="pi pi-search" style="font-size: 13px; top: 16px; margin-right: 5px;"></i> + <input pInputText class="flex form-control" name="search" id="search" placeholder="Search" type="text" + style="height: 34px" [(ngModel)]="searchValue"> + </span> + </div> + <div *ngIf="mode=== bulkTypeDomain" style="margin-right: 10px"> <button class="btn btn-primary" [routerLink]="['/admin/domains/bulks/new']">New deployment</button> </div> - <div *ngIf="mode=== bulkTypeApp"> + <div *ngIf="mode=== bulkTypeApp" style="margin-right: 10px"> <button class="btn btn-primary" [routerLink]="['/admin/apps/bulks/new']">New deployment</button> </div> - - <div class="flex"> - <div *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']" class="flex align-items-center mr-6"> - - <p-button - type="button" - class="mr-2" - (onClick)="sidebarVisible4 = true" + <div *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']" class="flex align-items-center mr-6"> + <p-button + type="button" + class="" + (onClick)="sidebarVisible4 = true" label="Deployments Queue" severity="secondary" - ></p-button> - <p-sidebar [(visible)]="sidebarVisible4" position="left" styleClass="w-30rem"> - <h3>Deployments Queue</h3> - <div class="flex flex-column"> - <div class="flex grid"> - <label class="col-10" for="jobInQueue">Jobs in queue</label> - <span class="col-2 " id="jobInQueue">{{queueDetails?.jobInQueue}}</span> - </div> - <div class="flex grid"> - <label class="col-10" for="jobInProcess">In progress</label> - <span class="col-2 " id="jobInProcess">{{queueDetails?.jobInProcess}}</span> - </div> - <div class="flex grid"> - <label class="col-10" for="jobInProcessId">Current proccessing bulk id</label> - <span class="col-2" id="jobInProcessId">{{queueDetails?.jobInProcessId}}</span> - </div> + ></p-button> + <p-sidebar [(visible)]="sidebarVisible4" position="right" styleClass="w-30rem"> + <h3>Deployments Queue</h3> + <div class="flex flex-column"> + <div class="flex grid"> + <label class="col-10" for="jobInQueue">Jobs in queue</label> + <span class="col-2 " id="jobInQueue">{{queueDetails?.jobInQueue}}</span> </div> - </p-sidebar> - - </div> - <div *roles="['ROLE_SYSTEM_ADMIN']" class="flex align-items-center mr-6 pt-2"> - <label *ngIf="mode=== bulkTypeApp" class="mr-2" for="showDeleted">Show all</label> - <p-inputSwitch *ngIf="mode=== bulkTypeApp" id="showDeleted" (onChange)="refreshBulks()" [(ngModel)]="showDeleted" ngDefaultControl/> - </div> - <div class="flex align-items-center mr-1">{{ 'BULK.LIST.PER_PAGE' | translate }}:</div> - <span id="selectionItems" class="dropdown" - style="vertical-align: middle; display: inline-block; margin-right: 1rem;"> - <button class="dropdown-toggle btn" data-toggle="dropdown" data-close-others="true"> - {{maxItemsOnPage}} - </button> - <ul class="dropdown-menu"> - <li *ngFor="let item of itemsPerPage" [ngClass]="{'active': maxItemsOnPage == item}"> - <a (click)="setItems(item)"> - <span>{{item.toString()}}</span> - </a> - </li> - </ul> - </span> - <input pInputText class="flex" name="search" id="search" placeholder="Search" type="text" - style="height: 34px" [(ngModel)]="searchValue"> + <div class="flex grid"> + <label class="col-10" for="jobInProcess">In progress</label> + <span class="col-2 " id="jobInProcess">{{queueDetails?.jobInProcess}}</span> + </div> + <div class="flex grid"> + <label class="col-10" for="jobInProcessId">Current proccessing bulk id</label> + <span class="col-2" id="jobInProcessId">{{queueDetails?.jobInProcessId}}</span> + </div> + </div> + </p-sidebar> + + </div> + <div *roles="['ROLE_SYSTEM_ADMIN']" class="flex align-items-center mr-6 pt-2"> + <label *ngIf="mode=== bulkTypeApp" class="mr-2" for="showDeleted">Show all</label> + <p-inputSwitch *ngIf="mode=== bulkTypeApp" id="showDeleted" (onChange)="refreshBulks()" [(ngModel)]="showDeleted" ngDefaultControl/> </div> </div> - <table *ngIf="mode === 'DOMAIN'" class="table table-hover table-condensed" style="margin-top: 3rem" - aria-describedby="Bulk deployment table" sortable-table (sorted)="onSort($event)"> - <thead> - <tr> - <th scope="col" class="column-sortable" sortable-column="id">{{'BULK.LIST.ID' | translate}}</th> - <th scope="col" class="column-sortable" sortable-column="creator">{{'BULK.LIST.CREATOR' | translate}}</th> - <th scope="col" class="column-sortable" sortable-column="date">{{'BULK.LIST.CREATION_DATE' | translate}}</th> - <th scope="col" class="column-sortable" sortable-column="state">{{'BULK.LIST.STATE' | translate}}</th> - <th scope="col" ></th> - </tr> - </thead> + <h4 class="header">{{header | translate}}</h4> +<!-- <div class="" style="display: flex; justify-content: space-between; margin-top: 10px;">--> +<!-- <div class="flex">--> +<!-- <div class="flex align-items-center mr-1">{{ 'BULK.LIST.PER_PAGE' | translate }}:</div>--> +<!-- <span id="selectionItems" class="dropdown"--> +<!-- style="vertical-align: middle; display: inline-block; margin-right: 1rem;">--> +<!-- <button class="dropdown-toggle btn" data-toggle="dropdown" data-close-others="true">--> +<!-- {{maxItemsOnPage}}--> +<!-- </button>--> +<!-- <ul class="dropdown-menu">--> +<!-- <li *ngFor="let item of itemsPerPage" [ngClass]="{'active': maxItemsOnPage == item}">--> +<!-- <a (click)="setItems(item)">--> +<!-- <span>{{item.toString()}}</span>--> +<!-- </a>--> +<!-- </li>--> +<!-- </ul>--> +<!-- </span>--> - <tbody> - <ng-template ngFor let-bulk [ngForOf]="bulks" let-i="index"> - <tr class="table-row"> - <td style="width: 10%">{{bulk?.id}}</td> - <td style="width: 25%">{{bulk?.creator.username}}</td> - <td style="width: 25%">{{bulk?.creationDate | date: 'dd-MM-yyyy HH:mm'}}</td> - <td style="width: 15%">{{'BULK.STATE.' + bulk?.state | translate}}</td> - <td style="width: 20%" class="text-right"> - <span class="dropdown"> - <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" - data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> - </a> - <ul class="dropdown-menu pull-right-drop"> - <li *ngIf="mode === bulkTypeDomain"> - <a [routerLink]="['/admin/domains/bulks/', bulk?.id]">{{ 'BULK.LIST.DETAILS' | translate }}</a> - </li> - </ul> - </span> - </td> - </tr> - </ng-template> - </tbody> - </table> - <table *ngIf="mode === 'APPLICATION'" class="table table-hover table-condensed" style="margin-top: 3rem" - aria-describedby="Bulk deployment table" sortable-table (sorted)="onSort($event)"> - <thead> - <tr> - <th scope="col" class="column-sortable" sortable-column="id">{{'BULK.LIST.ID' | translate}}</th> - <th scope="col" class="column-sortable" sortable-column="creator">{{'BULK.LIST.CREATOR' | translate}}</th> - <th scope="col" class="column-sortable" sortable-column="app_name">{{'BULK.LIST.APP_NAME' | translate}}</th> - <th scope="col" class="column-sortable" - sortable-column="instance_no">{{'BULK.LIST.INSTANCE_NO' | translate}}</th> - <th scope="col" class="column-sortable" sortable-column="date" - sort-direction="desc">{{'BULK.LIST.CREATION_DATE' | translate}}</th> - <th scope="col" class="column-sortable" sortable-column="state">{{'BULK.LIST.STATE' | translate}}</th> - <th scope="col"></th> - </tr> - </thead> +<!-- </div>--> +<!-- </div>--> + <div *ngIf="mode === 'DOMAIN'" class="background-section"> + <p-table *ngIf="mode === 'DOMAIN'" [value]="bulks" class="p-datatable-hover p-datatable-sm" [responsive]="true" (onSort)="onSort($event)"> + <ng-template pTemplate="header"> + <tr> + <th pSortableColumn="id">{{'BULK.LIST.ID' | translate}} + <p-sortIcon field="id"></p-sortIcon> + </th> + <th pSortableColumn="creator">{{'BULK.LIST.CREATOR' | translate}} + <p-sortIcon field="creator"></p-sortIcon> + </th> + <th pSortableColumn="date">{{'BULK.LIST.CREATION_DATE' | translate}} + <p-sortIcon field="date"></p-sortIcon> + </th> + <th pSortableColumn="state">{{'BULK.LIST.STATE' | translate}} + <p-sortIcon field="state"></p-sortIcon> + </th> + <th></th> + </tr> + </ng-template> - <tbody> - <ng-template ngFor let-bulk - [ngForOf]="bulks | searchBulk: searchValue: true | paginate: {itemsPerPage: maxItemsOnPage, currentPage: p}" - let-i="index"> - <tr class="table-row"> - <td style="width: 5%">{{bulk?.id}}</td> - <td style="width: 15%">{{bulk?.creator.username}}</td> - <td style="width: 20%">{{getApplicationName(bulk?.details)}}</td> - <td style="width: 20%">{{getInstancesNumber(bulk?.details)}}</td> - <td style="width: 15%">{{bulk?.creationDate | date: 'dd-MM-yyyy HH:mm'}}</td> - <td style="width: 20%">{{'BULK.STATE.' + bulk?.state | translate}}</td> - <td style="width: 5%" class="text-right"> - <span class="dropdown"> - <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" - data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> - </a> - <ul class="dropdown-menu pull-right-drop"> - <li *ngIf="mode === bulkTypeApp"> - <a [routerLink]="['/admin/apps/bulks/', bulk?.id]">{{ 'BULK.LIST.DETAILS' | translate }}</a> - </li> - <li *ngIf="mode === bulkTypeApp && bulk?.state !== 'REMOVED'"> - <a (click)="getAppBulkDetails(bulk?.id)"> {{"BULK.APP.DOWNLOAD_CSV" | translate}}</a> - </li> - <li *ngIf="mode === bulkTypeApp && !(bulk?.state === 'REMOVED' || bulk?.deleted )"> - <a (click)="modal.show(); removeBulkId=bulk?.id">{{ 'BULK.LIST.REMOVE' | translate }}</a> - </li> - </ul> - </span> - </td> - </tr> - </ng-template> - </tbody> - </table> - <pagination-controls class="text-right" (pageChange)="p = $event" - previousLabel="{{ 'PAGINATION.PREVIOUS' | translate }}" - nextLabel="{{ 'PAGINATION.NEXT' | translate }}" - screenReaderPaginationLabel="{{ 'PAGINATION.SCREEN_READER.PAGINATION' | translate }}" - screenReaderPageLabel="{{ 'PAGINATION.SCREEN_READER.PAGE' | translate }}" - screenReaderCurrentLabel="{{ 'PAGINATION.SCREEN_READER.CURRENT' | translate }}"></pagination-controls> + <ng-template pTemplate="body" let-bulk> + <tr class="table-row"> + <td>{{bulk?.id}}</td> + <td>{{bulk?.creator.username}}</td> + <td>{{bulk?.creationDate | date: 'dd-MM-yyyy HH:mm'}}</td> + <td>{{'BULK.STATE.' + bulk?.state | translate}}</td> + <td class="text-right"> + <span class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> + <em class="fas fa-cog icon-black icon-bigger"></em> + </a> + <ul class="dropdown-menu pull-right-drop"> + <li *ngIf="mode === bulkTypeDomain"> + <a [routerLink]="['/admin/domains/bulks/', bulk?.id]">{{ 'BULK.LIST.DETAILS' | translate }}</a> + </li> + </ul> + </span> + </td> + </tr> + </ng-template> + </p-table> + + +<!-- <table *ngIf="mode === 'DOMAIN'" class="table table-hover table-condensed" style="margin-top: 3rem"--> +<!-- aria-describedby="Bulk deployment table" sortable-table (sorted)="onSort($event)">--> +<!-- <thead>--> +<!-- <tr>--> +<!-- <th scope="col" class="column-sortable" sortable-column="id">{{'BULK.LIST.ID' | translate}}</th>--> +<!-- <th scope="col" class="column-sortable" sortable-column="creator">{{'BULK.LIST.CREATOR' | translate}}</th>--> +<!-- <th scope="col" class="column-sortable" sortable-column="date">{{'BULK.LIST.CREATION_DATE' | translate}}</th>--> +<!-- <th scope="col" class="column-sortable" sortable-column="state">{{'BULK.LIST.STATE' | translate}}</th>--> +<!-- <th scope="col" ></th>--> +<!-- </tr>--> +<!-- </thead>--> + +<!-- <tbody>--> +<!-- <ng-template ngFor let-bulk [ngForOf]="bulks" let-i="index">--> +<!-- <tr class="table-row">--> +<!-- <td style="width: 10%">{{bulk?.id}}</td>--> +<!-- <td style="width: 25%">{{bulk?.creator.username}}</td>--> +<!-- <td style="width: 25%">{{bulk?.creationDate | date: 'dd-MM-yyyy HH:mm'}}</td>--> +<!-- <td style="width: 15%">{{'BULK.STATE.' + bulk?.state | translate}}</td>--> +<!-- <td style="width: 20%" class="text-right">--> +<!-- <span class="dropdown">--> +<!-- <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true"--> +<!-- data-toggle="dropdown" href="#" role="button">--> +<!-- <em class="fas fa-cog icon-black icon-bigger"></em>--> +<!-- </a>--> +<!-- <ul class="dropdown-menu pull-right-drop">--> +<!-- <li *ngIf="mode === bulkTypeDomain">--> +<!-- <a [routerLink]="['/admin/domains/bulks/', bulk?.id]">{{ 'BULK.LIST.DETAILS' | translate }}</a>--> +<!-- </li>--> +<!-- </ul>--> +<!-- </span>--> +<!-- </td>--> +<!-- </tr>--> +<!-- </ng-template>--> +<!-- </tbody>--> +<!-- </table>--> + </div> + + <div *ngIf="mode === 'APPLICATION'" class="background-section"> + <p-table *ngIf="mode === 'APPLICATION'" [value]="bulks | searchBulk: searchValue: true " + class="p-datatable-hover p-datatable-sm" + [responsive]="true" + [paginator]="true" + [rows]="maxItemsOnPage" + [rowsPerPageOptions]="[15, 20, 25, 30, 50]" + (onSort)="onSort($event)"> + <ng-template pTemplate="header"> + <tr> + <th pSortableColumn="id">{{'BULK.LIST.ID' | translate}} + <p-sortIcon field="id"></p-sortIcon> + </th> + <th pSortableColumn="creator">{{'BULK.LIST.CREATOR' | translate}} + <p-sortIcon field="creator"></p-sortIcon> + </th> + <th pSortableColumn="app_name">{{'BULK.LIST.APP_NAME' | translate}} + <p-sortIcon field="app_name"></p-sortIcon> + </th> + <th pSortableColumn="instance_no">{{'BULK.LIST.INSTANCE_NO' | translate}} + <p-sortIcon field="instance_no"></p-sortIcon> + </th> + <th pSortableColumn="date" pSortOrder="-1">{{'BULK.LIST.CREATION_DATE' | translate}} + <p-sortIcon field="date"></p-sortIcon> + </th> + <th pSortableColumn="state">{{'BULK.LIST.STATE' | translate}} + <p-sortIcon field="state"></p-sortIcon> + </th> + <th></th> + </tr> + </ng-template> + + <ng-template pTemplate="body" let-bulk> + <tr class="table-row"> + <td>{{bulk?.id}}</td> + <td>{{bulk?.creator.username}}</td> + <td>{{getApplicationName(bulk?.details)}}</td> + <td>{{getInstancesNumber(bulk?.details)}}</td> + <td>{{bulk?.creationDate | date: 'dd-MM-yyyy HH:mm'}}</td> + <td>{{'BULK.STATE.' + bulk?.state | translate}}</td> + <td class="text-right"> + <span class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> + <em class="fas fa-cog icon-black icon-bigger"></em> + </a> + <ul class="dropdown-menu pull-right-drop"> + <li *ngIf="mode === bulkTypeApp"> + <a [routerLink]="['/admin/apps/bulks/', bulk?.id]">{{ 'BULK.LIST.DETAILS' | translate }}</a> + </li> + <li *ngIf="mode === bulkTypeApp && bulk?.state !== 'REMOVED'"> + <a (click)="getAppBulkDetails(bulk?.id)"> {{"BULK.APP.DOWNLOAD_CSV" | translate}}</a> + </li> + <li *ngIf="mode === bulkTypeApp && !(bulk?.state === 'REMOVED' || bulk?.deleted )"> + <a (click)="modal.show(); removeBulkId=bulk?.id">{{ 'BULK.LIST.REMOVE' | translate }}</a> + </li> + </ul> + </span> + </td> + </tr> + </ng-template> + </p-table> + +<!-- <table *ngIf="mode === 'APPLICATION'" class="table table-hover table-condensed" style="margin-top: 3rem"--> +<!-- aria-describedby="Bulk deployment table" sortable-table (sorted)="onSort($event)">--> +<!-- <thead>--> +<!-- <tr>--> +<!-- <th scope="col" class="column-sortable" sortable-column="id">{{'BULK.LIST.ID' | translate}}</th>--> +<!-- <th scope="col" class="column-sortable" sortable-column="creator">{{'BULK.LIST.CREATOR' | translate}}</th>--> +<!-- <th scope="col" class="column-sortable" sortable-column="app_name">{{'BULK.LIST.APP_NAME' | translate}}</th>--> +<!-- <th scope="col" class="column-sortable"--> +<!-- sortable-column="instance_no">{{'BULK.LIST.INSTANCE_NO' | translate}}</th>--> +<!-- <th scope="col" class="column-sortable" sortable-column="date"--> +<!-- sort-direction="desc">{{'BULK.LIST.CREATION_DATE' | translate}}</th>--> +<!-- <th scope="col" class="column-sortable" sortable-column="state">{{'BULK.LIST.STATE' | translate}}</th>--> +<!-- <th scope="col"></th>--> +<!-- </tr>--> +<!-- </thead>--> + +<!-- <tbody>--> +<!-- <ng-template ngFor let-bulk--> +<!-- [ngForOf]="bulks | searchBulk: searchValue: true | paginate: {itemsPerPage: maxItemsOnPage, currentPage: p}"--> +<!-- let-i="index">--> +<!-- <tr class="table-row">--> +<!-- <td style="width: 5%">{{bulk?.id}}</td>--> +<!-- <td style="width: 15%">{{bulk?.creator.username}}</td>--> +<!-- <td style="width: 20%">{{getApplicationName(bulk?.details)}}</td>--> +<!-- <td style="width: 20%">{{getInstancesNumber(bulk?.details)}}</td>--> +<!-- <td style="width: 15%">{{bulk?.creationDate | date: 'dd-MM-yyyy HH:mm'}}</td>--> +<!-- <td style="width: 20%">{{'BULK.STATE.' + bulk?.state | translate}}</td>--> +<!-- <td style="width: 5%" class="text-right">--> +<!-- <span class="dropdown">--> +<!-- <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true"--> +<!-- data-toggle="dropdown" href="#" role="button">--> +<!-- <em class="fas fa-cog icon-black icon-bigger"></em>--> +<!-- </a>--> +<!-- <ul class="dropdown-menu pull-right-drop">--> +<!-- <li *ngIf="mode === bulkTypeApp">--> +<!-- <a [routerLink]="['/admin/apps/bulks/', bulk?.id]">{{ 'BULK.LIST.DETAILS' | translate }}</a>--> +<!-- </li>--> +<!-- <li *ngIf="mode === bulkTypeApp && bulk?.state !== 'REMOVED'">--> +<!-- <a (click)="getAppBulkDetails(bulk?.id)"> {{"BULK.APP.DOWNLOAD_CSV" | translate}}</a>--> +<!-- </li>--> +<!-- <li *ngIf="mode === bulkTypeApp && !(bulk?.state === 'REMOVED' || bulk?.deleted )">--> +<!-- <a (click)="modal.show(); removeBulkId=bulk?.id">{{ 'BULK.LIST.REMOVE' | translate }}</a>--> +<!-- </li>--> +<!-- </ul>--> +<!-- </span>--> +<!-- </td>--> +<!-- </tr>--> +<!-- </ng-template>--> +<!-- </tbody>--> +<!-- </table>--> + + </div> + +<!-- <pagination-controls class="text-right" (pageChange)="p = $event"--> +<!-- previousLabel="{{ 'PAGINATION.PREVIOUS' | translate }}"--> +<!-- nextLabel="{{ 'PAGINATION.NEXT' | translate }}"--> +<!-- screenReaderPaginationLabel="{{ 'PAGINATION.SCREEN_READER.PAGINATION' | translate }}"--> +<!-- screenReaderPageLabel="{{ 'PAGINATION.SCREEN_READER.PAGE' | translate }}"--> +<!-- screenReaderCurrentLabel="{{ 'PAGINATION.SCREEN_READER.CURRENT' | translate }}"></pagination-controls>--> </div> diff --git a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.html b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.html index 9c870590051db4025f7b3d0dffbaeee499a71785..eabf5c0f2d06498168af5f2f5d6fe8efc599a9d5 100644 --- a/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.html +++ b/src/app/appmarket/bulkDeployment/bulk-view/bulk-view.component.html @@ -1,285 +1,288 @@ -<div class="col-sm-12 col-sm-offset-1 col-sm-10 col-md-offset-1 col-md-10"> - <div *ngIf="bulk && bulkType === 'DOMAIN' "> - <h3>{{'BULK.DOMAIN.HEADER_VIEW' | translate}}</h3> - <div class="" style="padding-bottom: 5rem; margin-top: 3rem"> - <label for="id" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.ID' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="id" name="id" [disabled]="true" - [(ngModel)]="bulk.id" #name="ngModel"> +<div class=""> + <div class="background-section"> + <div *ngIf="bulk && bulkType === 'DOMAIN' "> + <h3>{{'BULK.DOMAIN.HEADER_VIEW' | translate}}</h3> + <div class="" style="padding-bottom: 5rem; margin-top: 3rem"> + <label for="id" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.ID' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="id" name="id" [disabled]="true" + [(ngModel)]="bulk.id" #name="ngModel"> + </div> </div> - </div> - <div class="" style="padding-bottom: 5rem"> - <label for="creator" - class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATOR' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="creator" name="creator" [disabled]="true" - placeholder="{{bulk.creator.username}} ({{bulk.creator.firstname}} {{bulk.creator.lastname}})"> + <div class="" style="padding-bottom: 5rem"> + <label for="creator" + class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATOR' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="creator" name="creator" [disabled]="true" + placeholder="{{bulk.creator.username}} ({{bulk.creator.firstname}} {{bulk.creator.lastname}})"> + </div> </div> - </div> - <div class="" style="padding-bottom: 5rem"> - <label for="date" - class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATION_DATE' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="date" name="date" [disabled]="true" - placeholder="{{bulk.creationDate | date: 'dd-MM-yyyy HH:mm'}}"> + <div class="" style="padding-bottom: 5rem"> + <label for="date" + class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATION_DATE' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="date" name="date" [disabled]="true" + placeholder="{{bulk.creationDate | date: 'dd-MM-yyyy HH:mm'}}"> + </div> </div> - </div> - <div class="form-group" style="padding-bottom: 5rem"> - <label for="state" - class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.STATE' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="state" name="state" [disabled]="true" - placeholder="{{'BULK.STATE.' + bulk.state | translate}}"> + <div class="form-group" style="padding-bottom: 5rem"> + <label for="state" + class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.STATE' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="state" name="state" [disabled]="true" + placeholder="{{'BULK.STATE.' + bulk.state | translate}}"> + </div> </div> - </div> - <div class="panel panel-default" style="margin-top: 3rem"> - <div class="panel-heading"> + <div class="panel panel-default" style="margin-top: 3rem"> + <div class="panel-heading"> - <div style="display: flex; justify-content: start; align-items: center"> - <div> - {{ 'BULK.DOMAIN.DEPLOYMENTS' | translate }} + <div style="display: flex; justify-content: start; align-items: center"> + <div> + {{ 'BULK.DOMAIN.DEPLOYMENTS' | translate }} + </div> </div> </div> - </div> - <div class="panel-body"> - <table class="table table-hover table-condensed" aria-describedby="Domains in Group table"> - <thead> - <tr> - <th scope="col">{{'BULK.LIST.STATE' | translate}}</th> - <th scope="col">{{'BULK.LIST.CREATED' | translate}}</th> - <th scope="col">{{'BULK.LIST.DOMAIN_ID' | translate}}</th> - <th scope="col">{{'BULK.LIST.DOMAIN_NAME' | translate}}</th> - <th scope="col">{{'BULK.LIST.DOMAIN_CODENAME' | translate}}</th> - <th style="width: 5%" scope="col"></th> - </tr> - <ng-template ngFor let-response [ngForOf]="bulk.entries" let-i="index"> - <tr *ngIf="response.type === 'DOMAIN'" class="table-row"> - <td>{{'BULK.STATE.' + response.state | translate}}</td> - <td>{{response.created}}</td> - <td>{{getDomainId(response)}}</td> - <td>{{getDomainName(response)}}</td> - <td>{{getDomainCodeName(response)}}</td> - <td style="width: 5%" class="text-right" *ngIf="bulk.state !== 'REMOVED'"> - <i *ngIf="response.type === 'DOMAIN'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/domains/view/', response?.details['domainId']]"></i> - <i *ngIf="response.type === 'USER'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/users/view', response?.details['userId']]"></i> - <!-- <span class="dropdown"> - <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" - data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> - </a> - <ul class="dropdown-menu pull-right-drop"> - <li *ngIf="response.type === 'DOMAIN'"> - <a [routerLink]="['/admin/domains/view/', response?.details['domainId']]">{{ 'BULK.LIST.MOVE_DOMAIN' | translate }}</a> - </li> - <li *ngIf="response.type === 'USER'"> - <a [routerLink]="['/admin/users/view', response?.details['userId']]">{{ 'BULK.LIST.MOVE_USER' | translate }}</a> - </li> - </ul> - </span> --> - </td> + <div class="panel-body"> + <table class="table table-hover table-condensed" aria-describedby="Domains in Group table"> + <thead> + <tr> + <th scope="col">{{'BULK.LIST.STATE' | translate}}</th> + <th scope="col">{{'BULK.LIST.CREATED' | translate}}</th> + <th scope="col">{{'BULK.LIST.DOMAIN_ID' | translate}}</th> + <th scope="col">{{'BULK.LIST.DOMAIN_NAME' | translate}}</th> + <th scope="col">{{'BULK.LIST.DOMAIN_CODENAME' | translate}}</th> + <th style="width: 5%" scope="col"></th> </tr> - </ng-template> - </thead> - </table> + <ng-template ngFor let-response [ngForOf]="bulk.entries" let-i="index"> + <tr *ngIf="response.type === 'DOMAIN'" class="table-row"> + <td>{{'BULK.STATE.' + response.state | translate}}</td> + <td>{{response.created}}</td> + <td>{{getDomainId(response)}}</td> + <td>{{getDomainName(response)}}</td> + <td>{{getDomainCodeName(response)}}</td> + <td style="width: 5%" class="text-right" *ngIf="bulk.state !== 'REMOVED'"> + <i *ngIf="response.type === 'DOMAIN'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/domains/view/', response?.details['domainId']]"></i> + <i *ngIf="response.type === 'USER'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/users/view', response?.details['userId']]"></i> + <!-- <span class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" + data-toggle="dropdown" href="#" role="button"> + <em class="fas fa-cog icon-black icon-bigger"></em> + </a> + <ul class="dropdown-menu pull-right-drop"> + <li *ngIf="response.type === 'DOMAIN'"> + <a [routerLink]="['/admin/domains/view/', response?.details['domainId']]">{{ 'BULK.LIST.MOVE_DOMAIN' | translate }}</a> + </li> + <li *ngIf="response.type === 'USER'"> + <a [routerLink]="['/admin/users/view', response?.details['userId']]">{{ 'BULK.LIST.MOVE_USER' | translate }}</a> + </li> + </ul> + </span> --> + </td> + </tr> + </ng-template> + </thead> + </table> + </div> </div> - </div> - <div class="panel panel-default" style="margin-top: 3rem"> - <div class="panel-heading"> + <div class="panel panel-default" style="margin-top: 3rem"> + <div class="panel-heading"> - <div style="display: flex; justify-content: start; align-items: center"> - <div> - {{'BULK.USER.DEPLOYMENTS' | translate }} + <div style="display: flex; justify-content: start; align-items: center"> + <div> + {{'BULK.USER.DEPLOYMENTS' | translate }} + </div> </div> </div> - </div> - <div class="panel-body"> - <table class="table table-hover table-condensed" aria-describedby="Domains in Group table"> - <thead> - <tr> - <th scope="col">{{'BULK.LIST.STATE' | translate}}</th> - <th scope="col">{{'BULK.LIST.CREATED' | translate}}</th> - <th scope="col">{{'BULK.LIST.USER_ID' | translate}}</th> - <th scope="col">{{'BULK.LIST.USER_NAME' | translate}}</th> - <th scope="col">{{'BULK.LIST.EMAIL' | translate}}</th> - <th style="width: 5%" scope="col"></th> - </tr> - <ng-template ngFor let-response [ngForOf]="bulk.entries" let-i="index"> - <tr *ngIf="response.type === 'USER'" class="table-row"> - <td>{{'BULK.STATE.' + response.state | translate}}</td> - <td>{{response.created}}</td> - <td>{{getUserId(response)}}</td> - <td>{{getUsername(response)}}</td> - <td>{{getEmail(response)}}</td> - <td style="width: 5%" class="text-right"> - <i *ngIf="response.type === 'DOMAIN'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/domains/view/', response?.details['domainId']]"></i> - <i *ngIf="response.type === 'USER'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/users/view', response?.details['userId']]"></i> - - <!-- <span class="dropdown"> - <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" - data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> - </a> - <ul class="dropdown-menu pull-right-drop"> - <li *ngIf="response.type === 'DOMAIN'"> - <a [routerLink]="['/admin/domains/view/', response?.details['domainId']]">{{ 'BULK.LIST.MOVE_DOMAIN' | translate }}</a> - </li> - <li *ngIf="response.type === 'USER'"> - <a [routerLink]="['/admin/users/view', response?.details['userId']]">{{ 'BULK.LIST.MOVE_USER' | translate }}</a> - </li> - </ul> - </span> --> - </td> + <div class="panel-body"> + <table class="table table-hover table-condensed" aria-describedby="Domains in Group table"> + <thead> + <tr> + <th scope="col">{{'BULK.LIST.STATE' | translate}}</th> + <th scope="col">{{'BULK.LIST.CREATED' | translate}}</th> + <th scope="col">{{'BULK.LIST.USER_ID' | translate}}</th> + <th scope="col">{{'BULK.LIST.USER_NAME' | translate}}</th> + <th scope="col">{{'BULK.LIST.EMAIL' | translate}}</th> + <th style="width: 5%" scope="col"></th> </tr> - </ng-template> - </thead> - </table> + <ng-template ngFor let-response [ngForOf]="bulk.entries" let-i="index"> + <tr *ngIf="response.type === 'USER'" class="table-row"> + <td>{{'BULK.STATE.' + response.state | translate}}</td> + <td>{{response.created}}</td> + <td>{{getUserId(response)}}</td> + <td>{{getUsername(response)}}</td> + <td>{{getEmail(response)}}</td> + <td style="width: 5%" class="text-right"> + <i *ngIf="response.type === 'DOMAIN'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/domains/view/', response?.details['domainId']]"></i> + <i *ngIf="response.type === 'USER'" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/admin/users/view', response?.details['userId']]"></i> - </div> - </div> + <!-- <span class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" + data-toggle="dropdown" href="#" role="button"> + <em class="fas fa-cog icon-black icon-bigger"></em> + </a> + <ul class="dropdown-menu pull-right-drop"> + <li *ngIf="response.type === 'DOMAIN'"> + <a [routerLink]="['/admin/domains/view/', response?.details['domainId']]">{{ 'BULK.LIST.MOVE_DOMAIN' | translate }}</a> + </li> + <li *ngIf="response.type === 'USER'"> + <a [routerLink]="['/admin/users/view', response?.details['userId']]">{{ 'BULK.LIST.MOVE_USER' | translate }}</a> + </li> + </ul> + </span> --> + </td> + </tr> + </ng-template> + </thead> + </table> + </div> + </div> - </div> - <div *ngIf="bulk && bulkType === 'APPLICATION' "> - <div class="flex justify-content-between"> - <h3>{{'BULK.APP.VIEW_HEADER' | translate}}</h3> - <img alt="App logo" style="width: 50px" - [src]="(appImagesService.getAppLogoUrl(bulk.details['appId']) | secure) || 'assets/images/app-logo-example.png'"/> </div> - - <div class="" style="padding-bottom: 5rem; margin-top: 3rem"> - <label for="id" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.ID' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="id" name="id" [disabled]="true" - [(ngModel)]="bulk.id" #name="ngModel"> + + <div *ngIf="bulk && bulkType === 'APPLICATION' "> + <div class="flex justify-content-between"> + <h3>{{'BULK.APP.VIEW_HEADER' | translate}}</h3> + <img alt="App logo" style="width: 50px" + [src]="(appImagesService.getAppLogoUrl(bulk.details['appId']) | secure) || 'assets/images/app-logo-example.png'"/> </div> - </div> - <div class="" style="padding-bottom: 5rem;"> - <label for="id" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.APP_NAME' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="id" name="id" [disabled]="true" - [(ngModel)]="bulk.details['appName']" #name="ngModel"> + <div class="" style="padding-bottom: 5rem; margin-top: 3rem"> + <label for="id" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.ID' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="id" name="id" [disabled]="true" + [(ngModel)]="bulk.id" #name="ngModel"> + </div> </div> - - </div> - <div class="" style="padding-bottom: 5rem"> - <label for="creator" - class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATOR' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="creator" name="creator" [disabled]="true" - placeholder="{{bulk.creator.username}} ({{bulk.creator.firstname}} {{bulk.creator.lastname}})"> + <div class="" style="padding-bottom: 5rem;"> + <label for="id" class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.APP_NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="id" name="id" [disabled]="true" + [(ngModel)]="bulk.details['appName']" #name="ngModel"> + </div> + </div> - </div> - <div class="" style="padding-bottom: 5rem"> - <label for="date" - class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATION_DATE' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="date" name="date" [disabled]="true" - placeholder="{{bulk.creationDate | date: 'dd-MM-yyyy HH:mm'}}"> + <div class="" style="padding-bottom: 5rem"> + <label for="creator" + class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATOR' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="creator" name="creator" [disabled]="true" + placeholder="{{bulk.creator.username}} ({{bulk.creator.firstname}} {{bulk.creator.lastname}})"> + </div> </div> - </div> - <div class="" style="padding-bottom: 5rem"> - <label for="state" - class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.STATE' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="state" name="state" [disabled]="true" - placeholder="{{'BULK.STATE.' + bulk.state | translate}}"> + <div class="" style="padding-bottom: 5rem"> + <label for="date" + class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.CREATION_DATE' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="date" name="date" [disabled]="true" + placeholder="{{bulk.creationDate | date: 'dd-MM-yyyy HH:mm'}}"> + </div> </div> - </div> - <div class="" style="padding-bottom: 5rem"> - <label for="completionDate" - class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.COMPLETION_DATE' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="completionDate" name="completionDate" [disabled]="true" - placeholder="{{completionDate}}"> + <div class="" style="padding-bottom: 5rem"> + <label for="state" + class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.STATE' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="state" name="state" [disabled]="true" + placeholder="{{'BULK.STATE.' + bulk.state | translate}}"> + </div> + </div> + + <div class="" style="padding-bottom: 5rem"> + <label for="completionDate" + class="col-sm-2 control-label text-right mt-2">{{ 'BULK.LIST.COMPLETION_DATE' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="completionDate" name="completionDate" [disabled]="true" + placeholder="{{completionDate}}"> + </div> </div> - </div> - - <div *ngIf="bulk.state !== 'REMOVED'" class="flex justify-content-end" style="padding-right: 1.5rem"> - <button class="btn btn-primary mr-2" (click)="refreshStates()">{{'BULK.APP.REFRESH' | translate}}</button> - <button *ngIf="bulk.state !== 'FAILED'" class="btn btn-primary" (click)="getAppBulkDetails(this.bulkId)">{{'BULK.APP.DOWNLOAD_CSV' | translate}}</button> - </div> - <div class="mt-4"> - <p-progressBar [mode]="progressBarMode"[value]="progressBarValue" id="progressBarr" [style]="{ height: '18px' }" [styleClass]="jobDone ? 'job-done-bar' : 'normal-bar'"> - <ng-template pTemplate="content" let-value> - <span>{{queueDetails?.jobDone}} <span *ngIf="queueDetails?.jobDone !== bulk.entries.length">(+{{queueDetails?.jobInProcess}})</span> / {{bulk.entries.length}}</span> + <div *ngIf="bulk.state !== 'REMOVED'" class="flex justify-content-end" style="padding-right: 1.5rem"> + <button class="btn btn-primary mr-2" (click)="refreshStates()">{{'BULK.APP.REFRESH' | translate}}</button> + + <button *ngIf="bulk.state !== 'FAILED'" class="btn btn-primary" (click)="getAppBulkDetails(this.bulkId)">{{'BULK.APP.DOWNLOAD_CSV' | translate}}</button> + </div> + <div class="mt-4"> + <p-progressBar [mode]="progressBarMode"[value]="progressBarValue" id="progressBarr" [style]="{ height: '18px' }" [styleClass]="jobDone ? 'job-done-bar' : 'normal-bar'"> + <ng-template pTemplate="content" let-value> + <span>{{queueDetails?.jobDone}} <span *ngIf="queueDetails?.jobDone !== bulk.entries.length">(+{{queueDetails?.jobInProcess}})</span> / {{bulk.entries.length}}</span> </ng-template> - </p-progressBar> + </p-progressBar> - </div> - <div class="panel panel-default" style="margin-top: 1rem"> - <div class="panel-heading"> + </div> + <div class="panel panel-default" style="margin-top: 1rem"> + <div class="panel-heading"> - <div style="display: flex; justify-content: start; align-items: center"> - <div> - {{ 'BULK.APP.DEPLOYMENTS' | translate }} + <div style="display: flex; justify-content: start; align-items: center"> + <div> + {{ 'BULK.APP.DEPLOYMENTS' | translate }} + </div> </div> </div> - </div> - <div class="panel-body"> - - - <table class="table table-hover table-condensed" aria-describedby="Domains in Group table"> - <thead> - <tr #column> - <th scope="col">{{'BULK.LIST.STATE' | translate}}</th> - <th scope="col">{{'BULK.APP.INSTANCE_ID' | translate}}</th> - <th scope="col">{{'BULK.APP.INSTANCE_NAME' | translate}}</th> - <th scope="col">{{'BULK.APP.DOMAIN' | translate}}</th> - <th style="width: 5%" scope="col"></th> - </tr> - <ng-template ngFor let-response [ngForOf]="bulk.entries" let-i="index"> - <tr *ngIf="response.type === 'APPLICATION'" class="table-row"> - <td>{{'BULK.STATE.' + response.state | translate}} <em - *ngIf="response.state == 'PROCESSING'" - class="pi pi-spin pi-spinner ml-1" - style="font-size: 1.4rem"></em> - <em *ngIf="response.state == 'FAILED' && response?.details['appInstanceId'] === undefined" class="pi pi-info-circle" - style="font-size: 1.4rem" - pTooltip="{{response?.details['errorMessage']}}" - tooltipStyleClass="p-tooltip-width " [fitContent]="false"></em></td> - <td>{{getAppInstanceId(response)}}</td> - <td>{{getAppInstanceName(response)}}</td> - <td>{{getDomainCodeName(response)}}</td> - <td style="width: 5%" class="text-right" > - <i *ngIf="response?.details['appInstanceId'] !== undefined" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/instances/', response?.details['appInstanceId']]"></i> - <!-- <span *ngIf="response?.details['appInstanceId'] !== undefined" class="dropdown"> - <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" - data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> - </a> - <ul class="dropdown-menu pull-right-drop"> - <li *ngIf="response.type === 'APPLICATION' && response?.details['appInstanceId'] !== undefined"> - <a [routerLink]="['/instances/', response?.details['appInstanceId']]">{{ 'BULK.LIST.MOVE_APP' | translate }}</a> - </li> - <li *ngIf="response.type === 'APPLICATION' && response.state !== 'COMPLETED'"> - <a>{{ 'BULK.APP.CHECK_STATE' | translate }}</a> - </li> - </ul> - </span> --> - </td> + <div class="panel-body"> + + + <table class="table table-hover table-condensed" aria-describedby="Domains in Group table"> + <thead> + <tr #column> + <th scope="col">{{'BULK.LIST.STATE' | translate}}</th> + <th scope="col">{{'BULK.APP.INSTANCE_ID' | translate}}</th> + <th scope="col">{{'BULK.APP.INSTANCE_NAME' | translate}}</th> + <th scope="col">{{'BULK.APP.DOMAIN' | translate}}</th> + <th style="width: 5%" scope="col"></th> </tr> - </ng-template> - </thead> - </table> + <ng-template ngFor let-response [ngForOf]="bulk.entries" let-i="index"> + <tr *ngIf="response.type === 'APPLICATION'" class="table-row"> + <td>{{'BULK.STATE.' + response.state | translate}} <em + *ngIf="response.state == 'PROCESSING'" + class="pi pi-spin pi-spinner ml-1" + style="font-size: 1.4rem"></em> + <em *ngIf="response.state == 'FAILED' && response?.details['appInstanceId'] === undefined" class="pi pi-info-circle" + style="font-size: 1.4rem" + pTooltip="{{response?.details['errorMessage']}}" + tooltipStyleClass="p-tooltip-width " [fitContent]="false"></em></td> + <td>{{getAppInstanceId(response)}}</td> + <td>{{getAppInstanceName(response)}}</td> + <td>{{getDomainCodeName(response)}}</td> + <td style="width: 5%" class="text-right" > + <i *ngIf="response?.details['appInstanceId'] !== undefined" class="pi pi-search" style="font-size: 1.8rem; cursor: pointer" [routerLink]="['/instances/', response?.details['appInstanceId']]"></i> + <!-- <span *ngIf="response?.details['appInstanceId'] !== undefined" class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" + data-toggle="dropdown" href="#" role="button"> + <em class="fas fa-cog icon-black icon-bigger"></em> + </a> + <ul class="dropdown-menu pull-right-drop"> + <li *ngIf="response.type === 'APPLICATION' && response?.details['appInstanceId'] !== undefined"> + <a [routerLink]="['/instances/', response?.details['appInstanceId']]">{{ 'BULK.LIST.MOVE_APP' | translate }}</a> + </li> + <li *ngIf="response.type === 'APPLICATION' && response.state !== 'COMPLETED'"> + <a>{{ 'BULK.APP.CHECK_STATE' | translate }}</a> + </li> + </ul> + </span> --> + </td> + </tr> + </ng-template> + </thead> + </table> + </div> </div> - </div> + </div> </div> + </div> diff --git a/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.css b/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.css new file mode 100644 index 0000000000000000000000000000000000000000..f2c387a9db7ec1e2c9aeefc41a92b4d6336c9fdb --- /dev/null +++ b/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.css @@ -0,0 +1,38 @@ +:host ::ng-deep input[type=file]{ + display:none; +} +:host ::ng-deep .p-button{ + width: unset; + margin-right: 5px; + background: var(--primary-button-color); + color: var(--button-text-color); +} +:host ::ng-deep .p-button:hover{ + background: var(--primary-button-hover); + border:none; +} +:host ::ng-deep .p-button-label{ + font-weight: normal; +} +:host ::ng-deep .p-fileupload .p-fileupload-buttonbar{ + border: none; + background: transparent; + margin-bottom: 10px; + padding: 0; +} +:host ::ng-deep .p-fileupload .p-fileupload-content{ + border: none; + padding: 0; + border-radius: 3px; + +} +:host ::ng-deep .p-fileupload-content .p-progressbar{ + display: none; +} +textarea{ + border-color: #ccc; +} +:host ::ng-deep .p-inputtext:enabled:focus{ + box-shadow: none; + border-color: var(--l-text-color); +} diff --git a/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.html b/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.html index 71d0eff138b7dc6da3d9f249f33ce97de8f63dc2..3a56ddc0d02e4864d2cd351ce33f7d06bdbbb772 100644 --- a/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.html +++ b/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.html @@ -1,28 +1,31 @@ -<div style="width: 80%; margin: auto; margin-top: 4rem;"> - <div style="margin-bottom: 2rem"> - <h2>{{'BULK.DOMAIN.NAVIGATION' | translate}}</h2> - <p class="mt-4">{{'BULK.DOMAIN.UPLOAD' | translate}}</p> - </div> - <p-fileUpload name="file[]" customUpload="true" (uploadHandler)="myUploader($event)" accept=".csv" - multiple="false" uploadLabel="{{'BULK.BUTTON' | translate}}"></p-fileUpload> +<div style="width: 100%; margin: auto; margin-top: 4rem;"> + <h3>{{'BULK.DOMAIN.NAVIGATION' | translate}}</h3> + <div class="background-section"> + <div style="margin-bottom: 2rem"> - <div style="margin-top: 1.5rem; display: flex; justify-content: center; flex-direction: column"> - <div style="margin-top: 1.5rem; margin-bottom: 1rem"> - <p>{{'BULK.APP.UPLOAD_TEXT' | translate}}</p> + <p class="">{{'BULK.DOMAIN.UPLOAD' | translate}}</p> </div> - <textarea pInputTextarea [(ngModel)]="csvText" rows="10" cols="127" (keyup)="changeDetector = true"></textarea> - </div> + <p-fileUpload name="file[]" (uploadHandler)="myUploader($event)" accept=".csv" + multiple="false" uploadLabel="{{'BULK.BUTTON' | translate}}" ></p-fileUpload> - <div *ngIf="errorMessage !== ''" style="margin-top: 1rem; display: flex; justify-content: start; color: indianred"> - <p>{{errorMessage}}</p> - </div> + <div style="margin-top: 1.5rem; display: flex; justify-content: center; flex-direction: column"> + <div style="margin-top: 1.5rem; margin-bottom: 1rem"> + <p>{{'BULK.APP.UPLOAD_TEXT' | translate}}</p> + </div> + <textarea pInputTextarea [(ngModel)]="csvText" rows="10" cols="127" [autoResize]="true" (keyup)="changeDetector = true"></textarea> + </div> - <div style="margin-top: 2rem; display: flex; justify-content: center;" > + <div *ngIf="errorMessage !== ''" style="margin-top: 1rem; display: flex; justify-content: start; color: indianred"> + <p>{{errorMessage}}</p> + </div> + + <div *ngIf="showProgressBar" style="margin-top: 20px;"> + <p>{{ 'BULK.DOMAIN.DEPLOYMENT_IN_PROGRESS' | translate }}</p> + <p-progressBar mode="indeterminate" [style]="{height : '8px'}"></p-progressBar> + </div> + </div> + <div style="margin-top: 2rem; display: flex; justify-content: end;" > <button class="btn btn-primary" (click)="uploadText(); this.errorMessage=''" [disabled]="csvText === '' || !changeDetector"> {{'BULK.UPLOAD_BUTTON' | translate}}</button> </div> - <div *ngIf="showProgressBar" style="margin-top: 20px;"> - <p>{{ 'BULK.DOMAIN.DEPLOYMENT_IN_PROGRESS' | translate }}</p> - <p-progressBar mode="indeterminate" [style]="{height : '8px'}"></p-progressBar> - </div> </div> diff --git a/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.ts b/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.ts index 7919ba6ecd003708201643d666a6c445a99516ba..cc11875e86b51b7446c66f0834dd38d6c6c82893 100644 --- a/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.ts +++ b/src/app/appmarket/bulkDeployment/domainDeployment/domainupload/domainupload.component.ts @@ -7,7 +7,7 @@ import {DomainService} from '../../../../service'; @Component({ selector: 'app-domainupload', templateUrl: './domainupload.component.html', - styleUrls: [] + styleUrls: ['./domainupload.component.css'] }) export class DomainuploadComponent { diff --git a/src/app/appmarket/domains/domain-annotations/domain-annotations.component.html b/src/app/appmarket/domains/domain-annotations/domain-annotations.component.html index b7a1c8994df0487f51786029da9a476fb7173f56..45ab00bcd295953589a3e5c2ddc03f9bc7d2b0aa 100644 --- a/src/app/appmarket/domains/domain-annotations/domain-annotations.component.html +++ b/src/app/appmarket/domains/domain-annotations/domain-annotations.component.html @@ -1,7 +1,7 @@ -<div class="col-sm-12 col-sm-offset-1 col-sm-10 col-md-offset-1 col-md-10"> +<div class=""> <h3> {{'DOMAINS.ANNOTATIONS.HEADER' | translate}}</h3> <p>{{'DOMAINS.ANNOTATIONS.SUBHEADER' | translate}} </p> - <div class="flex flex-grow-1 mt-6 col-sm-12" style="width: 100% !important;"> + <div class="" style="width: 100% !important;"> <app-domain-namespace-annotations [annotationRead]="annotations" [globalSettings]="true" (annotations)="handleAnnotationsUpdate($event)" (trigerDelete)="handleDelete($event)" class="flex flex-grow-1"></app-domain-namespace-annotations> </div> diff --git a/src/app/appmarket/domains/domain-group-view/domain-group-view.component.html b/src/app/appmarket/domains/domain-group-view/domain-group-view.component.html index c42594752a704d430b01c6c597c099788a2ccb41..56d68346cc403a477bb1ce1a6a05d258bbaa0e54 100644 --- a/src/app/appmarket/domains/domain-group-view/domain-group-view.component.html +++ b/src/app/appmarket/domains/domain-group-view/domain-group-view.component.html @@ -1,40 +1,49 @@ -<div class="col-sm-12 col-sm-offset-1 col-sm-10 col-md-offset-1 col-md-10"> - <h3>{{ 'DOMAINS.LIST.GROUP' | translate }}</h3> +<div class=""> + <h4 class="header">{{ 'DOMAINS.LIST.GROUP' | translate }}</h4> <form *ngIf="domainGroup" (submit)="submit(false)" class="form-horizontal" #domainForm="ngForm"> - <div class="form-group"> - <label for="name" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.NAME' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="name" name="name" - [(ngModel)]="domainGroup.name" #name="ngModel" required> - <div *ngIf="name.invalid && (name.dirty || name.touched)" - class="alert alert-danger"> - <div *ngIf="name.errors.required">{{ 'DOMAIN_DETAILS.NAME_IS_REQUIRED_MESSAGE' | translate }}</div> + <div class="background-section"> + <div class="form-group"> + <label for="name" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="name" name="name" + [(ngModel)]="domainGroup.name" #name="ngModel" required> + <div *ngIf="name.invalid && (name.dirty || name.touched)" + class="alert alert-danger"> + <div *ngIf="name.errors.required">{{ 'DOMAIN_DETAILS.NAME_IS_REQUIRED_MESSAGE' | translate }}</div> + </div> </div> </div> - </div> - <div class="form-group" *ngIf="!isInMode(ComponentMode.EDIT)"> - <label for="codename" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.CODE_NAME' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" id="codename" - name="codename" pattern="[a-zA-Z0-9-]*" - [(ngModel)]="domainGroup.codename" #codename="ngModel" minlength="2" maxlength="20" required> - <div *ngIf="codename.invalid && (codename.dirty || codename.touched)" - class="alert alert-danger"> + <div class="form-group" *ngIf="!isInMode(ComponentMode.EDIT)"> + <label for="codename" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.CODE_NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="codename" + name="codename" pattern="[a-zA-Z0-9-]*" + [(ngModel)]="domainGroup.codename" #codename="ngModel" minlength="2" maxlength="20" required> + <div *ngIf="codename.invalid && (codename.dirty || codename.touched)" + class="alert alert-danger"> - <div *ngIf="codename.errors.required">{{ 'DOMAIN_DETAILS.CODE_NAME_IS_REQUIRED_MESSAGE' | translate }}</div> - <div *ngIf="codename.errors.pattern">{{ 'DOMAIN_DETAILS.CODE_NAME_PATTERN_MESSAGE_1' | translate }}</div> - <div *ngIf="codename.errors.minlength || codename.errors.maxlength">{{ 'DOMAIN_DETAILS.CODE_NAME_PATTERN_MESSAGE_2' | translate }}</div> + <div *ngIf="codename.errors.required">{{ 'DOMAIN_DETAILS.CODE_NAME_IS_REQUIRED_MESSAGE' | translate }}</div> + <div *ngIf="codename.errors.pattern">{{ 'DOMAIN_DETAILS.CODE_NAME_PATTERN_MESSAGE_1' | translate }}</div> + <div *ngIf="codename.errors.minlength || codename.errors.maxlength">{{ 'DOMAIN_DETAILS.CODE_NAME_PATTERN_MESSAGE_2' | translate }}</div> + </div> </div> </div> </div> - <div *ngIf="!this.addingMode" class="panel panel-default" style="margin-top: 3rem"> + + <div *ngIf="!this.addingMode" class="background-section" style="margin-top: 3rem"> <div class="panel-heading"> - <div style="display: flex; justify-content: start; align-items: center"> - <div> + <div style="display: flex; justify-content:space-between"> + <h4 style="font-size:15px; font-weight: bold"> {{ 'DOMAINS.GROUP.ACCESS_USER' | translate }} + </h4> + <div *roles="['ROLE_SYSTEM_ADMIN']" style="display: flex; justify-content: end"> + <button type="button" class="btn btn-text" (click)="showModalUser()">{{'DOMAINS.GROUP.ADD_USERS' | translate}}</button> + </div> + <div *roles="['ROLE_VL_MANAGER']" style="display: flex; justify-content: end"> + <button type="button" class="btn btn-warning" (click)="removeMyAccess()">{{'DOMAINS.GROUP.DELETE_MYSELF' | translate}}</button> </div> </div> </div> @@ -42,7 +51,7 @@ <div *roles="['ROLE_SYSTEM_ADMIN']" style="display: flex; justify-content: end"> <button type="button" class="btn btn-secondary" (click)="showModalUser()">{{'DOMAINS.GROUP.ADD_USERS' | translate}}</button> </div> - <div *roles="['ROLE_VL_MANAGER']" style="display: flex; justify-content: end"> + <div *roles="['ROLE_GROUP_MANAGER']" style="display: flex; justify-content: end"> <button type="button" class="btn btn-warning" (click)="removeMyAccess()">{{'DOMAINS.GROUP.DELETE_MYSELF' | translate}}</button> </div> <table class="table table-hover table-condensed" aria-describedby="Domains in Group table" style="margin-top: 2rem"> @@ -211,8 +220,8 @@ </table> </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" (click)="closeModal()" [disabled]="false">{{'DOMAINS.LIST.ADD' | translate}}</button> <button type="button" class="btn btn-secondary" (click)="modal.hide()">{{'APP_CHANGE_STATE_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-primary" (click)="closeModal()" [disabled]="false">{{'DOMAINS.LIST.ADD' | translate}}</button> </div> </nmaas-modal> @@ -258,7 +267,7 @@ </table> </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" (click)="saveUsers()" [disabled]="false">{{'DOMAINS.GROUP.ADD_USERS' | translate}}</button> <button type="button" class="btn btn-secondary" (click)="closeModalUserAccess()">{{'APP_CHANGE_STATE_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-primary" (click)="saveUsers()" [disabled]="false">{{'DOMAINS.GROUP.ADD_USERS' | translate}}</button> </div> </nmaas-modal> diff --git a/src/app/appmarket/domains/domain-group-view/domain-group-view.component.spec.ts b/src/app/appmarket/domains/domain-group-view/domain-group-view.component.spec.ts index 82d18dc612c8aeaee246b0f383482b4108fe5bdc..fa207e464e4238670e8196973654cbecb6738ce1 100644 --- a/src/app/appmarket/domains/domain-group-view/domain-group-view.component.spec.ts +++ b/src/app/appmarket/domains/domain-group-view/domain-group-view.component.spec.ts @@ -1,32 +1,68 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { DomainGroupViewComponent } from './domain-group-view.component'; -import {RouterTestingModule} from '@angular/router/testing'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; -import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {of} from 'rxjs'; -import createSpyObj = jasmine.createSpyObj; -import {UserService} from '../../../service'; -import {AuthService} from '../../../auth/auth.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { of } from 'rxjs'; +import { DomainService, UserService } from '../../../service'; +import { AuthService } from '../../../auth/auth.service'; +import { ProfileService } from '../../../service/profile.service'; +import { ModalComponent } from '../../../shared'; import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { DomainGroup } from '../../../model/domaingroup'; +import { Domain } from '../../../model/domain'; +import { User } from '../../../model/user'; +import { Role } from '../../../model/userrole'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DomainApplicationStatePerDomain } from '../../../model/domainapplicationstateperdomain'; describe('DomainGroupViewComponent', () => { let component: DomainGroupViewComponent; let fixture: ComponentFixture<DomainGroupViewComponent>; + let mockDomainService: jasmine.SpyObj<DomainService>; + let mockUserService: jasmine.SpyObj<UserService>; + let mockAuthService: jasmine.SpyObj<AuthService>; + let mockProfileService: jasmine.SpyObj<ProfileService>; + let mockModal: jasmine.SpyObj<ModalComponent>; + let mockActivatedRoute: any; + let mockRouter: jasmine.SpyObj<Router>; + - const userServiceSpy = createSpyObj('UserService', ['getAll']) - userServiceSpy.getAll.and.returnValue(of([])) - - const authServiceSpy = createSpyObj('AuthService', ['hasRole', 'hasDomainRole', 'getRoles']); - authServiceSpy.hasRole.and.returnValue(true) - authServiceSpy.hasDomainRole.and.returnValue(true) - authServiceSpy.getRoles.and.returnValue(['']) + beforeEach(async () => { + mockDomainService = jasmine.createSpyObj('DomainService', [ + 'getDomainGroup', + 'createDomainGroup', + 'updateDomainGroup', + 'deleteDomainFromGroup', + 'getAll', + 'addDomainsToGroup', + 'updateDomainGroupManagers', + 'getGlobalDomainId', + ]); + mockDomainService.getAll.and.returnValue(of([])); // Mock getAll to return an empty array + mockDomainService.getGlobalDomainId.and.returnValue(0); // Mock getGlobalDomainId + mockDomainService.getDomainGroup.and.returnValue(of({ + id: 0, + name: '', + codename: '', + domains: [], + applicationStatePerDomain: [], + managers: [] + } as DomainGroup)); // Mock getDomainGroup to return a valid DomainGroup object + mockUserService = jasmine.createSpyObj('UserService', ['getUserBySearchManagers']); + mockAuthService = jasmine.createSpyObj('AuthService', ['getUsername']); + mockRouter = jasmine.createSpyObj('Router', ['navigate']); + mockProfileService = jasmine.createSpyObj('ProfileService', ['getOne']); + mockModal = jasmine.createSpyObj('ModalComponent', ['show', 'hide']); + mockActivatedRoute = { + snapshot: { data: { mode: 'VIEW' } }, + params: of({ id: 1 }) // Mock route parameters + }; - beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ DomainGroupViewComponent ], + declarations: [DomainGroupViewComponent, ModalComponent], imports: [ RouterTestingModule, HttpClientTestingModule, @@ -35,26 +71,424 @@ describe('DomainGroupViewComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateFakeLoader - } + useClass: TranslateFakeLoader, + }, }), ], providers: [ - {provide: UserService, useValue: userServiceSpy}, - {provide: AuthService, useValue: authServiceSpy}, + { provide: DomainService, useValue: mockDomainService }, + { provide: UserService, useValue: mockUserService }, + { provide: AuthService, useValue: mockAuthService }, + { provide: Router, useValue: mockRouter }, + { provide: ProfileService, useValue: mockProfileService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute } // Provide mocked ActivatedRoute ], schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], - }) - .compileComponents(); + }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(DomainGroupViewComponent); component = fixture.componentInstance; + component.userAccessModal = mockModal; // Assign mocked user access modal fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize and fetch domain group data based on route params', () => { + const mockDomainGroup: DomainGroup = { + id: 1, + name: 'Test Group', + codename: 'test-group', + domains: [], + applicationStatePerDomain: [], + managers: [] + }; + mockDomainService.getDomainGroup.and.returnValue(of(mockDomainGroup)); + + component.ngOnInit(); + + expect(mockDomainService.getDomainGroup).toHaveBeenCalledWith(1); + expect(component.domainGroup).toEqual(mockDomainGroup); + }); + + it('should initialize and fetch domain group data', () => { + const mockDomainGroup: DomainGroup = { + id: 1, + name: 'Test Group', + codename: 'test-group', + domains: [], + applicationStatePerDomain: [], + managers: [] + }; + mockDomainService.getDomainGroup.and.returnValue(of(mockDomainGroup)); + + component.domainGroupId = 1; + component.refresh(); + + expect(mockDomainService.getDomainGroup).toHaveBeenCalledWith(1); + expect(component.domainGroup).toEqual(mockDomainGroup); + }); + + it('should show modal and filter domains', () => { + component.domains = [{ id: 1 } as Domain, { id: 2 } as Domain]; + component.domainGroup.domains = [{ id: 1 } as Domain]; + + spyOn(component.modal, 'show'); + + component.showModal(); + + expect(component.modal.show).toHaveBeenCalled(); + expect(component.domains).toEqual([{ id: 2 } as Domain]); + }); + + it('should close modal and add domains to group', () => { + component.domainsToAdd = [{ id: 1 } as Domain]; + mockDomainService.addDomainsToGroup.and.returnValue(of(null)); + spyOn(component.modal, 'hide'); + + component.closeModal(); + + expect(mockDomainService.addDomainsToGroup).toHaveBeenCalledWith(component.domainGroup.codename, [1]); + expect(component.modal.hide).toHaveBeenCalled(); + }); + + it('should delete a domain from the group', () => { + mockDomainService.deleteDomainFromGroup.and.returnValue(of(null)); + spyOn(component, 'refresh'); + + component.domainGroup.id = 1; + component.deleteDomainFromGroup({ id: 2 } as Domain); + + expect(mockDomainService.deleteDomainFromGroup).toHaveBeenCalledWith(1, 2); + expect(component.refresh).toHaveBeenCalled(); + }); + + it('should search and filter users for adding', () => { + const mockUsers: User[] = [ + { + id: 1, + username: 'user1', + enabled: true, + firstname: 'User', + lastname: 'One', + email: 'user1@example.com', + roles: [], + termsOfUseAccepted: true, + privacyPolicyAccepted: true, + ssoUser: false, + selectedLanguage: 'en', + defaultDomain: 1, + lastSuccessfulLoginDate: new Date(), + firstLoginDate: new Date(), + sshKeys: [], + hasSshKeys: false, + getRoles(): Role[] { return []; }, + getDomainIds(): number[] {return [];}, + }, + { + id: 2, + username: 'user2', + enabled: true, + firstname: 'User', + lastname: 'Two', + email: 'user2@example.com', + roles: [], + termsOfUseAccepted: true, + privacyPolicyAccepted: true, + ssoUser: false, + selectedLanguage: 'en', + defaultDomain: 1, + lastSuccessfulLoginDate: new Date(), + firstLoginDate: new Date(), + sshKeys: [], + hasSshKeys: false, + getRoles(): Role[] { return []; }, + getDomainIds(): number[] {return [];}, + }, + ]; + mockUserService.getUserBySearchManagers.and.returnValue(of(mockUsers)); + component.domainGroup.managers = [{ id: 1 } as User]; + + component.searchUsers('user'); + + expect(mockUserService.getUserBySearchManagers).toHaveBeenCalledWith('user'); + expect(component.usersFound).toEqual([mockUsers[1]]); + }); + + it('should add a user to the group', () => { + const mockUser: User = { + id: 1, + username: 'user1', + enabled: true, + firstname: 'User', + lastname: 'One', + email: 'user1@example.com', + roles: [], + termsOfUseAccepted: true, + privacyPolicyAccepted: true, + ssoUser: false, + selectedLanguage: 'en', + defaultDomain: 1, + lastSuccessfulLoginDate: new Date(), + firstLoginDate: new Date(), + sshKeys: [], + hasSshKeys: false, + getRoles(): Role[] { return []; }, + getDomainIds(): number[] { return []; }, + }; + component.usersToAdd = []; + component.usersFound = [mockUser]; + + component.addUser(mockUser); + + expect(component.usersToAdd).toContain(jasmine.objectContaining({ + id: mockUser.id, + username: mockUser.username, + firstname: mockUser.firstname, + lastname: mockUser.lastname + })); + expect(component.usersFound).not.toContain(mockUser); + }); + + it('should save users and update managers', () => { + const mockUser: User = { + id: 1, + username: 'user1', + enabled: true, + firstname: 'User', + lastname: 'One', + email: 'user1@example.com', + roles: [], + termsOfUseAccepted: true, + privacyPolicyAccepted: true, + ssoUser: false, + selectedLanguage: 'en', + defaultDomain: 1, + lastSuccessfulLoginDate: new Date(), + firstLoginDate: new Date(), + sshKeys: [], + hasSshKeys: false, + getRoles(): Role[] { return []; }, + getDomainIds(): number[] {return [];}, + }; + component.usersToAdd = [mockUser]; + component.domainGroup.managers = []; + mockDomainService.updateDomainGroupManagers.and.returnValue(of({ + id: component.domainGroupId, + name: 'Test Group', + codename: 'test-group', + domains: [], + applicationStatePerDomain: [], + managers: [mockUser] + })); + spyOn(component.userAccessModal, 'hide'); + + component.saveUsers(); + + expect(mockDomainService.updateDomainGroupManagers).toHaveBeenCalledWith([mockUser], component.domainGroupId); + expect(component.userAccessModal.hide).toHaveBeenCalled(); + }); + + it('should remove user access from the group', () => { + const mockUser: User = { + id: 1, + username: 'user1', + enabled: true, + firstname: 'User', + lastname: 'One', + email: 'user1@example.com', + roles: [], + termsOfUseAccepted: true, + privacyPolicyAccepted: true, + ssoUser: false, + selectedLanguage: 'en', + defaultDomain: 1, + lastSuccessfulLoginDate: new Date(), + firstLoginDate: new Date(), + sshKeys: [], + hasSshKeys: false, + getRoles(): Role[] { return []; }, + getDomainIds(): number[] {return []}, + }; + component.domainGroup.managers = [mockUser]; + mockDomainService.updateDomainGroupManagers.and.returnValue(of({ + id: component.domainGroupId, + name: 'Test Group', + codename: 'test-group', + domains: [], + applicationStatePerDomain: [], + managers: [] + })); + + component.deleteUserAccess(mockUser); + + expect(mockDomainService.updateDomainGroupManagers).toHaveBeenCalledWith([], component.domainGroupId); + expect(component.domainGroup.managers).toEqual([]); + }); + + it('should toggle all application states', () => { + const mockApplicationState: DomainApplicationStatePerDomain[] = [ + { applicationBaseId: 1, applicationBaseName: 'App1', enabled: true, pvStorageSizeLimit: 100 }, + { applicationBaseId: 2, applicationBaseName: 'App2', enabled: false, pvStorageSizeLimit: 200 } + ]; + component.domainGroup.applicationStatePerDomain = mockApplicationState; + + const mockElement = document.createElement('div'); + mockElement.classList.add('show'); + spyOn(document, 'querySelector').and.returnValue(mockElement); + + component.toggleAll(); + + expect(mockElement.classList.contains('show')).toBeTruthy(); + }); + + it('should sort applications by name', () => { + component.domainGroup.applicationStatePerDomain = [ + { applicationBaseId: 3, applicationBaseName: 'Zebra', enabled: true, pvStorageSizeLimit: 300 }, + { applicationBaseId: 1, applicationBaseName: 'Apple', enabled: true, pvStorageSizeLimit: 100 }, + { applicationBaseId: 2, applicationBaseName: 'Mango', enabled: false, pvStorageSizeLimit: 200 } + ]; + + component.sortApplication(); + + expect(component.domainGroup.applicationStatePerDomain).toEqual([ + { applicationBaseId: 1, applicationBaseName: 'Apple', enabled: true, pvStorageSizeLimit: 100 }, + { applicationBaseId: 2, applicationBaseName: 'Mango', enabled: false, pvStorageSizeLimit: 200 }, + { applicationBaseId: 3, applicationBaseName: 'Zebra', enabled: true, pvStorageSizeLimit: 300 } + ]); + }); + + it('should handle domain group creation', () => { + const mockOwner = { id: 1, username: 'owner' } as User; + const mockCreatedGroup = { id: 2, name: 'New Group' } as DomainGroup; + mockProfileService.getOne.and.returnValue(of(mockOwner)); + mockDomainService.createDomainGroup.and.returnValue(of(mockCreatedGroup)); + + component.domainGroup = new DomainGroup(); + component.submit(); + + expect(mockProfileService.getOne).toHaveBeenCalled(); + expect(mockDomainService.createDomainGroup).toHaveBeenCalledWith(jasmine.objectContaining({ + managers: [mockOwner] + })); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/admin/domains/groups/', mockCreatedGroup.id]); + }); + + it('should handle domain group update', () => { + const mockUpdatedGroup = { id: 1, name: 'Updated Group' } as DomainGroup; + mockDomainService.updateDomainGroup.and.returnValue(of(mockUpdatedGroup)); + spyOn(component, 'refresh'); + + component.domainGroup = mockUpdatedGroup; + component.domainGroupId = 1; + component.submit(); + + expect(mockDomainService.updateDomainGroup).toHaveBeenCalledWith(mockUpdatedGroup, 1); + expect(component.refresh).toHaveBeenCalled(); + }); + + it('should handle domain group update and navigate away', () => { + const mockUpdatedGroup = { id: 1, name: 'Updated Group' } as DomainGroup; + mockDomainService.updateDomainGroup.and.returnValue(of(mockUpdatedGroup)); + + component.domainGroup = mockUpdatedGroup; + component.domainGroupId = 1; + component.submit(false); + + expect(mockDomainService.updateDomainGroup).toHaveBeenCalledWith(mockUpdatedGroup, 1); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/admin/domains/groups']); + }); + + it('should remove user from selected users to add', () => { + const mockUser: User = { id: 1, username: 'user1' } as User; + component.usersToAdd = [mockUser]; + + component.removeUserFromSelected(mockUser); + + expect(component.usersToAdd).not.toContain(mockUser); + }); + + it('should close user access modal and reset users', () => { + spyOn(component.userAccessModal, 'hide'); + component.usersToAdd = [{ id: 1, username: 'user1' } as User]; + component.usersFound = [{ id: 2, username: 'user2' } as User]; + + component.closeModalUserAccess(); + + expect(component.userAccessModal.hide).toHaveBeenCalled(); + expect(component.usersToAdd).toEqual([]); + expect(component.usersFound).toEqual([]); + }); + + it('should remove current user access and navigate away', () => { + const mockUsername = 'currentUser'; + mockAuthService.getUsername.and.returnValue(mockUsername); + mockDomainService.updateDomainGroup.and.returnValue(of(null)); + + component.domainGroup.managers = [ + { + id: 1, + username: 'currentUser', + enabled: true, + firstname: '', + lastname: '', + email: '', + roles: [], + termsOfUseAccepted: true, + privacyPolicyAccepted: true, + ssoUser: false, + selectedLanguage: 'en', + defaultDomain: 1, + lastSuccessfulLoginDate: new Date(), + firstLoginDate: new Date(), + sshKeys: [], + hasSshKeys: false, + getRoles(): Role[] { return []; }, + getDomainIds(): number[] { return []; } + }, + { + id: 2, + username: 'otherUser', + enabled: true, + firstname: '', + lastname: '', + email: '', + roles: [], + termsOfUseAccepted: true, + privacyPolicyAccepted: true, + ssoUser: false, + selectedLanguage: 'en', + defaultDomain: 1, + sshKeys: [], + hasSshKeys: false, + } as User + ]; + component.domainGroupId = 1; + + component.removeMyAccess(); + + expect(component.domainGroup.managers).toEqual([{ + id: 2, + username: 'otherUser', + enabled: true, + firstname: '', + lastname: '', + email: '', + roles: [], + termsOfUseAccepted: true, + privacyPolicyAccepted: true, + ssoUser: false, + selectedLanguage: 'en', + defaultDomain: 1, + + sshKeys: [], + hasSshKeys: false, + } as User]); + expect(mockDomainService.updateDomainGroup).toHaveBeenCalledWith(component.domainGroup, 1); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/admin/domains/groups']); + }); }); diff --git a/src/app/appmarket/domains/domain-group-view/domain-group-view.component.ts b/src/app/appmarket/domains/domain-group-view/domain-group-view.component.ts index 9d6a7fa59b5209a0bccc3d7987c14233089f0271..40016cb87969f989e696a01ab2b68dbf6ff4e6a0 100644 --- a/src/app/appmarket/domains/domain-group-view/domain-group-view.component.ts +++ b/src/app/appmarket/domains/domain-group-view/domain-group-view.component.ts @@ -117,6 +117,7 @@ export class DomainGroupViewComponent extends BaseComponent implements OnInit { this.refresh(); this.domainsToAdd = []; this.refreshDomainForAdd(); + }); this.modal.hide(); } diff --git a/src/app/appmarket/domains/domain-groups/domain-groups.component.css b/src/app/appmarket/domains/domain-groups/domain-groups.component.css index 7cc4c1ecabca30691fff692308eb4bbbd24dd7fe..e7436baab1c633ac509d2387be02c6255cfeea48 100644 --- a/src/app/appmarket/domains/domain-groups/domain-groups.component.css +++ b/src/app/appmarket/domains/domain-groups/domain-groups.component.css @@ -21,3 +21,64 @@ margin-left: 5px; margin-right: 5px; } + +:host ::ng-deep .p-datatable .p-datatable-thead > tr > th{ + border: 1px solid #E0E2E5; + background:transparent; + border-width: 0 0 1px 0; +} +:host ::ng-deep .p-datatable .p-datatable-tbody > tr > td { + text-align: left; + border: 1px solid #E0E2E5; + border-width: 0 0 1px 0; + padding: 1rem 1rem; +} +:host ::ng-deep .p-datatable .p-paginator-bottom{ + height: 40px; + background: transparent; + border: none; + margin-top:10px; +} +:host ::ng-deep .p-datatable .p-datatable-tbody > tr{ + background: transparent; +} + +:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page{ + transition: unset; + border-radius: 50%; + min-width:3.5rem; + height:3.5rem; + margin:0 5px; + font-size: 14px; +} + +:host ::ng-deep .p-paginator-element{ + border-radius:50%; + margin:0 5px; + min-width:3.5rem; + height:3.5rem; + font-size: 14px; +} +:host ::ng-deep .p-paginator .p-dropdown{ + height:3rem; +} +:host ::ng-deep .p-paginator-icon{ + height: 1.5rem; + width: 1.5rem; +} +:host ::ng-deep .p-paginator .p-dropdown .p-dropdown-label{ + padding-right: 10px; +} + +label{ + padding-left:5px; + display: unset; + margin-bottom: 0; + font-weight: unset; +} +:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page.p-highlight{ + background: var(--user-button-background-hover); +} +:host ::ng-deep .p-datatable>.p-datatable-wrapper { + overflow: visible; +} diff --git a/src/app/appmarket/domains/domain-groups/domain-groups.component.html b/src/app/appmarket/domains/domain-groups/domain-groups.component.html index 53ae0e2083a29f2718b9b1cbd78c80e2039d1fe5..0e054bd69320aa30d884104e74f13f0865f7923e 100644 --- a/src/app/appmarket/domains/domain-groups/domain-groups.component.html +++ b/src/app/appmarket/domains/domain-groups/domain-groups.component.html @@ -1,50 +1,60 @@ -<div class="col-sm-12 col-sm-offset-1 col-sm 10 col-md-offset-1 col-md-10"> - <h3>{{'DOMAINS.LIST.GROUPS' | translate}}</h3> - <div class="flex space-between"> - <div class="flex"> - <a *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']" [routerLink]="['/admin/domains/groups/add']" class="btn btn-primary" - role="button">{{'DOMAINS.ADD_BUTTON' | translate}}</a> - </div> - <div class="flex"> - <input pInputText class="flex" name="search" id="search" placeholder="Search" type="text" style="height: 34px" [(ngModel)]="searchValue"> - </div> +<div style="display: flex; align-items: center; margin-top:20px"> + <div style="margin-right:20px"> + <span class="p-input-icon-right" style="width: 100%"> + <i class="pi pi-search" style="font-size: 13px; top: 16px; margin-right: 5px;"></i> + <input pInputText class="flex form-control" name="search" id="search" placeholder="Search" type="text" style="height: 34px" [(ngModel)]="searchValue"> + </span> </div> - <table class="table table-hover table-condensed" aria-describedby="Apps management table" style="margin-top: 20px;"> - <thead> - <tr> - <th scope="col"></th> - <th scope="col">{{'APPS_MANAGEMENT.NAME' | translate}}</th> - <th scope="col">{{'DOMAINS.CODE_NAME' | translate}}</th> - <th scope="col">{{'DOMAINS.LIST.DOMAIN_NAME' | translate}}</th> - <th scope="col">{{'DOMAINS.LIST.DOMAIN_CODE_NAME' | translate}}</th> - </tr> - </thead> - - <tbody> - <ng-template ngFor let-domainGroup [ngForOf]="groups | searchDomainGroup: searchValue" let-i="index"> - <tr class="table-row" > - <td style="width: 5%" (click)="clickTableRow(i)" *ngIf="!domainsRowVisible[i]"><span class="glyphicon glyphicon-chevron-right"></span></td> - <td style="width: 5%" (click)="clickTableRow(i)" *ngIf="domainsRowVisible[i]"><span class="glyphicon glyphicon-chevron-down"></span></td> + <div class="flex" style="margin-right:20px"> + <button *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']" [routerLink]="['/admin/domains/groups/add']" class="btn btn-primary" + role="button">{{'DOMAINS.ADD_BUTTON' | translate}}</button> + </div> +</div> +<h4 class="header">{{'DOMAINS.LIST.GROUPS' | translate}}</h4> + +<div class="background-section"> + + <p-table [value]="groups | searchDomainGroup: searchValue" [rowHover]="true"> + <ng-template pTemplate="header"> + <tr> + <th></th> + <th>{{'APPS_MANAGEMENT.NAME' | translate}}</th> + <th>{{'DOMAINS.CODE_NAME' | translate}}</th> + <th>{{'DOMAINS.LIST.DOMAIN_NAME' | translate}}</th> + <th>{{'DOMAINS.LIST.DOMAIN_CODE_NAME' | translate}}</th> + <th></th> + </tr> + </ng-template> + + <ng-template pTemplate="body" let-domainGroup let-i="rowIndex"> + <tr class="table-row"> + <td style="width: 5%" (click)="clickTableRow(i)" *ngIf="!domainsRowVisible[i]"> + <span class="pi pi-chevron-right"></span> + </td> + <td style="width: 5%" (click)="clickTableRow(i)" *ngIf="domainsRowVisible[i]"> + <span class="pi pi-chevron-down"></span> + </td> <td style="width: 25%" (click)="clickTableRow(i)">{{domainGroup?.name}}</td> <td style="width: 20%" (click)="clickTableRow(i)">{{domainGroup?.codename}}</td> <td style="width: 15%" (click)="clickTableRow(i)"></td> <td style="width: 15%" (click)="clickTableRow(i)"></td> <td style="width: 20%" class="text-right"> <span class="dropdown"> - <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> - </a> - <ul class="dropdown-menu pull-right-drop"> - <li> - <a [routerLink]="['/admin/domains/groups/', domainGroup?.id]">{{ 'APPS_MANAGEMENT.EDIT_BUTTON' | translate }}</a> - </li> - <li> - <a (click)="deleteDomainGroup(domainGroup?.id)">{{ 'APP_INSTANCE.REMOVE_BUTTON' | translate }}</a> - </li> - </ul> - </span> + <a style="display: inline-block" class="dropdown-toggle" aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> + <em class="pi pi-cog" style="font-size: 1.8rem; color: var(--l-text-color)"></em> + </a> + <ul class="dropdown-menu pull-right-drop"> + <li> + <a [routerLink]="['/admin/domains/groups/', domainGroup?.id]">{{ 'APPS_MANAGEMENT.EDIT_BUTTON' | translate }}</a> + </li> + <li> + <a (click)="deleteDomainGroup(domainGroup?.id)">{{ 'APP_INSTANCE.REMOVE_BUTTON' | translate }}</a> + </li> + </ul> + </span> </td> </tr> + <ng-template ngFor let-domain [ngForOf]="domainGroup.domains"> <tr *ngIf="domainsRowVisible[i]" class="table-row pointer" [routerLink]="['/admin/domains/view/', domain.id]"> <td></td> @@ -52,12 +62,62 @@ <td></td> <td>{{domain.name}}</td> <td>{{domain.codename}}</td> - <td class="text-right"> - </td> + <td class="text-right"></td> </tr> </ng-template> </ng-template> - </tbody> - </table> + </p-table> </div> + +<!-- <table class="table table-hover table-condensed" aria-describedby="Apps management table" style="margin-top: 20px;">--> +<!-- <thead>--> +<!-- <tr>--> +<!-- <th scope="col"></th>--> +<!-- <th scope="col">{{'APPS_MANAGEMENT.NAME' | translate}}</th>--> +<!-- <th scope="col">{{'DOMAINS.CODE_NAME' | translate}}</th>--> +<!-- <th scope="col">{{'DOMAINS.LIST.DOMAIN_NAME' | translate}}</th>--> +<!-- <th scope="col">{{'DOMAINS.LIST.DOMAIN_CODE_NAME' | translate}}</th>--> +<!-- </tr>--> +<!-- </thead>--> + +<!-- <tbody>--> +<!-- <ng-template ngFor let-domainGroup [ngForOf]="groups | searchDomainGroup: searchValue" let-i="index">--> +<!-- <tr class="table-row" >--> +<!-- <td style="width: 5%" (click)="clickTableRow(i)" *ngIf="!domainsRowVisible[i]"><span class="glyphicon glyphicon-chevron-right"></span></td>--> +<!-- <td style="width: 5%" (click)="clickTableRow(i)" *ngIf="domainsRowVisible[i]"><span class="glyphicon glyphicon-chevron-down"></span></td>--> +<!-- <td style="width: 25%" (click)="clickTableRow(i)">{{domainGroup?.name}}</td>--> +<!-- <td style="width: 20%" (click)="clickTableRow(i)">{{domainGroup?.codename}}</td>--> +<!-- <td style="width: 15%" (click)="clickTableRow(i)"></td>--> +<!-- <td style="width: 15%" (click)="clickTableRow(i)"></td>--> +<!-- <td style="width: 20%" class="text-right">--> +<!-- <span class="dropdown">--> +<!-- <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button">--> +<!-- <em class="fas fa-cog icon-black icon-bigger"></em>--> +<!-- </a>--> +<!-- <ul class="dropdown-menu pull-right-drop">--> +<!-- <li>--> +<!-- <a [routerLink]="['/admin/domains/groups/', domainGroup?.id]">{{ 'APPS_MANAGEMENT.EDIT_BUTTON' | translate }}</a>--> +<!-- </li>--> +<!-- <li>--> +<!-- <a (click)="deleteDomainGroup(domainGroup?.id)">{{ 'APP_INSTANCE.REMOVE_BUTTON' | translate }}</a>--> +<!-- </li>--> +<!-- </ul>--> +<!-- </span>--> +<!-- </td>--> +<!-- </tr>--> +<!-- <ng-template ngFor let-domain [ngForOf]="domainGroup.domains">--> +<!-- <tr *ngIf="domainsRowVisible[i]" class="table-row pointer" [routerLink]="['/admin/domains/view/', domain.id]">--> +<!-- <td></td>--> +<!-- <td></td>--> +<!-- <td></td>--> +<!-- <td>{{domain.name}}</td>--> +<!-- <td>{{domain.codename}}</td>--> +<!-- <td class="text-right">--> +<!-- </td>--> +<!-- </tr>--> +<!-- </ng-template>--> +<!-- </ng-template>--> +<!-- </tbody>--> +<!-- </table>--> + diff --git a/src/app/appmarket/domains/domain/domain.component.css b/src/app/appmarket/domains/domain/domain.component.css index 2c587d11ef0fc19812ddea12ab21bb9029db6350..5bcb6ebc246def5aa232ad2b49d95b88155ff827 100644 --- a/src/app/appmarket/domains/domain/domain.component.css +++ b/src/app/appmarket/domains/domain/domain.component.css @@ -55,3 +55,8 @@ input.ng-dirty.ng-invalid { .no-padding-top { padding-top: 0!important; } +.form-control[disabled], fieldset[disabled] .form-control{ + background: transparent; + border:none; + box-shadow: none; +} diff --git a/src/app/appmarket/domains/domain/domain.component.html b/src/app/appmarket/domains/domain/domain.component.html index 00208f2f896690b530f2229310fa7dd3b6c375dc..0963f7ae40c3656822bfd6c2d4fb6734d09292e3 100644 --- a/src/app/appmarket/domains/domain/domain.component.html +++ b/src/app/appmarket/domains/domain/domain.component.html @@ -1,108 +1,111 @@ -<div class="col-sm-12 col-sm-offset-1 col-sm-10 col-md-offset-1 col-md-10"> +<div class=""> <h3>{{ 'DOMAIN_DETAILS.TITLE' | translate }}</h3> + <form *ngIf="domain" (submit)="submit()" class="form-horizontal" #domainForm="ngForm"> - <div class="form-group"> - <label for="name" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.NAME' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW) || authService.hasRole('ROLE_OPERATOR')" id="name" name="name" - [(ngModel)]="domain.name" #name="ngModel" required> - <div *ngIf="name.invalid && (name.dirty || name.touched)" - class="alert alert-danger"> - <div *ngIf="name.errors.required">{{ 'DOMAIN_DETAILS.NAME_IS_REQUIRED_MESSAGE' | translate }}</div> + <div class="background-section"> + <div class="form-group"> + <label for="name" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW) || authService.hasRole('ROLE_OPERATOR')" id="name" name="name" + [(ngModel)]="domain.name" #name="ngModel" required> + <div *ngIf="name.invalid && (name.dirty || name.touched)" + class="alert alert-danger"> + <div *ngIf="name.errors.required">{{ 'DOMAIN_DETAILS.NAME_IS_REQUIRED_MESSAGE' | translate }}</div> + </div> </div> </div> - </div> - <div class="form-group" *ngIf="!isInMode(ComponentMode.EDIT)"> - <label for="codename" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.CODE_NAME' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="codename" - name="codename" pattern="[a-z0-9-]*" - [(ngModel)]="domain.codename" #codename="ngModel" minlength="2" maxlength="12" required> - <div *ngIf="codename.invalid && (codename.dirty || codename.touched)" - class="alert alert-danger"> + <div class="form-group" *ngIf="!isInMode(ComponentMode.EDIT)"> + <label for="codename" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.CODE_NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="codename" + name="codename" pattern="[a-z0-9-]*" + [(ngModel)]="domain.codename" #codename="ngModel" minlength="2" maxlength="12" required> + <div *ngIf="codename.invalid && (codename.dirty || codename.touched)" + class="alert alert-danger"> - <div *ngIf="codename.errors.required">{{ 'DOMAIN_DETAILS.CODE_NAME_IS_REQUIRED_MESSAGE' | translate }}</div> - <div *ngIf="codename.errors.pattern">{{ 'DOMAIN_DETAILS.CODE_NAME_PATTERN_MESSAGE_1' | translate }}</div> - <div *ngIf="codename.errors.minlength || codename.errors.maxlength">{{ 'DOMAIN_DETAILS.CODE_NAME_PATTERN_MESSAGE_2' | translate }}</div> + <div *ngIf="codename.errors.required">{{ 'DOMAIN_DETAILS.CODE_NAME_IS_REQUIRED_MESSAGE' | translate }}</div> + <div *ngIf="codename.errors.pattern">{{ 'DOMAIN_DETAILS.CODE_NAME_PATTERN_MESSAGE_1' | translate }}</div> + <div *ngIf="codename.errors.minlength || codename.errors.maxlength">{{ 'DOMAIN_DETAILS.CODE_NAME_PATTERN_MESSAGE_2' | translate }}</div> + </div> </div> </div> - </div> - <div class="form-group" *ngIf="!isInMode(ComponentMode.CREATE) && (authService.hasRole('ROLE_SYSTEM_ADMIN') || authService.hasRole('ROLE_OPERATOR'))"> - <label class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.ID_IN_DB' | translate }}</label> - <div class="col-sm-10"> - <p class="form-control-static">{{domain.id}}</p> + <div class="form-group" *ngIf="!isInMode(ComponentMode.CREATE) && (authService.hasRole('ROLE_SYSTEM_ADMIN') || authService.hasRole('ROLE_OPERATOR'))"> + <label class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.ID_IN_DB' | translate }}</label> + <div class="col-sm-10"> + <p class="form-control-static">{{domain.id}}</p> + </div> + </div> + <hr/> + + <div class="form-group"> + <label for="kubernetesNamespace" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.KUBERNETES_NAMESPACE' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="kubernetesNamespace" pattern="[a-z0-9-]*" maxlength="64" #namespace="ngModel" + name="kubernetesNamespace" [(ngModel)]="domain.domainTechDetails.kubernetesNamespace" placeholder="{{'DOMAIN_DETAILS.KUBERNETES_NAMESPACE_PLACEHOLDER' | translate}}"> + <div *ngIf="namespace.invalid && (namespace.dirty || namespace.touched)" class="alert alert-danger"> + <div *ngIf="namespace.errors.pattern">{{ 'DOMAIN_DETAILS.NAMESPACE_PATTERN_VALIDATION_MESSAGE' | translate }}</div> + <div *ngIf="namespace.errors.maxlength">{{ 'DOMAIN_DETAILS.NAMESPACE_MAX_LENGTH_VALIDATION_MESSAGE' | translate }}</div> + </div> + </div> </div> - </div> - - <hr/> - <div class="form-group"> - <label for="kubernetesNamespace" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.KUBERNETES_NAMESPACE' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="kubernetesNamespace" pattern="[a-z0-9-]*" maxlength="64" #namespace="ngModel" - name="kubernetesNamespace" [(ngModel)]="domain.domainTechDetails.kubernetesNamespace" placeholder="{{'DOMAIN_DETAILS.KUBERNETES_NAMESPACE_PLACEHOLDER' | translate}}"> - <div *ngIf="namespace.invalid && (namespace.dirty || namespace.touched)" class="alert alert-danger"> - <div *ngIf="namespace.errors.pattern">{{ 'DOMAIN_DETAILS.NAMESPACE_PATTERN_VALIDATION_MESSAGE' | translate }}</div> - <div *ngIf="namespace.errors.maxlength">{{ 'DOMAIN_DETAILS.NAMESPACE_MAX_LENGTH_VALIDATION_MESSAGE' | translate }}</div> + <div class="form-group" *ngIf="domain.id !== domainService.getGlobalDomainId()"> + <label for="kubernetesStorageClass" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.KUBERNETES_STORAGE_CLASS' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="kubernetesStorageClass" + name="kubernetesStorageClass" [(ngModel)]="domain.domainTechDetails.kubernetesStorageClass"> </div> </div> - </div> - <div class="form-group" *ngIf="domain.id !== domainService.getGlobalDomainId()"> - <label for="kubernetesStorageClass" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.KUBERNETES_STORAGE_CLASS' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="kubernetesStorageClass" - name="kubernetesStorageClass" [(ngModel)]="domain.domainTechDetails.kubernetesStorageClass"> - </div> - </div> - - <div class="form-group" *ngIf="domain.id !== domainService.getGlobalDomainId()"> - <label for="kubernetesIngressClass" class="col-sm-2 control-label">{{'DOMAIN_DETAILS.KUBERNETES_INGRESS_CLASS' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="kubernetesIngressClass" - name="kubernetesIngressClass" [(ngModel)]="domain.domainTechDetails.kubernetesIngressClass"> + <div class="form-group" *ngIf="domain.id !== domainService.getGlobalDomainId()"> + <label for="kubernetesIngressClass" class="col-sm-2 control-label">{{'DOMAIN_DETAILS.KUBERNETES_INGRESS_CLASS' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="kubernetesIngressClass" + name="kubernetesIngressClass" [(ngModel)]="domain.domainTechDetails.kubernetesIngressClass"> + </div> </div> - </div> - <div class="form-group" *ngIf="domain.id !== domainService.getGlobalDomainId()"> - <label for="externalServiceDomain " class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.EXTERNAL_SERVICE_DOMAIN' | translate }}</label> - <div class="col-sm-10"> - <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="externalServiceDomain " - name="externalServiceDomain " [(ngModel)]="domain.domainTechDetails.externalServiceDomain"> + <div class="form-group" *ngIf="domain.id !== domainService.getGlobalDomainId()"> + <label for="externalServiceDomain " class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.EXTERNAL_SERVICE_DOMAIN' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="isInMode(ComponentMode.VIEW)" id="externalServiceDomain " + name="externalServiceDomain " [(ngModel)]="domain.domainTechDetails.externalServiceDomain"> + </div> </div> - </div> - <div class="form-group" *ngIf="domain?.id !== domainService.getGlobalDomainId()"> - <label class="col-sm-2 control-label text-right" for="dcnDeploymentType">{{'DOMAIN_DETAILS.DCN_DEPLOYMENT_TYPE' | translate }}</label> - <div class="col-sm-10"> - <select class="form-control" id="dcnDeploymentType" name="dcnDeploymentType" [(ngModel)]="domain.domainDcnDetails.dcnDeploymentType" [disabled]="isInMode(ComponentMode.VIEW)" required> - <option *ngFor="let type of keys" [value]="type">{{type | titlecase}}</option> - </select> + <div class="form-group" *ngIf="domain?.id !== domainService.getGlobalDomainId()"> + <label class="col-sm-2 control-label text-right" for="dcnDeploymentType">{{'DOMAIN_DETAILS.DCN_DEPLOYMENT_TYPE' | translate }}</label> + <div class="col-sm-10"> + <select class="form-control" id="dcnDeploymentType" name="dcnDeploymentType" [(ngModel)]="domain.domainDcnDetails.dcnDeploymentType" [disabled]="isInMode(ComponentMode.VIEW)" required> + <option *ngFor="let type of keys" [value]="type">{{type | titlecase}}</option> + </select> + </div> </div> - </div> - <div class="form-group" *ngIf="domain?.id !== domainService.getGlobalDomainId() && isInMode(ComponentMode.VIEW) && isManual()"> - <label class="col-sm-2 control-label text-right" for="configured-status">{{ 'DOMAIN_DETAILS.DCN_STATUS' | translate }}</label> - <div class="col-sm-10" id="configured-status" style="padding-top: 6px;"> - <p *ngIf="domain?.domainDcnDetails?.dcnConfigured">{{ 'DOMAIN_DETAILS.CONFIGURED_VIEW' | translate }}</p> - <p *ngIf="!domain?.domainDcnDetails?.dcnConfigured">{{ 'DOMAIN_DETAILS.NOT_CONFIGURED_VIEW' | translate }}</p> + <div class="form-group" *ngIf="domain?.id !== domainService.getGlobalDomainId() && isInMode(ComponentMode.VIEW) && isManual()"> + <label class="col-sm-2 control-label text-right" for="configured-status">{{ 'DOMAIN_DETAILS.DCN_STATUS' | translate }}</label> + <div class="col-sm-10" id="configured-status" style="padding-top: 6px;"> + <p *ngIf="domain?.domainDcnDetails?.dcnConfigured">{{ 'DOMAIN_DETAILS.CONFIGURED_VIEW' | translate }}</p> + <p *ngIf="!domain?.domainDcnDetails?.dcnConfigured">{{ 'DOMAIN_DETAILS.NOT_CONFIGURED_VIEW' | translate }}</p> + </div> </div> - </div> - <div class="form-group" *ngIf="domain.id !== domainService.getGlobalDomainId() && !isInMode(ComponentMode.VIEW) && isManual()"> - <label for="dcnConfigured" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.DCN_CONFIGURED' | translate }}</label> - <div class="col-sm-10"> - <input type="checkbox" class="btn btn-default" [disabled]="isInMode(ComponentMode.VIEW)" id="dcnConfigured" - name="dcnConfigured" [(ngModel)]="domain.domainDcnDetails.dcnConfigured" (change)="changeDcnFieldUpdatedFlag()"> + <div class="form-group" *ngIf="domain.id !== domainService.getGlobalDomainId() && !isInMode(ComponentMode.VIEW) && isManual()"> + <label for="dcnConfigured" class="col-sm-2 control-label">{{ 'DOMAIN_DETAILS.DCN_CONFIGURED' | translate }}</label> + <div class="col-sm-10"> + <input type="checkbox" class="btn btn-default" [disabled]="isInMode(ComponentMode.VIEW)" id="dcnConfigured" + name="dcnConfigured" [(ngModel)]="domain.domainDcnDetails.dcnConfigured" (change)="changeDcnFieldUpdatedFlag()"> + </div> </div> </div> - <div class="panel panel-default" *ngIf="isInMode(ComponentMode.VIEW) && !authService.hasRole('ROLE_OPERATOR')"> - <div class="panel-heading">{{ 'DOMAIN_DETAILS.DOMAIN_USERS' | translate }}</div> + + <div class="background-section" *ngIf="isInMode(ComponentMode.VIEW) && !authService.hasRole('ROLE_OPERATOR')"> + <h4 style="font-size:15px; font-weight: bold">{{ 'DOMAIN_DETAILS.DOMAIN_USERS' | translate }}</h4> <div class="panel-body"> <table class="table table-hover table-condensed" aria-describedby="Domains details table"> <thead> @@ -147,8 +150,8 @@ <app-domain-namespace-annotations [annotationRead]="annotations" (annotations)="handleAnnotationsChange($event)" (trigerDelete)="handleAnnotationDelete($event)"></app-domain-namespace-annotations> </div> - <div *ngIf="isInMode(ComponentMode.VIEW)" class="panel panel-default"> - <div class="panel-heading">{{ 'DOMAIN_DETAILS.APP_STATUS' | translate }}</div> + <div *ngIf="isInMode(ComponentMode.VIEW)" class="background-section"> + <h4 style="font-size:15px; font-weight: bold">{{ 'DOMAIN_DETAILS.APP_STATUS' | translate }}</h4> <div class="panel-body"> <table class="table table-hover table-condensed" aria-describedby="Domain details table"> <thead> @@ -207,8 +210,8 @@ </div> </div> - <div class="panel panel-default" *ngIf="displayCustomerNetworksSection && domain.id !== domainService.getGlobalDomainId()"> - <div class="panel-heading">{{'DOMAIN_DETAILS.CUSTOMER_NETWORKS' | translate}}</div> + <div class="background-section" *ngIf="displayCustomerNetworksSection && domain.id !== domainService.getGlobalDomainId()"> + <h4 style="font-size:15px; font-weight: bold">{{'DOMAIN_DETAILS.CUSTOMER_NETWORKS' | translate}}</h4> <div class="panel-body"> <div class="text-center" *ngIf="isInMode(ComponentMode.VIEW) && domain.domainDcnDetails.customerNetworks.length == 0"> <h5>{{'DOMAIN_DETAILS.CUSTOMER_NETWORKS_EMPTY_LIST_MESSAGE' | translate}}</h5> @@ -233,8 +236,8 @@ - <div class="panel panel-default" *ngIf="isInMode(ComponentMode.VIEW) && !authService.hasRole('ROLE_OPERATOR')"> - <div class="panel-heading">{{ 'DOMAINS.LIST.GROUP' | translate }}</div> + <div class="background-section" *ngIf="isInMode(ComponentMode.VIEW) && !authService.hasRole('ROLE_OPERATOR')"> + <h4 style="font-size:15px; font-weight: bold">{{ 'DOMAINS.LIST.GROUP' | translate }}</h4> <div class="panel-body"> <table class="table table-hover table-condensed" aria-describedby="Domain group list"> <thead> @@ -255,11 +258,59 @@ </tbody> </table> </div> + </div> + + + <!-- CLUSTER CONFIGURATION READ ONLY PRESENTATION --> + <div class="background-section" *ngIf="isInMode(ComponentMode.VIEW) && !authService.hasRole('ROLE_OPERATOR')"> + <h4 style="font-size:15px; font-weight: bold">{{ 'CLUSTERS.CONFIGURATION' | translate }}</h4> + + <div class="form-group" *ngIf="domain.clusters.length >0"> + <label for="clusterId" class="col-sm-2 control-label">{{ 'CLUSTERS.ID' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="true" id="clusterId " + name="clusterId " [(ngModel)]="cluster.id"> + </div> + </div> + + <div class="form-group" *ngIf="domain.clusters.length >0"> + <label for="clusterName" class="col-sm-2 control-label">{{ 'CLUSTERS.NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="true" id="clusterName " + name="clusterName " [(ngModel)]="cluster.name"> + </div> + </div> + + <div class="form-group" *ngIf="domain.clusters.length >0"> + <label for="clusterCodeName" class="col-sm-2 control-label">{{ 'CLUSTERS.CODENAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="true" id="clusterCodeName " + name="clusterCodeName " [(ngModel)]="cluster.codename"> + </div> + </div> + + <div class="form-group" *ngIf="domain.clusters.length >0"> + <label for="clusterDescription" class="col-sm-2 control-label">{{ 'CLUSTERS.DESCRIPTION' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" [disabled]="true" id="clusterDescription " + name="clusterDescription " [(ngModel)]="cluster.description"> + </div> + </div> + + + + + </div> <div class="flex justify-content-end"> <button *ngIf="!isInMode(ComponentMode.VIEW)" type="submit" class="btn btn-primary" [disabled]="!domainForm.form.valid">{{ 'DOMAIN_DETAILS.SUBMIT_BUTTON' | translate }}</button> </div> + + <br *ngIf="errorMessage"> + <div class="alert alert-danger text-left" *ngIf="errorMessage"> + {{errorMessage}} + </div> </form> </div> diff --git a/src/app/appmarket/domains/domain/domain.component.ts b/src/app/appmarket/domains/domain/domain.component.ts index eecd96f7840b8cacb482cbfaa219fc6cab1cdf15..b31df76200441f3eaaeb2f9a9dc7294991749ac7 100644 --- a/src/app/appmarket/domains/domain/domain.component.ts +++ b/src/app/appmarket/domains/domain/domain.component.ts @@ -9,13 +9,14 @@ import {User} from '../../../model'; import {Observable, of} from 'rxjs'; import {UserRole} from '../../../model/userrole'; import {AuthService} from '../../../auth/auth.service'; -import {ModalComponent} from '../../../shared'; +import {ModalComponent} from '../../../shared'; import {map, shareReplay, take} from 'rxjs/operators'; import {DcnDeploymentType} from '../../../model/dcndeploymenttype'; import {CustomerNetwork} from '../../../model/customernetwork'; import {MinLengthDirective} from '../../../directive/min-length.directive'; import {MaxLengthDirective} from '../../../directive/max-length.directive'; import {DomainAnnotation} from '../../../model/domain-annotation'; +import { ClusterManager } from '../../../model/cluster-manager'; @Component({ @@ -48,6 +49,10 @@ export class DomainComponent extends BaseComponent implements OnInit { public annotations : Observable<DomainAnnotation[]> = of([]); + public errorMessage = ""; + + public cluster: ClusterManager = new ClusterManager(); + constructor(public domainService: DomainService, protected userService: UserService, private router: Router, @@ -70,6 +75,9 @@ export class DomainComponent extends BaseComponent implements OnInit { this.domainService.getOne(this.domainId).subscribe( (domain: Domain) => { this.domain = domain; + if(domain.clusters.length >0) { + this.cluster = domain.clusters[0]; + } this.domain.applicationStatePerDomain .sort((a, b) => a.applicationBaseName.localeCompare(b.applicationBaseName)) }, @@ -103,7 +111,14 @@ export class DomainComponent extends BaseComponent implements OnInit { if (this.domainId !== undefined) { this.updateExistingDomain(); } else { - this.domainService.add(this.domain).subscribe(() => this.router.navigate(['admin/domains/'])); + this.domainService.add(this.domain).subscribe(() => { + this.router.navigate(['admin/domains/']) + }, err => { + console.error(err); + if(err.statusCode !== 409 && err?.message !== undefined) this.errorMessage = err.message; + else this.errorMessage = err; + + }); } this.domainService.setUpdateRequiredFlag(true); } diff --git a/src/app/appmarket/domains/domains.routes.ts b/src/app/appmarket/domains/domains.routes.ts index 12fb5a90bfaa0a6e3363b6e89855cebc7bbc4f9f..65701e24f5b398eecaa6d87baf0bee021ffb024c 100644 --- a/src/app/appmarket/domains/domains.routes.ts +++ b/src/app/appmarket/domains/domains.routes.ts @@ -12,46 +12,46 @@ import { DomainAnnotationsComponent } from './domain-annotations/domain-annotati export const DomainsRoutes: Route[] = [ { - path: 'admin/domains', component: DomainsListComponent, canActivate: [AuthGuard, RoleGuard], - data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_DOMAIN_ADMIN', 'ROLE_OPERATOR', 'ROLE_VL_DOMAIN_ADMIN', 'ROLE_VL_MANAGER']} + path: 'domains', component: DomainsListComponent, canActivate: [AuthGuard, RoleGuard], + data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_DOMAIN_ADMIN', 'ROLE_OPERATOR', 'ROLE_GROUP_DOMAIN_ADMIN', 'ROLE_GROUP_MANAGER']} }, { - path: 'admin/domains/add', component: DomainComponent, canActivate: [AuthGuard, RoleGuard], + path: 'domains/add', component: DomainComponent, canActivate: [AuthGuard, RoleGuard], data: {mode: ComponentMode.CREATE, roles: ['ROLE_SYSTEM_ADMIN']} }, { - path: 'admin/domains/annotations', component: DomainAnnotationsComponent, canActivate: [AuthGuard, RoleGuard], + path: 'domains/annotations', component: DomainAnnotationsComponent, canActivate: [AuthGuard, RoleGuard], data: {mode: ComponentMode.CREATE, roles: ['ROLE_SYSTEM_ADMIN']} }, { - path: 'admin/domains/view/:id', component: DomainComponent, canActivate: [AuthGuard, RoleGuard], - data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_DOMAIN_ADMIN', 'ROLE_OPERATOR', 'ROLE_VL_DOMAIN_ADMIN']} + path: 'domains/view/:id', component: DomainComponent, canActivate: [AuthGuard, RoleGuard], + data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_DOMAIN_ADMIN', 'ROLE_OPERATOR', 'ROLE_GROUP_DOMAIN_ADMIN']} }, { - path: 'admin/domains/edit/:id', component: DomainComponent, canActivate: [AuthGuard, RoleGuard], + path: 'domains/edit/:id', component: DomainComponent, canActivate: [AuthGuard, RoleGuard], data: {mode: ComponentMode.EDIT, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']} }, { - path: 'admin/domains/groups', component: DomainGroupsComponent, canActivate: [AuthGuard, RoleGuard], - data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']} + path: 'domains/groups', component: DomainGroupsComponent, canActivate: [AuthGuard, RoleGuard], + data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']} }, { - path: 'admin/domains/groups/add', component: DomainGroupViewComponent, canActivate: [AuthGuard, RoleGuard], - data: {mode: ComponentMode.CREATE, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']} + path: 'domains/groups/add', component: DomainGroupViewComponent, canActivate: [AuthGuard, RoleGuard], + data: {mode: ComponentMode.CREATE, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']} }, { - path: 'admin/domains/groups/:id', component: DomainGroupViewComponent, canActivate: [AuthGuard, RoleGuard], - data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']} + path: 'domains/groups/:id', component: DomainGroupViewComponent, canActivate: [AuthGuard, RoleGuard], + data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']} }, { - path: 'admin/domains/bulks/new', component: DomainuploadComponent, - data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']}}, + path: 'domains/bulks/new', component: DomainuploadComponent, + data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']}}, { - path: 'admin/domains/bulks', component: BulkDomainListComponent, canActivate: [AuthGuard, RoleGuard], - data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']} + path: 'domains/bulks', component: BulkDomainListComponent, canActivate: [AuthGuard, RoleGuard], + data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']} }, { - path: 'admin/domains/bulks/:id', component: BulkViewComponent, canActivate: [AuthGuard, RoleGuard], - data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']} + path: 'domains/bulks/:id', component: BulkViewComponent, canActivate: [AuthGuard, RoleGuard], + data: {roles: ['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']} } ]; diff --git a/src/app/appmarket/domains/list/domainslist.component.css b/src/app/appmarket/domains/list/domainslist.component.css index 4b35785d88db8ee0325b8ab438f15b17163535c1..f53cede1f87da0dc9913a49b194585761fc63a40 100644 --- a/src/app/appmarket/domains/list/domainslist.component.css +++ b/src/app/appmarket/domains/list/domainslist.component.css @@ -13,66 +13,18 @@ tr.clickable { cursor: pointer; } -.dropdown:hover .dropdown-menu { - display: block; -} +/*.dropdown:hover .dropdown-menu {*/ +/* display: block;*/ +/*}*/ .space-between { display: flex; justify-content: space-between; } - -:host ::ng-deep .p-datatable .p-datatable-thead > tr > th{ - border: 1px solid #E0E2E5; - background:transparent; - border-width: 0 0 1px 0; -} -:host ::ng-deep .p-datatable .p-datatable-tbody > tr > td { - text-align: left; - border: 1px solid #E0E2E5; - border-width: 0 0 1px 0; - padding: 1rem 1rem; -} -:host ::ng-deep .p-datatable .p-paginator-bottom{ - height: 40px; - background: transparent; - border: none; - margin-top:10px; -} -:host ::ng-deep .p-datatable .p-datatable-tbody > tr{ - background: transparent; -} - -:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page{ - transition: unset; - border-radius: 50%; - min-width:3.5rem; - height:3.5rem; - margin:0 5px; - font-size: 14px; -} - -:host ::ng-deep .p-paginator-element{ - border-radius:50%; - margin:0 5px; - min-width:3.5rem; - height:3.5rem; - font-size: 14px; -} -:host ::ng-deep .p-paginator .p-dropdown{ - height:3rem; -} -:host ::ng-deep .p-paginator-icon{ - height: 1.5rem; - width: 1.5rem; -} -:host ::ng-deep .p-paginator .p-dropdown .p-dropdown-label{ - padding-right: 10px; -} - label{ padding-left:5px; display: unset; margin-bottom: 0; font-weight: unset; } + diff --git a/src/app/appmarket/domains/list/domainslist.component.html b/src/app/appmarket/domains/list/domainslist.component.html index 53b85f9f4ab3e1f11e921ba8274eddd9a5a249b1..5a279e5cd877cd3b2c0ba1978ecc8549aa6e3dd8 100644 --- a/src/app/appmarket/domains/list/domainslist.component.html +++ b/src/app/appmarket/domains/list/domainslist.component.html @@ -18,7 +18,7 @@ <label for="showNotActive"> {{'DOMAINS.NOTACTIVE' | translate}}</label> </div> </div> - <h4 style="margin-top:40px; font-weight: bold">{{ 'DOMAINS.TITLE' | translate }}</h4> + <h4 class="header">{{ 'DOMAINS.TITLE' | translate }}</h4> <div class="background-section"> <p-table [value]="domains | async | searchDomain: searchValue: showNotActive" @@ -44,35 +44,33 @@ </tr> </ng-template> <ng-template pTemplate="body" let-domain> - <tr [routerLink]="['view/', domain.id]" *ngIf="!domain.deleted"> - <td>{{domain?.codename}}</td> + <tr *ngIf="!domain.deleted"> + <td [routerLink]="['view/', domain.id]">{{domain?.codename}}</td> <td>{{domain?.name}}</td> <td> <span class="glyphicon glyphicon-ok" *ngIf="domain?.active"></span> <span class="glyphicon glyphicon-remove" *ngIf="!(domain?.active)"></span> </td> <td class="text-right"> - <span class="dropdown"> - <a style="display: inline-block; text-align:right" class="dropdown-toggle " aria-expanded="false" - aria-haspopup="true" - data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> - </a> - <ul class="dropdown-menu pull-right-drop" [appendTo]="'body'" > - <li><a [routerLink]="['view/', domain.id]" class=""> - {{ 'DOMAINS.DETAILS_BUTTON' | translate }}</a> - </li> - <li><a *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']" [routerLink]="['edit/', domain.id]" - class="">{{ 'DOMAINS.EDIT_BUTTON' | translate }}</a> - </li> - <li><a *roles="['ROLE_SYSTEM_ADMIN']" (click)="$event.stopPropagation(); changeState(domain)" - class="">{{ getStateLabel(domain?.active) }}</a> - </li> - <li><a *roles="['ROLE_SYSTEM_ADMIN']" (click)="$event.stopPropagation(); openRemovalModal(domain)" - class="">{{ 'DOMAINS.DELETE_BUTTON' | translate }}</a> - </li> - </ul> - </span> + <span class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle" aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> + <em class="pi pi-cog" style="font-size: 1.8rem; color: var(--l-text-color)"></em> + </a> + <ul class="dropdown-menu pull-right-drop" [appendTo]="'body'" > + <li><a [routerLink]="['view/', domain.id]" class=""> + {{ 'DOMAINS.DETAILS_BUTTON' | translate }}</a> + </li> + <li><a *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']" [routerLink]="['edit/', domain.id]" + class="">{{ 'DOMAINS.EDIT_BUTTON' | translate }}</a> + </li> + <li><a *roles="['ROLE_SYSTEM_ADMIN']" (click)="$event.stopPropagation(); changeState(domain)" + class="">{{ getStateLabel(domain?.active) }}</a> + </li> + <li><a *roles="['ROLE_SYSTEM_ADMIN']" (click)="$event.stopPropagation(); openRemovalModal(domain)" + class="">{{ 'DOMAINS.DELETE_BUTTON' | translate }}</a> + </li> + </ul> + </span> </td> </tr> </ng-template> diff --git a/src/app/appmarket/domains/list/domainslist.component.spec.ts b/src/app/appmarket/domains/list/domainslist.component.spec.ts index d82cc80409c2a22df51857dfa53c48528ade455d..d4acc1fe17cb3df9bf196fae53136cff9b88de6d 100644 --- a/src/app/appmarket/domains/list/domainslist.component.spec.ts +++ b/src/app/appmarket/domains/list/domainslist.component.spec.ts @@ -19,7 +19,8 @@ describe('DomainslistComponent', () => { const authServiceSpy = createSpyObj('AuthService', ['hasRole']); authServiceSpy.hasRole.and.returnValue(true) - const domainServiceSpy = createSpyObj('DomainService', ['getGlobalDomainId', 'getAll']) + const domainServiceSpy = createSpyObj('DomainService', ['getGlobalDomainId', 'getAll', 'getAllBase']); + domainServiceSpy.getAllBase.and.returnValue(of([])) domainServiceSpy.getAll.and.returnValue(of([])) domainServiceSpy.getGlobalDomainId.and.returnValue(1) diff --git a/src/app/appmarket/domains/list/domainslist.component.ts b/src/app/appmarket/domains/list/domainslist.component.ts index 133d11d612c2f1f61336315499b654c48d2e0b5d..c86a3150db827baa4177c60f44e799babf1bee33 100644 --- a/src/app/appmarket/domains/list/domainslist.component.ts +++ b/src/app/appmarket/domains/list/domainslist.component.ts @@ -58,11 +58,11 @@ export class DomainsListComponent implements OnInit { protected getDomainsObservable(): Observable<Domain[]> { if (this.authService.hasRole(Role[Role.ROLE_SYSTEM_ADMIN]) || this.authService.hasRole(Role[Role.ROLE_OPERATOR])) { - return this.domainService.getAll().pipe( + return this.domainService.getAllBase().pipe( map((domains) => domains.filter((domain) => domain.id !== this.domainService.getGlobalDomainId()))); } else { return this.domainService.getMyDomains().pipe( - map((domains) => domains.filter((domain) => this.authService.hasDomainRole(domain.id, Role[Role.ROLE_DOMAIN_ADMIN]) || this.authService.hasDomainRole(domain.id, Role[Role.ROLE_VL_DOMAIN_ADMIN])))); + map((domains) => domains.filter((domain) => this.authService.hasDomainRole(domain.id, Role[Role.ROLE_DOMAIN_ADMIN]) || this.authService.hasDomainRole(domain.id, Role[Role.ROLE_GROUP_DOMAIN_ADMIN])))); } } diff --git a/src/app/appmarket/users/list/userslist.component.html b/src/app/appmarket/users/list/userslist.component.html index 6d9333ad9f77cb06d0cc389dc13401150af6c16e..b5540012889ac83ee00f113ef2a46c7e17626cf0 100644 --- a/src/app/appmarket/users/list/userslist.component.html +++ b/src/app/appmarket/users/list/userslist.component.html @@ -1,4 +1,4 @@ -<div class="col-sm-12" *ngIf="!domainMode" > +<div class="" *ngIf="!domainMode" > <div *roles="['ROLE_SYSTEM_ADMIN']" > <nmaas-userslist *ngIf="!isInAddToDomainMode" [users]="allUsers" [allowedModes]="[ComponentMode.VIEW, ComponentMode.DELETE]" (onUserRoleChange)="onUserRoleChange($event)" (onView)="onUserView($event)" (onModeChange)="onModeChange($event)" (onDelete)="onUserDelete($event)" (onRemoveFromDomain)="onRemoveRole($event)"> @@ -10,8 +10,8 @@ </div> -<div class="col-sm-12" *ngIf="domainMode"> - <div *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_VL_MANAGER']"> +<div class="" *ngIf="domainMode"> + <div *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_GROUP_MANAGER']"> <nmaas-userslist *ngIf="!isInAddToDomainMode" [users]="allUsers" [allowedModes]="[ComponentMode.VIEW, ComponentMode.DELETE]" [domainMode]="true" (onUserRoleChange)="onUserRoleChange($event)" (onView)="onUserView($event)" (onModeChange)="onModeChange($event)" (onDelete)="onUserDelete($event)" (onRemoveFromDomain)="onRemoveRole($event)"> </nmaas-userslist> diff --git a/src/app/appmarket/users/list/userslist.component.ts b/src/app/appmarket/users/list/userslist.component.ts index 3bb01ecd9dd280bf7bfc623d7a34e26f2d3e1727..dbcbc5545d6dd437cc30d86fa55c74823a1e8807 100644 --- a/src/app/appmarket/users/list/userslist.component.ts +++ b/src/app/appmarket/users/list/userslist.component.ts @@ -57,7 +57,7 @@ export class UsersListComponent implements OnInit { users = this.userService.getDomainUsersAsAdmin(this.domainId); } else if (this.authService.hasRole(Role[Role.ROLE_SYSTEM_ADMIN])) { users = this.userService.getAll(this.domainId); - } else if (this.domainId != null && (this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_DOMAIN_ADMIN]) || this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_VL_DOMAIN_ADMIN]))) { + } else if (this.domainId != null && (this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_DOMAIN_ADMIN]) || this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_GROUP_DOMAIN_ADMIN]))) { this.domainMode = true; users = this.userService.getAll(this.domainId); } else { diff --git a/src/app/appmarket/users/userdetails/userdetails.component.html b/src/app/appmarket/users/userdetails/userdetails.component.html index f728a79ce158c48bcbe6db89c05d4452d03a47c3..64eeb8ac7800129a795c0e1648aded5819dadbc5 100644 --- a/src/app/appmarket/users/userdetails/userdetails.component.html +++ b/src/app/appmarket/users/userdetails/userdetails.component.html @@ -1,5 +1,5 @@ <div class="col-sm-12 col-sm-12 col-md-12"> - <div class="page-header"> + <div class=""> <h3> {{'USER_DETAILS.USER' | translate}} {{user?.username}} </h3> diff --git a/src/app/appmarket/users/users.routes.ts b/src/app/appmarket/users/users.routes.ts index abe74e43673b587888d86cf984e1d54252e1e2fa..5c91077d5ce0107de3baff5206b3277b5f4401bd 100644 --- a/src/app/appmarket/users/users.routes.ts +++ b/src/app/appmarket/users/users.routes.ts @@ -5,10 +5,10 @@ import {RoleGuard} from '../../auth/role.guard'; import {ComponentMode} from '../../shared/common/componentmode'; export const UsersRoutes: Route[] = [ - { path: 'admin/users', component: UsersListComponent, canActivate: [AuthGuard, RoleGuard], + { path: 'users', component: UsersListComponent, canActivate: [AuthGuard, RoleGuard], data: {roles: ['ROLE_SYSTEM_ADMIN']}}, - { path: 'admin/users/view/:id', component: UserDetailsComponent, canActivate: [AuthGuard, RoleGuard], + { path: 'users/view/:id', component: UserDetailsComponent, canActivate: [AuthGuard, RoleGuard], data: {mode: ComponentMode.VIEW, roles: ['ROLE_SYSTEM_ADMIN']} }, { path: 'domain/users', component: UsersListComponent, canActivate: [AuthGuard, RoleGuard], - data: {roles: ['ROLE_DOMAIN_ADMIN', 'ROLE_VL_MANAGER', 'ROLE_VL_MANAGER']}}, + data: {roles: ['ROLE_DOMAIN_ADMIN', 'ROLE_GROUP_MANAGER']}}, ]; diff --git a/src/app/auth/auth.guard.ts b/src/app/auth/auth.guard.ts index 2fe88c217381e865b5ed27e7a814578d5c59aa32..f0c2074acada30084334a6ff28f2cd1b0686c995 100644 --- a/src/app/auth/auth.guard.ts +++ b/src/app/auth/auth.guard.ts @@ -4,7 +4,7 @@ import {AuthService} from './auth.service'; import {ConfigurationService} from '../service'; import { debounceTime } from 'rxjs'; -@Injectable() +@Injectable() export class AuthGuard { constructor(private auth: AuthService, private router: Router, private maintenanceService: ConfigurationService) {} @@ -12,13 +12,7 @@ export class AuthGuard { public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { if (this.auth.isLogged()) { - this.maintenanceService.getConfiguration().pipe(debounceTime(500)).subscribe(value => { - if (!this.auth.hasRole('ROLE_SYSTEM_ADMIN') && value.maintenance) { - this.auth.logout(); - this.router.navigate(['/welcome/login']); - return false; - } - }); + if(this.auth.hasRole('ROLE_INCOMPLETE') && route.url.toString() !== 'complete') { this.router.navigate(['/complete']); return false; diff --git a/src/app/auth/auth.service.spec.ts b/src/app/auth/auth.service.spec.ts index 6b0bb367feea3e0729c83c0209409e8d8d32dace..9e5569e278803124096a72dec6c7172feb75e927 100644 --- a/src/app/auth/auth.service.spec.ts +++ b/src/app/auth/auth.service.spec.ts @@ -1,16 +1,20 @@ /* tslint:disable:no-unused-variable */ -import { TestBed, waitForAsync } from '@angular/core/testing'; +import {fakeAsync, TestBed, waitForAsync} from '@angular/core/testing'; import {AuthService} from './auth.service'; -import {AppConfigService} from '../service'; +import {AppConfigService, ConfigurationService} from '../service'; import {JwtHelperService} from '@auth0/angular-jwt'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; -import {Role} from '../model/userrole'; +import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; +import {Role, UserRole} from '../model/userrole'; +import {ProfileService} from '../service/profile.service'; +import {Observable, of} from 'rxjs'; +import {Configuration} from '../model/configuration'; describe('Service: Auth', () => { let authService: AuthService; let appConfigServiceSpy: jasmine.SpyObj<AppConfigService>; let jwtHelperServiceSpy: jasmine.SpyObj<JwtHelperService>; - + let maintenanceServiceSpy: jasmine.SpyObj<ConfigurationService>; + let httpMock: HttpTestingController; let store: any = {}; beforeEach(waitForAsync(() => { @@ -18,34 +22,75 @@ describe('Service: Auth', () => { config: { apiUrl: 'http://api.url', tokenName: 'token', - } + }, + getTestInstanceModalKey: () => 'testModalKey' }; - const jwtSpy = jasmine.createSpyObj('JwtHelperService', ['decodeToken', 'isTokenExpired']); jwtSpy.decodeToken.and.returnValue({ + preferred_username: 'username', language: 'pl', sub: 'test-user', - scopes: [{authority: '1:' + Role[Role.ROLE_SYSTEM_ADMIN]}, {authority: '2:' + Role[Role.ROLE_USER]}] + global_role: ['ROLE_SYSTEM_ADMIN'], + roles: [`ROLE_USER`] }); jwtSpy.isTokenExpired.and.callFake((arg: string): boolean => { return arg !== 'valid'; }); + maintenanceServiceSpy = jasmine.createSpyObj('maintenanceService', ['getConfiguration']); + maintenanceServiceSpy.getConfiguration.and.returnValue(of()) + + class MockConfigurationService { + protected uri: string; + + constructor() { + this.uri = 'http://localhost/api'; + } + + public getApiUrl(): string { + return 'http://localhost/api'; + } + + public getConfiguration(): Observable<Configuration> { + return of<Configuration>(); + } + + public updateConfiguration(configuration: Configuration): Observable<any> { + return of<Configuration>(); + } + } + + + const userRole = new UserRole(); + userRole.role = Role.ROLE_SYSTEM_ADMIN; + userRole.domainName = 'test'; + userRole.domainId = 1; + const userRole2 = new UserRole(); + userRole2.role = Role.ROLE_USER; + userRole2.domainName = 'test2'; + userRole2.domainId = 2; + const profileServiceStub = jasmine.createSpyObj('ProfileService', ['getRoles']); + profileServiceStub.getRoles.and.returnValue(of([userRole, userRole2])) + TestBed.configureTestingModule({ imports: [ - HttpClientTestingModule + HttpClientTestingModule, ], providers: [ AuthService, {provide: AppConfigService, useValue: appConfigServiceStub}, {provide: JwtHelperService, useValue: jwtSpy}, + {provide: ProfileService, useValue: profileServiceStub}, + {provide: ConfigurationService, useClass: MockConfigurationService} ], }); + httpMock = TestBed.inject(HttpTestingController) authService = TestBed.get(AuthService); + authService.profile = [userRole, userRole2] appConfigServiceSpy = TestBed.get(AppConfigService); jwtHelperServiceSpy = TestBed.get(JwtHelperService); - // spyOn(appConfigServiceSpy, 'getTestInstanceModalKey').and.returnValue("test-instance-modal-key"); + // maintenanceServiceSpy = TestBed.get(ConfigurationService); // local store mock store = {token: 'valid'}; @@ -61,6 +106,10 @@ describe('Service: Auth', () => { }); })); + afterEach(() => { + httpMock.verify(); + store = {}; + }); it('should create service', () => { expect(authService).toBeTruthy(); @@ -99,7 +148,7 @@ describe('Service: Auth', () => { }); it('should return domains from roles', () => { - const result = authService.getDomains(); + const result = authService.getDomains(); expect(result).toContain(1); expect(result).toContain(2); store = {token: null}; @@ -137,8 +186,11 @@ describe('Service: Auth', () => { }); it('should remove token on logout', () => { + store['oidc-token'] = 'some-oidc-token'; authService.logout(); expect(store['token']).not.toBeDefined(); + const req = httpMock.expectOne('http://api.url/oidc/logout/some-oidc-token'); + req.flush({}); }); it('should be logged in when token is present and valid', () => { @@ -154,4 +206,79 @@ describe('Service: Auth', () => { expect(r).toEqual(false); }); + it('should store token and oidc token in localStorage', () => { + authService.storeToken('abc123'); + expect(store['token']).toEqual('abc123'); + + authService.storeOidcToken('oidc456'); + expect(store['oidc-token']).toEqual('oidc456'); + }); + + it('should remove roles from localStorage', () => { + store['rolesToken'] = 'some_roles'; + authService.removeRoles(); + expect(store['rolesToken']).toBeUndefined(); + }); + + it('should load and parse roles from localStorage', () => { + const roles = [{domainId: 1, role: Role.ROLE_USER, domainName: 'x'}]; + store['rolesToken'] = JSON.stringify(roles); + + const result = authService.loadRoles(); + expect(result.length).toEqual(1); + expect(result[0].role).toEqual(Role.ROLE_USER); + }); + + it('should assign loaded roles to profile', () => { + const roles = [{domainId: 2, role: Role.ROLE_DOMAIN_ADMIN, domainName: 'x'}]; + store['rolesToken'] = JSON.stringify(roles); + authService.loadAndSaveRoles(); + expect(authService.profile[0].role).toEqual(Role.ROLE_DOMAIN_ADMIN); + }); + it('should stringify and store roles', () => { + const roles = [new UserRole()]; + roles[0].domainId = 1; + roles[0].role = Role.ROLE_USER; + roles[0].domainName = 'dom1'; + + authService.storeRoles(roles); + expect(store['rolesToken']).toContain('ROLE_USER'); + }); + it('should get global role from token', () => { + const result = authService.getGlobalRole(); + expect(result).toContain('ROLE_SYSTEM_ADMIN'); + }); + + it('should handle login error with catchError', waitForAsync(() => { + authService.login('user', 'pass').subscribe({ + next: () => fail('Expected error'), + error: (err) => { + expect(err.status).toEqual(401); + } + }); + + const req = httpMock.expectOne('http://api.url/auth/basic/login'); + req.flush({message: 'Invalid credentials'}, {status: 401, statusText: 'Unauthorized'}); + })); + + it('should return only uniqe domainids', () => { + (authService as any).profile = [ + {domainId: 1}, {domainId: 2}, {domainId: 1} + ]; + const ids = authService.getDomainIds(); + expect(ids).toEqual([1, 2]); + }); + + it('getGlobalRole should return null when token is missing', () => { + delete store['token']; + expect(authService.getGlobalRole()).toBeNull(); + }); + + it('getPreferredUsername should return preferred_username or null', () => { + const name = authService.getPreferredUsername(); + expect(name).toBe('username'); + delete store['token']; + expect(authService.getPreferredUsername()).toBeNull(); + }); + }); diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts index ad7c6b8fcfa0166568751f3f9e1e3b4aa1a11938..2734f0b6b2f63738ed46639c80fbcfb10ff04aeb 100644 --- a/src/app/auth/auth.service.ts +++ b/src/app/auth/auth.service.ts @@ -1,17 +1,18 @@ -import {BehaviorSubject, Observable, Subject, throwError as observableThrowError} from 'rxjs'; +import {BehaviorSubject, Observable, of, Subject, throwError as observableThrowError} from 'rxjs'; import {catchError, debounceTime, map} from 'rxjs/operators'; import {Injectable} from '@angular/core'; -import {AppConfigService} from '../service'; +import {AppConfigService, ConfigurationService} from '../service'; import {JwtHelperService} from '@auth0/angular-jwt'; import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {Authority} from '../model'; +import {ProfileService} from '../service/profile.service'; +import {Role, UserRole} from '../model/userrole'; -export class DomainRoles { - constructor(private domainId: number, private roles: string[] = []) { - } - public getDomainId(): number { - return this.domainId; +export class DomainRoles { + constructor( + private domainId: number, + private roles: string[] = [] + ) { } public getRoles(): string[] { @@ -25,28 +26,132 @@ export class DomainRoles { @Injectable() export class AuthService { + + private static REFRESH_TOKEN: string = 'refresh-token'; + private static OIDC_TOKEN: string = 'oidc-token'; + public loginUsingSsoService: boolean; private readonly isLoggedInSubject: Subject<boolean> = new BehaviorSubject<boolean>(false); + public profile: UserRole[] + + private rolesTabelName = 'rolesToken' + + private refresh: any; + + private maintenance: boolean = false; constructor(private http: HttpClient, private appConfig: AppConfigService, - private jwtHelper: JwtHelperService) { + private jwtHelper: JwtHelperService, + private profileService: ProfileService, + private maintenanceService: ConfigurationService) { + this.loadAndSaveRoles(); + this.loadUser() + this.getConfigurationToCheckMaintenance(); } - //TODO make this static again and serive this feature in other way + + public loadUser(): void { + + this.profileService.getRoles().subscribe(roles => { + this.profile = roles + this.storeRoles(roles) + }) + } + + public refreshUserRoles(): void { + this.refresh = setInterval(() => { + if (this.isLogged()) { + this.refreshToken() + this.loadUser(); + } + }, 60000); + } + + private getConfigurationToCheckMaintenance() { + this.maintenanceService.getConfiguration().subscribe(value => { + if (value !== undefined && value !== null && value.maintenance) { + console.warn('Maintenance is on. Disabled login.') + this.isLoggedInSubject.next(false); + this.logout(); + this.maintenance = true; + return false; + } + }); + } + public storeToken(token: string): void { localStorage.setItem(this.appConfig.config.tokenName, token); } + public storeRefreshToken(token: string): void { + localStorage.setItem(AuthService.REFRESH_TOKEN, token); + } + + public storeOidcToken(token: string): void { + localStorage.setItem(AuthService.OIDC_TOKEN, token); + } + + public refreshToken() { + return this.http.post<any>(this.appConfig.config.apiUrl + '/auth/basic/token', { + refreshToken: this.getRefreshToken() + }).subscribe(response => { + const token = response && response['token']; + const refreshToken = response && response[AuthService.REFRESH_TOKEN]; + if (token) { + this.storeToken(token); + } + if (refreshToken) { + this.storeRefreshToken(refreshToken) + } + }) + + } + + public storeRoles(roles: UserRole[]): void { + const rolesString = JSON.stringify(roles); + localStorage.setItem(this.rolesTabelName, rolesString); + } + + public loadAndSaveRoles() { + this.profile = this.loadRoles(); + } + + public loadRoles(): UserRole[] { + const rolesString = localStorage.getItem(this.rolesTabelName); + if (!rolesString) { + return null; + } + + const parsed = JSON.parse(rolesString); + return parsed.map((item: any) => Object.assign(new UserRole(), item)); + } + + public removeRoles(): void { + localStorage.removeItem(this.rolesTabelName) + } + private getToken(): string { return localStorage.getItem(this.appConfig.config.tokenName) } + private getOidcToken(): string { + return localStorage.getItem(AuthService.OIDC_TOKEN) + } + + private getRefreshToken(): string { + return localStorage.getItem(AuthService.REFRESH_TOKEN) + } + private removeToken(): void { localStorage.removeItem(this.appConfig.config.tokenName); } + private removeOidcToken(): void { + localStorage.removeItem(AuthService.OIDC_TOKEN); + } + public getSelectedLanguage(): string { if (localStorage.getItem('lang') != null) { return localStorage.getItem('lang') @@ -59,62 +164,60 @@ export class AuthService { return (token ? this.jwtHelper.decodeToken(token).sub : null); } - public hasRole(name: string): boolean { + public getPreferredUsername(): string { const token = this.getToken(); - const authorities: Authority[] = this.jwtHelper.decodeToken(token).scopes; - for (let i = 0; i < authorities.length; i++) { - if (authorities[i].authority.indexOf(name) > -1) { + return (token ? this.jwtHelper.decodeToken(token).preferred_username : null); + } + + public hasRole(name: string): boolean { + + const roles = this.getRoles() + + for (const role of roles) { + if (role === name) { return true; } } return false; + } public hasDomainRole(domainId: number, name: string): boolean { - const token = this.getToken(); - const authorities: Authority[] = this.jwtHelper.decodeToken(token).scopes; - for (let i = 0; i < authorities.length; i++) { - if (authorities[i].authority.indexOf(domainId + ':' + name) > -1) { - return true; + let result = false; + const domainRoles: Map<number, DomainRoles> = this.getDomainRoles(); + for (const [mapDomainId, domainRolesValue] of domainRoles) { + if (mapDomainId === domainId) { + domainRolesValue.getRoles().forEach(role => { + if (role === name) { + result = true; + } + }) } } - return false; + return result; } - public getDomainRoles(): Map<number, DomainRoles> { - const drMap: Map<number, DomainRoles> = new Map<number, DomainRoles>(); - + public getGlobalRole(): string[] { const token = this.getToken(); if (token == null) { - return drMap; + return null; } + return this.jwtHelper.decodeToken(token).global_role; + } - const authorities: Authority[] = this.jwtHelper.decodeToken(token).scopes; - if (authorities == null) { - return drMap; - } + public getDomainRoles(): Map<number, DomainRoles> { + const domainRolesMap: Map<number, DomainRoles> = new Map<number, DomainRoles>(); - for (let index = 0; index < authorities.length; index++) { - if (authorities[index].authority === undefined) { - continue; - } + const domains: number[] = this.getDomains(); + for (const domain of domains) { + const roles: string[] = this.profile + .filter(userRole => userRole.domainId === domain) + .map(userRole => Role[userRole.role]) - const domainRole: string[] = authorities[index].authority.split(':', 2); - if (domainRole.length !== 2) { - continue; - } - const domainId: number = Number.parseInt(domainRole[0], 10); - const role: string = domainRole[1]; + domainRolesMap.set(domain, new DomainRoles(domain, roles)); - let dr: DomainRoles; - if (!drMap.has(domainId)) { - drMap.set(domainId, new DomainRoles(domainId, [])); - } - dr = drMap.get(domainId); - dr.getRoles().push(role); } - - return drMap; + return domainRolesMap; } public getRoles(): string[] { @@ -124,56 +227,35 @@ export class AuthService { if (token == null) { return roles; } + const domainRoles: string[] = this.jwtHelper.decodeToken(token).roles; + const globalRole: string[] = this.jwtHelper.decodeToken(token).global_role; - const authorities: Authority[] = this.jwtHelper.decodeToken(token).scopes; - for (let index = 0; index < authorities.length; index++) { - if (authorities[index].authority === undefined) { - continue; - } + roles.push(globalRole[0]); - const domainRole: string[] = authorities[index].authority.split(':', 2); - if (domainRole.length !== 2) { - continue; - } - const role: string = domainRole[1]; - if (roles.indexOf(role) === -1) { - roles.push(role); - } + for (const role of domainRoles) { + + roles.push(role); } + return roles; } public getDomains(): number[] { - const domains: number[] = []; - - const token = this.getToken(); - if (token == null) { - return domains; - } - - const authorities: Authority[] = this.jwtHelper.decodeToken(token).scopes; - - for (let index = 0; index < authorities.length; index++) { - if (authorities[index].authority === undefined) { - continue; + if (this.isLogged()) { + if (this.profile !== undefined && this.profile !== null) { + return this.getDomainIds(); + } else { + return []; } - const domainIdStr: string[] = authorities[index].authority.split(':', 1); - if (domainIdStr.length === 0) { - continue; - } - const domainId: number = Number.parseInt(domainIdStr[0], 10); - if (domains.indexOf(domainId) === -1) { - domains.push(domainId); - } } - return domains; + return []; + } public getDomainsWithRole(name: string): number[] { const domainsWithRole: number[] = []; - const domains: number[] = this.getDomains(); domains.forEach((domainId) => { if (this.hasDomainRole(domainId, name)) { @@ -184,10 +266,60 @@ export class AuthService { return domainsWithRole; } + public oidcLinkingLogin(oidcToken: string, + email: string, + password: string, + uuid: string, + firstName: string, + lastName: string) { + const headers = new HttpHeaders({'Content-Type': 'application/json', 'Accept': 'application/json'}); + + return this.http.post(this.appConfig.config.apiUrl + '/oidc/link', + JSON.stringify( + { + 'oidcToken': oidcToken, + 'email': email, + 'password': password, + 'uuid': uuid, + 'firstName': firstName, + 'lastName': lastName, + } + ), + {headers: headers}).pipe( + debounceTime(1000), + map((res: Response) => { + const token = res && res['token']; + const oidcToken = res && res['oidcToken']; + if (token && oidcToken) { + this.storeToken(token); + this.storeOidcToken(oidcToken); + this.loginUsingSsoService = false; + this.isLoggedInSubject.next(true); + this.profileService.getRoles().subscribe(profile => { + this.profile = profile + this.storeRoles(profile); + return true; + }) + } else { + this.isLoggedInSubject.next(false); + return false; + } + } + ), + ) + } + public login(username: string, password: string): Observable<boolean> { // hack so test instance modal is shown onl after login localStorage.setItem(this.appConfig.getTestInstanceModalKey(), 'True'); + if (this.maintenance) { + this.isLoggedInSubject.next(false); + console.warn('Maintenance is on. Disabled login.') + //add toast here + return of(false); + } + const headers = new HttpHeaders({'Content-Type': 'application/json', 'Accept': 'application/json'}); return this.http.post(this.appConfig.config.apiUrl + '/auth/basic/login', JSON.stringify({'username': username, 'password': password}), {headers: headers}).pipe( @@ -196,9 +328,11 @@ export class AuthService { console.debug('Login response: ' + response.statusText); // login successful if there's a jwt token in the response const token = response && response['token']; - if (token) { + const refreshToken = response && response[AuthService.REFRESH_TOKEN]; + if (token && refreshToken) { // set token property this.storeToken(token); + this.storeRefreshToken(refreshToken) console.debug('AUTH | User: ' + this.getUsername()); console.debug('AUTH | Domains: ' + this.getDomains()); @@ -206,7 +340,11 @@ export class AuthService { console.debug('AUTH | DomainRoles: ' + this.getDomainRoles()); this.loginUsingSsoService = false; this.isLoggedInSubject.next(true); - return true; + this.profileService.getRoles().subscribe(profile => { + this.profile = profile + this.storeRoles(profile); + return true; + }) } else { // return false to indicate failed login this.isLoggedInSubject.next(false); @@ -226,48 +364,33 @@ export class AuthService { })); } - public propagateSSOLogin(userid: string): Observable<boolean> { - console.debug('propagateSSOLogin'); - console.debug('propagateSSOLogin ' + this.appConfig.config.apiUrl); - console.debug('propagateSSOLogin ' + this.appConfig.config.apiUrl + '/auth/sso/login'); - console.debug('propagateSSOLogin ' + userid); - // hack so test instance modal is shown onl after login - localStorage.setItem(this.appConfig.getTestInstanceModalKey(), 'True'); - - const headers = new HttpHeaders({'Content-Type': 'application/json', 'Accept': 'application/json'}); - return this.http.post(this.appConfig.config.apiUrl + '/auth/sso/login', - JSON.stringify({'userid': userid}), {headers: headers}).pipe( - debounceTime(10000), - map((response: Response) => { - console.debug('SSO login response: ' + response); - // login successful if there's a jwt token in the response - const token = response && response['token']; + public logout(): void { + const oidcToken = this.getOidcToken(); + this.refresh = null; + if (oidcToken === null) { + this.removeToken(); + this.isLoggedInSubject.next(false); + localStorage.removeItem('_expiredTime'); + } else { + this.removeToken(); + this.removeOidcToken(); + this.isLoggedInSubject.next(false); + localStorage.removeItem('_expiredTime'); + this.http.get(this.appConfig.config.apiUrl + '/oidc/logout/' + oidcToken).subscribe(() => { + }) + } + } - if (token) { - this.storeToken(token); - console.debug('SSO AUTH | User: ' + this.getUsername()); - console.debug('SSO AUTH | Domains: ' + this.getDomains()); - console.debug('SSO AUTH | Roles: ' + this.getRoles()); - console.debug('SSO AUTH | DomainRoles: ' + this.getDomainRoles()); - this.loginUsingSsoService = true; - this.isLoggedInSubject.next(true); - return true; - } else { - // return false to indicate failed login - this.isLoggedInSubject.next(false); - return false; - } - }), - catchError((error) => { - console.error('SSO login error: ' + error.error['message']); - return observableThrowError(error); - })); + public oidcLogout(oidcToken: string): void { + this.http.get(this.appConfig.config.apiUrl + '/oidc/logout/' + oidcToken).subscribe(() => { + }) } - public logout(): void { - this.removeToken(); - this.isLoggedInSubject.next(false); - localStorage.removeItem('_expiredTime'); + get isLoggedIn$(): Observable<boolean> { + this.isLoggedInSubject.next(this.isLogged()); + return this.isLoggedInSubject.pipe( + debounceTime(100), // use debounceTime to aggregate multiple emissions https://rxjs.dev/api/operators/debounceTime + ); } public isLogged(): boolean { @@ -278,11 +401,9 @@ export class AuthService { return (token ? !this.jwtHelper.isTokenExpired(token) : false); } - get isLoggedIn$(): Observable<boolean> { - this.isLoggedInSubject.next(this.isLogged()); - return this.isLoggedInSubject.pipe( - debounceTime(100), // use debounceTime to aggregate multiple emissions https://rxjs.dev/api/operators/debounceTime - ); + + public getDomainIds(): number[] { + return Array.from(new Set(this.profile.map(ur => ur.domainId))); } } diff --git a/src/app/auth/login-success/login-success.component.ts b/src/app/auth/login-success/login-success.component.ts index 02d3386850147f1e758a80f4b435d973e5abb42f..f4f72dda8b1d19b812957eee50f7144afe815f74 100644 --- a/src/app/auth/login-success/login-success.component.ts +++ b/src/app/auth/login-success/login-success.component.ts @@ -17,11 +17,19 @@ export class LoginSuccessComponent implements OnInit { ngOnInit(): void { this.route.queryParams.subscribe(params => { const token = params['token']; - const refreshToken = params['refresh_token']; + const refreshToken = params['refresh-token']; + const oidcToken = params['oidc-token']; if (token) { this.authService.storeToken(token); } - this.router.navigate(['/portal']) + if (oidcToken) { + this.authService.storeOidcToken(oidcToken); + } + if (refreshToken) { + this.authService.storeRefreshToken(refreshToken); + } + this.authService.loadUser(); + this.router.navigate(['/']) }) } diff --git a/src/app/directive/roles-exluded.directive.ts b/src/app/directive/roles-exluded.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b003ab52b1403e88a538ac016bc9e7cdde22636 --- /dev/null +++ b/src/app/directive/roles-exluded.directive.ts @@ -0,0 +1,30 @@ +import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; +import { AuthService } from '../auth/auth.service'; + +@Directive({ + selector: '[rolesExcluded]' +}) +export class RolesExcludedDirective { + private _excluded: Array<string> = []; + + constructor( + private _templateRef: TemplateRef<any>, + private _viewContainer: ViewContainerRef, + private authService: AuthService + ) {} + + @Input() set rolesExcluded(excludedRoles: Array<string>) { + this._excluded = excludedRoles; + this.updateState(); + } + + private updateState() { + this._viewContainer.clear(); + + const hasExcludedRole = this._excluded.some(role => this.authService.hasRole(role)); + if (!hasExcludedRole) { + // If user has exluded role hide the element + this._viewContainer.createEmbeddedView(this._templateRef); + } + } +} \ No newline at end of file diff --git a/src/app/directive/roles.directive.ts b/src/app/directive/roles.directive.ts index 68f2e12346693fe23de1f14c4f90b100cfd8b095..c2155e3c2a02438a75fd56ae119b5a12c910b3d8 100644 --- a/src/app/directive/roles.directive.ts +++ b/src/app/directive/roles.directive.ts @@ -3,10 +3,7 @@ import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core'; class RoleState { public allowed: Array<string> = new Array<string>(); - public excluded: Array<string> = new Array<string>() } - - @Directive({ selector: '[roles]', inputs: ['roles'] @@ -15,8 +12,6 @@ export class RolesDirective { private _allowed: Array<string> = new Array<string>(); - private _excluded: Array<string> = new Array<string>(); - constructor(private _templateRef: TemplateRef<any>, private _viewContainer: ViewContainerRef, private authService: AuthService) { @@ -26,53 +21,18 @@ export class RolesDirective { @Input() set roles(allowedRoles: Array<string>) { this._allowed = allowedRoles; this.updateState({ - allowed: this._allowed, - excluded: this._excluded - }) - } - - // Excluded roles have priority than allowed roles - // If user have excluded role template would not be shown - - @Input() set rolesExcluded(excluded: Array<string>) { - this._excluded = excluded; - this.updateState({ - allowed: this._allowed, - excluded: this._excluded + allowed: this._allowed }) } updateState(state: RoleState) { this._viewContainer.clear(); - - let show: boolean = false; - let notAllowed: boolean = false; - - const allowedRoles = state.allowed; - - for (let exclude of state.excluded) { - if (this.authService.hasRole(exclude)) { - notAllowed = true; - break; - } + + + const hasAllowedRole = state.allowed.some(role => this.authService.hasRole(role)); + if (hasAllowedRole) { + this._viewContainer.createEmbeddedView(this._templateRef); } - if (notAllowed) { - this._viewContainer.clear(); - } else { - for (let allowedRole of allowedRoles) { - if (this.authService.hasRole(allowedRole)) { - show = true; - break; - } - } - - if (show) { - this._viewContainer.createEmbeddedView(this._templateRef); - } else { - this._viewContainer.clear(); - } - } - } } diff --git a/src/app/model/app-instance.ts b/src/app/model/app-instance.ts index 76edca2f6b68caf22d2f7702e9a2eb6f06d5c237..27119dad519db59323259ae6544037dc14fcd478 100644 --- a/src/app/model/app-instance.ts +++ b/src/app/model/app-instance.ts @@ -28,6 +28,7 @@ export class AppInstance { public id: number = undefined; public domainId: number = undefined; + public domainName: string = undefined; public applicationId: number = undefined; public applicationName: string = undefined; public applicationVersion: string = undefined; diff --git a/src/app/model/cluster-manager.ts b/src/app/model/cluster-manager.ts new file mode 100644 index 0000000000000000000000000000000000000000..f37b9747cb8cc223d1ee1d4d8b5f142cf582b270 --- /dev/null +++ b/src/app/model/cluster-manager.ts @@ -0,0 +1,20 @@ +import { ClusterDeployment , ClusterExtNetwork, ClusterIngress} from './cluster'; + + +export class ClusterManager { + public id: number; + public name: string; + public codename: string; + public description: string; + public creationDate: Date; + public modificationDate: Date; + public clusterConfigFile: string; + public pathConfigFile: string; + public ingress: ClusterIngress; + public deployment: ClusterDeployment; + public externalNetworks: ClusterExtNetwork[] + public domainNames: string[]; + public state : string; + public currentStateSince: Date; + public contactEmail: string; +} \ No newline at end of file diff --git a/src/app/model/cluster.ts b/src/app/model/cluster.ts index c08513213109a0eb2c1ca9075c726a032130a468..3468c121486e3069ce8c2b0f69815eb1c9f3200f 100644 --- a/src/app/model/cluster.ts +++ b/src/app/model/cluster.ts @@ -2,7 +2,7 @@ export class ClusterInfo { public id: number; } -class ClusterDeployment { +export class ClusterDeployment { public smtpServerHostname: string=""; public smtpServerPort: string=""; public smtpServerUsername: string=""; @@ -24,7 +24,7 @@ export class ClusterExtNetwork { public id: number; } -class ClusterIngress { +export class ClusterIngress { public id: number; public controllerConfigOption: string; public controllerChartName: string; diff --git a/src/app/model/configuration.ts b/src/app/model/configuration.ts index 5bdb54fdace5d822fef1526fbfe092aa13117288..40896c07bfc2b1439633aef88f05cb1660beceec 100644 --- a/src/app/model/configuration.ts +++ b/src/app/model/configuration.ts @@ -12,4 +12,6 @@ export class Configuration { public bulkDeploymentJobCron: string; public parallelDeploymentsLimit: number; public bulkDeploymentQueueRefresh: number; + public deploymentPrefix: string; + public bulkDeploymentTimeThreshold: number; } diff --git a/src/app/model/domain.ts b/src/app/model/domain.ts index 524de2483f92a0f7a69036624fdc351665b535e1..04b91adb2465594e36ca2d13197445d6011c05ef 100644 --- a/src/app/model/domain.ts +++ b/src/app/model/domain.ts @@ -4,6 +4,7 @@ import {DomainApplicationStatePerDomain} from './domainapplicationstateperdomain import {DomainGroup} from './domaingroup'; import {KeyValue} from './key-value'; import { DomainAnnotation } from './domain-annotation'; +import { ClusterManager } from './cluster-manager'; export class Domain { public id: number = undefined; @@ -16,4 +17,5 @@ export class Domain { public groups: DomainGroup[] = []; public deleted: boolean; public annotations: DomainAnnotation[] = []; + public clusters: ClusterManager[] = []; } diff --git a/src/app/model/userrole.ts b/src/app/model/userrole.ts index ee06e84b3828a14565b91448cae2c595b73fda3a..acf7ac2f0228e50b8220b4e1812e47fd4377a473 100644 --- a/src/app/model/userrole.ts +++ b/src/app/model/userrole.ts @@ -1,14 +1,14 @@ export enum Role { - ROLE_SYSTEM_ADMIN, - ROLE_DOMAIN_ADMIN, - ROLE_OPERATOR, - ROLE_TOOL_MANAGER, - ROLE_USER, - ROLE_GUEST, - ROLE_INCOMPLETE, - ROLE_NOT_ACCEPTED, - ROLE_VL_MANAGER, - ROLE_VL_DOMAIN_ADMIN + ROLE_SYSTEM_ADMIN = 'ROLE_SYSTEM_ADMIN', + ROLE_DOMAIN_ADMIN = 'ROLE_DOMAIN_ADMIN', + ROLE_OPERATOR = "ROLE_OPERATOR", + ROLE_TOOL_MANAGER = "ROLE_TOOL_MANAGER", + ROLE_USER = "ROLE_USER", + ROLE_GUEST = "ROLE_GUEST", + ROLE_INCOMPLETE = "ROLE_INCOMPLETE", + ROLE_NOT_ACCEPTED = "ROLE_NOT_ACCEPTED", + ROLE_GROUP_MANAGER = "ROLE_GROUP_MANAGER", + ROLE_GROUP_DOMAIN_ADMIN = "ROLE_GROUP_DOMAIN_ADMIN", } export function RoleAware(constructor: Function) { diff --git a/src/app/service/appconfig.service.ts b/src/app/service/appconfig.service.ts index e15d9b5c331c6cfdc8d7bf68ed9c24ec3d8bed3e..df61388c90ac79ee006e8922fb695a032bae7ed8 100644 --- a/src/app/service/appconfig.service.ts +++ b/src/app/service/appconfig.service.ts @@ -64,6 +64,7 @@ export class AppConfigService { } public getSiteKey(): string { + console.log("Site key:", this.config.captchaKey) if (this.config == null) { return ''; } diff --git a/src/app/service/cluster-manager.service.ts b/src/app/service/cluster-manager.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..a298dfe92ec77fd4fb80cdd8d4ecc6248b5f3c70 --- /dev/null +++ b/src/app/service/cluster-manager.service.ts @@ -0,0 +1,39 @@ +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { AppConfigService } from "./appconfig.service"; +import { Observable } from "rxjs"; +import { ClusterManager } from "../model/cluster-manager"; + +@Injectable({ + providedIn: 'root', +}) +export class ClusterManagerService { + + protected url: string; + + + constructor(private http: HttpClient, + private appConfig: AppConfigService) { + this.url = this.appConfig.getApiUrl() + '/management/cluster'; + } + + public sendCluster(file: File, view: ClusterManager): Observable<ClusterManager> { + const formParams = new FormData(); + formParams.append('file', file); + formParams.append('data', new Blob([JSON.stringify(view)], { type: 'application/json' })); + return this.http.post<ClusterManager>(this.url, formParams); + } + + public getAllClusters(): Observable<ClusterManager[]> { + return this.http.get<ClusterManager[]>(this.url + '/all'); + } + + public getClusterDetails(id: number): Observable<ClusterManager> { + return this.http.get<ClusterManager>(`${this.url}/${id}`); + } + + public updateCluster(cluster: ClusterManager): Observable<ClusterManager> { + return this.http.put<ClusterManager>(`${this.url}/${cluster.id}`, cluster); + } + +} \ No newline at end of file diff --git a/src/app/service/cluster.service.ts b/src/app/service/cluster.service.ts index dace40815e83787a5897843b9d7206c153ca2686..4830699766480719290f664be76a554b8c85725d 100644 --- a/src/app/service/cluster.service.ts +++ b/src/app/service/cluster.service.ts @@ -17,22 +17,11 @@ export class ClusterService extends GenericDataService { constructor(http: HttpClient, appConfig: AppConfigService) { super(http, appConfig); - this.url = this.appConfig.getApiUrl() + '/management/kubernetes/'; + this.url = this.appConfig.getApiUrl() + '/management/kubernetes'; } public getCluster(): Observable<Cluster> { return this.get<Cluster>(this.url); } - public add(cluster: Cluster): Observable<any> { - return this.post(this.url, cluster); - } - - public update(cluster: Cluster): Observable<any> { - return this.put(this.url + cluster.id, cluster); - } - - public remove(clusterId: number): Observable<any> { - return this.http.delete(this.url + clusterId); - } } diff --git a/src/app/service/dashboard.service.spec.ts b/src/app/service/dashboard.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..24d7a1a55c4c188c015e11bf161b50823ba4ab3c --- /dev/null +++ b/src/app/service/dashboard.service.spec.ts @@ -0,0 +1,42 @@ +import { TestBed, inject} from '@angular/core/testing'; + +import { DashboardService } from './dashboard.service'; +import {HttpClient, HttpHandler} from '@angular/common/http'; +import {Observable, of} from 'rxjs'; +import {Configuration} from '../model/configuration'; +import {AppConfigService} from './appconfig.service'; + +class MockConfigurationService{ + protected uri:string; + + constructor() { + this.uri = 'http://localhost/api'; + } + + public getApiUrl(): string { + return 'http://localhost/api'; + } + + public getConfiguration():Observable<Configuration>{ + return of<Configuration>(); + } + + public updateConfiguration(configuration:Configuration):Observable<any>{ + return of<Configuration>(); + } +} + +describe('DashboardService', () => { + let service: DashboardService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DashboardService, HttpHandler, HttpClient, {provide: AppConfigService, useClass: MockConfigurationService}] + }); + service = TestBed.inject(DashboardService); + }); + + it('should be created', inject([DashboardService], (service: DashboardService) =>{ + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/service/dashboard.service.ts b/src/app/service/dashboard.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..eadf24747dfe0e98f2aca7dc637916e917fa4f03 --- /dev/null +++ b/src/app/service/dashboard.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {AppConfigService} from './appconfig.service'; +import {GenericDataService} from './genericdata.service'; + +@Injectable({ + providedIn: 'root' +}) +export class DashboardService extends GenericDataService { + + constructor(http: HttpClient, appConfig: AppConfigService) { + super(http, appConfig); + } + + public getAdmin() { + return this.get(this.appConfig.getApiUrl() + '/dashboard/admin') + } + + public getDomainAdmin(domainId?: number) { + return this.get(this.appConfig.getApiUrl() + '/dashboard/domain/' + domainId) + } +} diff --git a/src/app/service/domain.service.spec.ts b/src/app/service/domain.service.spec.ts index 4ea9fa4e4fc2f5afff4f3674affb79ff7afbf19a..0efb7b70d3a410434874a88ace9f1f352b6c37e3 100644 --- a/src/app/service/domain.service.spec.ts +++ b/src/app/service/domain.service.spec.ts @@ -5,6 +5,8 @@ import {HttpClient, HttpHandler} from "@angular/common/http"; import {AppConfigService} from "./appconfig.service"; import {Observable, of} from "rxjs"; import {Configuration} from "../model/configuration"; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { Domain } from '../model/domain'; class MockConfigurationService{ protected uri:string; @@ -26,14 +28,130 @@ class MockConfigurationService{ } } + + describe('DomainService', () => { + let service: DomainService; + let httpMock: HttpTestingController; + let appConfigService: jasmine.SpyObj<AppConfigService>; + const mockApiUrl = 'http://localhost/api'; + + const mockDomain: Domain = { + id: 1, + name: 'Test Domain', + codename: 'test-domain', + active: true, + domainDcnDetails: { + id: 1, + domainCodename: 'test-domain', + dcnConfigured: false, + dcnDeploymentType: 'type', + customerNetworks: [] + }, + domainTechDetails: null, + applicationStatePerDomain: [], + groups: [], + deleted: false, + annotations: [], + clusters: [], + }; + beforeEach(() => { + const appConfigSpy = jasmine.createSpyObj('AppConfigService', ['getApiUrl', 'getNmaasGlobalDomainId', 'getHttpTimeout']); + appConfigSpy.getApiUrl.and.returnValue(mockApiUrl); + appConfigSpy.getNmaasGlobalDomainId.and.returnValue(1); + appConfigSpy.getHttpTimeout.and.returnValue(30000); // Dodano mock dla getHttpTimeout + TestBed.configureTestingModule({ - providers: [DomainService, HttpHandler, HttpClient, {provide: AppConfigService, useClass: MockConfigurationService}] + imports: [HttpClientTestingModule], + providers: [ + DomainService, + {provide: AppConfigService, useValue: appConfigSpy}] }); + + service = TestBed.inject(DomainService); + httpMock = TestBed.inject(HttpTestingController); }); + + + + afterEach(() => { + httpMock.verify(); + }); + it('should be created', inject([DomainService], (service: DomainService) => { expect(service).toBeTruthy(); })); + + + + it('should get global domain', () => { + service.getGlobalDomain().subscribe((domain) => { + expect(domain).toEqual(mockDomain); + }); + + const req = httpMock.expectOne(`${mockApiUrl}/domains/1`); + expect(req.request.method).toBe('GET'); + req.flush(mockDomain); + }); + + it('should get all domains', () => { + const mockDomains: Domain[] = [mockDomain, { ...mockDomain, id: 2, name: 'Another Domain' }]; + + service.getAll().subscribe((domains) => { + expect(domains.length).toBe(2); + expect(domains).toEqual(mockDomains); + }); + + const req = httpMock.expectOne(`${mockApiUrl}/domains`); + expect(req.request.method).toBe('GET'); + req.flush(mockDomains); + }); + + it('should add a new domain', () => { + const mockId= { id: 1 }; + + service.add(mockDomain).subscribe((id) => { + expect(id).toEqual(mockId); + }); + + const req = httpMock.expectOne(`${mockApiUrl}/domains`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(mockDomain); + req.flush(mockId); + }); + + it('should update a domain', () => { + service.update(mockDomain).subscribe((response) => { + expect(response).toBeTruthy(); + }); + + const req = httpMock.expectOne(`${mockApiUrl}/domains/1`); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual(mockDomain); + req.flush({}); + }); + + it('should delete a domain', () => { + service.remove(1).subscribe((response) => { + expect(response).toBeTruthy(); + }); + + const req = httpMock.expectOne(`${mockApiUrl}/domains/1`); + expect(req.request.method).toBe('DELETE'); + req.flush({}); + }); + + + it('should update domain state', () => { + service.updateDomainState(mockDomain).subscribe((response) => { + expect(response).toBeTruthy(); + }); + + const req = httpMock.expectOne(`${mockApiUrl}/domains/1/state?active=false`); + expect(req.request.method).toBe('PATCH'); + req.flush({}); + }); + }); diff --git a/src/app/service/domain.service.ts b/src/app/service/domain.service.ts index 7ed0257a4aca76390edd525a84274cb6667c150d..7f6a3e2c51420794fc1317ac2e88ddabfdfdc7d8 100644 --- a/src/app/service/domain.service.ts +++ b/src/app/service/domain.service.ts @@ -19,12 +19,15 @@ export class DomainService extends GenericDataService { protected url: string; + protected urlGroups: string; + private updateRequiredFlag: boolean; constructor(http: HttpClient, appConfig: AppConfigService) { super(http, appConfig); this.updateRequiredFlag = false; this.url = this.appConfig.getApiUrl() + '/domains'; + this.urlGroups = this.appConfig.getApiUrl() + '/groups'; } public getGlobalDomainId(): number { @@ -39,6 +42,10 @@ export class DomainService extends GenericDataService { return this.get<Domain[]>(this.url); } + public getAllBase(): Observable<Domain[]> { + return this.get<Domain[]>(this.url + '/base'); + } + public getOne(domainId: number): Observable<Domain> { return this.get<Domain>(this.url + '/' + domainId); } @@ -89,35 +96,35 @@ export class DomainService extends GenericDataService { // GROUPS public getAllDomainGroups(): Observable<DomainGroup[]> { - return this.get<DomainGroup[]>(this.url + '/group'); + return this.get<DomainGroup[]>(this.urlGroups); } public getDomainGroup(domainGroupId: number): Observable<DomainGroup> { - return this.get<DomainGroup>(this.url + '/group/' + domainGroupId); + return this.get<DomainGroup>(this.urlGroups + '/' + domainGroupId); } public deleteDomainGroup(domainGroupId: number): Observable<void> { - return this.delete<void>(this.url + '/group/' + domainGroupId); + return this.delete<void>(this.urlGroups + '/' + domainGroupId); } public addDomainsToGroup(groupCodeName: string, domainIds: number[]): Observable<DomainGroup> { - return this.post(this.url + '/group/' + groupCodeName, domainIds); + return this.post(this.urlGroups + '/' + groupCodeName, domainIds); } public deleteDomainFromGroup(groupId: number, domainId: number): Observable<DomainGroup> { - return this.patch(this.url + '/group/' + groupId, domainId); + return this.patch(this.urlGroups + '/' + groupId, domainId); } public createDomainGroup(domainGroup: DomainGroup): Observable<Id> { - return this.post(this.url + '/group', domainGroup); + return this.post(this.urlGroups , domainGroup); } public updateDomainGroup(domainGroup: DomainGroup, id: number): Observable<Id> { - return this.put(this.url + '/group/' + id, domainGroup); + return this.put(this.urlGroups+ "/" + id, domainGroup); } public updateDomainGroupManagers(managers: User[], id: number): Observable<DomainGroup> { - return this.put(this.url + '/group/members/' + id, managers); + return this.put(this.urlGroups + '/' + id + "/members", managers); } public getAnnotations(): Observable<DomainAnnotation[]> { diff --git a/src/app/service/profile.service.ts b/src/app/service/profile.service.ts index 0c745763a950266d06cfa37e2f574eeb336a2cc8..ee77d26c2101c874456c1d6ebd1e868aa0cc68d7 100644 --- a/src/app/service/profile.service.ts +++ b/src/app/service/profile.service.ts @@ -4,6 +4,7 @@ import {AppConfigService} from './appconfig.service'; import {Observable} from 'rxjs'; import {User} from '../model'; import {HttpClient} from '@angular/common/http'; +import { UserRole } from '../model/userrole'; @Injectable({ @@ -19,6 +20,10 @@ export class ProfileService extends GenericDataService { return this.http.get<User>(this.getProfileUrl() + 'user') } + public getRoles(): Observable<UserRole[]> { + return this.http.get<UserRole[]>(this.getProfileUrl() + 'user/roles') + } + protected getProfileUrl(): string { return this.appConfig.getApiUrl() + '/profile/' } diff --git a/src/app/service/shell-client.service.ts b/src/app/service/shell-client.service.ts index 7a1cb49aef95fb181afe999a4bfaa17bc8599141..7d920a07bae5f470a5fc97ea19b286993a4b1ecf 100644 --- a/src/app/service/shell-client.service.ts +++ b/src/app/service/shell-client.service.ts @@ -18,11 +18,11 @@ export class ShellClientService { public initConnection(id: number, pod: string): Observable<string> { // @ts-ignore - return this.http.post<string>(this.appConfig.getApiUrl() + '/shell/' + id + '/init/' + pod, {}, {responseType: 'text'}); + return this.http.post<string>(this.appConfig.getApiUrl() + '/pods/shell/' + id + '/init/' + pod, {}, {responseType: 'text'}); } public sendCommand(sessionId: string, command: Object = {}): Observable<any> { - return this.http.post(this.appConfig.getApiUrl() + '/shell/' + sessionId + '/command', command); + return this.http.post(this.appConfig.getApiUrl() + '/pods/shell/' + sessionId + '/command', command); } /** @@ -30,7 +30,7 @@ export class ShellClientService { */ public closeConnection(sessionId: string) { this.closeEventStream() - this.http.delete(this.appConfig.getApiUrl() + '/shell/' + sessionId).subscribe( + this.http.delete(this.appConfig.getApiUrl() + '/pods/shell/' + sessionId).subscribe( () => console.log('session completed: ', sessionId), error => console.error('error completing session', error)) } @@ -42,7 +42,7 @@ export class ShellClientService { public getServerSentEvent(sessionId: string): Observable<OnMessageEvent> { return new Observable<OnMessageEvent>(observableEvents => { - const events = this._sseService.getEventSource(this.appConfig.getApiUrl() + '/shell/' + sessionId); + const events = this._sseService.getEventSource(this.appConfig.getApiUrl() + '/pods/shell/' + sessionId); events.onopen = onopenEvent => { this._zone.run(() => { @@ -67,7 +67,7 @@ export class ShellClientService { } public getPossiblePods(id: number): Observable<PodInfo[]> { - return this.http.get<PodInfo[]>(this.appConfig.getApiUrl() + '/shell/' + id + '/podnames'); + return this.http.get<PodInfo[]>(this.appConfig.getApiUrl() + '/pods/shell/' + id + '/podnames'); } } diff --git a/src/app/service/sso.service.spec.ts b/src/app/service/sso.service.spec.ts deleted file mode 100644 index 447e1159701776ff09a20d63fefdee219f341f06..0000000000000000000000000000000000000000 --- a/src/app/service/sso.service.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { TestBed, inject } from '@angular/core/testing'; - -import { SSOService } from './sso.service'; -import {Observable, of} from 'rxjs'; -import {Configuration} from '../model/configuration'; -import {HttpClient, HttpHandler} from '@angular/common/http'; -import {AppConfigService} from './appconfig.service'; - -class MockConfigurationService { - protected uri: string; - - constructor() { - this.uri = 'http://localhost/api'; - } - - public getApiUrl(): string { - return 'http://localhost/api'; - } - - public getConfiguration(): Observable<Configuration> { - return of<Configuration>(); - } - - public updateConfiguration(configuration: Configuration): Observable<any> { - return of<Configuration>(); - } -} - -describe('SSOService', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [SSOService, HttpHandler, HttpClient, {provide: AppConfigService, useClass: MockConfigurationService}] - }); - }); - - it('should be created', inject([SSOService], (service: SSOService) => { - expect(service).toBeTruthy(); - })); -}); diff --git a/src/app/service/sso.service.ts b/src/app/service/sso.service.ts deleted file mode 100644 index c3ff73bfa55445414fc44f6c923af9607c6dc61b..0000000000000000000000000000000000000000 --- a/src/app/service/sso.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from '@angular/core'; -import {GenericDataService} from './genericdata.service'; -import {HttpClient} from '@angular/common/http'; -import {AppConfigService} from './appconfig.service'; -import {Observable} from 'rxjs'; -import {SSOConfig} from '../model/sso'; - -@Injectable() -export class SSOService extends GenericDataService { - - protected url: string; - - constructor(http: HttpClient, appConfig: AppConfigService) { - super(http, appConfig); - this.url = this.appConfig.getApiUrl() + '/auth/sso'; - } - - public getOne(): Observable<SSOConfig> { - return this.get<SSOConfig>(this.url); - } - -} diff --git a/src/app/service/user.service.ts b/src/app/service/user.service.ts index e139180f752173a84594c97211e16e8ddb17ae96..8393eb431889e4f88d5c70304a12104b3a1634d7 100644 --- a/src/app/service/user.service.ts +++ b/src/app/service/user.service.ts @@ -20,7 +20,7 @@ export class UserService extends GenericDataService { public getAll(domainId?: number): Observable<User[]> { return this.get<User[]>(domainId === undefined || domainId === this.domainService.getGlobalDomainId() ? - this.getUsersUrlWithoutDash() : this.getDomainUsersUrl(domainId)); + this.getUsersUrlWithoutDash() : this.getDomainUsersUrlWithoutDash(domainId)); } public getOne(userId: number, domainId?: number): Observable<User> { @@ -96,12 +96,16 @@ export class UserService extends GenericDataService { return this.appConfig.getApiUrl() + '/domains/' + domainId + '/users/'; } + protected getDomainUsersUrlWithoutDash(domainId: number): string { + return this.appConfig.getApiUrl() + '/domains/' + domainId + '/users'; + } + public getDomainUsersAsAdmin(domainId: number): Observable<User[]> { return this.get<User[]>(this.appConfig.getApiUrl() + '/domains/' + domainId + '/users/admin'); } protected getUserAcceptanceUrl(): string { - return this.appConfig.getApiUrl() + '/users/terms/'; + return this.appConfig.getApiUrl() + '/users/terms'; } protected getEnableOrDisableUsersUrl(userId: number, enabled: boolean): string { diff --git a/src/app/shared/about/about.component.html b/src/app/shared/about/about.component.html index df6d0eb479e590e7436abc90906638ecbe4fb3d0..93662491047eb9332ea1a61af958ffacc4007969 100644 --- a/src/app/shared/about/about.component.html +++ b/src/app/shared/about/about.component.html @@ -1,4 +1,4 @@ -<div class="col-md-offset-1 col-md-10 col-lg-offset-1 col-lg-10 row position" style="padding-bottom: 80px; padding-top: 80px;"> +<div class=" row position" style="margin:40px"> <div class="col-md-6"> <div class="" style="padding-bottom: 15px;"> <h2>{{ 'ABOUT.CHANGELOG_TITLE' | translate }}</h2> @@ -26,12 +26,12 @@ <p class="form-control-static">{{gitInfo?.commitName}}</p> </div> </div> - <div class="row"> - <label class="control-label col-sm-2">{{'GIT_INFO.BRANCH_NAME' | translate}}:</label> - <div class="col-sm-10"> - <p class="form-control-static">{{gitInfo?.branchName}}</p> - </div> - </div> +<!-- <div class="row">--> +<!-- <label class="control-label col-sm-2">{{'GIT_INFO.BRANCH_NAME' | translate}}:</label>--> +<!-- <div class="col-sm-10">--> +<!-- <p class="form-control-static">{{gitInfo?.branchName}}</p>--> +<!-- </div>--> +<!-- </div>--> </div> </form> </div> diff --git a/src/app/shared/admin-dashboard/admin-dashboard.component.css b/src/app/shared/admin-dashboard/admin-dashboard.component.css new file mode 100644 index 0000000000000000000000000000000000000000..fdab80cb4c0d886552fe88c52387d4e9616f0493 --- /dev/null +++ b/src/app/shared/admin-dashboard/admin-dashboard.component.css @@ -0,0 +1,69 @@ +td{ + padding: 10px; + background: transparent; +} +th{ + padding: 10px; +} +:host ::ng-deep .p-datatable .p-datatable-thead > tr > th{ + border: 1px solid #E0E2E5; + background:transparent; + border-width: 0 0 1px 0; +} +:host ::ng-deep .p-datatable .p-datatable-tbody > tr > td { + text-align: left; + border: 1px solid #E0E2E5; + border-width: 0 0 1px 0; + padding: 1rem 1rem; +} +:host ::ng-deep .p-datatable .p-datatable-tbody > tr{ + background: transparent; +} +:host ::ng-deep .p-datatable .p-paginator-bottom{ + height: 40px; + background: transparent; + border: none; + margin-top:10px; +} +:host ::ng-deep .p-datatable .p-datatable-tbody > tr{ + background: transparent; +} + +:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page{ + transition: unset; + border-radius: 50%; + min-width:3.5rem; + height:3.5rem; + margin:0 5px; + font-size: 14px; +} + +:host ::ng-deep .p-paginator-element{ + border-radius:50%; + margin:0 5px; + min-width:3.5rem; + height:3.5rem; + font-size: 14px; +} +:host ::ng-deep .p-paginator-icon{ + height: 1.5rem; + width: 1.5rem; +} +:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page.p-highlight{ + background: var(--user-button-background-hover); +} +:host ::ng-deep .p-datatable-wrapper { + max-height: 50vh +} +:host ::ng-deep .p-datatable.p-datatable-scrollable > .p-datatable-wrapper > .p-datatable-table > .p-datatable-thead, .p-datatable.p-datatable-scrollable > .p-datatable-wrapper > .p-datatable-table > .p-datatable-tfoot, .p-datatable.p-datatable-scrollable > .p-datatable-wrapper > .p-scroller-viewport > .p-scroller > .p-datatable-table > .p-datatable-thead, .p-datatable.p-datatable-scrollable > .p-datatable-wrapper > .p-scroller-viewport > .p-scroller > .p-datatable-table > .p-datatable-tfoot{ + background: var(--app-background-color); +} +.width-50 { + width: 49% +} +@media screen and (max-width: 1390px){ + .width-50 { + width:100% + } +} + diff --git a/src/app/shared/admin-dashboard/admin-dashboard.component.html b/src/app/shared/admin-dashboard/admin-dashboard.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b5e72786990f7cd04b73149b71c0554f90c99594 --- /dev/null +++ b/src/app/shared/admin-dashboard/admin-dashboard.component.html @@ -0,0 +1,112 @@ +<div *roles="['ROLE_SYSTEM_ADMIN']"> + <div style="display: flex; flex-direction:column"> + <div style="display: flex; flex-wrap: wrap;"> + <div class="background-section" style="flex: 1 1 30%; margin-right: 20px"> + <h5 style="font-weight: bold">Current number of user </h5> + <h1 style="font-weight: bold">{{ adminData.userCount}}</h1> + </div> + <div class="background-section" style="flex: 1 1 30%; margin-right: 20px"> + <h5 style="font-weight: bold">Current number of domains </h5> + <h1 style="font-weight: bold">{{ adminData.domainsCount}}</h1> + </div> + <div class="background-section" style="flex: 1 1 30% ;"> + <h5 style="font-weight: bold">Current number of deployed applications </h5> + <h1 style="font-weight: bold">{{ adminData.instanceCount}}</h1> + </div> + </div> + <div class="grid" style="display: flex; justify-content: space-between;"> + <div class="background-section width-50 " style="margin-right: 20px"> + <h5 style="font-weight: bold">Application deployments in the last week</h5> + <p-table [value]="instanceCountInPeriodDetails" [scrollable]="true" [style]="{'width': '100%', 'max-height': '50vh'}"> + <ng-template pTemplate="header"> + <tr> + <th></th> + <th>Name</th> + <th>Version</th> + <th>Domain</th> + </tr> + </ng-template> + <ng-template pTemplate="body" let-instance> + <tr> + <td><img style="height: 40px" src="../../../assets/images/app-logo-example.png"/></td> + <td>{{instance.applicationName}}</td> + <td>{{instance.applicationVersion}}</td> + <td>{{instance.domainName}}</td> + </tr> + </ng-template> + </p-table> + </div> + <div class="width-50" > + <div class="background-section" style=""> + <h5 style="font-weight: bold">Most popular applications</h5> + <p-chart type="bar" [data]="popularAppsChartData" [options]="basicOptions" width="100%" height="50vh" /> + </div> + </div> + </div> + </div> +</div> +<div *roles="['ROLE_DOMAIN_ADMIN','ROLE_SYSTEM_ADMIN']"> + <div *ngIf="domainId !== 1"> + <div style="display: flex"> + <div class="background-section" style="flex: 1 1 50%; margin-right: 20px"> + <h5 style="font-weight: bold">Last login to the domain </h5> + <p-table [value]="domainAdminData?.userLogins | keyvalue"> + <ng-template pTemplate="header"> + <tr> + <th>User</th> + <th>Last login</th> + </tr> + </ng-template> + <ng-template pTemplate="body" let-login> + <tr> + <td>{{ login.key }}</td> + <td>{{ formatDate(login.value) }}</td> + </tr> + </ng-template> + </p-table> + </div> + <div class="background-section" style="flex: 1 1 50%;"> + <h5 style="font-weight: bold">Number of deployed applications per user </h5> + <p-table [value]="domainAdminData?.applicationDeployed | keyvalue"> + <ng-template pTemplate="header"> + <tr> + <th>User</th> + <th>Deployment</th> + </tr> + </ng-template> + <ng-template pTemplate="body" let-deployment> + <tr> + <td>{{ deployment.key }}</td> + <td>{{ deployment.value }}</td> + </tr> + </ng-template> + </p-table> + </div> + </div> + <div class="background-section" style=""> + <h5 style="font-weight: bold">Application status</h5> + <p-table [value]="applicationUpgradeStatus" [paginator]="true" [rows]="4" [scrollable]="true" [style]="{'width': '100%'}"> + <ng-template pTemplate="header"> + <tr> + <th></th> + <th>Name</th> + <th>Id</th> + <th>Instance name</th> + <th>Version</th> + <th>Need upgrade</th> + </tr> + </ng-template> + <ng-template pTemplate="body" let-app> + <tr> + <td><img style="height: 40px" src="../../../assets/images/app-logo-example.png"/></td> + <td>{{app.appName}}</td> + <td>{{app.appId}}</td> + <td>{{app.instanceName}}</td> + <td>{{app.appVersion}}</td> + <td>{{app.upgradePossible}}</td> + </tr> + </ng-template> + </p-table> + </div> + </div> +</div> diff --git a/src/app/shared/admin-dashboard/admin-dashboard.component.spec.ts b/src/app/shared/admin-dashboard/admin-dashboard.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1877b765c09ce11d4eb5b6db1900c7627f0837b2 --- /dev/null +++ b/src/app/shared/admin-dashboard/admin-dashboard.component.spec.ts @@ -0,0 +1,67 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { AdminDashboardComponent } from './admin-dashboard.component'; +import { DashboardService } from '../../service/dashboard.service'; +import { UserDataService } from '../../service/userdata.service'; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('AdminDashboardComponent', () => { + let component: AdminDashboardComponent; + let fixture: ComponentFixture<AdminDashboardComponent>; + let mockDashboardService: jasmine.SpyObj<DashboardService>; + let mockUserDataService: jasmine.SpyObj<UserDataService>; + + beforeEach(async () => { + mockDashboardService = jasmine.createSpyObj('DashboardService', ['getAdmin', 'getDomainAdmin']); + mockUserDataService = jasmine.createSpyObj('UserDataService', ['selectedDomainId']); + mockUserDataService.selectedDomainId = of(123); // Replace 'test-domain-id' with a numeric value + mockDashboardService.getAdmin.and.returnValue(of({ + popularApps: { App1: 10, App2: 20 }, + instanceCountInPeriodDetails: [] + })); + mockDashboardService.getDomainAdmin.and.returnValue(of({ + applicationUpgradeStatus: [] + })); + + await TestBed.configureTestingModule({ + declarations: [AdminDashboardComponent], + providers: [ + { provide: DashboardService, useValue: mockDashboardService }, + { provide: UserDataService, useValue: mockUserDataService } + ], + schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] // Add this to allow unknown properties + }).compileComponents(); + + fixture = TestBed.createComponent(AdminDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with admin data and chart data', () => { + expect(component.adminData).toBeDefined(); + expect(component.popularAppsChartData).toBeDefined(); + expect(component.instanceCountInPeriodDetails).toEqual([]); + }); + + it('should subscribe to selectedDomainId and fetch domain admin data', () => { + expect(component.domainId).toBe(123); + expect(component.domainAdminData).toBeDefined(); + expect(component.applicationUpgradeStatus).toEqual([]); + }); + + it('should format date correctly', () => { + const date = '2023-01-01T00:00:00Z'; + const formattedDate = component.formatDate(date); + expect(formattedDate).toBe(new Date(date).toLocaleString()); + }); + + it('should call chartData method and populate chart data', () => { + component.chartData(); + expect(component.popularAppsChartData.labels).toEqual(['App1', 'App2']); + expect(component.popularAppsChartData.datasets[0].data).toEqual([10, 20]); + }); +}); diff --git a/src/app/shared/admin-dashboard/admin-dashboard.component.ts b/src/app/shared/admin-dashboard/admin-dashboard.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3ae9e16672ced2b5176ef5633728141465828473 --- /dev/null +++ b/src/app/shared/admin-dashboard/admin-dashboard.component.ts @@ -0,0 +1,106 @@ +import { Component } from '@angular/core'; +import {DashboardService} from '../../service/dashboard.service'; +import {UserDataService} from '../../service/userdata.service'; + +@Component({ + selector: 'app-admin-dashboard', + templateUrl: './admin-dashboard.component.html', + styleUrl: './admin-dashboard.component.css' +}) +export class AdminDashboardComponent { + popularAppsChartData: any; + + basicOptions: any; + adminData: any; + domainAdminData: any; + instanceCountInPeriodDetails: any[] = []; + applicationUpgradeStatus: any[] = []; + domainId; + + constructor(protected dashboardService: DashboardService, + private userDataService: UserDataService) { + } + + + ngOnInit() { + this.userDataService.selectedDomainId.subscribe((domainId) => { + this.domainId = domainId + this.getDomainAdmin() + }); + this.dashboardService.getAdmin().subscribe( + (response) => { + this.adminData = response; + this.chartData(); + this.instanceCountInPeriodDetails = this.adminData.instanceCountInPeriodDetails; + } + ) + const documentStyle = getComputedStyle(document.documentElement); + const textColor = documentStyle.getPropertyValue('--text-color'); + const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary'); + const surfaceBorder = documentStyle.getPropertyValue('--surface-border'); + + + this.basicOptions = { + plugins: { + legend: { + labels: { + color: textColor + } + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + color: textColorSecondary, + callback: function(value) { + return Number(value).toFixed(0); + } + }, + grid: { + color: surfaceBorder, + drawBorder: false + } + }, + x: { + ticks: { + color: textColorSecondary + }, + grid: { + color: surfaceBorder, + drawBorder: false + } + } + } + }; + } + + chartData() { + const appNames = Object.keys(this.adminData.popularApps); + const appValues = Object.values(this.adminData.popularApps); + + this.popularAppsChartData = { + labels: appNames, + datasets: [ + { + label: 'Count of deployments', + data: appValues, + borderColor: '#42A5F5', + backgroundColor: ['rgba(66, 165, 245, 0.2)', 'rgba(255, 208, 208, 0.7)', 'rgba(115, 104, 193, 0.7)', 'rgba(255, 193, 130, 0.7)', 'rgba(140, 193, 104, 0.7)'], + fill: true + } + ] + }; + } + formatDate(date: any): string { + return new Date(date).toLocaleString(); + } + getDomainAdmin() { + this.dashboardService.getDomainAdmin(this.domainId).subscribe( + (response) => { + this.domainAdminData = response; + this.applicationUpgradeStatus = this.domainAdminData.applicationUpgradeStatus; + } + ) + } +} diff --git a/src/app/shared/admin-left-menu/admin-left-menu.component.css b/src/app/shared/admin-left-menu/admin-left-menu.component.css new file mode 100644 index 0000000000000000000000000000000000000000..afd8e55a067061f61d251e45aee4ab6787b740e5 --- /dev/null +++ b/src/app/shared/admin-left-menu/admin-left-menu.component.css @@ -0,0 +1,62 @@ +.menu { + background-color: var(--menu-color); + color: var(--l-text-color); + /*height: calc(100vh - 2rem);*/ + /*position: static;*/ + /*top: 0;*/ + /*left: 0;*/ + display: flex; + flex-direction: column; + padding: 1rem; + } + .menu ul { + list-style: none; + padding: 0; + } + .menu li { + padding: 10px 10px; + margin: 0.5rem 0; + border-radius: 4px; + } +.menu li:hover { + padding: 10px 5px; + background: var(--background); + border-left: 5px solid var(--menu-pink) +} +.menu li.active{ + padding: 10px 5px; +} +.menu li.active:hover{ + padding: 10px 5px; +} +.active{ + padding: 10px 5px; + background: white; + border-left: 5px solid var(--menu-pink) +} + .menu a { + color: var(--l-text-color); + text-decoration: none; + } +:host ::ng-deep .p-button{ + padding: 8px 10px; + width:100%; + background: var(--user-button-background); + border: none; +} +:host ::ng-deep .p-button:hover{ + background: var(--user-button-background-hover) +} +:host ::ng-deep .p-menu.p-menu-overlay{ + position: static; + width:100%; + margin-bottom:5px; + border:none; +} +:host ::ng-deep .p-menu .p-menuitem > .p-menuitem-content .p-menuitem-link { + text-decoration: none; + padding: 1.3rem; +} +:host ::ng-deep .p-menu .p-menuitem:not(.p-highlight):not(.p-disabled) > .p-menuitem-content:hover{ + background:var(--user-button-background-hover); +} diff --git a/src/app/shared/admin-left-menu/admin-left-menu.component.html b/src/app/shared/admin-left-menu/admin-left-menu.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5f787a0b75252eaf9cf74e511a8df9c4bca5f8bc --- /dev/null +++ b/src/app/shared/admin-left-menu/admin-left-menu.component.html @@ -0,0 +1,54 @@ +<!-- <div class="flex flex-column justify-content-between "> + <div class="menu flex"> + <div> + <img src="../../../assets/images/logo-small.png" width="250px"> + </div> + <div style="margin-top: 30px"> + <nmaas-domain-filter class="drop-domain"></nmaas-domain-filter> + </div> + <ul style="margin-top: 30px"> + <li [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <a style="display: flex; align-items: center;" [routerLink]="['/dashboard']"> + <i class="pi pi-chart-bar" style="margin-right:10px; font-size: 15px"></i>Dashboard</a> + </li> + <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR', 'ROLE_VL_MANAGER', 'ROLE_VL_DOMAIN_ADMIN']" + [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <a style="display: flex; align-items: center;" [routerLink]="['/admin/domains']"> + <i class="pi pi-server" style="margin-right:10px; font-size: 15px"></i>{{ 'NAVBAR.DOMAINS' | translate }}</a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <a style="display: flex; align-items: center;" [routerLink]="['/admin/users']"> + <i class="pi pi-users" style="margin-right:10px; font-size: 15px"></i>{{ 'NAVBAR.USERS' | translate }}</a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <a style="display: flex; align-items: center;" [routerLink]="['/']"> + <i class="pi pi-th-large" style="margin-right:10px; font-size: 15px"></i>Catalog</a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <a style="display: flex; align-items: center;" [routerLink]="['/admin/configuration']"> + <i class="pi pi-cog" style="margin-right:10px; font-size: 15px"></i>Settings</a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <a style="display: flex; align-items: center;" [routerLink]="['/admin/languages']"> + <i class="pi pi-tags" style="margin-right:10px; font-size: 15px"></i>{{'NAVBAR.LANGUAGES' | translate }}</a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <a style="display: flex; align-items: center;" [routerLink]="['/admin/monitor']"> + <i class="pi pi-chart-line" style="margin-right:10px; font-size: 15px"></i>{{ 'NAVBAR.MONITOR' | translate }}</a> + </li> + + </ul> + </div> + <div class="menu flex"> + <p-menu #menu [model]="items" [popup]="true" class="test" /> + <p-button *ngIf="!toggleAdmin" (onClick)="menu.toggle($event)" class="user-button"><i class="pi pi-user" style="font-size: 13px; margin-right:10px"></i> User</p-button> + <p-button *ngIf="!toggleAdmin" style="margin-top:10px"><i class="pi pi-sign-in" style="margin-right:10px; font-size: 15px"></i>Go to admin panel</p-button> + <p-button *ngIf="toggleAdmin" style="margin-top:10px"><i class="pi pi-sign-in" style="margin-right:10px; font-size: 15px"></i>Go to user panel</p-button> + </div> +</div> --> + +<div style="margin:40px"> + <router-outlet></router-outlet> +</div> + + diff --git a/src/app/shared/admin-left-menu/admin-left-menu.component.spec.ts b/src/app/shared/admin-left-menu/admin-left-menu.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1b9df7593065ac1bf629c4f81b7406a23fbbae7 --- /dev/null +++ b/src/app/shared/admin-left-menu/admin-left-menu.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminLeftMenuComponent } from './admin-left-menu.component'; +import { MessageService } from 'primeng/api'; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('AdminLeftMenuComponent', () => { + let component: AdminLeftMenuComponent; + let fixture: ComponentFixture<AdminLeftMenuComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AdminLeftMenuComponent], + providers: [MessageService], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA, + NO_ERRORS_SCHEMA + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminLeftMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/admin-left-menu/admin-left-menu.component.ts b/src/app/shared/admin-left-menu/admin-left-menu.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b14f03859bb36cb68b2030ef212d7b15cfcfe7dd --- /dev/null +++ b/src/app/shared/admin-left-menu/admin-left-menu.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { MenuItem } from 'primeng/api'; +import { ToastContainerComponent } from '../toast-container/toast-container.component'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-admin-left-menu', + templateUrl: './admin-left-menu.component.html', + styleUrl: './admin-left-menu.component.css' +}) +export class AdminLeftMenuComponent { + + constructor() { + } + +} diff --git a/src/app/shared/admin/clusters/details/clusterdetails.component.html b/src/app/shared/admin/clusters/details/clusterdetails.component.html index 8d086868fe9beadb049eb635e58a6153ec82e304..2b5781131cc346714da10005bda01870e480bddc 100644 --- a/src/app/shared/admin/clusters/details/clusterdetails.component.html +++ b/src/app/shared/admin/clusters/details/clusterdetails.component.html @@ -1,9 +1,10 @@ -<div class="panel panel-default"> - <div class="panel-heading">{{ 'CLUSTERS.TITLE' | translate }}</div> +<div class="background-section"> + <h4 style="font-size:15px; font-weight: bold">{{ 'CLUSTERS.TITLE' | translate }}</h4> <div class="panel-body"> <form *ngIf="cluster" (submit)="submit()" class="form-horizontal" #clusterForm="ngForm"> - <div class="panel-default panel-heading">{{ 'CLUSTERS.INGRESS' | translate }}</div> + <div class="panel-default panel-heading">{{ 'CLUSTERS.INGRESS' | translate }} </div> <div class="panel-body"> + <div class="form-group"> <label for="ingresscontrollerconfigoption" class="col-sm-2 control-label">{{ 'CLUSTERS.CONTROLLER_CONFIG_OPTION' | translate }}</label> <div class="col-sm-10"> @@ -243,9 +244,10 @@ <div *ngIf="this.error" class="alert alert-danger"> <p>{{this.error}}</p> </div> - - <button *ngIf="!isInMode(ComponentMode.VIEW)" [disabled]="!clusterForm.form.valid" type="submit" - class="btn btn-primary">{{ 'CLUSTERS.SUBMIT_BUTTON' | translate }}</button> + <div class="flex justify-content-end"> + <button *ngIf="!isInMode(ComponentMode.VIEW)" [disabled]="!clusterForm.form.valid" type="submit" + class="btn btn-primary">{{ 'CLUSTERS.SUBMIT_BUTTON' | translate }}</button> + </div> </form> </div> </div> diff --git a/src/app/shared/admin/clusters/details/clusterdetails.component.spec.ts b/src/app/shared/admin/clusters/details/clusterdetails.component.spec.ts index 327d8c58004c56f36a2be8389fcb5be0e57e1c5f..b253949bbfdf2e2825951a1c9c53f7ea43dc7806 100644 --- a/src/app/shared/admin/clusters/details/clusterdetails.component.spec.ts +++ b/src/app/shared/admin/clusters/details/clusterdetails.component.spec.ts @@ -5,12 +5,18 @@ import {FormsModule} from '@angular/forms'; import {RouterTestingModule} from '@angular/router/testing'; import {MissingTranslationHandler, TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core'; import {CustomMissingTranslationService} from '../../../../i18n/custommissingtranslation.service'; +import { ClusterManagerService } from '../../../../service/cluster-manager.service'; describe('ClusterDetailsComponent', () => { let component: ClusterDetailsComponent; let fixture: ComponentFixture<ClusterDetailsComponent>; + let clusterService: jasmine.SpyObj<ClusterManagerService>; + beforeEach(waitForAsync(() => { + + const clusterServiceSpy = jasmine.createSpyObj('ClusterManagerService', ['sendCluster']); + TestBed.configureTestingModule({ declarations: [ ClusterDetailsComponent ], imports: [ @@ -22,9 +28,14 @@ describe('ClusterDetailsComponent', () => { provide: TranslateLoader, useClass: TranslateFakeLoader } - })] + })], + providers: [ + { provide: ClusterManagerService, useValue: clusterServiceSpy } ] }) .compileComponents(); + + clusterService = TestBed.inject(ClusterManagerService) as jasmine.SpyObj<ClusterManagerService>; + })); beforeEach(() => { diff --git a/src/app/shared/admin/clusters/details/clusterdetails.component.ts b/src/app/shared/admin/clusters/details/clusterdetails.component.ts index 34a96c5caa337ec592690b03f488d06115dc6014..f59594415860614d488b0e3e5a60c940b7e40272 100644 --- a/src/app/shared/admin/clusters/details/clusterdetails.component.ts +++ b/src/app/shared/admin/clusters/details/clusterdetails.component.ts @@ -6,6 +6,8 @@ import { IngressResourceConfigOption, NamespaceConfigOption } from '../../../../model/cluster'; +import { ClusterManager } from '../../../../model/cluster-manager'; +import { ClusterManagerService } from '../../../../service/cluster-manager.service'; import {BaseComponent} from '../../../common/basecomponent/base.component'; import {Component, EventEmitter, Input, Output} from '@angular/core'; import {Router} from '@angular/router'; @@ -37,11 +39,23 @@ export class ClusterDetailsComponent extends BaseComponent { @Output() public onDelete: EventEmitter<string> = new EventEmitter<string>(); - constructor(private router: Router) { + constructor(private router: Router, private clusterService: ClusterManagerService) { super(); this.initializeMaps(); } + public sendCluster(event: any) { + console.log(event); + const file = event.files[0]; + const view = new ClusterManager(); + view.name = "test" + view.description="testest" + this.clusterService.sendCluster(file, view).subscribe(result => { + console.log(result); + } + ) + } + public submit(): void { this.onSave.emit(this.cluster); } diff --git a/src/app/shared/admin/clusters/manager/manager.component.css b/src/app/shared/admin/clusters/manager/manager.component.css new file mode 100644 index 0000000000000000000000000000000000000000..00fa651ed9049179e0c2b26eb4f142fb21b00d92 --- /dev/null +++ b/src/app/shared/admin/clusters/manager/manager.component.css @@ -0,0 +1,44 @@ +label{ + padding-left:5px; + display: unset; + margin-bottom: 0; + font-weight: unset; +} +:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page.p-highlight{ + background: var(--user-button-background-hover); +} +:host ::ng-deep .p-datatable>.p-datatable-wrapper { + overflow: visible; +} + +:host ::ng-deep input[type=file]{ + display:none; +} +:host ::ng-deep .p-button{ + width: unset; + margin-right: 5px; + background: var(--primary-button-color); + color: var(--button-text-color); +} +:host ::ng-deep .p-button:hover{ + background: var(--primary-button-hover); + border:none; +} +:host ::ng-deep .p-button-label{ + font-weight: normal; +} +:host ::ng-deep .p-fileupload .p-fileupload-buttonbar{ + border: none; + background: transparent; + margin-bottom: 10px; + padding: 0; +} +:host ::ng-deep .p-fileupload .p-fileupload-content{ + border: none; + padding: 0; + border-radius: 3px; + +} +:host ::ng-deep .p-fileupload-content .p-progressbar{ + display: none; +} diff --git a/src/app/shared/admin/clusters/manager/manager.component.html b/src/app/shared/admin/clusters/manager/manager.component.html new file mode 100644 index 0000000000000000000000000000000000000000..cf693202ea648e7e341fc231b6df0e9becbdfcce --- /dev/null +++ b/src/app/shared/admin/clusters/manager/manager.component.html @@ -0,0 +1,115 @@ +<div style="display: flex; align-items: center; margin-top:20px"> + <div style="margin-right:20px"> + <div > + <button class="btn btn-primary" (click)="openModal()">New Cluster</button> + </div> + </div> + <div class="flex" style="margin-right:20px"> + + </div> +</div> + + <h4 class="header">{{ 'CLUSTERS.CONFIGURATION' | translate }}</h4> + +<div class="background-section"> + <p-table + [value]="clusters" + [paginator]="true" + [rows]="maxItemsOnPage" + [rowsPerPageOptions]="[15, 20, 25, 30, 50]" + [responsiveLayout]="'scroll'"> + <ng-template pTemplate="header"> + <tr> + <th pSortableColumn="id" id="id"> {{ 'CLUSTERS.ID' | translate }} + <p-sortIcon field="id"></p-sortIcon> + </th> + <th pSortableColumn="name" id="name"> {{ 'CLUSTERS.NAME' | translate }} + <p-sortIcon field="name"></p-sortIcon> + </th> + <th pSortableColumn="codename" id="codename"> {{ 'CLUSTERS.CODENAME' | translate }} + <p-sortIcon field="codename"></p-sortIcon> + </th> + <th pSortableColumn="state" id="state"> {{ 'CLUSTERS.STATE' | translate }} + <p-sortIcon field="state"></p-sortIcon> + </th> + <th pSortableColumn="creationDate" id="creationDate"> {{ 'CLUSTERS.CREATION_DATE' | translate }} + <p-sortIcon field="creationDate"></p-sortIcon> + </th> + <th pSortableColumn="modificationDate" id="modificationDate"> {{ 'CLUSTERS.MODIFICATION_DATE' | translate }} + <p-sortIcon field="modificationDate"></p-sortIcon> + </th> + <th></th> + </tr> + </ng-template> + <ng-template pTemplate="body" let-cluster> + <tr> + <td [routerLink]="[cluster.id]">{{cluster.id}}</td> + <td>{{cluster.name}}</td> + <td>{{cluster.codename}}</td> + <td>{{('CLUSTERS.'+cluster.state.toString().toUpperCase() ) | translate}}</td> + <td>{{cluster.creationDate | date: 'dd-MM-yyyy HH:mm'}}</td> + <td>{{cluster.modificationDate | date: 'dd-MM-yyyy HH:mm'}}</td> + <td class="text-right"> + <span class="dropdown"> + <a style="display: inline-block" class="dropdown-toggle" aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> + <em class="pi pi-cog" style="font-size: 1.8rem; color: var(--l-text-color)"></em> + </a> + <ul class="dropdown-menu pull-right-drop" > + <li><a [routerLink]="[ cluster.id]" class=""> + {{ 'CLUSTERS.DETAILS' | translate }}</a> + </li> + </ul> + </span> + </td> + </tr> + </ng-template> + </p-table> + +</div> + + +<nmaas-modal > + <div class="nmaas-modal-header">{{'CLUSTERS.CONNECT_CLUSTER' | translate}}</div> + <div class="nmaas-modal-body"> + <div class="flex flex-column"> + <div class="mt-4"> + <label for="name">{{'CLUSTERS.NAME' | translate}}</label> + <input id="name" type="text" class="form-control" [(ngModel)]="addedCluster.name" [ngModelOptions]="{standalone: true}"> + </div> + + <div class="mt-4"> + <label for="desc">{{'CLUSTERS.DESCRIPTION' | translate}}</label> + <input id="desc" type="text" class="form-control" [(ngModel)]="addedCluster.description" [ngModelOptions]="{standalone: true}"> + </div> + <div class="mt-4"> + <label for="contactEmail">{{'CLUSTERS.CONTACT_MAIL' | translate}}</label> + <input id="contactEmail" type="text" class="form-control" [(ngModel)]="addedCluster.contactEmail" [ngModelOptions]="{standalone: true}"> + </div> + + + <div class="mt-4"> + <label for="name">{{'CLUSTERS.DOMAIN' | translate}}</label> + <select id="domain" #domainSelect class="form-control" (change)="onDomainSelection(domainSelect.value)"> + <option *ngFor="let domain of domains" [value]="domain.name" + >{{domain.name}} + </option> + </select> + </div> + + + <div class="card flex justify-content-center mt-4" > + <p-fileUpload name="json" (onUpload)="saveFile($event)" customUpload="true" [draggable]="true" pTooltip="Upload file is required before save" + (uploadHandler)="saveFile($event)" [multiple]="false" accept=".yaml" maxFileSize="1000000"> + + </p-fileUpload> + </div> + </div> + </div> + <div class="nmaas-modal-footer"> + <button type="button" class="btn btn-primary" [disabled]="updatedFile === null || addedCluster?.name === null || addedCluster?.description === null"(click)="closeModalAndSaveCluster()" + pTooltip="Upload file is required before save" showDelay="2000" >{{'CLUSTERS.SAVE' | translate}}</button> + <button type="button" class="btn btn-secondary" + (click)="this.modal.hide()">{{'UNDEPLOY_MODAL.CANCEL_BUTTON' | translate}}</button> + + </div> + </nmaas-modal> diff --git a/src/app/shared/admin/clusters/manager/manager.component.spec.ts b/src/app/shared/admin/clusters/manager/manager.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0d38438323ff1ce2d736bdc4df03ba3f6460054 --- /dev/null +++ b/src/app/shared/admin/clusters/manager/manager.component.spec.ts @@ -0,0 +1,146 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ClusterManagerComponent } from './manager.component'; +import { ClusterManagerService } from '../../../../service/cluster-manager.service'; +import { of } from 'rxjs'; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { ClusterManager } from '../../../../model/cluster-manager'; +import { ModalComponent } from '../../../modal'; + +describe('ClusterManagerComponent', () => { + let component: ClusterManagerComponent; + let fixture: ComponentFixture<ClusterManagerComponent>; + let clusterService: jasmine.SpyObj<ClusterManagerService>; + let mockClusters: any[]; + let mockModal: jasmine.SpyObj<ModalComponent>; + + + beforeEach(async () => { + const clusterServiceSpy = jasmine.createSpyObj('ClusterManagerService', ['getAllClusters', 'sendCluster']); + const mockModalSpy = jasmine.createSpyObj('ModalComponent', ['hide']); + mockModalSpy.hide.and.returnValue(null); + + mockClusters = [ + { + id: 1, + name: 'Cluster A', + codename: 'CodeA', + creationDate: new Date('2025-01-01'), + modificationDate: new Date('2025-02-01'), + pathConfigFile: '/path/to/configA.yaml', + description: 'Description A', + clusterConfigFile: 'ConfigA', + ingress: { + id: 1, + controllerConfigOption: 'OptionA', + controllerChartName: 'ChartA', + controllerChartArchive: 'ArchiveA', + resourceConfigOption: 'ResourceA', + externalServiceDomain: 'domainA.com', + tlsSupported: true, + supportedIngressClass: 'ClassA', + certificateConfigOption: 'CertOptionA', + issuerOrWildcardName: 'WildcardA', + ingressPerDomain: false, + publicIngressClass: 'PublicClassA', + publicServiceDomain: 'public.domainA.com' + }, + deployment: { + smtpServerHostname: 'smtp.domainA.com', + smtpServerPort: '587', + smtpServerUsername: 'userA', + smtpServerPassword: 'passwordA', + defaultNamespace: 'namespaceA', + defaultStorageClass: 'storageClassA', + id: 1, + namespaceConfigOption: 'OptionA', + forceDedicatedWorkers: true + }, + externalNetworks: [ + { + assigned: true, + assignedSince: '2025-01-01', + assignedTo: 'UserA', + externalIp: '192.168.1.1', + externalNetwork: 'NetworkA', + externalNetworkMaskLength: 24, + id: 1 + } + ] + }, + { + id: 2, + name: 'Cluster B', + codename: 'CodeB', + creationDate: new Date('2025-01-15'), + modificationDate: new Date('2025-02-15'), + pathConfigFile: '/path/to/configB.yaml', + description: 'Description B', + clusterConfigFile: 'ConfigB', + ingress: 'IngressB', + deployment: 'DeploymentB', + externalNetworks: [] + } + ]; + + clusterServiceSpy.getAllClusters.and.returnValue(of(mockClusters)); + + await TestBed.configureTestingModule({ + declarations: [ClusterManagerComponent], + imports: [HttpClientTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader + } + }), + ], + providers: [ + { provide: ClusterManagerService, useValue: clusterServiceSpy } + ], + schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ClusterManagerComponent); + component = fixture.componentInstance; + component.modal = mockModalSpy; + clusterService = TestBed.inject(ClusterManagerService) as jasmine.SpyObj<ClusterManagerService>; + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should call getAllClusters on initialization', () => { + component.getAllClusters(); + expect(clusterService.getAllClusters).toHaveBeenCalled(); + expect(component.clusters).toEqual(mockClusters); + }); + + it('should call saveFile and store the uploaded file', () => { + const mockFile = new File(['test content'], 'test.yaml', { type: 'application/x-yaml' }); + const mockEvent = { files: [mockFile] }; + + component.saveFile(mockEvent); + + expect(component.updatedFile).toBe(mockFile); + expect(component.updatedFile.name).toBe('test.yaml'); + }); + + it('should call closeModalAndSaveCluster and reset state after saving', () => { + const mockFile = new File(['test content'], 'test.yaml', { type: 'application/x-yaml' }); + // const mockCluster = { id: 3, name: 'Cluster C', codename: 'CodeC' }; + component.updatedFile = mockFile; + component.addedCluster = mockClusters[0]; + + clusterService.sendCluster.and.returnValue(of(mockClusters[0])); + + component.closeModalAndSaveCluster(); + + expect(clusterService.sendCluster).toHaveBeenCalledWith(mockFile, mockClusters[0]); + // expect(component.updatedFile).toBeNull(); + // expect(component.addedCluster).toEqual(new ClusterManager()); + }); +}); \ No newline at end of file diff --git a/src/app/shared/admin/clusters/manager/manager.component.ts b/src/app/shared/admin/clusters/manager/manager.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e9df32629c98c7ed0181231b3fe064f3da72753 --- /dev/null +++ b/src/app/shared/admin/clusters/manager/manager.component.ts @@ -0,0 +1,77 @@ +import { Component, ViewChild } from '@angular/core'; +import { ClusterManagerService } from '../../../../service/cluster-manager.service'; +import { ClusterManager } from '../../../../model/cluster-manager'; +import { ModalComponent } from '../../../modal'; +import { DomainService } from '../../../../service'; + +@Component({ + selector: 'app-manager', + templateUrl: './manager.component.html', + styleUrl: './manager.component.css' +}) +export class ClusterManagerComponent { + + public clusters: ClusterManager[] = []; + + public addedCluster: ClusterManager = new ClusterManager(); + public updatedFile : File = null; + public maxItemsOnPage = 15; + + public domains = []; + + @ViewChild(ModalComponent, { static: true }) + public modal: ModalComponent; + + constructor(private clusterService: ClusterManagerService, + private domainService: DomainService + ) { + this.getAllClusters(); + this.domainService.getAllBase().subscribe(result => { + this.domains = result.filter(d => d.id !== this.domainService.getGlobalDomainId()); + }); + } + + public saveFile(event: any) { + console.log(event); + this.updatedFile =event.files[0]; + } + +public getAllClusters() { + this.clusterService.getAllClusters().subscribe(result => { + console.log(result); + this.clusters = result; + }) + } + +public closeModalAndSaveCluster() { + this.clusterService.sendCluster(this.updatedFile, this.addedCluster).subscribe(result => { + console.log(result); + this.getAllClusters(); + this.modal.hide(); + this.updatedFile = null; + this.addedCluster = new ClusterManager(); + }, error => { + console.error(error); + + } +) +} + + +public onDomainSelection(event: any) { + + console.log(event); + this.addedCluster.domainNames = [event] + +} + +public openModal() { + if(this.domains.length > 0) { + this.addedCluster.domainNames = [this.domains[0].name]; + } + this.modal.show(); +} + + + +} diff --git a/src/app/shared/admin/clusters/managerdetails/managerdetails.component.css b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/shared/admin/clusters/managerdetails/managerdetails.component.html b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9f7c872af6f0fe143d19d6bf09102cd51986309c --- /dev/null +++ b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.html @@ -0,0 +1,377 @@ +<div class=""> + <div class="background-section"> + <h4 style="font-size:15px; font-weight: bold">{{ 'CLUSTERS.TITLE_GENERAL' | translate }}</h4> + + <div class="panel-body"> + <form *ngIf="cluster" (submit)="submit()" class="form-horizontal" #clusterFormGeneral="ngForm"> + <div class="panel-default panel-heading">{{ 'CLUSTERS.GENERAL' | translate }} </div> + <div class="panel-body"> + + <div class="form-group"> + <label for="clusterId" class="col-sm-2 control-label">{{ 'CLUSTERS.ID' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterId" name="clusterId" + [(ngModel)]="cluster.id" [disabled]="true"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="clusterName" class="col-sm-2 control-label">{{ 'CLUSTERS.NAME' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterName" name="clusterName" + [(ngModel)]="cluster.name" [disabled]="false"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="ClusterCodeName" class="col-sm-2 control-label">{{ 'CLUSTERS.CODENAME' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="ClusterCodeName" name="ClusterCodeName" + [(ngModel)]="cluster.codename" [disabled]="false"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="clusterDescription" class="col-sm-2 control-label">{{ 'CLUSTERS.DESCRIPTION' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterDescription" name="clusterDescription" + [(ngModel)]="cluster.description" [disabled]="false"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="clusterCreationDate" class="col-sm-2 control-label">{{ 'CLUSTERS.DOMAIN' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <select id="domain" #domainSelect class="form-control" (change)="onDomainSelection(domainSelect.value)"> + <option *ngFor="let domain of domains" [selected]="cluster.domainNames[0] === domain.name" [value]="domain.name" + >{{domain.name}} + </option> + </select> + </div> + </div> + </div> + + <div class="form-group"> + <label for="state" class="col-sm-2 control-label">{{ 'CLUSTERS.STATE' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="state" name="state" + placeholder="{{'CLUSTERS.' + cluster.state.toString().toUpperCase()| translate}}" [disabled]="true"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="state" class="col-sm-2 control-label">{{ 'CLUSTERS.STATE_SINCE' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="state" name="state" + [ngModel]="formatDate(cluster.currentStateSince)" [disabled]="true"> + </div> + </div> + </div> + + + <div class="form-group"> + <label for="clusterCreationDate" class="col-sm-2 control-label">{{ 'CLUSTERS.CREATION_DATE' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterCreationDate" name="clusterCreationDate" + [ngModel]="formatDate(cluster.creationDate)" [disabled]="true"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="clusterModificationnDate" class="col-sm-2 control-label">{{ 'CLUSTERS.MODIFICATION_DATE' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterModificationnDate" name="clusterModificationnDate" + [ngModel]="formatDate(cluster.modificationDate) " [disabled]="true"> + </div> + </div> + </div> + + <div class="form-group"> + <label for="clusterPathConfigFile" class="col-sm-2 control-label">{{ 'CLUSTERS.PATH_TO_CONFIG' | translate }}</label> + <div class="col-sm-10"> + <div class="col-sm-10"> + <input type="text" class="form-control" id="clusterPathConfigFile" name="clusterPathConfigFile" + [(ngModel)]="cluster.pathConfigFile" [disabled]="true"> + </div> + </div> + </div> + + </div> + </form> + </div> +</div> +</div> + +<div class=""> + <div class="background-section"> + <h4 style="font-size:15px; font-weight: bold">{{ 'CLUSTERS.TITLE' | translate }}</h4> + <div class="panel-body"> + <form *ngIf="cluster" (submit)="submit()" class="form-horizontal" #clusterForm="ngForm"> + <div class="panel-default panel-heading">{{ 'CLUSTERS.INGRESS' | translate }} </div> + <div class="panel-body"> + + <div class="form-group"> + <label for="ingresscontrollerconfigoption" class="col-sm-2 control-label">{{ 'CLUSTERS.CONTROLLER_CONFIG_OPTION' | translate }}</label> + <div class="col-sm-10" *ngIf="controllerConfigOption"> + <select class="form-control" id="ingresscontrollerconfigoption" name="ingresscontrollerconfigoption" + [(ngModel)]="cluster.ingress.controllerConfigOption" [disabled]="" required> + <option *ngFor="let configOption of getKeys(controllerConfigOption)" [value]="controllerConfigOption.get(configOption)">{{configOption}}</option> + </select> + </div> + </div> + <div class="form-group"> + <label for="suportedingressclass" class="col-sm-2 control-label">{{ 'CLUSTERS.SUPPORTED_INGRESS_CLASS' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="suportedingressclass" name="suportedingressclass" + [(ngModel)]="cluster.ingress.supportedIngressClass" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="publicingressclass" class="col-sm-2 control-label">{{ 'CLUSTERS.PUBLIC_INGRESS_CLASS' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="publicingressclass" name="publicingressclass" + [(ngModel)]="cluster.ingress.publicIngressClass" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="ingresscontrollerchartname" class="col-sm-2 control-label">{{ 'CLUSTERS.CONTROLLER_CHART_NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="ingresscontrollerchartname" name="ingresscontrollerchartname" + [(ngModel)]="cluster.ingress.controllerChartName" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="ingresscontrollerchartarchive" class="col-sm-2 control-label">{{ 'CLUSTERS.CONTROLLER_CHART_ARCHIVE' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="ingresscontrollerchartarchive" name="ingresscontrollerchartarchive" + [(ngModel)]="cluster.ingress.controllerChartArchive" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="ingressresourceconfigoption" class="col-sm-2 control-label">{{ 'CLUSTERS.RESOURCE_CONFIG_OPTION' | translate }}</label> + <div class="col-sm-10"> + <select class="form-control" id="ingressresourceconfigoption" name="ingressresourceconfigoption" + [(ngModel)]="cluster.ingress.resourceConfigOption" [disabled]="" required> + <option *ngFor="let configOption of getKeys(resourceConfigOption)" [value]="resourceConfigOption.get(configOption)">{{configOption}}</option> + </select> + </div> + </div> + <div class="form-group"> + <label for="ingressexternalservicedomain" class="col-sm-2 control-label">{{ 'CLUSTERS.EXTERNAL_SERVICE_DOMAIN' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="ingressexternalservicedomain" name="ingressexternalservicedomain" + [(ngModel)]="cluster.ingress.externalServiceDomain" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="ingresspublicservicedomain" class="col-sm-2 control-label">{{ 'CLUSTERS.PUBLIC_SERVICE_DOMAIN' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="ingresspublicservicedomain" name="ingresspublicservicedomain" + [(ngModel)]="cluster.ingress.publicServiceDomain" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="ingresstlssupported" class="col-sm-2 control-label">{{ 'CLUSTERS.TLS_SUPPORTED' | translate }}</label> + <div class="col-sm-10"> + <input type="checkbox" id="ingresstlssupported" name="ingresstlssupported" + [(ngModel)]="cluster.ingress.tlsSupported" [checked]="cluster.ingress.tlsSupported == true" [disabled]=""> + </div> + </div> + <div class="form-group" *ngIf="cluster.ingress.tlsSupported"> + <label for="ingresscertificateconfigoption" class="col-sm-2 control-label">{{ 'CLUSTERS.INGRESS_CERTIFICATE_CONFIG_OPTION' | translate }}</label> + <div class="col-sm-10"> + <select class="form-control" id="ingresscertificateconfigoption" name="ingresscertificateconfigoption" + [(ngModel)]="cluster.ingress.certificateConfigOption" [disabled]="" required> + <option *ngFor="let configOption of getKeys(certificateConfigOption)" [value]="certificateConfigOption.get(configOption)">{{configOption}}</option> + </select> + </div> + </div> + <div class="form-group" *ngIf="cluster.ingress.tlsSupported"> + <label for="ingressissuerorwildcardname" class="col-sm-2 control-label">{{ 'CLUSTERS.INGRESS_ISSUER_OR_WILDCARD_NAME' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="ingressissuerorwildcardname" name="ingressissuerorwildcardname" + [(ngModel)]="cluster.ingress.issuerOrWildcardName" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="ingressperdomain" class="col-sm-2 control-label">{{ 'CLUSTERS.INGRESS_PER_DOMAIN' | translate }}</label> + <div class="col-sm-10"> + <input type="checkbox" id="ingressperdomain" name="ingressperdomain" + [(ngModel)]="cluster.ingress.ingressPerDomain" [checked]="cluster.ingress.ingressPerDomain == true" [disabled]=""> + </div> + </div> + </div> + + <div class="panel-default panel-heading">{{ 'CLUSTERS.DEPLOYMENT' | translate }}</div> + <div class="panel-body"> + <div class="form-group"> + <label for="deploymentnamespaceconfigoption" class="col-sm-2 control-label">{{ 'CLUSTERS.NAMESPACE_CONFIG_OPTION' | translate }}</label> + <div class="col-sm-10" *ngIf="controllerConfigOption"> + <select class="form-control" id="deploymentnamespaceconfigoption" name="deploymentnamespaceconfigoption" + [(ngModel)]="cluster.deployment.namespaceConfigOption" [disabled]="" required> + <option *ngFor="let configOption of getKeys(namespaceConfigOption)" [value]="namespaceConfigOption.get(configOption)">{{configOption}}</option> + </select> + </div> + </div> + <div class="form-group"> + <label for="deploymentdefaultnamespace" class="col-sm-2 control-label">{{ 'CLUSTERS.DEFAULT_NAMESPACE' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="deploymentdefaultnamespace" name="deploymentdefaultnamespace" #namespace="ngModel" pattern="[a-z-]*" maxlength="64" + [(ngModel)]="cluster.deployment.defaultNamespace" [disabled]=""> + <div *ngIf="namespace.invalid && (namespace.dirty || namespace.touched)" class="alert alert-danger"> + <div *ngIf="namespace.errors.pattern">{{ 'CLUSTERS.NAMESPACE_PATTERN_VALIDATION_MESSAGE' | translate }}</div> + <div *ngIf="namespace.errors.maxlength">{{ 'CLUSTERS.NAMESPACE_MAX_LENGTH_VALIDATION_MESSAGE' | translate }}</div> + </div> + </div> + </div> + <div class="form-group"> + <label for="deploymentdefaultstorageclass" class="col-sm-2 control-label">{{ 'CLUSTERS.DEFAULT_STORAGE_CLASS' | translate }}</label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="deploymentdefaultstorageclass" name="deploymentdefaultstorageclass" + [(ngModel)]="cluster.deployment.defaultStorageClass" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="forceDedicatedWorkers" class="col-sm-2 control-label">{{ 'CLUSTERS.FORCE_DEDICATED_WORKERS' | translate }}</label> + <div class="col-sm-10"> + <input type="checkbox" id="forceDedicatedWorkers" name="forceDedicatedWorkers" + [(ngModel)]="cluster.deployment.forceDedicatedWorkers" [checked]="cluster.deployment.forceDedicatedWorkers == true" [disabled]="isInMode(ComponentMode.VIEW)"> + </div> + </div> + <div class="form-group"> + <label for="smtpServerHostname" class="col-sm-2 control-label">{{ 'CLUSTERS.SMTP_SERVER_HOSTNAME' | translate }}: </label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="smtpServerHostname" name="smtpServerHostname" + [(ngModel)]="cluster.deployment.smtpServerHostname" [disabled]="" required #smtpServerHostname="ngModel"> + <div class="alert alert-danger" *ngIf="smtpServerHostname.invalid && (smtpServerHostname.dirty || smtpServerHostname.touched)"> + <div *ngIf="smtpServerHostname.errors.required">SMTP server hostname is required</div> + </div> + </div> + </div> + <div class="form-group"> + <label for="smtpServerPort" class="col-sm-2 control-label">{{ 'CLUSTERS.SMTP_SERVER_PORT' | translate }}: </label> + <div class="col-sm-10"> + <input type="number" class="form-control no-spin" id="smtpServerPort" name="smtpServerPort" + [(ngModel)]="cluster.deployment.smtpServerPort" [disabled]="" required #smtpServerPort="ngModel"> + <div class="alert alert-danger" *ngIf="smtpServerPort.invalid && (smtpServerPort.dirty || smtpServerPort.touched)"> + <div *ngIf="smtpServerPort.errors.required">SMTP server port is required</div> + </div> + </div> + </div> + <div class="form-group"> + <label for="smtpServerUsername" class="col-sm-2 control-label">{{ 'CLUSTERS.SMTP_SERVER_USERNAME' | translate }}: </label> + <div class="col-sm-10"> + <input type="text" class="form-control" id="smtpServerUsername" name="smtpServerUsername" + [(ngModel)]="cluster.deployment.smtpServerUsername" [disabled]=""> + </div> + </div> + <div class="form-group"> + <label for="smtpServerPass" class="col-sm-2 control-label">{{ 'CLUSTERS.SMTP_SERVER_PASSWORD' | translate }}: </label> + <div class="col-sm-10"> + <input type="password" class="form-control" id="smtpServerPass" name="smtpServerPass" + [(ngModel)]="cluster.deployment.smtpServerPassword" [disabled]=""> + </div> + </div> + </div> + + <!-- Hide external networks section --> + <div [hidden]="true" class="panel-default panel-heading">{{ 'CLUSTERS.EXTERNAL_NETWORKS' | translate }}</div> + <div [hidden]="true" class="panel-body"> + <table class="table table-hover table-condensed" aria-describedby="Clusters details table"> + <thead> + <tr> + <th scope="col">{{ 'CLUSTERS.ADDRESS' | translate }}</th> + <th scope="col">{{ 'CLUSTERS.NETWORK' | translate }}</th> + <th scope="col">{{ 'CLUSTERS.NETMASK_LENGTH' | translate }}</th> + <th scope="col">{{ 'CLUSTERS.ASSIGNED' | translate }}</th> + <th scope="col">{{ 'CLUSTERS.ASSIGNED_SINCE' | translate }}</th> + <th scope="col">{{ 'CLUSTERS.ASSIGNED_TO' | translate }}</th> + <th scope="col"> </th> + </tr> + </thead> + <tbody> + <ng-template ngFor let-extnetwork [ngForOf]="cluster.externalNetworks" let-i="index" [ngForTrackBy]=""> + <tr> + <td> + <input type="text" class="form-control" [name]="'extnetextip'+i" + [(ngModel)]="extnetwork.externalIp" [disabled]="" required pattern="[0-9.]*" #ip="ngModel"> + <div class="alert alert-danger" *ngIf="ip.invalid && (ip.dirty || ip.touched)"> + <div *ngIf="ip.errors.required">{{ 'CLUSTERS.IP_REQUIRED_MESSAGE' | translate }}</div> + <div *ngIf="ip.errors.pattern">{{ 'CLUSTERS.IP_VALIDATION_MESSAGE' | translate }}</div> + </div> + </td> + <td> + <input type="text" class="form-control" [name]="'extnetextnet'+i" + [(ngModel)]="extnetwork.externalNetwork" [disabled]="" required pattern="[0-9.]*" #network="ngModel"> + <div class="alert alert-danger" *ngIf="network.invalid && (network.dirty || network.touched)"> + <div *ngIf="network.errors.required">{{ 'CLUSTERS.NETWORK_REQUIRED_MESSAGE' | translate }}</div> + <div *ngIf="network.errors.pattern">{{ 'CLUSTERS.NETWORK_ADDRESS_VALIDATION_MESSAGE' | translate }}</div> + </div> + </td> + <td> + <input type="text" class="form-control" [name]="'extnetmask'+i" + [(ngModel)]="extnetwork.externalNetworkMaskLength" [disabled]="" min="0" max="32" #mask="ngModel"> + <div class="alert alert-danger" *ngIf="mask.invalid && (mask.dirty || mask.touched)"> + <div *ngIf="mask.errors.min || mask.errors.max">{{ 'CLUSTERS.MASK_VALIDATION_MESSAGE' | translate }}</div> + </div> + </td> + <td> + <input type="checkbox" [name]="'extnetassigned'+i" + [(ngModel)]="extnetwork.assigned" [checked]="extnetwork.assigned == true" [disabled]=""> + </td> + <td> + <input type="text" class="form-control" [name]="'extnetassince'+i" + [(ngModel)]="extnetwork.assignedSince" [disabled]=" !extnetwork.assigned"> + </td> + <td> + <input type="text" class="form-control" [name]="'extnetassto'+i" + [(ngModel)]="extnetwork.assignedTo" [disabled]=" !extnetwork.assigned"> + </td> + <td class="text-right"> + <button *ngIf="" type="button" class="btn btn-sm btn-danger" + (click)="removeNetwork(extnetwork.id)">{{ 'CLUSTERS.REMOVE_BUTTON' | translate }} + </button> + </td> + </tr> + </ng-template> + <tr *ngIf=""> + <td></td><td></td><td></td><td></td><td></td><td></td> + <td> + <button type="button" class="btn btn-sm btn-secondary" + (click)="addNetwork()">{{ 'CLUSTERS.ADD_BUTTON' | translate }} + </button> + </td> + </tr> + </tbody> + </table> + </div> + + <div *ngIf="this.error" class="alert alert-danger"> + <p>{{this.error}}</p> + </div> + + <div class="flex justify-content-end"> + <button [disabled]="!clusterForm.form.valid" type="submit" class="btn btn-primary" + type="submit">{{ 'PORTAL_CONFIGURATION.SUBMIT_BUTTON' | translate }}</button> + </div> + <!-- <button + class="btn btn-primary">{{ 'CLUSTERS.SUBMIT_BUTTON' | translate }}</button> --> + </form> + </div> +</div> +</div> diff --git a/src/app/shared/admin/clusters/managerdetails/managerdetails.component.spec.ts b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9b928804c25a5b8ba4817fc5bab13369cef47cf --- /dev/null +++ b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.spec.ts @@ -0,0 +1,196 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ClusterManagerDetailsComponent } from './managerdetails.component'; +import { ClusterManagerService } from '../../../../service/cluster-manager.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CommonModule, DatePipe } from '@angular/common'; +import { of } from 'rxjs'; +import { ClusterManager } from '../../../../model/cluster-manager'; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { TranslateFakeLoader, TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { FormsModule } from '@angular/forms'; +import { IngressCertificateConfigOption, IngressControllerConfigOption, IngressResourceConfigOption, NamespaceConfigOption } from '../../../../model/cluster'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('ClusterManagerDetailsComponent', () => { + let component: ClusterManagerDetailsComponent; + let fixture: ComponentFixture<ClusterManagerDetailsComponent>; + let clusterService: jasmine.SpyObj<ClusterManagerService>; + let mockRouter: jasmine.SpyObj<Router>; + let mockActivatedRoute: any; + + const mockCluster: ClusterManager = { + id: 1, + name: 'Test Cluster', + state: "UP", + currentStateSince: new Date('2025-01-01'), + contactEmail: "test@test.test", + description: 'Test Description', + externalNetworks: [], + creationDate: new Date('2025-01-01'), + modificationDate: new Date('2025-02-01'), + codename: 'test-cluster', + pathConfigFile: '/path/to/config.yaml', + clusterConfigFile: 'Config', + domainNames: ["test"], + ingress: { + id: 1, + controllerConfigOption: IngressControllerConfigOption.USE_EXISTING, + controllerChartName: 'nginx-ingress', + controllerChartArchive: 'nginx-ingress-1.0.0.tgz', + resourceConfigOption: IngressResourceConfigOption.DEPLOY_FROM_CHART, + externalServiceDomain: 'example.com', + tlsSupported: true, + supportedIngressClass: 'nginx', + certificateConfigOption: IngressCertificateConfigOption.USE_LETSENCRYPT, + issuerOrWildcardName: 'letsencrypt', + ingressPerDomain: false, + publicIngressClass: 'nginx-public', + publicServiceDomain: 'public.example.com' + }, + deployment: { + id: 1, + smtpServerHostname: 'smtp.example.com', + smtpServerPort: '587', + smtpServerUsername: 'user@example.com', + smtpServerPassword: 'password', + defaultNamespace: 'default', + defaultStorageClass: 'standard', + namespaceConfigOption: NamespaceConfigOption.USE_DEFAULT_NAMESPACE, + forceDedicatedWorkers: false + } + }; + + beforeEach(waitForAsync(() => { + const clusterServiceSpy = jasmine.createSpyObj('ClusterManagerService', ['getClusterDetails', 'sendCluster', 'updateCluster']); + const routerSpy = jasmine.createSpyObj('Router', ['navigate']); + mockActivatedRoute = { + params: of({ id: 1 }) + }; + + + + TestBed.configureTestingModule({ + declarations: [ClusterManagerDetailsComponent], + imports: [FormsModule, + CommonModule, + HttpClientTestingModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader + } + }), + ], + providers: [ + { provide: ClusterManagerService, useValue: clusterServiceSpy }, + { provide: Router, useValue: routerSpy }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + DatePipe + ], + schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + + clusterService = TestBed.inject(ClusterManagerService) as jasmine.SpyObj<ClusterManagerService>; + mockRouter = TestBed.inject(Router) as jasmine.SpyObj<Router>; + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ClusterManagerDetailsComponent); + component = fixture.componentInstance; + + component.controllerConfigOption = new Map<string, IngressControllerConfigOption>(); + component.resourceConfigOption = new Map<string, IngressResourceConfigOption>(); + component.namespaceConfigOption = new Map<string, NamespaceConfigOption>(); + component.certificateConfigOption = new Map<string, IngressCertificateConfigOption>(); + + + clusterService.getClusterDetails.and.returnValue(of(mockCluster)); + fixture.detectChanges(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize and fetch cluster details', () => { + expect(clusterService.getClusterDetails).toHaveBeenCalledWith(1); + expect(component.cluster).toEqual(mockCluster); + }); + + it('should add a new network', () => { + component.cluster = { ...mockCluster, externalNetworks: [] }; + component.addNetwork(); + expect(component.cluster.externalNetworks.length).toBe(1); + }); + + it('should remove a network by id', () => { + component.cluster = { + ...mockCluster, + externalNetworks: [{ + id: 1, + assigned: false, + assignedSince: '', + assignedTo: '', + externalIp: '', + externalNetwork: '', + externalNetworkMaskLength: 0 + }, { + id: 2, + assigned: false, + assignedSince: '', + assignedTo: '', + externalIp: '', + externalNetwork: '', + externalNetworkMaskLength: 0 + }] + }; + component.removeNetwork(1); + expect(component.cluster.externalNetworks.length).toBe(1); + expect(component.cluster.externalNetworks[0].id).toBe(2); + }); + + it('should format date correctly', () => { + const date = new Date('2025-01-01T12:00:00'); + const formattedDate = component.formatDate(date); + expect(formattedDate).toBe('01-01-2025 12:00'); + }); + + it('should submit updated cluster', () => { + const updatedCluster = { ...mockCluster, name: 'Updated Cluster' }; + clusterService.updateCluster.and.returnValue(of(updatedCluster)); + + component.cluster = mockCluster; + component.submit(); + + expect(clusterService.updateCluster).toHaveBeenCalledWith(mockCluster); + expect(component.cluster).toEqual(updatedCluster); + }); + + it('should send cluster with file', () => { + const mockFile = new File(['test content'], 'test.yaml', { type: 'application/x-yaml' }); + const mockEvent = { files: [mockFile] }; + const mockResponse: ClusterManager = { + id: 1, + name: 'Test Cluster', + state:"UP", + currentStateSince: new Date('2025-01-01'), + contactEmail: "test@test.test", + description: 'Test Description', + externalNetworks: [], + creationDate: new Date('2025-01-01'), + modificationDate: new Date('2025-02-01'), + codename: 'test-cluster', + pathConfigFile: '/path/to/config.yaml', + clusterConfigFile: 'Config', + ingress: null, + deployment: null, + domainNames: ["test"] + }; + + clusterService.sendCluster.and.returnValue(of(mockResponse)); + + component.sendCluster(mockEvent); + + expect(clusterService.sendCluster).toHaveBeenCalledWith(mockFile, jasmine.any(ClusterManager)); + }); +}); \ No newline at end of file diff --git a/src/app/shared/admin/clusters/managerdetails/managerdetails.component.ts b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f275e021482b68feb11db39c7a9709ea70cda5b --- /dev/null +++ b/src/app/shared/admin/clusters/managerdetails/managerdetails.component.ts @@ -0,0 +1,118 @@ +import { Component, OnInit } from '@angular/core'; +import { ClusterManagerService } from '../../../../service/cluster-manager.service'; +import { ClusterManager } from '../../../../model/cluster-manager'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BaseComponent } from '../../../common/basecomponent/base.component'; +import { ClusterExtNetwork, IngressCertificateConfigOption, IngressControllerConfigOption, IngressResourceConfigOption, NamespaceConfigOption } from '../../../../model/cluster'; +import { DatePipe } from '@angular/common'; +import { DomainService } from '../../../../service'; + +@Component({ + selector: 'app-manager-details', + templateUrl: './managerdetails.component.html', + styleUrl: './managerdetails.component.css' +}) +export class ClusterManagerDetailsComponent extends BaseComponent implements OnInit { + + public cluster: ClusterManager ; + public cluterId; + public error = ""; + + public domains = []; + + + controllerConfigOption: Map<string, IngressControllerConfigOption> = new Map<string, IngressControllerConfigOption>(); + + resourceConfigOption: Map<string, IngressResourceConfigOption> = new Map<string, IngressResourceConfigOption>(); + + namespaceConfigOption: Map<string, NamespaceConfigOption> = new Map<string, NamespaceConfigOption>(); + + certificateConfigOption: Map<string, IngressCertificateConfigOption> = new Map<string, IngressCertificateConfigOption>(); + + + constructor(private clusterService: ClusterManagerService, + public router: Router, + private route: ActivatedRoute, + private datePipe: DatePipe, + private domainService: DomainService + + ) { + super(); + this.initializeMaps(); + } + + public ngOnInit() { + this.domainService.getAllBase().subscribe(result => this.domains = result); + + this.route.params.subscribe(params => { + this.cluterId = +params['id']; + + this.clusterService.getClusterDetails(this.cluterId).subscribe(result => { + console.log(result); + this.cluster = result; + } ) + }) + } + + public sendCluster(event: any) { + console.log(event); + const file = event.files[0]; + const view = new ClusterManager(); + view.name = "test" + view.description="testest" + this.clusterService.sendCluster(file, view).subscribe(result => { + console.log(result); + } + ) + } + + private initializeMaps() { + this.resourceConfigOption.set('Do nothing', IngressResourceConfigOption.NOT_USED); + this.resourceConfigOption.set('Deploy new resource from the definition in the application chart', IngressResourceConfigOption.DEPLOY_FROM_CHART); + this.controllerConfigOption.set('Use existing', IngressControllerConfigOption.USE_EXISTING); + this.controllerConfigOption.set('Deploy new controller from chart repository', IngressControllerConfigOption.DEPLOY_NEW_FROM_REPO); + this.controllerConfigOption.set('Deploy new controller from local chart archive', IngressControllerConfigOption.DEPLOY_NEW_FROM_ARCHIVE); + this.namespaceConfigOption.set('Use default namespace', NamespaceConfigOption.USE_DEFAULT_NAMESPACE); + this.namespaceConfigOption.set('Use domain namespace', NamespaceConfigOption.USE_DOMAIN_NAMESPACE); + this.namespaceConfigOption.set('Create namespace', NamespaceConfigOption.CREATE_NAMESPACE); + this.certificateConfigOption.set('Use my own wildcard certificate', IngressCertificateConfigOption.USE_WILDCARD); + this.certificateConfigOption.set('Generate LetsEncrypt certificates automatically', IngressCertificateConfigOption.USE_LETSENCRYPT); + } + + public getKeys(map) { + return Array.from(map.keys()); + } + + public removeNetwork(id) { + this.cluster.externalNetworks.splice( + this.cluster.externalNetworks.findIndex( + function (i) { + return i.id = id; + }), 1); + } + + public addNetwork() { + const newobj: ClusterExtNetwork = new ClusterExtNetwork(); + this.cluster.externalNetworks.push(newobj); + } + + public formatDate(date: Date) { + return this.datePipe.transform(date, 'dd-MM-yyyy HH:mm'); + } + + public submit(): void { + console.log(this.cluster); + this.clusterService.updateCluster(this.cluster).subscribe(result => { + console.log(result); + this.cluster = result; + }); + } + + public onDomainSelection(event: any) { + + console.log(event); + this.cluster.domainNames = [event] + + } + +} diff --git a/src/app/shared/applications/applications.component.html b/src/app/shared/applications/applications.component.html index 533fc8862b187fa46f722615edd9a63f1560082d..840b0e5448d17aa65e8bb0445b661d5992a57484 100644 --- a/src/app/shared/applications/applications.component.html +++ b/src/app/shared/applications/applications.component.html @@ -16,6 +16,16 @@ </select> </div> </div> + <div class="col-xs-12 col-sm-12 col-md-4 col-lg-3" style="padding: 8px 15px; display: flex"> + <p-checkbox + inputId="subscribed" + binary="true" + [(ngModel)]="showSubscribed" + [ngModelOptions]="{standalone: true}" + id="subscribed" + ngDefaultControl/> + <label style="margin: 0; padding-left: 5px; font-weight: unset; text-wrap: nowrap" for="subscribed">Show subscribed only</label> + </div> <!-- <div class="col-xs-12 col-sm-2 col-md-2 col-lg-2">--> <!-- <div class="btn-toolbar" role="toolbar">--> <!-- <div class="btn-group pull-right">--> @@ -31,4 +41,4 @@ </div> <hr> -<nmaas-applist [appView]="appView" [listType]="selectedListType" [applications]="applications" [selected]="selected" [domainId]="domainId" [domain]="domain"></nmaas-applist> +<nmaas-applist [appView]="appView" [listType]="selectedListType" [showSubscribed]="showSubscribed" [applications]="applications" [selected]="selected" [domainId]="domainId" [domain]="domain"></nmaas-applist> diff --git a/src/app/shared/applications/applications.component.spec.ts b/src/app/shared/applications/applications.component.spec.ts index d45f571f8995d6bb44529fd4672c7173b6bf67e7..296500a694818afb5a8b519a9638002e64ebc1fd 100644 --- a/src/app/shared/applications/applications.component.spec.ts +++ b/src/app/shared/applications/applications.component.spec.ts @@ -18,6 +18,7 @@ import {of} from 'rxjs'; import {AppInstallModalComponent} from '../modal/appinstall'; import {ModalComponent} from '../modal'; import {Domain} from '../../model/domain'; +import {CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from '@angular/core'; describe('ApplicationsComponent', () => { let component: ApplicationsViewComponent; @@ -52,7 +53,8 @@ describe('ApplicationsComponent', () => { } }), ], - providers: [AppsService, AppSubscriptionsService, UserDataService, AppConfigService, TagService, DomainService, AppInstanceService] + providers: [AppsService, AppSubscriptionsService, UserDataService, AppConfigService, TagService, DomainService, AppInstanceService], + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA] }) .compileComponents(); })); diff --git a/src/app/shared/applications/applications.component.ts b/src/app/shared/applications/applications.component.ts index 03a1b40421b93fceead7206aefb4d2e6c9ffcace..0c5ca512ab2e7d7b9d2e5d763290d15628ed62e1 100644 --- a/src/app/shared/applications/applications.component.ts +++ b/src/app/shared/applications/applications.component.ts @@ -60,6 +60,7 @@ export class ApplicationsViewComponent implements OnInit, OnChanges { public sortMode = 'NAME'; private popStats: any = {}; + public showSubscribed = false; constructor(private appsService: AppsService, private appSubsService: AppSubscriptionsService, @@ -76,7 +77,6 @@ export class ApplicationsViewComponent implements OnInit, OnChanges { this.popStats = data; } ) - } ngOnChanges(changes: SimpleChanges) { diff --git a/src/app/shared/applications/list/applist.component.html b/src/app/shared/applications/list/applist.component.html index 4d0aafd6788ff2fa08c866c1756e6428e56e3c09..62290e16b297d01e643bf0bc1c05fc4642aa2f8f 100644 --- a/src/app/shared/applications/list/applist.component.html +++ b/src/app/shared/applications/list/applist.component.html @@ -3,7 +3,7 @@ <div *ngIf="listType === ListType.GRID" class="tab-pane fade in" [class.active]="listType === ListType.GRID" id="tab-grid"> <div class="row auto-clear"> - <nmaas-applist-element *ngFor="let app of applications | async" [app]="app" [domainId]="domainId" [selected]="(selected | async)?.has(app.id)" [domain]="domainObject" ></nmaas-applist-element> + <nmaas-applist-element *ngFor="let app of applications | async" [app]="app" [showSubscribed]="showSubscribed" [domainId]="domainId" [selected]="(selected | async)?.has(app.id)" [domain]="domainObject" ></nmaas-applist-element> </div> </div> <div *ngIf="listType === ListType.TABLE" class="tab-pane fade in" diff --git a/src/app/shared/applications/list/applist.component.ts b/src/app/shared/applications/list/applist.component.ts index 7d8cccabdb8b86d45bdf78ea3825e755ba2f84ea..eef0d2b72ac4589058931269fbdff4cb1a565ea7 100644 --- a/src/app/shared/applications/list/applist.component.ts +++ b/src/app/shared/applications/list/applist.component.ts @@ -42,9 +42,13 @@ export class AppListComponent implements OnInit, OnChanges { @Input() public domain: Observable<Domain>; + @Input() + public showSubscribed: boolean; + public domainObject: Domain = undefined; + constructor(private appSubscriptionService: AppSubscriptionsService, private userDataService: UserDataService, private appConfig: AppConfigService, diff --git a/src/app/shared/applications/list/element/appelement.component.html b/src/app/shared/applications/list/element/appelement.component.html index 8bb64633ab90a936f0d0cd4a8448f38db892a069..0a4686b1dd0589b99b239b9987108fd2f48de632 100644 --- a/src/app/shared/applications/list/element/appelement.component.html +++ b/src/app/shared/applications/list/element/appelement.component.html @@ -1,5 +1,23 @@ -<div *ngIf="app && showAppInList" class="col-xs-12 col-sm-12 col-md-6 col-lg-4 col-xl-3"> +<div *ngIf="app && showAppInList && !showSubscribed" class="col-xs-12 col-sm-12 col-md-6 col-lg-4 col-xl-3"> + <div class="app-card clickable-tail" [routerLink]="['/apps', app.id]"> + <div class = "element-container"> + <div [class.subscribed]="selected"> + <i [class.pi]="selected" [class.pi-star-fill]="selected" [class.star]="selected"></i> + </div> + <div class="image-container-outer"> + <img class="center center-block image-container" alt="App logo" [src]="appImagesService.getAppLogoUrl(app?.id) | secure" + onError="this.src='assets/images/app-logo-example.png';" /> + </div> + <div class="text-center description-container"> + <h3 class="app-name">{{app?.name}}</h3> + <div class="text-two-lines">{{getDescription()?.briefDescription}}</div> + </div> + </div> + </div> +</div> + +<div *ngIf="selected && showAppInList && showSubscribed" class="col-xs-12 col-sm-12 col-md-6 col-lg-4 col-xl-3"> <div class="app-card clickable-tail" [routerLink]="['/apps', app.id]"> <div class = "element-container"> <div [class.subscribed]="selected"> diff --git a/src/app/shared/applications/list/element/appelement.component.ts b/src/app/shared/applications/list/element/appelement.component.ts index 434d17c702c85570022e2b406c17281a856e9960..d18b0371caf3129e34fd486cb02436f80b85b0e3 100644 --- a/src/app/shared/applications/list/element/appelement.component.ts +++ b/src/app/shared/applications/list/element/appelement.component.ts @@ -40,6 +40,9 @@ export class AppElementComponent implements OnInit, OnChanges { @Input() public domain: Domain; + @Input() + public showSubscribed: boolean; + @ViewChild(AppInstallModalComponent) public readonly modal: AppInstallModalComponent; diff --git a/src/app/shared/common/domainfilter/domainfilter.component.html b/src/app/shared/common/domainfilter/domainfilter.component.html index b7fe493cd5b083b6be70c23477a0e53e06cb4c94..a307df6d8ce63368b3b289c83c0cdccf9a2b0e4d 100644 --- a/src/app/shared/common/domainfilter/domainfilter.component.html +++ b/src/app/shared/common/domainfilter/domainfilter.component.html @@ -20,11 +20,13 @@ optionLabel="name" [filter]="false" [filterPlaceholder]="'SEARCH' | translate" - (onChange)="changeDomain($event.value.id, $event.value.name)"> + (onChange)="changeDomain($event.value.id, $event.value.name)" + appendTo="body"> <ng-template pTemplate="selectedItem" let-item> <span style="color: #414F6B;"> - {{ "FILTER.DOMAIN" | translate }}: {{ item?.name }} +<!-- {{ "FILTER.DOMAIN" | translate }}: --> + {{ item?.name }} </span> </ng-template> diff --git a/src/app/shared/common/domainfilter/domainfilter.component.spec.ts b/src/app/shared/common/domainfilter/domainfilter.component.spec.ts index 1dfabffcdf0244881d130a98c96a00ae59ebc875..4f293574d7139510c4f2b6c61f7c8e53c94578e4 100644 --- a/src/app/shared/common/domainfilter/domainfilter.component.spec.ts +++ b/src/app/shared/common/domainfilter/domainfilter.component.spec.ts @@ -28,6 +28,7 @@ describe('DomainFilterComponent', () => { applicationStatePerDomain: [], groups: [], annotations: [], + clusters: [], } const domain1: Domain = { @@ -41,6 +42,7 @@ describe('DomainFilterComponent', () => { applicationStatePerDomain: [], groups: [], annotations: [], + clusters: [], }; const domain2: Domain = { @@ -54,6 +56,7 @@ describe('DomainFilterComponent', () => { applicationStatePerDomain: [], groups: [], annotations: [], + clusters: [], }; beforeEach(waitForAsync(() => { diff --git a/src/app/shared/common/domainfilter/domainfilter.component.ts b/src/app/shared/common/domainfilter/domainfilter.component.ts index 426978d47713b97d1e8a8f1c019d4c1ea738d4a7..57723deda8e95bddd8f115b8ca89e7f32fdeb62a 100644 --- a/src/app/shared/common/domainfilter/domainfilter.component.ts +++ b/src/app/shared/common/domainfilter/domainfilter.component.ts @@ -74,7 +74,7 @@ export class DomainFilterComponent implements OnInit { public updateDomains(): void { if (this.authService.hasRole('ROLE_SYSTEM_ADMIN')) { - this.domains = this.domainService.getAll(); + this.domains = this.domainService.getAllBase(); } else { this.domains = this.domainService.getMyDomains(); const globalDomainId = this.domainService.getGlobalDomainId(); @@ -109,6 +109,7 @@ export class DomainFilterComponent implements OnInit { private sortDomains(): void { const globalDomainId = this.domainService.getGlobalDomainId(); + console.log(this.domains); this.domains = this.domains.pipe( map( domains => { @@ -126,10 +127,13 @@ export class DomainFilterComponent implements OnInit { domains.unshift(defaultDomain) } this.domainsLocal = domains; + this.filteredDomainsSub.next(this.domainsLocal); return domains } ) ) + console.log(this.domainsLocal); + } public changeDomain(domainId: number, domainName: string) { diff --git a/src/app/shared/contact/contact.component.ts b/src/app/shared/contact/contact.component.ts index 732955b972b12bf62d045cb8aa57edfbc34ba737..2ccb5d3e20301d266483167f47290518c7b79a2b 100644 --- a/src/app/shared/contact/contact.component.ts +++ b/src/app/shared/contact/contact.component.ts @@ -125,8 +125,11 @@ export class ContactComponent implements OnInit { private sendMail(data: any): Observable<void> { // submit captcha request return this.recaptchaV3Service.execute('contactForm').pipe( - catchError(_ => of('')), // in case of captcha error return empty token + catchError(error => { + console.error(error); + return of(error)}), // in case of captcha error return empty token map((token) => { + console.log(token) const result = {token, mail: new Mail()} // create mail object result.mail.otherAttributes = data; // set properties and mail attributes result.mail.otherAttributes.subType = this.formType.key; diff --git a/src/app/shared/domain-namespace-annotations/domain-namespace-annotations.component.css b/src/app/shared/domain-namespace-annotations/domain-namespace-annotations.component.css index f28c0dc3b4b161c71b93acbce3445f3e467cb425..1ffc9808117f220b152d39a389897a633805cf4c 100644 --- a/src/app/shared/domain-namespace-annotations/domain-namespace-annotations.component.css +++ b/src/app/shared/domain-namespace-annotations/domain-namespace-annotations.component.css @@ -1,3 +1,4 @@ .border-red { border: 1px solid red; } + diff --git a/src/app/shared/domain-namespace-annotations/domain-namespace-annotations.component.html b/src/app/shared/domain-namespace-annotations/domain-namespace-annotations.component.html index 826defac24c854dbfe46797077b2e1331ea1dc36..b710e9074aaa9c5bfd6b9d37c19c7ffa2f5566f8 100644 --- a/src/app/shared/domain-namespace-annotations/domain-namespace-annotations.component.html +++ b/src/app/shared/domain-namespace-annotations/domain-namespace-annotations.component.html @@ -1,14 +1,15 @@ -<div class="panel panel-default" style="width: 100% !important;" > +<div class="background-section" style="width: 100% !important;" > <div class="panel-heading"> - <div style="display: flex; justify-content: start; align-items: center"> - <div> + <div style="display: flex; justify-content:space-between"> + <h4 style="font-size:15px; font-weight: bold"> {{'DOMAINS.ANNOTATIONS.CREATION' | translate}} - </div> + </h4> + <button type="button" class="btn btn-text" (click)="addAnnotation()">{{'DOMAINS.ANNOTATIONS.ADD'| translate}}</button> </div> </div> <div class="panel-body"> <div style="display: flex; justify-content: end"> - <button type="button" class="btn btn-primary" (click)="addAnnotation()">{{'DOMAINS.ANNOTATIONS.ADD'| translate}}</button> + </div> <div class="grid flex flex-grow-1"> <div class="col-4"> @@ -63,8 +64,8 @@ </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" (click)="closeModal()" [disabled]="isKeyNotUniqueAdd(newAnnotations) || !isKeyPatternCorrect">{{'DOMAINS.ADD_BUTTON' | translate}}</button> <button type="button" class="btn btn-secondary" (click)="modal.hide()">{{'APP_CHANGE_STATE_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-primary" (click)="closeModal()" [disabled]="isKeyNotUniqueAdd(newAnnotations) || !isKeyPatternCorrect">{{'DOMAINS.ADD_BUTTON' | translate}}</button> </div> </nmaas-modal> @@ -94,7 +95,7 @@ </div> <div class="nmaas-modal-footer"> - <button type="button" class="btn btn-primary" (click)="closeModalEdit()" [disabled]="isEditAnnotationCorrect(editAnnotation) || !isKeyPatternCorrect">{{'DOMAINS.EDIT_BUTTON' | translate}}</button> <button type="button" class="btn btn-secondary" (click)="editModal.hide()">{{'APP_CHANGE_STATE_MODAL.CANCEL_BUTTON' | translate}}</button> + <button type="button" class="btn btn-primary" (click)="closeModalEdit()" [disabled]="isEditAnnotationCorrect(editAnnotation) || !isKeyPatternCorrect">{{'DOMAINS.EDIT_BUTTON' | translate}}</button> </div> </nmaas-modal> diff --git a/src/app/shared/footer/footer.component.css b/src/app/shared/footer/footer.component.css index 1d1b633e0b99bf386b188289e88d4fa62337f785..d1bab419a3afca8ce4d51a601485adb82bfaed8d 100644 --- a/src/app/shared/footer/footer.component.css +++ b/src/app/shared/footer/footer.component.css @@ -1,8 +1,10 @@ footer { flex-shrink: 0; - padding-bottom: 15px; + padding-bottom: 0px; padding-top: 15px; - background-color: #e7e7e7; + background-color: var(--menu-color); + display:flex; + /* margin-left: -1rem; */ } /*explicit text alignment for chrome*/ @@ -17,9 +19,9 @@ a:link{ text-decoration: underline; } -a:hover { - font-weight: 600; -} +/*a:hover {*/ +/* font-weight: 600;*/ +/*}*/ .img-footer{ max-height: 34px; @@ -86,6 +88,6 @@ a:hover { flex-direction: row; } .container-width { - width: 85vw; + width: calc(100vw - var(--left-panel-width)); margin: auto; } diff --git a/src/app/shared/footer/footer.component.html b/src/app/shared/footer/footer.component.html index bbce48b72740f03e6b38520708430af57b838036..e39c025be237c2abd69a09b6811a31051f220a54 100644 --- a/src/app/shared/footer/footer.component.html +++ b/src/app/shared/footer/footer.component.html @@ -1,29 +1,34 @@ <footer class="footer col-xs-12" id="global-footer"> <div class="container-width"> - <div class="row row-center"> - <div class="col-sm-2"> - <!-- nmaas Logo optionally --> - <a href="https://www.geant.org/"> - <img alt="Geant Logo" src="/assets/images/geant-logo.png" width="200" class="image-link"/> - </a> - </div> - <div class="col-sm-3"> - <img alt="EU flag" src="/assets/images/cofunded.png" width="200" style="padding-top: 15px; "/> - </div> - <div class="col-sm-2 col-sm-offset-5"> - <p> - <a *ngIf="landingProfile === 'VNOC'" href="https://docs.nmaas.eu/use-cases/virtual-noc/vnoc-introduction/">Documentation</a> - <a *ngIf="landingProfile !== 'VNOC'" href="https://docs.nmaas.eu/use-cases/virtual-lab/vlab-introduction/">Documentation</a> - </p> - <p><a routerLink="/privacy">{{ 'FOOTER.NOTICE' | translate }}</a></p> - <p><a routerLink="/aup">{{ 'FOOTER.AUP' | translate }}</a></p> - <p><a routerLink="/about">{{ 'FOOTER.CONTACT' | translate }}</a></p> + <div class="" style="display: flex; justify-content: space-between"> + <div style="width:50%; display: flex"> + <div class="" style="margin-right: 20px"> + <!-- nmaas Logo optionally --> + <a href="https://www.geant.org/"> + <img alt="GÉANT Logo" src="/assets/images/geant-logo.png" width="100" class="image-link"/> + </a> + </div> + <div class=""> + <img alt="EU flag" src="/assets/images/cofunded.png" width="180" style="padding-top: 5px; "/> + </div> + </div> + <div class="" style="display: flex; width: 50%; justify-content: end; margin-right: 50px"> + <div style="margin-right: 30px"> + <p> + <a *ngIf="landingProfile === 'VNOC'" href="https://docs.nmaas.eu/use-cases/virtual-noc/vnoc-introduction/">Documentation</a> + <a *ngIf="landingProfile !== 'VNOC'" href="https://docs.nmaas.eu/use-cases/virtual-lab/vlab-introduction/">Documentation</a> + </p> + <p><a routerLink="/about">{{ 'FOOTER.CONTACT' | translate }}</a></p> + </div> + <div> + <p><a routerLink="/privacy">{{ 'FOOTER.NOTICE' | translate }}</a></p> + <p><a routerLink="/aup">{{ 'FOOTER.AUP' | translate }}</a></p> + </div> </div> </div> - - <div class="row"> - <div class="col-xs-10"> - </div> +<!-- <div class="row">--> +<!-- <div class="col-xs-10">--> +<!-- </div>--> <!-- <div class="col-xs-2">--> <!-- <div>--> <!-- <a class="footer-light footer-move-top" (click)="this.moveToTop();">--> @@ -31,7 +36,6 @@ <!-- </a>--> <!-- </div>--> <!-- </div>--> - </div> - +<!-- </div>--> </div> </footer> diff --git a/src/app/shared/left-menu/left-menu.component.css b/src/app/shared/left-menu/left-menu.component.css index afd8e55a067061f61d251e45aee4ab6787b740e5..4ae8e6cfc231bdce82773cef601b773e09fe7be0 100644 --- a/src/app/shared/left-menu/left-menu.component.css +++ b/src/app/shared/left-menu/left-menu.component.css @@ -8,16 +8,17 @@ display: flex; flex-direction: column; padding: 1rem; - } - .menu ul { +} +.menu ul { list-style: none; padding: 0; - } - .menu li { - padding: 10px 10px; - margin: 0.5rem 0; - border-radius: 4px; - } +} +.menu li { + padding: 10px 10px; + margin: 0.5rem 0; + border-radius: 4px; +} + .menu li:hover { padding: 10px 5px; background: var(--background); @@ -29,15 +30,18 @@ .menu li.active:hover{ padding: 10px 5px; } +.collapsed{ + /*width:40px;*/ +} .active{ padding: 10px 5px; background: white; border-left: 5px solid var(--menu-pink) } - .menu a { +.menu a { color: var(--l-text-color); text-decoration: none; - } +} :host ::ng-deep .p-button{ padding: 8px 10px; width:100%; @@ -60,3 +64,46 @@ :host ::ng-deep .p-menu .p-menuitem:not(.p-highlight):not(.p-disabled) > .p-menuitem-content:hover{ background:var(--user-button-background-hover); } +:host ::ng-deep .p-accordion .p-accordion-header .p-accordion-header-link{ + display:flex; + flex-direction: row-reverse; + justify-content: space-between; + border:none; +} +:host ::ng-deep a { + font-weight: normal; + color: var(--l-text-color) +} +:host ::ng-deep a:hover { + text-decoration:none; + color: var(--l-text-color); + outline:none; +} +:host ::ng-deep a:focus{ + outline: none; + text-decoration: none; +} +:host ::ng-deep .p-accordion .p-accordion-content{ + background: transparent; + border: none; +} + +.toggle-button{ + width: 30px; + height:30px; + background: var(--menu-color); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + border-radius: 50px; + position: absolute; + left: 284px; + display: flex; + align-items: center; + justify-content: center; +} + +.collapsed-user{ + width: 130px; +} +::ng-deep app-modal-notification-send { + +} diff --git a/src/app/shared/left-menu/left-menu.component.html b/src/app/shared/left-menu/left-menu.component.html index ad8ae052055756b291a667dcd50382ccc53e3e8f..38d7687035637e3d3ee3cc8913e63affe9d93231 100644 --- a/src/app/shared/left-menu/left-menu.component.html +++ b/src/app/shared/left-menu/left-menu.component.html @@ -1,58 +1,175 @@ -<div class="flex flex-column justify-content-between "> +<div class="flex flex-column justify-content-between menu-tr" [ngStyle]="{'width': isCollapsed ? '100px' : '300px'}" style="background: var(--menu-color); height: 100%; position: fixed;"> <div class="menu flex"> - <div> - <img src="../../../assets/images/logo-small.png" width="250px"> + <div style="display: flex; align-items: center"> + <div class="logo-container"> + <img *ngIf="!isCollapsed" class="logo" src="../../../assets/images/logo-small.png" width="250px"> + <img *ngIf="isCollapsed" class="logo" src="../../../assets/images/nmaas-cloud.png" width="50px"> + </div> + <div class="toggle-button" (click)="toggleMenu()" [ngStyle]="{'left': isCollapsed ? '85px' : '284px'}"> + <i style="font-size: 1.8rem" class="pi pi-angle-left"></i> + </div> </div> <div style="margin-top: 30px"> <nmaas-domain-filter class="drop-domain"></nmaas-domain-filter> </div> - <ul *ngIf="!toggleAdmin" style="margin-top: 30px"> - <li [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > - <a style="display: flex; align-items: center;" [routerLink]="['/']"> - <i class="pi pi-th-large" style="margin-right:10px; font-size: 15px"></i>Applications</a> - </li> - <li [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > - <a href="#" style="display: flex; align-items: center;" [routerLink]="['/instances']"> - <i class="pi pi-server" style="margin-right:10px; font-size: 15px"></i>Instances</a> - </li> + <ul *ngIf="!toggleAdmin" style="margin-top: 30px" > + <li [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['/']"> + <i class="pi pi-th-large" style="margin-right:10px; font-size: 18px" title="Application"></i> + <span *ngIf="!isCollapsed"> + Applications + </span> + </a> + </li> + <li [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a href="#" style="display: flex; align-items: center;" [routerLink]="['/instances']"> + <i class="pi pi-server" style="margin-right:10px; font-size: 18px" title=" Instances"></i> + <span *ngIf="!isCollapsed"> + Instances + </span> + </a> + </li> </ul> <ul *ngIf="toggleAdmin" style="margin-top: 30px"> - <li [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > - <a style="display: flex; align-items: center;" [routerLink]="['/dashboard']"> - <i class="pi pi-chart-bar" style="margin-right:10px; font-size: 15px"></i>Dashboard</a> + <li [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['admin/dashboard']"> + <i class="pi pi-chart-bar" style="margin-right:10px; font-size: 18px" title="Dashboard"></i> + <span *ngIf="!isCollapsed"> + Dashboard + </span> + </a> </li> - <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR', 'ROLE_VL_MANAGER', 'ROLE_VL_DOMAIN_ADMIN']" - [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > - <a style="display: flex; align-items: center;" [routerLink]="['/admin/domains']"> - <i class="pi pi-server" style="margin-right:10px; font-size: 15px"></i>{{ 'NAVBAR.DOMAINS' | translate }}</a> + <p-accordion> + <p-accordionTab *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR', 'ROLE_GROUP_MANAGER', 'ROLE_GROUP_DOMAIN_ADMIN']"> + <ng-template pTemplate="header"> + <div> + <i class="pi pi-server" style="margin-right:10px; font-size: 18px" title="{{ 'NAVBAR.DOMAINS' | translate }}"></i> + <span *ngIf="!isCollapsed"> + {{ 'NAVBAR.DOMAINS' | translate }} + </span> + </div> + </ng-template> + <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR', 'ROLE_GROUP_MANAGER', 'ROLE_GROUP_DOMAIN_ADMIN']" + [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}" > + <a style="display: flex; align-items: center;" [routerLink]="['/admin/domains']"> + <i class="pi pi-list" style="margin-right:10px; font-size: 18px" title="List"></i> + <span *ngIf="!isCollapsed"> + List + </span> + </a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']" + [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['/admin/domains/groups']"> + <i class="pi pi-table" style="margin-right:10px; font-size: 18px" title="Group"></i> + <span *ngIf="!isCollapsed"> + Group + </span> + </a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']" + [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['/admin/domains/bulks']"> + <i class="pi pi-sitemap" style="margin-right:10px; font-size: 18px" title="Bulk deployments"></i> + <span *ngIf="!isCollapsed"> + Bulk deployments + </span> + </a> + </li> + </p-accordionTab> + </p-accordion> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['/admin/apps/bulks']"> + <i class="pi pi-box" style="margin-right:10px; font-size: 18px" title="{{ 'BULK.APP.HEADER' | translate }}"></i> + <span *ngIf="!isCollapsed"> + {{ 'BULK.APP.HEADER' | translate }} + </span> + </a> </li> - <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> <a style="display: flex; align-items: center;" [routerLink]="['/admin/users']"> - <i class="pi pi-users" style="margin-right:10px; font-size: 15px"></i>{{ 'NAVBAR.USERS' | translate }}</a> + <i class="pi pi-users" style="margin-right:10px; font-size: 18px" title="{{ 'NAVBAR.USERS' | translate }}"></i> + <span *ngIf="!isCollapsed"> + {{ 'NAVBAR.USERS' | translate }} + </span> + </a> </li> - <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > - <a style="display: flex; align-items: center;" [routerLink]="['/']"> - <i class="pi pi-th-large" style="margin-right:10px; font-size: 15px"></i>Catalog</a> + <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_GROUP_DOMAIN_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['/admin/domain/users']"> + <i class="pi pi-users" style="margin-right:10px; font-size: 18px" title="{{ 'NAVBAR.DOMAIN_USERS' | translate }}"></i> + <span *ngIf="!isCollapsed"> + {{ 'NAVBAR.DOMAIN_USERS' | translate }} + </span> + </a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['/admin/apps']"> + <i class="pi pi-th-large" style="margin-right:10px; font-size: 18px" title="Catalog"></i> + <span *ngIf="!isCollapsed"> + Catalog + </span> + </a> </li> - <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> <a style="display: flex; align-items: center;" [routerLink]="['/admin/configuration']"> - <i class="pi pi-cog" style="margin-right:10px; font-size: 15px"></i>Settings</a> + <i class="pi pi-cog" style="margin-right:10px; font-size: 18px" title="Settings"></i> + <span *ngIf="!isCollapsed"> + Settings + </span> + </a> </li> - <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> + <a style="display: flex; align-items: center;" [routerLink]="['/admin/manage/clusters']"> + <i class="pi pi-cog" style="margin-right:10px; font-size: 18px"></i> + <span *ngIf="!isCollapsed"> + {{ 'CLUSTERS.CONFIGURATION' | translate }} + </span> + </a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> <a style="display: flex; align-items: center;" [routerLink]="['/admin/languages']"> - <i class="pi pi-tags" style="margin-right:10px; font-size: 15px"></i>{{'NAVBAR.LANGUAGES' | translate }}</a> + <i class="pi pi-tags" style="margin-right:10px; font-size: 18px" title=" {{'NAVBAR.LANGUAGES' | translate }}"></i> + <span *ngIf="!isCollapsed"> + {{'NAVBAR.LANGUAGES' | translate }} + </span> + </a> </li> - <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" > + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" [ngClass]="{'collapsed': isCollapsed}"> <a style="display: flex; align-items: center;" [routerLink]="['/admin/monitor']"> - <i class="pi pi-chart-line" style="margin-right:10px; font-size: 15px"></i>{{ 'NAVBAR.MONITOR' | translate }}</a> + <i class="pi pi-chart-line" style="margin-right:10px; font-size: 18px" title="{{ 'NAVBAR.MONITOR' | translate }}"></i> + <span *ngIf="!isCollapsed"> + {{ 'NAVBAR.MONITOR' | translate }} + </span> + </a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN']" (click)="showNotificationModal()" [ngClass]="{'collapsed': isCollapsed}"> + <i class="pi pi-send" style="margin-right:10px; font-size: 18px" title="{{ 'NAVBAR.ALL_USERS' | translate }}"></i> + <span *ngIf="!isCollapsed">{{ 'NAVBAR.ALL_USERS' | translate }}</span> </li> </ul> </div> <div class="menu flex"> - <p-menu #menu [model]="items" [popup]="true" class="test" /> - <p-button *ngIf="!toggleAdmin" (onClick)="menu.toggle($event)" class="user-button"><i class="pi pi-user" style="font-size: 13px; margin-right:10px"></i> User</p-button> - <p-button *ngIf="!toggleAdmin" (onClick)="adminPanel()" style="margin-top:10px"><i class="pi pi-sign-in" style="margin-right:10px; font-size: 15px"></i>Go to admin panel</p-button> - <p-button *ngIf="toggleAdmin" (onClick)="adminPanel()" style="margin-top:10px"><i class="pi pi-sign-in" style="margin-right:10px; font-size: 15px"></i>Go to user panel</p-button> + <p-menu #menu [model]="items" [popup]="true" class="test" [ngClass]="{'collapsed-user': isCollapsed}"/> + <p-button (onClick)="menu.toggle($event)" class="user-button"><i class="pi pi-user" style="font-size: 18px; margin-right:10px"[ngClass]="{'collapsed': isCollapsed}" title="User"></i> + <span *ngIf="!isCollapsed"> + User + </span> + </p-button> + <div style="margin-top:10px" [routerLink]="['admin/dashboard']"> + <p-button *ngIf="!toggleAdmin" (onClick)="adminPanel()" ><i class="pi pi-sign-in" style="margin-right:10px; font-size: 18px"[ngClass]="{'collapsed': isCollapsed}" title=" Go to admin panel"></i> + <span *ngIf="!isCollapsed"> + Admin panel + </span> + </p-button> + </div> + <div [routerLink]="['/']" > + <p-button *ngIf="toggleAdmin" (onClick)="adminPanel()" ><i class="pi pi-sign-in" style="margin-right:10px; font-size: 18px"[ngClass]="{'collapsed': isCollapsed}" title="Go to user panel"></i> + <span *ngIf="!isCollapsed"> + User panel + </span> + </p-button> + </div> </div> </div> +<app-modal-notification-send></app-modal-notification-send> diff --git a/src/app/shared/left-menu/left-menu.component.spec.ts b/src/app/shared/left-menu/left-menu.component.spec.ts index 320e5ed342b010cabd35a5156b469321d75f65f5..b1c9a408ff29d9de9477256c88225cbe5ad1bfd8 100644 --- a/src/app/shared/left-menu/left-menu.component.spec.ts +++ b/src/app/shared/left-menu/left-menu.component.spec.ts @@ -1,30 +1,81 @@ -// import { ComponentFixture, TestBed } from '@angular/core/testing'; - -// import { LeftMenuComponent } from './left-menu.component'; -// import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; -// import { MessageService } from 'primeng/api'; - -// describe('LeftMenuComponent', () => { -// let component: LeftMenuComponent; -// let fixture: ComponentFixture<LeftMenuComponent>; - -// beforeEach(async () => { -// await TestBed.configureTestingModule({ -// imports: [LeftMenuComponent], -// providers: [MessageService], -// schemas: [ -// CUSTOM_ELEMENTS_SCHEMA, -// NO_ERRORS_SCHEMA -// ] -// }) -// .compileComponents(); - -// fixture = TestBed.createComponent(LeftMenuComponent); -// component = fixture.componentInstance; -// fixture.detectChanges(); -// }); - -// it('should create', () => { -// expect(component).toBeTruthy(); -// }); -// }); +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { LeftMenuComponent } from './left-menu.component'; +import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; +import { ToastContainerComponent } from '../toast-container/toast-container.component'; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { of, Subject } from 'rxjs'; + +describe('LeftMenuComponent', () => { + let component: LeftMenuComponent; + let fixture: ComponentFixture<LeftMenuComponent>; + let mockRouter: any; + let mockToast: jasmine.SpyObj<ToastContainerComponent>; + let routerEventsSubject: Subject<any>; + let mockActivatedRoute: any; + + beforeEach(async () => { + routerEventsSubject = new Subject(); + mockRouter = { + events: routerEventsSubject.asObservable(), + navigate: jasmine.createSpy('navigate') + }; + mockToast = jasmine.createSpyObj('ToastContainerComponent', ['show']); + mockActivatedRoute = { + snapshot: { params: {}, queryParams: {} } + }; + + await TestBed.configureTestingModule({ + declarations: [LeftMenuComponent], + providers: [ + { provide: Router, useValue: mockRouter }, + { provide: ToastContainerComponent, useValue: mockToast }, + { provide: ActivatedRoute, useValue: mockActivatedRoute } + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA] + }).compileComponents(); + + fixture = TestBed.createComponent(LeftMenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize menu items and collapsed state', () => { + expect(component.items.length).toBeGreaterThan(0); + expect(component.isCollapsed).toBeFalse(); + }); + + it('should update currentUrl and toggleAdmin on NavigationEnd event', () => { + const testUrl = '/admin/dashboard'; + routerEventsSubject.next(new NavigationEnd(1, testUrl, testUrl)); + expect(component.currentUrl).toBe(testUrl); + expect(component.toggleAdmin).toBeTrue(); + }); + + it('should toggle menu collapsed state and update CSS variable', () => { + component.toggleMenu(); + expect(component.isCollapsed).toBeTrue(); + expect(sessionStorage.getItem('menuCollapsed')).toBe('true'); + + component.toggleMenu(); + expect(component.isCollapsed).toBeFalse(); + expect(sessionStorage.getItem('menuCollapsed')).toBe('false'); + }); + + it('should show toast message', () => { + component.showToastTest(); + expect(mockToast.show).toHaveBeenCalledWith('Test test', jasmine.anything(), 'HEADER'); + }); + + it('should toggle admin panel visibility', () => { + component.toggleAdmin = false; + component.adminPanel(); + expect(component.toggleAdmin).toBeTrue(); + + component.adminPanel(); + expect(component.toggleAdmin).toBeFalse(); + }); +}); diff --git a/src/app/shared/left-menu/left-menu.component.ts b/src/app/shared/left-menu/left-menu.component.ts index 064c24b730830328829606ca4485edd794cc1434..84ae20e7257e52be870f5f64f9edda7fbc7362c6 100644 --- a/src/app/shared/left-menu/left-menu.component.ts +++ b/src/app/shared/left-menu/left-menu.component.ts @@ -1,7 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit, ViewChild} from '@angular/core'; import { ToastContainerComponent, ToastMode } from '../toast-container/toast-container.component'; -import {Router} from '@angular/router'; +import {ActivatedRoute, NavigationEnd, Router} from '@angular/router'; import {MenuItem} from 'primeng/api'; +import {ModalNotificationSendComponent} from '../modal/modal-notification-send/modal-notification-send.component'; @Component({ selector: 'app-left-menu', @@ -9,11 +10,17 @@ import {MenuItem} from 'primeng/api'; styleUrl: './left-menu.component.css' }) export class LeftMenuComponent implements OnInit { + @ViewChild(ModalNotificationSendComponent, {static: true}) + public notificationModal; + items: MenuItem[]; toggleAdmin = false; + currentUrl : string ; + isCollapsed = false; constructor(private toast: ToastContainerComponent, - public router: Router) { + public router: Router, + private readonly activeRoute: ActivatedRoute,) { this.items = [ { label: 'Profile', @@ -28,10 +35,23 @@ export class LeftMenuComponent implements OnInit { routerLink: ['/logout'] } ] + const storedState = sessionStorage.getItem('menuCollapsed'); + this.isCollapsed = storedState === 'true'; } public ngOnInit(): void { - console.log("test left menu ") + this.router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + this.currentUrl = event.urlAfterRedirects; + console.log('Aktualny URL:', this.currentUrl); + if(this.currentUrl.includes('admin')) { + this.toggleAdmin = true; + } + } + }) + console.log("test left menu ") + const newWidth = this.isCollapsed ? '100px' : '300px'; + document.documentElement.style.setProperty('--left-panel-width', newWidth); } public showToastTest() { @@ -40,5 +60,14 @@ export class LeftMenuComponent implements OnInit { adminPanel() { this.toggleAdmin = !this.toggleAdmin; } + toggleMenu() { + this.isCollapsed = !this.isCollapsed; + const newWidth = this.isCollapsed ? '100px' : '300px'; + document.documentElement.style.setProperty('--left-panel-width', newWidth); + sessionStorage.setItem('menuCollapsed', this.isCollapsed.toString()); + } + public showNotificationModal(): void { + this.notificationModal.show(); + } } diff --git a/src/app/shared/modal/appinstall/appinstallmodal.component.spec.ts b/src/app/shared/modal/appinstall/appinstallmodal.component.spec.ts index 38b1ba8fd6fb049ce222eccb9fa86a4552792944..859826d73ed6c3685ef23a0b2868f692c0ec0f3f 100644 --- a/src/app/shared/modal/appinstall/appinstallmodal.component.spec.ts +++ b/src/app/shared/modal/appinstall/appinstallmodal.component.spec.ts @@ -47,6 +47,7 @@ describe('AppInstallmodalComponent', () => { applicationStatePerDomain: [], groups: [], annotations: [], + clusters: [], } beforeEach(waitForAsync(() => { diff --git a/src/app/shared/modal/modal-notification-send/modal-notification-send.component.html b/src/app/shared/modal/modal-notification-send/modal-notification-send.component.html index 435992b9e9b3e71156a5ded7e6ca4008a93fd501..4b2cbdcdab4c8f136b1224cc865065dc43c96642 100644 --- a/src/app/shared/modal/modal-notification-send/modal-notification-send.component.html +++ b/src/app/shared/modal/modal-notification-send/modal-notification-send.component.html @@ -13,7 +13,7 @@ </form> </div> <div class="nmaas-modal-footer"> + <button type="button" (click)="modal.hide()" class="btn btn-secondary text-center">{{'MESSAGE_MODAL.DISMISS' | translate}}</button> <button type="submit" class="btn btn-primary text-center" [disabled]="!f.valid" form="notification-form">{{'MESSAGE_MODAL.CONFIRM' | translate}}</button> - <button type="button" (click)="modal.hide()" class="btn btn-secondary text-center">{{'MESSAGE_MODAL.DISMISS' | translate}}</button> </div> </nmaas-modal> diff --git a/src/app/shared/navbar/navbar.component.css b/src/app/shared/navbar/navbar.component.css index 5978cfb11627290845a547fca380052b4cb72606..55bdd7192499c31c22991e19beac919fb0bbf82d 100644 --- a/src/app/shared/navbar/navbar.component.css +++ b/src/app/shared/navbar/navbar.component.css @@ -26,11 +26,6 @@ .lang-circle-icon{ height: 26px; } -.navbar-left{ - padding-top: 6px; - display: flex; - align-items: center; -} .navbar-right{ display:flex; align-items: center; @@ -48,7 +43,6 @@ } .navbar-left,.navbar-right { float: none !important; - display:block; } .navbar-toggle { display: block; @@ -203,18 +197,16 @@ color: #414F6B; } .navbar-default .navbar-nav>li>a{ - color: #414F6B; - padding:10px; + color: #6D788E; } .navbar-default .navbar-nav>.active>a{ border-radius: 5px; color: #142548; - padding:10px; + /*font-weight: bold;*/ background-color: #D1D1D1; } .navbar-default .navbar-nav>.open>a{ border-radius: 5px; color: #142548; - padding:10px; background-color: #D1D1D1; } diff --git a/src/app/shared/navbar/navbar.component.html b/src/app/shared/navbar/navbar.component.html index b75127c4b75f76f275941006c1910aa93457bac3..123273f20eedc423a1110af5d56a3c2a4cfabb81 100644 --- a/src/app/shared/navbar/navbar.component.html +++ b/src/app/shared/navbar/navbar.component.html @@ -1,7 +1,8 @@ <nav class="navbar navbar-default" id="navbar" role="navigation" style="margin:0"> <div class="container-fluid"> <div class="navbar-header"> - <a routerLink="/"><img alt="Geant" src="assets/images/logo-small.png" style="margin: 8px; padding:4px; height:35px"></a> + <a routerLink="/"><img alt="GÉANT" src="assets/images/logo-small.png" + style="margin: 8px; padding:4px; height:35px"></a> <button class="navbar-toggle" data-target="#navbarCollapse" data-toggle="collapse" type="button"> <span class="sr-only">Toggle Navigation</span> <em class="fas fa-bars"></em> @@ -20,22 +21,22 @@ </ul> <ul *ngIf="authService.isLogged()" class="nav navbar-nav navbar-right"> <li *ngIf="showClock" class=""> - <div class="navbar-logout">{{'NAVBAR.EXPIRED_TIME' | translate}}: {{time}}</div> + <div class="navbar-logout">{{ 'NAVBAR.EXPIRED_TIME' | translate }}: {{ time }}</div> </li> <li *ngIf="showClock" class="divider-vertical"></li> <li *ngIf="checkUserRole()" class="drop-domain"> <nmaas-domain-filter class="drop-domain"></nmaas-domain-filter> </li> - <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']" class="divider-vertical"></li> - <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']" [routerLinkActiveOptions]="{exact:true}" + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']" class="divider-vertical"></li> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" class="dropdown"> <a aria-expanded="false" aria-haspopup="true" class="dropdown-toggle" data-toggle="dropdown" - role="button">{{'NAVBAR.ADVANCED' | translate}}<strong class="caret"></strong></a> + role="button">{{ 'NAVBAR.ADVANCED' | translate }}<strong class="caret"></strong></a> <ul class="dropdown-menu"> - <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']"> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']"> <a [routerLink]="['/admin/domains/bulks']">{{ 'BULK.DOMAIN.HEADER' | translate }}</a> </li> - <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']"> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']"> <a [routerLink]="['/admin/apps/bulks']">{{ 'BULK.APP.HEADER' | translate }}</a> </li> </ul> @@ -44,15 +45,15 @@ <li *roles="['ROLE_SYSTEM_ADMIN']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" class="dropdown"> <a aria-expanded="false" aria-haspopup="true" class="dropdown-toggle" data-toggle="dropdown" - role="button">{{'NAVBAR.NOTIFICATION' | translate}}<strong class="caret"></strong></a> + role="button">{{ 'NAVBAR.NOTIFICATION' | translate }}<strong class="caret"></strong></a> <ul class="dropdown-menu"> <li *roles="['ROLE_SYSTEM_ADMIN']"> - <span (click)="showNotificationModal()">{{'NAVBAR.ALL_USERS' | translate}}</span> + <span (click)="showNotificationModal()">{{ 'NAVBAR.ALL_USERS' | translate }}</span> </li> </ul> </li> <li *roles="['ROLE_SYSTEM_ADMIN']" class="divider-vertical"></li> - <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR', 'ROLE_DOMAIN_ADMIN', 'ROLE_TOOL_MANAGER', 'ROLE_VL_MANAGER']" + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR', 'ROLE_DOMAIN_ADMIN', 'ROLE_TOOL_MANAGER', 'ROLE_GROUP_MANAGER']" [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']" class="dropdown"> <a aria-expanded="false" aria-haspopup="true" class="dropdown-toggle" data-toggle="dropdown" @@ -61,25 +62,33 @@ <li *roles="['ROLE_SYSTEM_ADMIN']"><a [routerLink]="['/admin/configuration']">{{ 'NAVBAR.SETTINGS' | translate }}</a> </li> + <li *roles="['ROLE_SYSTEM_ADMIN']"><a + [routerLink]="['/admin/manage/clusters']">{{ 'CLUSTERS.CONFIGURATION' | translate }}</a> + </li> <li *roles="['ROLE_SYSTEM_ADMIN']" class="dropdown-divider"></li> <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_TOOL_MANAGER']"><a [routerLink]="['/admin/apps']">{{ 'NAVBAR.MARKET' | translate }}</a> </li> - <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR', 'ROLE_VL_MANAGER', 'ROLE_VL_DOMAIN_ADMIN']"><a - [routerLink]="['/admin/domains']">{{ 'NAVBAR.DOMAINS' | translate }}</a> + <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_SYSTEM_ADMIN', 'ROLE_OPERATOR', 'ROLE_GROUP_MANAGER', 'ROLE_GROUP_DOMAIN_ADMIN']"> + <a + [routerLink]="['/admin/domains']">{{ 'NAVBAR.DOMAINS' | translate }}</a> </li> - <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_VL_MANAGER']"> + <li *roles="['ROLE_SYSTEM_ADMIN', 'ROLE_GROUP_MANAGER']"> <a [routerLink]="['/admin/domains/groups']">{{ 'NAVBAR.DOMAIN_GROUPS' | translate }}</a> </li> <li *roles="['ROLE_SYSTEM_ADMIN']"><a [routerLink]="['/admin/users']">{{ 'NAVBAR.USERS' | translate }}</a> </li> - <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_VL_DOMAIN_ADMIN']"><a - [routerLink]="['/domain/users']">{{ 'NAVBAR.DOMAIN_USERS' | translate }}</a> + + <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_GROUP_DOMAIN_ADMIN']"> + <a *rolesExcluded="['ROLE_SYSTEM_ADMIN']" + [routerLink]="['/domain/users']">{{ 'NAVBAR.DOMAIN_USERS' | translate }}</a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN']"><a - [routerLink]="['/admin/languages']">{{'NAVBAR.LANGUAGES' | translate }}</a> + [routerLink]="['/admin/languages']">{{ 'NAVBAR.LANGUAGES' | translate }}</a> </li> <li *roles="['ROLE_SYSTEM_ADMIN']" class="dropdown-divider"></li> @@ -94,7 +103,7 @@ <a aria-expanded="false" aria-haspopup="true" class="dropdown-toggle" data-toggle="dropdown" href="#" role="button"> <span class="glyphicon glyphicon-user"></span> - {{authService.getUsername()}} <strong class="caret"></strong></a> + {{ authService.getPreferredUsername() }} <strong class="caret"></strong></a> <ul class="dropdown-menu"> <li [routerLinkActiveOptions]="{exact:true}" [routerLinkActive]="['active']"><a [routerLink]="['/profile']">{{ 'NAVBAR.PROFILE' | translate }}</a></li> @@ -108,12 +117,16 @@ <ul *ngIf="!authService.isLogged()" class="nav navbar-nav pull-right-lg" id="navbar-main-not-logged"> <li> - <button *ngIf="router.url == '/welcome' || router.url.startsWith('/welcome/login') || router.url == '/welcome/registration'" data-parent="#accordion" - class="btn navbar-btn accordion-group" data-target="#login-panel" data-toggle="collapse" [routerLink]="['welcome/login']"> + <button *ngIf="router.url == '/welcome' || router.url.startsWith('/welcome/login') || router.url == '/welcome/registration'" + data-parent="#accordion" + class="btn navbar-btn accordion-group" data-target="#login-panel" data-toggle="collapse" + [routerLink]="['welcome/login']"> {{ 'WELCOME.LOGIN' | translate }} </button> - <button *ngIf="router.url == '/welcome' || router.url.startsWith('/welcome/login') || router.url == '/welcome/registration'" data-parent="#accordion" - class="btn navbar-btn accordion-group" data-target="#register-panel" data-toggle="collapse" [routerLink]="['welcome/registration']"> + <button *ngIf="router.url == '/welcome' || router.url.startsWith('/welcome/login') || router.url == '/welcome/registration'" + data-parent="#accordion" + class="btn navbar-btn accordion-group" data-target="#register-panel" data-toggle="collapse" + [routerLink]="['welcome/registration']"> {{ 'WELCOME.REGISTER' | translate }} </button> </li> @@ -136,7 +149,7 @@ <a (click)="useLanguage(lang)"> <img alt="lang flag" class="lang-circle-icon" src="assets/images/country/{{lang}}_circle.png"/> - <span>{{lang.toUpperCase()}}</span> + <span>{{ lang.toUpperCase() }}</span> </a> </li> </ul> diff --git a/src/app/shared/navbar/navbar.component.spec.ts b/src/app/shared/navbar/navbar.component.spec.ts index 7f81b7e65f719b681db4b423c85550e84c0a7e0d..fe0dc2256e63c906623f7fd69c28c13ff2513064 100644 --- a/src/app/shared/navbar/navbar.component.spec.ts +++ b/src/app/shared/navbar/navbar.component.spec.ts @@ -49,11 +49,12 @@ describe('NavbarComponent_Shared', () => { const mockLanguageService = jasmine.createSpyObj(['getEnabledLanguages', 'shouldUpdate']); mockLanguageService.getEnabledLanguages.and.returnValue(of(['en', 'fr', 'pl'])); mockLanguageService.shouldUpdate.and.returnValue(false); - const mockAuthService = jasmine.createSpyObj(['isLogged', 'hasRole', 'getDomains', 'getRoles']); + const mockAuthService = jasmine.createSpyObj(['isLogged', 'hasRole', 'getDomains', 'getRoles', 'loadUser', 'refreshUserRoles']); mockAuthService.isLogged.and.returnValue(false); mockAuthService.hasRole.and.returnValue(false); mockAuthService.getDomains.and.returnValue([]); mockAuthService.getRoles.and.returnValue([]); + mockAuthService.refreshUserRoles.and.returnValue(); const mockUserDataService = jasmine.createSpyObj(['selectedDomainId']) mockUserDataService.selectedDomainId.and.returnValue(1) diff --git a/src/app/shared/navbar/navbar.component.ts b/src/app/shared/navbar/navbar.component.ts index 394fcdfe84fa6c6fb988684ebc71df8b9a90ff0e..ee1d343013ae4f642475899fd2a5d4d329e45d19 100644 --- a/src/app/shared/navbar/navbar.component.ts +++ b/src/app/shared/navbar/navbar.component.ts @@ -56,15 +56,15 @@ export class NavbarComponent implements OnInit { ngOnInit() { this.isServiceAvailable = this.serviceAvailability.isServiceAvailable; this.getSupportedLanguages(); + this.authService.refreshUserRoles(); if (this.authService.isLogged()) { - if (this.authService.hasRole('ROLE_SYSTEM_ADMIN')) { this.refresh = interval(5000).subscribe(next => { if (this.languageService.shouldUpdate()) { this.getSupportedLanguages(); this.languageService.setUpdateRequiredFlag(false); } }); - } + // } } this.intervalId = setInterval(() => { if (this.authService.isLogged()) { @@ -88,10 +88,12 @@ export class NavbarComponent implements OnInit { } public checkUserRole(): boolean { - return this.authService.getDomains().filter(value => value !== this.domainService.getGlobalDomainId()).length > 0 + if (this.authService.isLogged()) { + return this.authService.getDomains().filter(value => value !== this.domainService.getGlobalDomainId()).length > 0 || this.authService.getRoles().filter(value => value !== 'ROLE_INCOMPLETE') .filter(value => value !== 'ROLE_GUEST') .length > 0; + } } public showNotificationModal(): void { @@ -99,7 +101,7 @@ export class NavbarComponent implements OnInit { } public isOnlyGuestInGlobalDomain(): boolean { - const globalDomainRoles = this.authService.getDomainRoles().get(this.domainService.getGlobalDomainId()).getRoles() + const globalDomainRoles = this.authService.getGlobalRole() return globalDomainRoles // does have any role in global domain (not undefined) && globalDomainRoles.length === 1 // only one role in global domain && globalDomainRoles[0] === 'ROLE_GUEST' // this single role is ROLE_GUEST diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index b1f373c8a0f49f624fa8e4628df4e5888f290e8c..20ffb98d661d7af399933fb60a6111bbcaf9bf74 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -1,6 +1,6 @@ import {DefaultLogo} from '../directive/defaultlogo.directive'; import {RolesDirective} from '../directive/roles.directive'; -import {NgModule} from '@angular/core'; +import {CUSTOM_ELEMENTS_SCHEMA, NgModule, NO_ERRORS_SCHEMA} from '@angular/core'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {CommonModule, DatePipe} from '@angular/common'; @@ -37,7 +37,7 @@ import {PasswordStrengthMeterComponent} from 'angular-password-strength-meter'; import {AboutComponent} from './about/about.component'; import {ChangelogComponent} from './changelog/changelog.component'; import {NotificationService} from '../service/notification.service'; -import {RECAPTCHA_V3_SITE_KEY, RecaptchaV3Module} from 'ng-recaptcha'; +import {RECAPTCHA_V3_SITE_KEY, RecaptchaModule, RecaptchaV3Module} from 'ng-recaptcha'; import {SingleCommentComponent} from './comments/single-comment/single-comment.component'; import {TranslateStateModule} from './translate-state/translate-state.module'; import {MinLengthDirective} from '../directive/min-length.directive'; @@ -63,6 +63,17 @@ import { provideZxvbnServiceForPSM } from 'angular-password-strength-meter/zxcv import { AccessTokensComponent } from './users/access-token/access-tokens.component'; import { LeftMenuComponent } from './left-menu/left-menu.component'; import {TableModule} from 'primeng/table'; +import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component'; +import {CheckboxModule} from 'primeng/checkbox'; +import { InputGroupModule } from 'primeng/inputgroup'; +import { InputGroupAddonModule } from 'primeng/inputgroupaddon'; +import { ButtonModule } from 'primeng/button'; +import { BrowserModule } from '@angular/platform-browser'; +import {ChartModule} from 'primeng/chart'; +import { RolesExcludedDirective } from '../directive/roles-exluded.directive'; +import { FileUploadModule } from 'primeng/fileupload'; + + @NgModule({ @@ -73,7 +84,6 @@ import {TableModule} from 'primeng/table'; ServicesModule, RouterModule, ReactiveFormsModule, - RecaptchaV3Module, PasswordStrengthMeterComponent, TranslateModule.forChild(), NgxPaginationModule, @@ -83,6 +93,15 @@ import {TableModule} from 'primeng/table'; InputTextModule, FormioModule, TableModule, + CheckboxModule, + InputGroupModule, + InputGroupAddonModule, + ButtonModule, + RecaptchaV3Module, + ButtonModule, + ChartModule, + FileUploadModule, + TableModule ], declarations: [ RateComponent, @@ -99,6 +118,7 @@ import {TableModule} from 'primeng/table'; NavbarComponent, DefaultLogo, RolesDirective, + RolesExcludedDirective, MinLengthDirective, MaxLengthDirective, SearchComponent, @@ -128,7 +148,8 @@ import {TableModule} from 'primeng/table'; PreferencesComponent, SortableHeaderDirective, DomainNamespaceAnnotationsComponent, - AccessTokensComponent + AccessTokensComponent, + AdminDashboardComponent, ], providers: [ PasswordValidator, @@ -175,13 +196,16 @@ import {TableModule} from 'primeng/table'; ModalTestInstanceComponent, ModalNotificationSendComponent, DomainRolesDirective, + RolesExcludedDirective, SshKeysComponent, ModalProvideSshKeyComponent, PreferencesComponent, SortableHeaderDirective, DomainNamespaceAnnotationsComponent, AccessTokensComponent - ] + ], + schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] + }) export class SharedModule { } diff --git a/src/app/shared/users/access-token/access-tokens.component.html b/src/app/shared/users/access-token/access-tokens.component.html index e58fa61efbc62b07e6e9692877deb12fc3288b4c..81bd13e6bfcca04309f38b2a289c687c08046962 100644 --- a/src/app/shared/users/access-token/access-tokens.component.html +++ b/src/app/shared/users/access-token/access-tokens.component.html @@ -1,27 +1,29 @@ -<div style="margin-bottom: 15px;" class="panel panel-default"> - <div class="panel-heading">{{'TOKENS.HEADER' | translate}}</div> +<div style="padding-bottom: 15px;" class="background-section"> + <div style="display: flex; justify-content:space-between"> + <h4 style="font-size:15px; font-weight: bold">{{'TOKENS.HEADER' | translate}}</h4> + <button type="button" class="btn btn-text" + (click)="modal.show()">{{'TOKENS.NEW_TOKEN' | translate}}</button> + </div> <div class="panel-body"> <table class="table table-hover" aria-describedby="User access tokens table"> <thead> <tr> <th scope="col">{{'TOKENS.TABLE.ID' | translate}}</th> <th scope="col">{{'TOKENS.TABLE.NAME' | translate}}</th> - <th scope="col">{{'TOKENS.TABLE.VALUE' | translate}}</th> <th scope="col">{{'TOKENS.TABLE.VALID' | translate}}</th> <th scope="col">{{'TOKENS.TABLE.ACTIONS' | translate}}</th> </tr> </thead> <tbody> <tr *ngFor="let token of tokensList"> - <td>{{token.id}}</td> - <td>{{token.name}}</td> - <td>{{token.tokenValue}}</td> - <td>{{token.valid}}</td> - <td *ngIf="token.valid"> + <td style="width: 25%;">{{token.id}}</td> + <td style="width: 25%;">{{token.name}}</td> + <td style="width: 25%;">{{token.valid}}</td> + <td style="width: 25%;" *ngIf="token.valid"> <button type="button" class="btn btn-danger" (click)="invalidate(token.id)">{{'TOKENS.BUTTON_INVALIDATE' | translate}}</button> </td> - <td *ngIf="!token.valid"> + <td style="width: 25%;" *ngIf="!token.valid"> <button type="button" class="btn btn-danger" (click)="deleteToken(token.id)">{{'TOKENS.BUTTON_DELETE' | translate}}</button> </td> @@ -31,18 +33,14 @@ </tr> </tbody> </table> - <div> - <button type="button" class="btn btn-success" - (click)="modal.show()">{{'TOKENS.NEW_TOKEN' | translate}}</button> - </div> </div> </div> <nmaas-modal styleModal="info"> <div class="nmaas-modal-header">{{'TOKENS.MODAL.HEADER' | translate}}</div> <div class="nmaas-modal-body" style="height: 60%; max-height: 80vh;overflow-y: auto;"> - <form [formGroup]="requestForm" (ngSubmit)="createNewToken()"> + <form *ngIf="!showCopyToken" [formGroup]="requestForm" (ngSubmit)="createNewToken()"> <div class="form-group"> <label class="control-label" for="new-token-name"> {{'TOKENS.MODAL.NAME' | translate}}: @@ -56,11 +54,29 @@ <div *ngIf="name.errors.notUnique">{{name.errors.message}}</div> </div> - <input type="submit" class="btn btn-success" value="{{'SSH_KEYS.MODAL.BUTTON_ADD' | translate}}" - [disabled]="!requestForm.valid"> - <button type="button" class="btn btn-primary pull-right" - (click)="modal.hide()">{{'SSH_KEYS.MODAL.BUTTON_CANCEL' | translate}}</button> + <div style="display: flex; justify-content: flex-end"> + <button type="button" class="btn btn-secondary mr-2" + (click)="modal.hide()">{{'SSH_KEYS.MODAL.BUTTON_CANCEL' | translate}}</button> + <input type="submit" class="btn btn-primary" value="{{'SSH_KEYS.MODAL.BUTTON_ADD' | translate}}" + [disabled]="!requestForm.valid"> + </div> + + </form> + <div *ngIf="showCopyToken"> + <span class="text-bold"> + {{"TOKENS.MODAL.TOKEN_COPY" | translate}} + </span> + <div> + <p-inputGroup> + <input pInputText [(ngModel)]="newToken.tokenValue" readonly="true" placeholder="Token to copy" /> + <button class="btn btn-secondary" type="button" (click)="copyToClipboard()">{{"TOKENS.MODAL.COPY" | translate}}</button> + </p-inputGroup> + </div> + <div class="mt-6 flex "> + <button class="btn btn-primary" type="button" (click)="ConfirmAndClose()" >{{"TOKENS.MODAL.TOKEN_SAVED" | translate}}</button> + </div> + </div> </div> -</nmaas-modal> \ No newline at end of file +</nmaas-modal> diff --git a/src/app/shared/users/access-token/access-tokens.component.ts b/src/app/shared/users/access-token/access-tokens.component.ts index c901eb760d28787fd0d3209eaf474a858ec8f588..d3123d0cb97493131055103be59bf0058edb79c9 100644 --- a/src/app/shared/users/access-token/access-tokens.component.ts +++ b/src/app/shared/users/access-token/access-tokens.component.ts @@ -20,6 +20,9 @@ export class AccessTokensComponent implements OnInit { public newTokenName = ''; + public showCopyToken = false; + public newToken :AccessToken; + @ViewChild(ModalComponent, {static: true}) public readonly modal: ModalComponent; @@ -61,9 +64,9 @@ export class AccessTokensComponent implements OnInit { public createNewToken() { this.tokenService.createToken(this.requestForm.value.name.trim()).subscribe({ next: val => { - this.tokensList.push(val) this.requestForm.reset(); - this.modal.hide(); + this.showCopyToken = true; + this.newToken = val; }, error: err => { console.warn(err.error) @@ -73,6 +76,24 @@ export class AccessTokensComponent implements OnInit { }) } + public copyToClipboard() { + if (this.newToken.tokenValue) { + navigator.clipboard.writeText(this.newToken.tokenValue).then(() => { + console.log('Copied to clipbord'); + }, (err) => { + console.error('Some errors accoured: ', err); + }); + } + } + + public ConfirmAndClose() { + this.showCopyToken = false; + this.newToken = null; + this.getData(); + this.modal.hide(); + + } + get name() { return this.requestForm.get('name'); } diff --git a/src/app/shared/users/list/userslist.component.css b/src/app/shared/users/list/userslist.component.css index 293357ddfa9e87687990d062df9f777337186781..018a8b04f96d66b5a17cad0ef9d105004dc2ad50 100644 --- a/src/app/shared/users/list/userslist.component.css +++ b/src/app/shared/users/list/userslist.component.css @@ -13,9 +13,9 @@ tr.clickable { cursor: pointer; } -.dropdown:hover .dropdown-menu { - display: block; -} +/*.dropdown:hover .dropdown-menu {*/ +/* display: block;*/ +/*}*/ .align-vertically { display: flex; @@ -32,50 +32,4 @@ li::marker { content: ''; font-size: 0em; } -:host ::ng-deep .p-datatable .p-datatable-thead > tr > th{ - border: 1px solid #E0E2E5; - background:transparent; - border-width: 0 0 1px 0; -} -:host ::ng-deep .p-datatable .p-datatable-tbody > tr > td { - text-align: left; - border: 1px solid #E0E2E5; - border-width: 0 0 1px 0; - padding: 1rem 1rem; -} -:host ::ng-deep .p-datatable .p-paginator-bottom{ - height: 40px; - background: transparent; - border: none; - margin-top:10px; -} -:host ::ng-deep .p-datatable .p-datatable-tbody > tr{ - background: transparent; -} -:host ::ng-deep .p-paginator .p-paginator-pages .p-paginator-page{ - transition: unset; - border-radius: 50%; - min-width:3.5rem; - height:3.5rem; - margin:0 5px; - font-size: 14px; -} - -:host ::ng-deep .p-paginator-element{ - border-radius:50%; - margin:0 5px; - min-width:3.5rem; - height:3.5rem; - font-size: 14px; -} -:host ::ng-deep .p-paginator .p-dropdown{ - height:3rem; -} -:host ::ng-deep .p-paginator-icon{ - height: 1.5rem; - width: 1.5rem; -} -:host ::ng-deep .p-paginator .p-dropdown .p-dropdown-label{ - padding-right: 10px; -} diff --git a/src/app/shared/users/list/userslist.component.html b/src/app/shared/users/list/userslist.component.html index 1f8eb8717e6f04680a4850b9492365035b303f3c..39d46e17d1dd501a7dc60d261a9cf9338ce8aa36 100644 --- a/src/app/shared/users/list/userslist.component.html +++ b/src/app/shared/users/list/userslist.component.html @@ -1,4 +1,31 @@ - <div class="" style="display: flex"> +<!-- <div class="col-sm-12 col-sm-10 col-md-12"> + <h3> + {{ 'USERS.TITLE' | translate }}</h3> + <div class="flex space-between"> + <div class="flex"> + <button *ngIf="authService.hasDomainRole(domainId, 'ROLE_DOMAIN_ADMIN') || authService.hasDomainRole(domainId, 'ROLE_GROUP_DOMAIN_ADMIN')" + class="btn btn-primary" (click)="changeMode()"> + <span *ngIf="isModeAllowed(ComponentMode.DELETE)">{{'USERS.ADD_TO_DOMAIN_BUTTON' | translate}}</span> + <span *ngIf="isModeAllowed(ComponentMode.EDIT)">{{'USERS.GO_BACK_BUTTON' | translate}}</span> + </button> + </div> + <div class="" style="display: flex"> + <div *ngIf="isModeAllowed(ComponentMode.DELETE)" class="flex "> + <span class="mt-2 pr-1">{{ 'USERS.ITEMS_PER_PAGE' | translate }}:</span> + <span id="selectionItems" class="dropdown" + style="vertical-align: middle; display: inline-block; margin-right: 1rem;"> + <button class="dropdown-toggle btn" data-toggle="dropdown" data-close-others="true"> + {{maxItemsOnPage}} + </button> + <ul class="dropdown-menu"> + <li *ngFor="let item of itemsPerPage" [ngClass]="{'active': maxItemsOnPage == item}"> + <a (click)="setItems(item)"> + <span>{{item.toString()}}</span> + </a> + </li> + </ul> + </span> + </div> <!-- <div *ngIf="isModeAllowed(ComponentMode.DELETE)" class="flex ">--> <!-- <span class="mt-2 pr-1">{{ 'USERS.ITEMS_PER_PAGE' | translate }}:</span>--> <!-- <span id="selectionItems" class="dropdown"--> @@ -15,6 +42,8 @@ <!-- </ul>--> <!-- </span>--> <!-- </div>--> +<div class="" style="display: flex"> + <div *ngIf="isModeAllowed(ComponentMode.EDIT)" style="margin-right: 15px; padding-top: 5px;"> {{'USERS.SEARCH' | translate}}</div> @@ -29,43 +58,63 @@ class="form-control" (keyup)="onSearch($event.target.value)"> </span> </div> - <button *ngIf="authService.hasDomainRole(domainId, 'ROLE_DOMAIN_ADMIN') || authService.hasDomainRole(domainId, 'ROLE_VL_DOMAIN_ADMIN')" + <button *ngIf="authService.hasDomainRole(domainId, 'ROLE_DOMAIN_ADMIN') || authService.hasDomainRole(domainId, 'ROLE_GROUP_DOMAIN_ADMIN')" class="btn btn-primary" (click)="changeMode()"> <span *ngIf="isModeAllowed(ComponentMode.DELETE)" >{{'USERS.ADD_TO_DOMAIN_BUTTON' | translate}}</span> <span *ngIf="isModeAllowed(ComponentMode.EDIT)">{{'USERS.GO_BACK_BUTTON' | translate}}</span> </button> </div> - <h4 style="margin-top:40px; font-weight: bold"> {{ 'USERS.TITLE' | translate }}</h4> + <h4 class="header"> {{ 'USERS.TITLE' | translate }}</h4> <div class="background-section"> <p-table #dt [value]="displayUsers" [paginator]="true" [rows]="maxItemsOnPage" [rowsPerPageOptions]="[15, 20, 25, 30, 50]" > <ng-template pTemplate="header"> <tr> - <th scope="col" class="column-sortable" sortable-column="username" - sort-direction="asc">{{ 'USERS.USER_NAME' | translate }}</th> - <th scope="col" class="column-sortable" sortable-column="lastname">{{'USERS.NAME' | translate}}</th> - <th *ngIf="!domainMode" scope="col" class="column-sortable" - sortable-column="email">{{'USERS.EMAIL' | translate}}</th> - <th scope="col" class="column-sortable" sortable-column="domains" - *ngIf="domainId === domainService.getGlobalDomainId()">{{ 'USERS.DOMAINS' | translate }}</th> - <th scope="col" class="column-sortable" sortable-column="globalRole" - *ngIf="domainId === domainService.getGlobalDomainId()">{{ 'USERS.GLOBAL_ROLE' | translate }}</th> - <th scope="col" class="column-sortable" sortable-column="roles" - *ngIf="domainId !== domainService.getGlobalDomainId() && !isModeAllowed(ComponentMode.EDIT)">{{ 'USERS.ROLES' | translate }}</th> - <th scope="col" class="column-sortable" *ngIf="!isModeAllowed(ComponentMode.EDIT) && !domainMode" - sortable-column="firstLoginDate">{{ 'USERS.FIRST_LOGIN' | translate }}</th> - <th scope="col" class="column-sortable" *ngIf="!isModeAllowed(ComponentMode.EDIT) && !domainMode" - sortable-column="lastSuccessfulLoginDate">{{ 'USERS.LAST_SUCCESSFUL_LOGIN' | translate }}</th> - <th scope="col" class="column-sortable" sortable-column="enabled">{{ 'USERS.ENABLED' | translate }}</th> + <th scope="col" class="column-sortable" pSortableColumn="username"> + {{ 'USERS.USER_NAME' | translate }} + <p-sortIcon field="username"></p-sortIcon> + </th> + <th scope="col" class="column-sortable" pSortableColumn="lastname"> + {{'USERS.NAME' | translate}} + <p-sortIcon field="lastname"></p-sortIcon> + </th> + <th *ngIf="!domainMode" scope="col" class="column-sortable" pSortableColumn="email"> + {{'USERS.EMAIL' | translate}} + <p-sortIcon field="email"></p-sortIcon> + </th> + <th scope="col" class="column-sortable" pSortableColumn="domains" *ngIf="domainId === domainService.getGlobalDomainId()"> + {{ 'USERS.DOMAINS' | translate }} + <p-sortIcon field="domains"></p-sortIcon> + </th> + <th scope="col" class="column-sortable" pSortableColumn="globalRole" *ngIf="domainId === domainService.getGlobalDomainId()"> + {{ 'USERS.GLOBAL_ROLE' | translate }} + <p-sortIcon field="globalRole"></p-sortIcon> + </th> + <th scope="col" class="column-sortable" pSortableColumn="roles" *ngIf="domainId !== domainService.getGlobalDomainId() && !isModeAllowed(ComponentMode.EDIT)"> + {{ 'USERS.ROLES' | translate }} + <p-sortIcon field="roles"></p-sortIcon> + </th> + <th scope="col" class="column-sortable" *ngIf="!isModeAllowed(ComponentMode.EDIT) && !domainMode" pSortableColumn="firstLoginDate"> + {{ 'USERS.FIRST_LOGIN' | translate }} + <p-sortIcon field="firstLoginDate"></p-sortIcon> + </th> + <th scope="col" class="column-sortable" *ngIf="!isModeAllowed(ComponentMode.EDIT) && !domainMode" pSortableColumn="lastSuccessfulLoginDate"> + {{ 'USERS.LAST_SUCCESSFUL_LOGIN' | translate }} + <p-sortIcon field="lastSuccessfulLoginDate"></p-sortIcon> + </th> + <th scope="col" class="column-sortable" pSortableColumn="enabled"> + {{ 'USERS.ENABLED' | translate }} + <p-sortIcon field="enabled"></p-sortIcon> + </th> <th *ngIf="!isModeAllowed(ComponentMode.EDIT)" scope="col"> </th> <th *ngIf="isModeAllowed(ComponentMode.EDIT)" scope="col"> </th> </tr> </ng-template> <ng-template pTemplate="body" let-user> - <tr (click)="view(user.id)"> - <td>{{ user.username }}</td> + <tr> + <td (click)="view(user.id)">{{ user.username }}</td> <td>{{(user.firstname || '') + ' ' + (user.lastname || '')}}</td> <td *ngIf="!domainMode">{{user.email}}</td> <td *ngIf="domainId === domainService.getGlobalDomainId()"> @@ -88,7 +137,7 @@ <strong class="caret"></strong> </span> </a> - <ul class="dropdown-menu"> + <ul class="dropdown-menu" style="left: 0; right:unset;"> <li *ngFor="let role of getAllowedRoles()"> <a (click)="changeUserRole(user,domainId, {value:role})">{{"ENUM.USER_ROLES." + Role[role].toUpperCase() | translate}}</a> </li> @@ -121,7 +170,7 @@ <a style="display: inline-block" class="dropdown-toggle " aria-expanded="false" aria-haspopup="true" data-toggle="dropdown" href="#" role="button"> - <em class="fas fa-cog icon-black icon-bigger"></em> + <em class="pi pi-cog" style="font-size: 1.8rem; color: var(--l-text-color)"></em> </a> <ul class="dropdown-menu pull-right-drop"> <li *ngIf="isModeAllowed(ComponentMode.VIEW)"> diff --git a/src/app/shared/users/list/userslist.component.ts b/src/app/shared/users/list/userslist.component.ts index 7394575d57a4f6d527893ff57c82ad1d4d71b007..1ae3116b57b2d986b67eaaef9d6ca6a47f96d777 100644 --- a/src/app/shared/users/list/userslist.component.ts +++ b/src/app/shared/users/list/userslist.component.ts @@ -101,11 +101,20 @@ export class UsersListComponent extends BaseComponent implements OnInit, OnChang } public getAllDomain() { - this.domainService.getAll().subscribe(domains => { - domains.forEach(domain => { - this.domainCache.setData(domain.id, domain) + if(this.domainMode) { + this.domainService.getMyDomains().subscribe(domains => { + domains.forEach(domain => { + this.domainCache.setData(domain.id, domain) + } + ) }) + } else { + this.domainService.getAll().subscribe(domains => { + domains.forEach(domain => { + this.domainCache.setData(domain.id, domain) + }) }) - }) + } + } public getDomainName(domainId: number): Observable<string> { diff --git a/src/app/shared/users/new-ssh-key/new-ssh-key.component.html b/src/app/shared/users/new-ssh-key/new-ssh-key.component.html index d94339f5b282e7632f47b819c9eaf7e2a317b056..176ebdef2d70761f3ffeb7a3d6cfb241e931cb90 100644 --- a/src/app/shared/users/new-ssh-key/new-ssh-key.component.html +++ b/src/app/shared/users/new-ssh-key/new-ssh-key.component.html @@ -26,9 +26,10 @@ <div *ngIf="key.errors.required">{{'SSH_KEYS.MODAL.ERROR.KEY_REQUIRED' | translate}}</div> <div *ngIf="key.errors.pattern">{{'SSH_KEYS.MODAL.ERROR.KEY_PATTERN' | translate}}</div> </div> - - <input type="submit" style="border-radius: 4px" class="btn btn-primary" value="{{'SSH_KEYS.MODAL.BUTTON_ADD' | translate}}" [disabled]="!requestForm.valid"> - <button type="button" class="btn btn-secondary pull-right" (click)="modal.hide()">{{'SSH_KEYS.MODAL.BUTTON_CANCEL' | translate}}</button> + <div style="display: flex; justify-content: flex-end"> + <button type="button" class="btn btn-secondary mr-2" (click)="modal.hide()">{{'SSH_KEYS.MODAL.BUTTON_CANCEL' | translate}}</button> + <input type="submit" style="border-radius: 4px" class="btn btn-primary" value="{{'SSH_KEYS.MODAL.BUTTON_ADD' | translate}}" [disabled]="!requestForm.valid"> + </div> </form> <div *ngIf="error" class="alert alert-danger"> diff --git a/src/app/shared/users/privileges/userprivileges.component.ts b/src/app/shared/users/privileges/userprivileges.component.ts index a79b06dc31f7548cedea9b393c72cc847efbe11c..5c7e8d99875729dbfc43be8c7bab9b8e6920bd83 100644 --- a/src/app/shared/users/privileges/userprivileges.component.ts +++ b/src/app/shared/users/privileges/userprivileges.component.ts @@ -57,7 +57,7 @@ export class UserPrivilegesComponent extends BaseComponent implements OnInit { if (this.authService.hasRole(Role[Role.ROLE_SYSTEM_ADMIN]) && Number(this.newPrivilegeForm.get('domainId').value) === this.domainService.getGlobalDomainId()) { // admin (global) role set - roles = [Role.ROLE_OPERATOR, Role.ROLE_TOOL_MANAGER, Role.ROLE_SYSTEM_ADMIN, Role.ROLE_VL_MANAGER]; + roles = [Role.ROLE_OPERATOR, Role.ROLE_TOOL_MANAGER, Role.ROLE_SYSTEM_ADMIN, Role.ROLE_GROUP_MANAGER]; roles = this.filterRoles(roles, this.newPrivilegeForm.get('domainId').value); } else if (this.newPrivilegeForm.get('domainId').value != null) { // default (domain) role set diff --git a/src/app/shared/users/ssh-keys/ssh-keys.component.html b/src/app/shared/users/ssh-keys/ssh-keys.component.html index e9025bddb5cd7796a569b04f68bb8cd26b94c772..7cc7808076580c866bce02ff27d905eafb96c18b 100644 --- a/src/app/shared/users/ssh-keys/ssh-keys.component.html +++ b/src/app/shared/users/ssh-keys/ssh-keys.component.html @@ -1,9 +1,11 @@ -<div style="margin-bottom: 15px;" class="panel panel-default"> - <div class="panel-heading">{{'SSH_KEYS.HEADER' | translate}}</div> +<div style="padding-bottom: 15px;" class="background-section"> + + <div style="display: flex; justify-content:space-between"> + <h4 style="font-size:15px; font-weight: bold" >{{'SSH_KEYS.HEADER' | translate}}</h4> + <app-new-ssh-key [userMode]="userMode" [userId]="userId" (out)="getData()"></app-new-ssh-key> + </div> <div class="panel-body"> - <div class="flex justify-content-end mb-4"> - <app-new-ssh-key [userMode]="userMode" [userId]="userId" (out)="getData()"></app-new-ssh-key> - </div> + <table class="table table-hover" aria-describedby="User ssh keys table"> <thead> <tr> diff --git a/src/app/welcome/complete/complete.component.html b/src/app/welcome/complete/complete.component.html index 46cef36fa39df40e49ed75893af4cca1c04f3485..64acd24a4363e8f96d10fafc70258bc9b3eaea3c 100644 --- a/src/app/welcome/complete/complete.component.html +++ b/src/app/welcome/complete/complete.component.html @@ -6,7 +6,7 @@ <div class="row"> <div class="col-xs-3"></div> <div class="text-center col-xs-6"> - <img alt="Geant logo" src="assets/images/geant-logo-small.png"> + <img alt="GÉANT logo" src="assets/images/geant-logo-small.png"> </div> <div class="dropdown text-right drop-lang-div col-xs-3"> <a style="display: inline-block" class="dropdown-toggle" aria-expanded="false" aria-haspopup="true" diff --git a/src/app/welcome/link-account/link-account.component.css b/src/app/welcome/link-account/link-account.component.css new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/src/app/welcome/link-account/link-account.component.css @@ -0,0 +1 @@ + diff --git a/src/app/welcome/link-account/link-account.component.html b/src/app/welcome/link-account/link-account.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2d75205238a0c5f83811c5aa3ebcb229b4d79d89 --- /dev/null +++ b/src/app/welcome/link-account/link-account.component.html @@ -0,0 +1,51 @@ +<div style="display: flex; justify-content: center;"> + <div style=" +margin-top: 50px; + width: 60% +" class="panel panel-default"> + <div class="panel-heading">{{ 'ACCOUNT_LINKING.HEADER' | translate }}</div> + <div class="panel-body"> + <form *ngIf="user" + class="form-horizontal" #userDetailsForm="ngForm"> + <div> + <p> + {{ 'ACCOUNT_LINKING.INFO' | translate }} + </p> + </div> + <div class="form-group"> + <label class="col-sm-2 control-label">{{ 'USER_DETAILS.FIRST_NAME' | translate }}</label> + <div class="col-sm-10"> + <p class="form-control-static">{{ user.firstname }}</p> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-2 control-label">{{ 'USER_DETAILS.LAST_NAME' | translate }}</label> + <div class="col-sm-10"> + <p class="form-control-static">{{ user.lastname }}</p> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-2 control-label">{{ 'USER_DETAILS.EMAIL' | translate }}</label> + <div class="col-sm-10"> + <p class="form-control-static">{{ user.email }}</p> + </div> + </div> + + <div class="form-group"> + <label for="password" class="col-sm-2 control-label">{{ 'PASSWORD.PASSWORD' | translate }}</label> + <div class="col-sm-10"> + <input type="password" class="form-control" id="password" + name="password" [(ngModel)]="password"> + </div> + </div> + <button type="submit" class="btn btn-primary" + (click)="submit()">{{ 'ACCOUNT_LINKING.CONFIRM' | translate }} + </button> + <div *ngIf="error" class="alert alert-danger" style="margin-top: 20px">{{error}}</div> + </form> + <br> + </div> + </div> +</div> diff --git a/src/app/welcome/link-account/link-account.component.spec.ts b/src/app/welcome/link-account/link-account.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b0a9f98c9b851dbd8f044e900df4b02595ca2ca --- /dev/null +++ b/src/app/welcome/link-account/link-account.component.spec.ts @@ -0,0 +1,57 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {LinkAccountComponent} from './link-account.component'; +import {ActivatedRoute} from '@angular/router'; +import {of} from 'rxjs'; +import {AuthService} from '../../auth/auth.service'; +import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core'; + +describe('LinkAccountComponent', () => { + let component: LinkAccountComponent; + let fixture: ComponentFixture<LinkAccountComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LinkAccountComponent], + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader + } + })], + providers: [ + { + provide: ActivatedRoute, + useValue: { + queryParams: of({ + oidc_token: 'mocked.jwt.token' + }), + snapshot: { + paramMap: { + get: () => null + } + } + } + }, + { + provide: AuthService, + useValue: { + isLogged: () => true, + oidcLogout: jasmine.createSpy('oidcLogout'), + oidcLinkingLogin: jasmine.createSpy('oidcLinkingLogin').and.returnValue(of({})) + } + }, + + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LinkAccountComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/welcome/link-account/link-account.component.ts b/src/app/welcome/link-account/link-account.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..79b4133b77ec780ae9ab540481f9e87e4693d84d --- /dev/null +++ b/src/app/welcome/link-account/link-account.component.ts @@ -0,0 +1,84 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {User} from '../../model'; +import {ActivatedRoute, Router} from '@angular/router'; +import jwtDecode from 'jwt-decode'; +import {AuthService} from '../../auth/auth.service'; +import {TranslateService} from '@ngx-translate/core'; + + +@Component({ + selector: 'app-link-account', + templateUrl: './link-account.component.html', + styleUrl: './link-account.component.css' +}) +export class LinkAccountComponent implements OnInit, OnDestroy { + public user: User; + private token: string; + public password: string; + public error: string; + + constructor( + private readonly route: ActivatedRoute, + private readonly authService: AuthService, + private readonly router: Router, + private translate: TranslateService, + ) { + } + + ngOnDestroy() { + if (!this.authService.isLogged()) { + this.authService.oidcLogout(this.token) + } + } + + ngOnInit() { + this.route.queryParams.subscribe(param => { + this.token = param['oidc-token']; + const decoded: TokenPayload = jwtDecode<TokenPayload>(this.token); + this.user = new User(); + this.user.username = decoded.sub; + this.user.firstname = decoded.given_name; + this.user.lastname = decoded.family_name; + this.user.email = decoded.email; + }) + + } + + public submit(): void { + this.authService.oidcLinkingLogin( + this.token, + this.user.email, + this.password, + this.user.username, + this.user.firstname, + this.user.lastname, + ).subscribe( + () => { + this.router.navigate(['/']); + }, + err => { + this.error = this.translate.instant(this.getMessage(err)); + } + ) + } + private getMessage(err: any): string { + switch (err['status']) { + case 401: + return 'LOGIN.LOGIN_FAILURE_MESSAGE'; + case 406: + return 'LOGIN.APPLICATION_UNDER_MAINTENANCE_MESSAGE'; + case 409: + return 'GENERIC_MESSAGE.UNAVAILABLE_MESSAGE'; + default: + return 'GENERIC_MESSAGE.UNAVAILABLE_MESSAGE'; + } + } +} + + +interface TokenPayload { + sub: string; + email: string; + given_name: string; + family_name: string; +} diff --git a/src/app/welcome/login/login.component.html b/src/app/welcome/login/login.component.html index 7fd223061e3430e40066581208df5f152c1d463a..af758ef732c5a46c8e251d7f66b454f6ae62ec88 100644 --- a/src/app/welcome/login/login.component.html +++ b/src/app/welcome/login/login.component.html @@ -26,7 +26,8 @@ </fieldset> </form> <div class="form-group"> - <button type="submit" (click)="triggerOIDC()" class="btn btn-primary btn-block"> + <button type="submit" (click)="triggerOIDC()" class="btn btn-primary btn-block" + [disabled]="!this.configuration?.ssoLoginAllowed || loading || ssoLoading"> {{ 'LOGIN.LOGIN_WITH' | translate }} </button> <img alt="sso" *ngIf="ssoLoading" src="data:"/> diff --git a/src/app/welcome/login/login.component.spec.ts b/src/app/welcome/login/login.component.spec.ts index 35e39bc620879877c64f1d11636786124b0439f8..f5ebbcf5ea4bef842108fda08b35ae7bdc1dd7d4 100644 --- a/src/app/welcome/login/login.component.spec.ts +++ b/src/app/welcome/login/login.component.spec.ts @@ -7,7 +7,6 @@ import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-transl import {ModalComponent} from '../../shared/modal'; import {AuthService} from '../../auth/auth.service'; import {ConfigurationService, UserService} from '../../service'; -import {SSOService} from '../../service/sso.service'; import createSpyObj = jasmine.createSpyObj; import {of} from 'rxjs'; import { HttpClientTestingModule } from '@angular/common/http/testing'; @@ -40,7 +39,6 @@ describe('Component: Login', () => { providers: [ {provide: AuthService, useValue: {}}, {provide: ConfigurationService, useValue: configServiceSpy}, - {provide: SSOService, useValue: {}}, {provide: UserService, useValue: {}}, ], }).compileComponents(); diff --git a/src/app/welcome/login/login.component.ts b/src/app/welcome/login/login.component.ts index dbfcc78194a47a0585edbfd2d35d4b8ee52d32ff..0a4481b66e12318164be2249a30b512a6dfcf0c9 100644 --- a/src/app/welcome/login/login.component.ts +++ b/src/app/welcome/login/login.component.ts @@ -4,7 +4,6 @@ import {Router} from '@angular/router'; import {AuthService} from '../../auth/auth.service'; import {AppConfigService, ConfigurationService, UserService} from '../../service'; import {Configuration} from '../../model/configuration'; -import {SSOService} from '../../service/sso.service'; import {SSOConfig} from '../../model/sso'; import {UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms'; import {ModalComponent} from '../../shared/modal'; @@ -34,7 +33,6 @@ export class LoginComponent implements OnInit { constructor(private router: Router, private auth: AuthService, private configService: ConfigurationService, - private ssoService: SSOService, private fb: UntypedFormBuilder, private userService: UserService, private translate: TranslateService, @@ -47,12 +45,6 @@ export class LoginComponent implements OnInit { ngOnInit() { this.configService.getConfiguration().subscribe(config => { this.configuration = config; - if (config.ssoLoginAllowed) { - this.ssoService.getOne().subscribe(sso => { - this.ssoConfig = sso; - this.checkSSO(); - }); - } }); } @@ -72,42 +64,13 @@ export class LoginComponent implements OnInit { ); } + // only for use in linking accounts public triggerOIDC() { - window.location.href = this.appConfig.getOidcUrl(); - } - - public checkSSO() { - const params = this.router.parseUrl(this.router.url).queryParams; - - if ('ssoUserId' in params) { - // Got auth data, send to api - this.ssoLoading = true; - this.ssoError = ''; - this.auth.propagateSSOLogin(params.ssoUserId).subscribe( - result => { - if (result === true) { - this.ssoLoading = false; - this.translate.setDefaultLang(this.auth.getSelectedLanguage()); - this.translate.use(this.auth.getSelectedLanguage()); - this.router.navigate(['/']); - } else { - this.ssoError = 'Failed to propagate SSO user id'; - this.ssoLoading = false; - } - }, - err => { - this.ssoError = this.translate.instant(this.getMessage(err)); - this.ssoLoading = false; - } - ); + if (!this.configuration.maintenance) { + window.location.href = this.appConfig.getOidcUrl(); } } - public triggerSSO() { - const url = window.location.href.replace(/ssoUserId=.+/, ''); - window.location.href = this.ssoConfig.loginUrl + '?return=' + url; - } - public sendResetNotification() { if (this.resetPasswordForm.valid) { this.userService.resetPasswordNotification(this.resetPasswordForm.controls['email'].value).subscribe( diff --git a/src/app/welcome/logout/logout.component.spec.ts b/src/app/welcome/logout/logout.component.spec.ts index e2e7d4c106d3cb949a4e25bd17b26566d346b828..8db6bfeb9cc5fdfee95e2aea5e4e64266bb7f9db 100644 --- a/src/app/welcome/logout/logout.component.spec.ts +++ b/src/app/welcome/logout/logout.component.spec.ts @@ -6,7 +6,6 @@ import createSpyObj = jasmine.createSpyObj; import {of} from 'rxjs'; import {AuthService} from '../../auth/auth.service'; import {ConfigurationService} from '../../service'; -import {SSOService} from '../../service/sso.service'; describe('LogoutComponent', () => { let component: LogoutComponent; @@ -29,7 +28,6 @@ describe('LogoutComponent', () => { providers: [ {provide: AuthService, useValue: authServiceSpy}, {provide: ConfigurationService, useValue: configServiceSpy}, - {provide: SSOService, useValue: {}} ] }) .compileComponents(); diff --git a/src/app/welcome/logout/logout.component.ts b/src/app/welcome/logout/logout.component.ts index fb9c9e8263c2d0187a1cbcb3dd76bcee4bd1e3b1..4eaaa50f7df58c9ae99dccb68665c21597a652c5 100644 --- a/src/app/welcome/logout/logout.component.ts +++ b/src/app/welcome/logout/logout.component.ts @@ -3,7 +3,6 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from '../../auth/auth.service'; import {ConfigurationService} from '../../service'; -import {SSOService} from '../../service/sso.service'; @Component({ @@ -15,21 +14,12 @@ export class LogoutComponent implements OnInit { constructor(private router: Router, private auth: AuthService, - private configService: ConfigurationService, - private ssoService: SSOService) { } + private configService: ConfigurationService) { } ngOnInit() { this.auth.logout(); this.configService.getConfiguration().subscribe(config => { - if (config.ssoLoginAllowed && this.auth.loginUsingSsoService) { - const url = window.location.origin; - this.ssoService.getOne().subscribe(sso => { - // Shibboleth SP uses parameter 'target' instead of 'return' - window.location.href = sso.logoutUrl + '?return=' + url; - }); - } else { this.router.navigate(['/welcome']); - } }); } diff --git a/src/app/welcome/passwordreset/password-reset.component.html b/src/app/welcome/passwordreset/password-reset.component.html index c62301d65a9f27622c178c45b8c299fa04ca760a..8c60eb985068cb262ef92924dabd30b0ca432958 100644 --- a/src/app/welcome/passwordreset/password-reset.component.html +++ b/src/app/welcome/passwordreset/password-reset.component.html @@ -4,7 +4,7 @@ <div class="panel panel-default login-vertical-offset"> <div class="panel-heading"> <div class="text-center"> - <img alt="Geant logo" src="assets/images/geant-logo-small.png"> + <img alt="GÉANT logo" src="assets/images/geant-logo-small.png"> </div> </div> <div class="panel-body" *ngIf="user"> diff --git a/src/app/welcome/passwordreset/password-reset.component.ts b/src/app/welcome/passwordreset/password-reset.component.ts index 3694303c9c9f26926dba0b28bdb272d824c56160..755353da38a280b7147aab06bece2072de01f587 100644 --- a/src/app/welcome/passwordreset/password-reset.component.ts +++ b/src/app/welcome/passwordreset/password-reset.component.ts @@ -58,8 +58,11 @@ export class PasswordResetComponent implements OnInit { public resetPassword() { if (this.form.valid) { this.recaptchaV3Service.execute('password_reset').pipe( - catchError(_ => of('')), // in case of captcha error return empty token + catchError(error => { + console.error(error); + return of(error)}), // in case of captcha error return empty tokenin case of captcha error return empty token ).subscribe((captchaToken) => { + console.log(captchaToken) this.showLoading = true; this.passwordReset.password = this.form.controls['newPassword'].value; this.passwordReset.token = this.token; diff --git a/src/app/welcome/terms-acceptance/terms-acceptance.component.html b/src/app/welcome/terms-acceptance/terms-acceptance.component.html index ce70113e91c73008691ffb22a2181a56b6a206c0..3605f9376128d984c7d9a38596cbdb918d9373bd 100644 --- a/src/app/welcome/terms-acceptance/terms-acceptance.component.html +++ b/src/app/welcome/terms-acceptance/terms-acceptance.component.html @@ -2,7 +2,7 @@ <div class="row panel panel-default login-vertical-offset col-lg-offset-4 col-lg-4 col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-12"> <div class="panel-heading"> <div class="text-center"> - <img alt="Geant logo" src="assets/images/geant-logo-small.png"> + <img alt="GÉANT logo" src="assets/images/geant-logo-small.png"> </div> </div> <div class="panel-body"> @@ -47,4 +47,4 @@ </div> </nmaas-modal> <modal-info-terms></modal-info-terms> -<modal-info-policy></modal-info-policy> \ No newline at end of file +<modal-info-policy></modal-info-policy> diff --git a/src/app/welcome/welcome.module.ts b/src/app/welcome/welcome.module.ts index 1e0ecde52c243ae5ed4004c6dce60bbe39f8c075..e212c4f059d959724211aa423e911d38b087f067 100644 --- a/src/app/welcome/welcome.module.ts +++ b/src/app/welcome/welcome.module.ts @@ -17,43 +17,44 @@ import {CompleteComponent} from './complete/complete.component'; import {ContentDisplayService} from '../service/content-display.service'; import {TermsAcceptanceComponent} from './terms-acceptance/terms-acceptance.component'; import {TranslateModule} from '@ngx-translate/core'; -import {SSOService} from '../service/sso.service'; import {PasswordResetComponent} from './passwordreset/password-reset.component'; import {PasswordStrengthMeterComponent} from 'angular-password-strength-meter'; import {PolicySubpageComponent} from './policy-subpage/policy-subpage.component'; +import {LinkAccountComponent} from './link-account/link-account.component'; @NgModule({ - declarations: [ - WelcomeComponent, - LoginComponent, - LogoutComponent, - RegistrationComponent, - ProfileComponent, - CompleteComponent, - TermsAcceptanceComponent, - PasswordResetComponent, - PolicySubpageComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - CommonModule, - RouterModule, - SharedModule, - PipesModule, - AppMarketModule, - PasswordStrengthMeterComponent, - TranslateModule.forChild() - ], - exports: [ - WelcomeComponent - ], - providers: [ - RegistrationService, - UserService, - ChangelogService, - ContentDisplayService, - SSOService - ] + declarations: [ + WelcomeComponent, + LoginComponent, + LogoutComponent, + RegistrationComponent, + ProfileComponent, + CompleteComponent, + TermsAcceptanceComponent, + PasswordResetComponent, + PolicySubpageComponent, + LinkAccountComponent + ], + imports: [ + FormsModule, + ReactiveFormsModule, + CommonModule, + RouterModule, + SharedModule, + PipesModule, + AppMarketModule, + PasswordStrengthMeterComponent, + TranslateModule.forChild() + ], + exports: [ + WelcomeComponent + ], + providers: [ + RegistrationService, + UserService, + ChangelogService, + ContentDisplayService + ] }) -export class WelcomeModule {} +export class WelcomeModule { +} diff --git a/src/assets/formio/config-template.json b/src/assets/formio/config-template.json index 4f01f8c274ec539a2bf97f85f4d91142e72a2bb8..fe79f6dc84e9f22f76f9de4c119e722d51c99ebe 100644 --- a/src/assets/formio/config-template.json +++ b/src/assets/formio/config-template.json @@ -15,14 +15,7 @@ "input": true, "tab": 0, "key": "configuration", - "components": [ - { - "type": "htmlelement", - "input": false, - "content": "<p>All required configuration should be applied using graphical interface</p>", - "tab": 0 - } - ] + "components": [] } ] }, diff --git a/src/assets/images/nmaas-cloud.png b/src/assets/images/nmaas-cloud.png new file mode 100644 index 0000000000000000000000000000000000000000..2790aa3f466f627706c59bdf8ecb8ad0e1f6499b Binary files /dev/null and b/src/assets/images/nmaas-cloud.png differ diff --git a/src/styles.css b/src/styles.css index f96544036e2898e50df0f2e2b5111db494c2cd40..b467c8f239634fbb7639cfd5035144c2ca746c71 100644 --- a/src/styles.css +++ b/src/styles.css @@ -79,6 +79,10 @@ margin:20px 0; padding:30px; } +.header{ + margin-top:40px; + font-weight: bold +} .card { position: relative; display: -webkit-box; @@ -165,3 +169,74 @@ background: var(--primary-text-button-background-hover); color: var(--primary-text-button-text-hover) } + +.dropdown-menu{ + right: 0; + left:unset; +} + +.text-bold { + font-weight: 700;; +} + + +body .p-datatable .p-datatable-thead > tr > th{ + border: 1px solid #E0E2E5; + background:transparent; + border-width: 0 0 1px 0; +} +body .p-datatable .p-datatable-tbody > tr > td { + text-align: left; + border: 1px solid #E0E2E5; + border-width: 0 0 1px 0; + padding: 1rem 1rem; +} +body .p-datatable .p-paginator-bottom{ + height: 40px; + background: transparent; + border: none; + margin-top:10px; +} +body .p-datatable .p-datatable-tbody > tr{ + background: transparent; +} + +body .p-paginator .p-paginator-pages .p-paginator-page{ + transition: unset; + border-radius: 50%; + min-width:3.5rem; + height:3.5rem; + margin:0 5px; + font-size: 14px; +} + +body .p-paginator-element{ + border-radius:50%; + margin:0 5px; + min-width:3.5rem; + height:3.5rem; + font-size: 14px; +} +body .p-paginator .p-dropdown{ + height:3rem; +} +body .p-paginator-icon{ + height: 1.5rem; + width: 1.5rem; +} +body .p-paginator .p-dropdown .p-dropdown-label{ + padding-right: 10px; +} + +body .p-paginator .p-paginator-pages .p-paginator-page.p-highlight{ + background: var(--user-button-background-hover); +} +body .p-datatable>.p-datatable-wrapper { + overflow: visible; +} +body .p-datatable .p-sortable-column.p-highlight{ + color: var(--primary-button-color); +} +body .p-datatable .p-sortable-column.p-highlight .p-sortable-column-icon{ + color:var(--primary-button-color); +} diff --git a/ws/run_ws.sh b/ws/run_ws.sh deleted file mode 100644 index 8763a587feed5aac2fcb74b297ebaed9481b0859..0000000000000000000000000000000000000000 --- a/ws/run_ws.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -echo "Downloading WhiteSource agent..." -curl -LJO https://github.com/whitesource/unified-agent-distribution/releases/latest/download/wss-unified-agent.jar - -echo "Running WhiteSource scan..." -java -jar wss-unified-agent.jar -userKey ${USER_KEY} -apiKey ${API_KEY} -projectVersion ${PROJECT_VERSION} -projectToken ${PROJECT_TOKEN} -productVersion ${PRODUCT_VERSION} -productToken ${PRODUCT_TOKEN} -c ws.config -d ../ \ No newline at end of file