From 444364d531580481aabd4c852e49a6fc6dcd895b Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 21 Sep 2023 18:07:46 +0100 Subject: [PATCH 01/69] VUU25: Initialise module --- layout-server/.gitignore | 33 ++ layout-server/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + layout-server/mvnw | 308 ++++++++++++++++++ layout-server/mvnw.cmd | 205 ++++++++++++ layout-server/pom.xml | 54 +++ .../layoutserver/LayoutServerApplication.java | 13 + .../src/main/resources/application.properties | 1 + .../LayoutServerApplicationTests.java | 13 + pom.xml | 1 + 10 files changed, 630 insertions(+) create mode 100644 layout-server/.gitignore create mode 100644 layout-server/.mvn/wrapper/maven-wrapper.jar create mode 100644 layout-server/.mvn/wrapper/maven-wrapper.properties create mode 100644 layout-server/mvnw create mode 100644 layout-server/mvnw.cmd create mode 100644 layout-server/pom.xml create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/LayoutServerApplication.java create mode 100644 layout-server/src/main/resources/application.properties create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/LayoutServerApplicationTests.java diff --git a/layout-server/.gitignore b/layout-server/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/layout-server/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/layout-server/.mvn/wrapper/maven-wrapper.jar b/layout-server/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/layout-server/.mvn/wrapper/maven-wrapper.properties b/layout-server/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..2e76e189d --- /dev/null +++ b/layout-server/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/layout-server/mvnw b/layout-server/mvnw new file mode 100644 index 000000000..66df28542 --- /dev/null +++ b/layout-server/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/layout-server/mvnw.cmd b/layout-server/mvnw.cmd new file mode 100644 index 000000000..95ba6f54a --- /dev/null +++ b/layout-server/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/layout-server/pom.xml b/layout-server/pom.xml new file mode 100644 index 000000000..bfb06416b --- /dev/null +++ b/layout-server/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.3 + + + org.finos.vuu + layout-server + 0.0.1-SNAPSHOT + layout-server + layout-server + + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + true + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/LayoutServerApplication.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/LayoutServerApplication.java new file mode 100644 index 000000000..f0a1d10c7 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/LayoutServerApplication.java @@ -0,0 +1,13 @@ +package org.finos.vuu.layoutserver; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LayoutServerApplication { + + public static void main(String[] args) { + SpringApplication.run(LayoutServerApplication.class, args); + } + +} diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/layout-server/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/LayoutServerApplicationTests.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/LayoutServerApplicationTests.java new file mode 100644 index 000000000..0e56bd365 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/LayoutServerApplicationTests.java @@ -0,0 +1,13 @@ +package org.finos.vuu.layoutserver; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class LayoutServerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/pom.xml b/pom.xml index 06993cb6e..31a023435 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,7 @@ toolbox vuu vuu-ui + layout-server benchmark From 3e8e7386fb3314a969eaf1a6cbd5c68906a3047a Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 21 Sep 2023 20:24:14 +0100 Subject: [PATCH 02/69] VUU25: Basic Controller infrastructure --- .../controller/LayoutController.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java new file mode 100644 index 000000000..68cfc9367 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -0,0 +1,72 @@ +package org.finos.vuu.layoutserver.controller; + +import org.springframework.web.bind.annotation.*; + +@RestController("/layout") +public class LayoutController { + + /** + * Gets all layouts if no IDs are specified, otherwise gets the specified layouts + * + * @param ids IDs of the layouts to get + * @return the layouts + */ + @GetMapping + public String getLayouts(@RequestBody(required = false) String[] ids) { + return "Hello World"; + } + + /** + * Gets the specified layout + * + * @param id ID of the layout to get + * @return the layout + */ + @GetMapping("/{id}") + public String getLayout(@PathVariable String id) { + return "Hello World"; + } + + /** + * Gets metadata for all layouts if no IDs are specified, otherwise gets the metadata for specified layouts + * + * @param ids IDs of the layouts to get metadata for + * @return the metadata + */ + @GetMapping("/metadata") + public String getMetadata(@RequestBody(required = false) String[] ids) { + return "Hello World"; + } + + /** + * Creates a new layout + * + * @return the ID of the new layout + */ + @PostMapping + public String createLayout() { + return "Hello World"; + } + + /** + * Updates the specified layout + * + * @param id ID of the layout to update + * @return the ID of the updated layout + */ + @PutMapping("/{id}") + public String updateLayout(@PathVariable String id) { + return "Hello World"; + } + + /** + * Deletes the specified layout + * + * @param id ID of the layout to delete + * @return the ID of the deleted layout + */ + @DeleteMapping("/{id}") + public String deleteLayout(@PathVariable String id) { + return "Hello World"; + } +} From 7061d64174539568d3ab965e84b47779c8968fab Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 21 Sep 2023 20:50:41 +0100 Subject: [PATCH 03/69] VUU25: Implement Layout & Metadata models --- layout-server/pom.xml | 8 ++++++ .../controller/LayoutController.java | 19 ++++++++----- .../finos/vuu/layoutserver/model/Layout.java | 21 ++++++++++++++ .../vuu/layoutserver/model/Metadata.java | 28 +++++++++++++++++++ 4 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java diff --git a/layout-server/pom.xml b/layout-server/pom.xml index bfb06416b..e714cc33c 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -32,6 +32,14 @@ lombok true + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 68cfc9367..9583c10e4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,7 +1,12 @@ package org.finos.vuu.layoutserver.controller; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + @RestController("/layout") public class LayoutController { @@ -12,8 +17,8 @@ public class LayoutController { * @return the layouts */ @GetMapping - public String getLayouts(@RequestBody(required = false) String[] ids) { - return "Hello World"; + public List getLayouts(@RequestBody(required = false) String[] ids) { + return new ArrayList<>(); } /** @@ -23,8 +28,8 @@ public String getLayouts(@RequestBody(required = false) String[] ids) { * @return the layout */ @GetMapping("/{id}") - public String getLayout(@PathVariable String id) { - return "Hello World"; + public Layout getLayout(@PathVariable String id) { + return Layout.builder().build(); } /** @@ -34,14 +39,14 @@ public String getLayout(@PathVariable String id) { * @return the metadata */ @GetMapping("/metadata") - public String getMetadata(@RequestBody(required = false) String[] ids) { - return "Hello World"; + public List getMetadata(@RequestBody(required = false) String[] ids) { + return new ArrayList<>(); } /** * Creates a new layout * - * @return the ID of the new layout + * @return the ID of the new layout` */ @PostMapping public String createLayout() { diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java new file mode 100644 index 000000000..cb5aebe62 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -0,0 +1,21 @@ +package org.finos.vuu.layoutserver.model; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Data; + +@Entity +@Data +@Builder +public class Layout { + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + private String id; + + private String definition; + + @OneToOne(cascade = CascadeType.ALL) + private Metadata metadata; + + protected Layout() {} +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java new file mode 100644 index 000000000..7df0c2fa0 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -0,0 +1,28 @@ +package org.finos.vuu.layoutserver.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Data; + +import java.util.Date; + +@Entity +@Data +@Builder +public class Metadata { + + @Id + @GeneratedValue + private Long id; + + private String name; + private String group; + private String screenshot; + private String user; + private Date date; + + protected Metadata() { + } +} From d444cad38cd0371cdfde0ccd929f762f97f64ad4 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Fri, 22 Sep 2023 16:33:24 +0100 Subject: [PATCH 04/69] VUU25: Add OpenAPI docs (Swagger) --- layout-server/pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/layout-server/pom.xml b/layout-server/pom.xml index e714cc33c..32570e8ee 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -37,8 +37,9 @@ spring-boot-starter-web - org.springframework.boot - spring-boot-starter-data-jpa + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 From 2bb9c0aa3b322ed4dd19e08e74394a3c267158d6 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Fri, 22 Sep 2023 16:34:01 +0100 Subject: [PATCH 05/69] VUU25: Add basic H2 database --- layout-server/pom.xml | 17 +++++++++++++---- .../src/main/resources/application.properties | 8 +++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/layout-server/pom.xml b/layout-server/pom.xml index 32570e8ee..f1ab238a2 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -28,13 +28,22 @@ test - org.projectlombok - lombok - true + org.springframework.boot + spring-boot-starter-web org.springframework.boot - spring-boot-starter-web + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + + + org.projectlombok + lombok + true org.springdoc diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index 8b1378917..f95f5b025 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1 +1,7 @@ - +spring.datasource.url=jdbc:h2:file:/data/layout +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=true +server.port=8081 \ No newline at end of file From 55aa6327e23d1ada3dcd16249cf58c12de6c03da Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Fri, 22 Sep 2023 17:34:49 +0100 Subject: [PATCH 06/69] VUU25: Implement DTOs --- layout-server/pom.xml | 17 +--- .../controller/LayoutController.java | 77 ++++++++++++------- .../layoutserver/dto/response/LayoutDTO.java | 24 ++++++ .../dto/response/MetadataDTO.java | 31 ++++++++ .../finos/vuu/layoutserver/model/Layout.java | 14 ++-- .../vuu/layoutserver/model/Metadata.java | 24 +++--- .../src/main/resources/application.properties | 1 + 7 files changed, 128 insertions(+), 60 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutDTO.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataDTO.java diff --git a/layout-server/pom.xml b/layout-server/pom.xml index f1ab238a2..5b4cb720c 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -40,16 +40,15 @@ h2 runtime - - org.projectlombok - lombok - true - org.springdoc springdoc-openapi-starter-webmvc-ui 2.1.0 + + org.projectlombok + lombok + @@ -57,14 +56,6 @@ org.springframework.boot spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 9583c10e4..50dca326a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,24 +1,42 @@ package org.finos.vuu.layoutserver.controller; +import org.finos.vuu.layoutserver.dto.response.LayoutDTO; +import org.finos.vuu.layoutserver.dto.response.MetadataDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; -import org.springframework.web.bind.annotation.*; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; -import java.util.ArrayList; import java.util.List; -@RestController("/layout") +@RestController +@RequestMapping("/layout") public class LayoutController { - /** - * Gets all layouts if no IDs are specified, otherwise gets the specified layouts - * - * @param ids IDs of the layouts to get - * @return the layouts - */ - @GetMapping - public List getLayouts(@RequestBody(required = false) String[] ids) { - return new ArrayList<>(); + public static final String LAYOUT_ID = "testLayoutId"; + + // TODO: Delete dummy data + private Layout createDummyLayout(String id) { + Layout layout = new Layout(); + layout.setId(id); + layout.setDefinition("testDefinition"); + Metadata metadata = new Metadata(); + metadata.setId("testMetadataId"); + metadata.setLayout(layout); + metadata.setName("testName"); + metadata.setGroup("testGroup"); + metadata.setScreenshot("testScreenshot"); + metadata.setUser("testUser"); + layout.setMetadata(metadata); + return layout; } /** @@ -28,50 +46,51 @@ public List getLayouts(@RequestBody(required = false) String[] ids) { * @return the layout */ @GetMapping("/{id}") - public Layout getLayout(@PathVariable String id) { - return Layout.builder().build(); + public LayoutDTO getLayout(@PathVariable String id) { + Layout layout = createDummyLayout(id); + return LayoutDTO.fromEntity(layout); } /** - * Gets metadata for all layouts if no IDs are specified, otherwise gets the metadata for specified layouts + * Gets metadata for all layouts * - * @param ids IDs of the layouts to get metadata for * @return the metadata */ @GetMapping("/metadata") - public List getMetadata(@RequestBody(required = false) String[] ids) { - return new ArrayList<>(); + public List getMetadata() { + Layout layout = createDummyLayout(LAYOUT_ID); + return List.of(MetadataDTO.fromEntity(layout.getMetadata())); } /** * Creates a new layout * - * @return the ID of the new layout` + * @return the ID of the new layout */ + @ResponseStatus(HttpStatus.CREATED) @PostMapping - public String createLayout() { - return "Hello World"; + public String createLayout(@RequestBody LayoutDTO layoutDTO) { + return createDummyLayout(LAYOUT_ID).getId(); } /** * Updates the specified layout * - * @param id ID of the layout to update - * @return the ID of the updated layout + * @param id ID of the layout to update + * @param layoutDTO the new data to overwrite the layout with */ + @ResponseStatus(HttpStatus.ACCEPTED) @PutMapping("/{id}") - public String updateLayout(@PathVariable String id) { - return "Hello World"; + public void updateLayout(@PathVariable String id, @RequestBody LayoutDTO layoutDTO) { + createDummyLayout(LAYOUT_ID); } /** * Deletes the specified layout * * @param id ID of the layout to delete - * @return the ID of the deleted layout */ + @ResponseStatus(HttpStatus.ACCEPTED) @DeleteMapping("/{id}") - public String deleteLayout(@PathVariable String id) { - return "Hello World"; - } + public void deleteLayout(@PathVariable String id) {} } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutDTO.java new file mode 100644 index 000000000..d7c64edb5 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutDTO.java @@ -0,0 +1,24 @@ +package org.finos.vuu.layoutserver.dto.response; + +import lombok.Builder; +import lombok.Data; +import org.finos.vuu.layoutserver.model.Layout; + +import java.io.Serializable; + +@Data +@Builder +public class LayoutDTO implements Serializable { + + private String id; + private String definition; + private MetadataDTO metadata; + + public static LayoutDTO fromEntity(Layout layout) { + return LayoutDTO.builder() + .id(layout.getId()) + .definition(layout.getDefinition()) + .metadata(MetadataDTO.fromEntity(layout.getMetadata())) + .build(); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataDTO.java new file mode 100644 index 000000000..bd253a86a --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataDTO.java @@ -0,0 +1,31 @@ +package org.finos.vuu.layoutserver.dto.response; + +import lombok.Builder; +import lombok.Data; +import org.finos.vuu.layoutserver.model.Metadata; + +import java.io.Serializable; +import java.util.Date; + +@Data +@Builder +public class MetadataDTO implements Serializable { + + private String layoutId; + private String name; + private String group; + private String screenshot; + private String user; + private Date date; + + public static MetadataDTO fromEntity(Metadata metadata) { + return MetadataDTO.builder() + .layoutId(metadata.getLayout().getId()) + .name(metadata.getName()) + .group(metadata.getGroup()) + .screenshot(metadata.getScreenshot()) + .user(metadata.getUser()) + .date(metadata.getDate()) + .build(); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index cb5aebe62..9f1efef61 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -1,21 +1,23 @@ package org.finos.vuu.layoutserver.model; import jakarta.persistence.*; -import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; -@Entity @Data -@Builder +@Entity +@NoArgsConstructor public class Layout { @Id - @GeneratedValue(strategy= GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.AUTO) private String id; + @NonNull private String definition; @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "metadata_id", referencedColumnName = "id") + @NonNull private Metadata metadata; - - protected Layout() {} } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 7df0c2fa0..2a0eec777 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -1,28 +1,28 @@ package org.finos.vuu.layoutserver.model; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import lombok.Builder; +import jakarta.persistence.*; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; import java.util.Date; -@Entity @Data -@Builder +@NoArgsConstructor +@Entity public class Metadata { @Id - @GeneratedValue - private Long id; + @GeneratedValue(strategy = GenerationType.AUTO) + private String id; + + @OneToOne(mappedBy = "metadata") + @NonNull + private Layout layout; private String name; private String group; private String screenshot; private String user; - private Date date; - - protected Metadata() { - } + private Date date = new Date(); } diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index f95f5b025..fb9c7e613 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1,3 +1,4 @@ +server.servlet.contextPath=/api spring.datasource.url=jdbc:h2:file:/data/layout spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa From e0d6728170b90302c6bd1ec64071c51ca6ba5f15 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 26 Sep 2023 13:11:40 +0100 Subject: [PATCH 07/69] VUU25: Change server port and swagger URL --- layout-server/src/main/resources/application.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index fb9c7e613..7511c604a 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1,8 +1,9 @@ +server.port=8081 server.servlet.contextPath=/api +springdoc.swagger-ui.path=/swagger spring.datasource.url=jdbc:h2:file:/data/layout spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true -server.port=8081 \ No newline at end of file From 2cbb5e271fe69b9be2ec018eb9b05b21b653ca35 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 26 Sep 2023 13:17:41 +0100 Subject: [PATCH 08/69] VUU25: Wire up Controller, Service and Repository for full implementation - Also amend DTOs to differentiate request/response data --- .../controller/LayoutController.java | 58 +++++++++---------- .../dto/request/LayoutRequestDTO.java | 20 +++++++ .../dto/request/MetadataRequestDTO.java | 26 +++++++++ .../layoutserver/dto/response/LayoutDTO.java | 24 -------- .../dto/response/LayoutResponseDTO.java | 21 +++++++ ...adataDTO.java => MetadataResponseDTO.java} | 11 ++-- .../finos/vuu/layoutserver/model/Layout.java | 16 +++-- .../vuu/layoutserver/model/Metadata.java | 8 ++- .../repository/LayoutRepository.java | 10 ++++ .../layoutserver/service/LayoutService.java | 44 ++++++++++++++ .../src/main/resources/application.properties | 4 +- 11 files changed, 173 insertions(+), 69 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java delete mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutDTO.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/{MetadataDTO.java => MetadataResponseDTO.java} (69%) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 50dca326a..81f75156b 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,9 +1,11 @@ package org.finos.vuu.layoutserver.controller; -import org.finos.vuu.layoutserver.dto.response.LayoutDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataDTO; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; -import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.service.LayoutService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -16,28 +18,14 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.UUID; +@RequiredArgsConstructor @RestController @RequestMapping("/layout") public class LayoutController { - public static final String LAYOUT_ID = "testLayoutId"; - - // TODO: Delete dummy data - private Layout createDummyLayout(String id) { - Layout layout = new Layout(); - layout.setId(id); - layout.setDefinition("testDefinition"); - Metadata metadata = new Metadata(); - metadata.setId("testMetadataId"); - metadata.setLayout(layout); - metadata.setName("testName"); - metadata.setGroup("testGroup"); - metadata.setScreenshot("testScreenshot"); - metadata.setUser("testUser"); - layout.setMetadata(metadata); - return layout; - } + private final LayoutService layoutService; /** * Gets the specified layout @@ -46,9 +34,8 @@ private Layout createDummyLayout(String id) { * @return the layout */ @GetMapping("/{id}") - public LayoutDTO getLayout(@PathVariable String id) { - Layout layout = createDummyLayout(id); - return LayoutDTO.fromEntity(layout); + public LayoutResponseDTO getLayout(@PathVariable UUID id) { + return LayoutResponseDTO.fromEntity(layoutService.getLayout(id)); } /** @@ -57,9 +44,8 @@ public LayoutDTO getLayout(@PathVariable String id) { * @return the metadata */ @GetMapping("/metadata") - public List getMetadata() { - Layout layout = createDummyLayout(LAYOUT_ID); - return List.of(MetadataDTO.fromEntity(layout.getMetadata())); + public List getMetadata() { + return layoutService.getMetadata().stream().map(MetadataResponseDTO::fromEntity).toList(); } /** @@ -69,20 +55,26 @@ public List getMetadata() { */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public String createLayout(@RequestBody LayoutDTO layoutDTO) { - return createDummyLayout(LAYOUT_ID).getId(); + public UUID createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { + Layout layout = layoutToCreate.toEntity(); + + // TODO: Layout already created, updating instead + + return layoutService.createLayout(layout); } /** * Updates the specified layout * * @param id ID of the layout to update - * @param layoutDTO the new data to overwrite the layout with + * @param layoutToUpdate the new data to overwrite the layout with */ @ResponseStatus(HttpStatus.ACCEPTED) @PutMapping("/{id}") - public void updateLayout(@PathVariable String id, @RequestBody LayoutDTO layoutDTO) { - createDummyLayout(LAYOUT_ID); + public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO layoutToUpdate) { + Layout layout = layoutToUpdate.toEntity(); + + layoutService.updateLayout(id, layout); } /** @@ -92,5 +84,7 @@ public void updateLayout(@PathVariable String id, @RequestBody LayoutDTO layoutD */ @ResponseStatus(HttpStatus.ACCEPTED) @DeleteMapping("/{id}") - public void deleteLayout(@PathVariable String id) {} + public void deleteLayout(@PathVariable UUID id) { + layoutService.deleteLayout(id); + } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java new file mode 100644 index 000000000..b6a94eec3 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java @@ -0,0 +1,20 @@ +package org.finos.vuu.layoutserver.dto.request; + +import lombok.Data; +import org.finos.vuu.layoutserver.model.Layout; + +import java.io.Serializable; + +@Data +public class LayoutRequestDTO implements Serializable { + + private String definition; + private MetadataRequestDTO metadata; + + public Layout toEntity() { + Layout layout = new Layout(); + layout.setDefinition(definition); + layout.setMetadata(metadata.toEntity()); + return layout; + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java new file mode 100644 index 000000000..821933275 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java @@ -0,0 +1,26 @@ +package org.finos.vuu.layoutserver.dto.request; + +import lombok.Data; +import org.finos.vuu.layoutserver.model.Metadata; + +import java.io.Serializable; +import java.util.Date; + +@Data +public class MetadataRequestDTO implements Serializable { + + private String name; + private String group; + private String screenshot; + private String user; + + public Metadata toEntity() { + Metadata metadata = new Metadata(); + metadata.setName(name); + metadata.setGroup(group); + metadata.setScreenshot(screenshot); + metadata.setUser(user); + metadata.setUpdated(new Date()); + return metadata; + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutDTO.java deleted file mode 100644 index d7c64edb5..000000000 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.finos.vuu.layoutserver.dto.response; - -import lombok.Builder; -import lombok.Data; -import org.finos.vuu.layoutserver.model.Layout; - -import java.io.Serializable; - -@Data -@Builder -public class LayoutDTO implements Serializable { - - private String id; - private String definition; - private MetadataDTO metadata; - - public static LayoutDTO fromEntity(Layout layout) { - return LayoutDTO.builder() - .id(layout.getId()) - .definition(layout.getDefinition()) - .metadata(MetadataDTO.fromEntity(layout.getMetadata())) - .build(); - } -} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java new file mode 100644 index 000000000..1c64a82cf --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java @@ -0,0 +1,21 @@ +package org.finos.vuu.layoutserver.dto.response; + +import lombok.Builder; +import lombok.Data; +import org.finos.vuu.layoutserver.model.Layout; + +import java.io.Serializable; +import java.util.UUID; + +@Data +@Builder +public class LayoutResponseDTO implements Serializable { + + private UUID id; + private String definition; + private MetadataResponseDTO metadata; + + public static LayoutResponseDTO fromEntity(Layout layout) { + return LayoutResponseDTO.builder().id(layout.getId()).definition(layout.getDefinition()).metadata(MetadataResponseDTO.fromEntity(layout.getMetadata())).build(); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java similarity index 69% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java index bd253a86a..ec11e5ce5 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java @@ -6,26 +6,27 @@ import java.io.Serializable; import java.util.Date; +import java.util.UUID; @Data @Builder -public class MetadataDTO implements Serializable { +public class MetadataResponseDTO implements Serializable { - private String layoutId; + private UUID layoutId; private String name; private String group; private String screenshot; private String user; private Date date; - public static MetadataDTO fromEntity(Metadata metadata) { - return MetadataDTO.builder() + public static MetadataResponseDTO fromEntity(Metadata metadata) { + return MetadataResponseDTO.builder() .layoutId(metadata.getLayout().getId()) .name(metadata.getName()) .group(metadata.getGroup()) .screenshot(metadata.getScreenshot()) .user(metadata.getUser()) - .date(metadata.getDate()) + .date(metadata.getCreated()) .build(); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 9f1efef61..2a98ad3cd 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -1,17 +1,25 @@ package org.finos.vuu.layoutserver.model; -import jakarta.persistence.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; import lombok.Data; import lombok.NoArgsConstructor; import lombok.NonNull; +import java.util.UUID; + @Data -@Entity @NoArgsConstructor +@Entity public class Layout { @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private String id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @NonNull private String definition; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 2a0eec777..8d44d1b07 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -6,6 +6,7 @@ import lombok.NonNull; import java.util.Date; +import java.util.UUID; @Data @NoArgsConstructor @@ -13,8 +14,8 @@ public class Metadata { @Id - @GeneratedValue(strategy = GenerationType.AUTO) - private String id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @OneToOne(mappedBy = "metadata") @NonNull @@ -24,5 +25,6 @@ public class Metadata { private String group; private String screenshot; private String user; - private Date date = new Date(); + private Date created = new Date(); + private Date updated; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java new file mode 100644 index 000000000..d57af4897 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java @@ -0,0 +1,10 @@ +package org.finos.vuu.layoutserver.repository; + +import org.finos.vuu.layoutserver.model.Layout; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface LayoutRepository extends CrudRepository {} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java new file mode 100644 index 000000000..77451e0e8 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -0,0 +1,44 @@ +package org.finos.vuu.layoutserver.service; + +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.LayoutRepository; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@RequiredArgsConstructor +@Service +public class LayoutService { + + private final LayoutRepository layoutRepository; + + public Layout getLayout(UUID id) { + return layoutRepository.findById(id).orElseThrow(); + } + + public List getMetadata() { + List metadata = new ArrayList<>(); + layoutRepository.findAll().forEach(layout -> metadata.add(layout.getMetadata())); + return metadata; + } + + public UUID createLayout(Layout layout) { + return layoutRepository.save(layout).getId(); + } + + public void updateLayout(UUID id, Layout updatedLayout) { + Layout oldLayout = getLayout(id); + oldLayout.setDefinition(updatedLayout.getDefinition()); + oldLayout.setMetadata(updatedLayout.getMetadata()); + + layoutRepository.save(oldLayout); + } + + public void deleteLayout(UUID id) { + layoutRepository.deleteById(id); + } +} diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index 7511c604a..f85ed2a9c 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1,9 +1,11 @@ server.port=8081 server.servlet.contextPath=/api springdoc.swagger-ui.path=/swagger -spring.datasource.url=jdbc:h2:file:/data/layout +spring.datasource.url=jdbc:h2:file:/data/layout;NON_KEYWORDS=GROUP,USER spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.generate-ddl=true From 4f3f4cb619f8e5e7c9fef21c54fbd17b2235070a Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 26 Sep 2023 17:23:48 +0100 Subject: [PATCH 09/69] VUU25: Implement model mapper to replace manual entity<->DTO conversion --- layout-server/pom.xml | 5 +++ .../controller/LayoutController.java | 16 +++++++--- .../vuu/layoutserver/dto/MappingConfig.java | 31 +++++++++++++++++++ .../dto/request/LayoutRequestDTO.java | 12 +------ .../dto/request/MetadataRequestDTO.java | 16 +++------- .../dto/response/LayoutResponseDTO.java | 10 +----- .../dto/response/MetadataResponseDTO.java | 20 ++---------- .../finos/vuu/layoutserver/model/Layout.java | 2 +- .../vuu/layoutserver/model/Metadata.java | 2 +- .../src/main/resources/application.properties | 3 +- 10 files changed, 60 insertions(+), 57 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java diff --git a/layout-server/pom.xml b/layout-server/pom.xml index 5b4cb720c..ace79292f 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -49,6 +49,11 @@ org.projectlombok lombok + + org.modelmapper + modelmapper + 3.1.0 + diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 81f75156b..32f49e6e4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -6,6 +6,7 @@ import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.service.LayoutService; +import org.modelmapper.ModelMapper; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -26,6 +27,7 @@ public class LayoutController { private final LayoutService layoutService; + private final ModelMapper mapper; /** * Gets the specified layout @@ -35,7 +37,7 @@ public class LayoutController { */ @GetMapping("/{id}") public LayoutResponseDTO getLayout(@PathVariable UUID id) { - return LayoutResponseDTO.fromEntity(layoutService.getLayout(id)); + return mapper.map(layoutService.getLayout(id), LayoutResponseDTO.class); } /** @@ -45,7 +47,11 @@ public LayoutResponseDTO getLayout(@PathVariable UUID id) { */ @GetMapping("/metadata") public List getMetadata() { - return layoutService.getMetadata().stream().map(MetadataResponseDTO::fromEntity).toList(); + + return layoutService.getMetadata() + .stream() + .map(metadata -> mapper.map(metadata, MetadataResponseDTO.class)) + .toList(); } /** @@ -56,7 +62,7 @@ public List getMetadata() { @ResponseStatus(HttpStatus.CREATED) @PostMapping public UUID createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { - Layout layout = layoutToCreate.toEntity(); + Layout layout = mapper.map(layoutToCreate, Layout.class); // TODO: Layout already created, updating instead @@ -66,13 +72,13 @@ public UUID createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { /** * Updates the specified layout * - * @param id ID of the layout to update + * @param id ID of the layout to update * @param layoutToUpdate the new data to overwrite the layout with */ @ResponseStatus(HttpStatus.ACCEPTED) @PutMapping("/{id}") public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO layoutToUpdate) { - Layout layout = layoutToUpdate.toEntity(); + Layout layout = mapper.map(layoutToUpdate, Layout.class); layoutService.updateLayout(id, layout); } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java new file mode 100644 index 000000000..f20a93003 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java @@ -0,0 +1,31 @@ +package org.finos.vuu.layoutserver.dto; + +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; +import org.modelmapper.ModelMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MappingConfig { + + @Bean + public ModelMapper modelMapper() { + ModelMapper mapper = new ModelMapper(); + + // LayoutRequestDTO to Layout + mapper.typeMap(LayoutRequestDTO.class, Layout.class).addMappings(m -> m.skip(Layout::setId)); + + // Metadata to MetadataResponseDTO + mapper.typeMap(Metadata.class, MetadataResponseDTO.class) + .addMappings(m -> m.map(metadata -> metadata.getLayout().getId(), MetadataResponseDTO::setLayoutId)); + + // MetadataRequestDTO to Metadata + mapper.typeMap(MetadataRequestDTO.class, Metadata.class).addMappings(m -> m.skip(Metadata::setId)); + + return mapper; + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java index b6a94eec3..a3001f4f7 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java @@ -1,20 +1,10 @@ package org.finos.vuu.layoutserver.dto.request; import lombok.Data; -import org.finos.vuu.layoutserver.model.Layout; - -import java.io.Serializable; @Data -public class LayoutRequestDTO implements Serializable { +public class LayoutRequestDTO { private String definition; private MetadataRequestDTO metadata; - - public Layout toEntity() { - Layout layout = new Layout(); - layout.setDefinition(definition); - layout.setMetadata(metadata.toEntity()); - return layout; - } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java index 821933275..cef3205e5 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java @@ -1,26 +1,18 @@ package org.finos.vuu.layoutserver.dto.request; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; -import org.finos.vuu.layoutserver.model.Metadata; -import java.io.Serializable; import java.util.Date; @Data -public class MetadataRequestDTO implements Serializable { +public class MetadataRequestDTO { private String name; private String group; private String screenshot; private String user; - public Metadata toEntity() { - Metadata metadata = new Metadata(); - metadata.setName(name); - metadata.setGroup(group); - metadata.setScreenshot(screenshot); - metadata.setUser(user); - metadata.setUpdated(new Date()); - return metadata; - } + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private Date updated = new Date(); } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java index 1c64a82cf..835bcbdd3 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java @@ -1,21 +1,13 @@ package org.finos.vuu.layoutserver.dto.response; -import lombok.Builder; import lombok.Data; -import org.finos.vuu.layoutserver.model.Layout; -import java.io.Serializable; import java.util.UUID; @Data -@Builder -public class LayoutResponseDTO implements Serializable { +public class LayoutResponseDTO { private UUID id; private String definition; private MetadataResponseDTO metadata; - - public static LayoutResponseDTO fromEntity(Layout layout) { - return LayoutResponseDTO.builder().id(layout.getId()).definition(layout.getDefinition()).metadata(MetadataResponseDTO.fromEntity(layout.getMetadata())).build(); - } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java index ec11e5ce5..79ecf6c6c 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java @@ -1,32 +1,18 @@ package org.finos.vuu.layoutserver.dto.response; -import lombok.Builder; import lombok.Data; -import org.finos.vuu.layoutserver.model.Metadata; -import java.io.Serializable; import java.util.Date; import java.util.UUID; @Data -@Builder -public class MetadataResponseDTO implements Serializable { +public class MetadataResponseDTO { private UUID layoutId; private String name; private String group; private String screenshot; private String user; - private Date date; - - public static MetadataResponseDTO fromEntity(Metadata metadata) { - return MetadataResponseDTO.builder() - .layoutId(metadata.getLayout().getId()) - .name(metadata.getName()) - .group(metadata.getGroup()) - .screenshot(metadata.getScreenshot()) - .user(metadata.getUser()) - .date(metadata.getCreated()) - .build(); - } + private Date created; + private Date updated; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 2a98ad3cd..3dff00dd7 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -24,7 +24,7 @@ public class Layout { @NonNull private String definition; - @OneToOne(cascade = CascadeType.ALL) + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "metadata_id", referencedColumnName = "id") @NonNull private Metadata metadata; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 8d44d1b07..1f24e6ad4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -17,7 +17,7 @@ public class Metadata { @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - @OneToOne(mappedBy = "metadata") + @OneToOne(mappedBy = "metadata", cascade = CascadeType.ALL) @NonNull private Layout layout; diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index f85ed2a9c..81e7a32e3 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -7,5 +7,6 @@ spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true -spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update spring.jpa.generate-ddl=true +spring.jpa.show-sql=true From 9b2f6ff22b2999fbf99b90004901397b24579ee4 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 28 Sep 2023 17:41:33 +0100 Subject: [PATCH 10/69] VUU25: Fix one-to-one entity persistence issue --- .../layoutserver/controller/LayoutController.java | 11 ++++++----- .../org/finos/vuu/layoutserver/model/Layout.java | 4 +--- .../finos/vuu/layoutserver/model/Metadata.java | 10 ++++++---- .../repository/MetadataRepository.java | 10 ++++++++++ .../vuu/layoutserver/service/LayoutService.java | 15 +++++++++------ 5 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 32f49e6e4..7032d868f 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -73,14 +73,15 @@ public UUID createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { * Updates the specified layout * * @param id ID of the layout to update - * @param layoutToUpdate the new data to overwrite the layout with + * @param layoutToUpdate the updated layout */ - @ResponseStatus(HttpStatus.ACCEPTED) + @ResponseStatus(HttpStatus.OK) @PutMapping("/{id}") public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO layoutToUpdate) { - Layout layout = mapper.map(layoutToUpdate, Layout.class); + Layout layout = layoutService.getLayout(id); + mapper.map(layoutToUpdate, layout); - layoutService.updateLayout(id, layout); + layoutService.updateLayout(layout); } /** @@ -88,7 +89,7 @@ public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO la * * @param id ID of the layout to delete */ - @ResponseStatus(HttpStatus.ACCEPTED) + @ResponseStatus(HttpStatus.OK) @DeleteMapping("/{id}") public void deleteLayout(@PathVariable UUID id) { layoutService.deleteLayout(id); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 3dff00dd7..54c93224a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -9,7 +9,6 @@ import jakarta.persistence.OneToOne; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.NonNull; import java.util.UUID; @@ -17,15 +16,14 @@ @NoArgsConstructor @Entity public class Layout { + @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - @NonNull private String definition; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "metadata_id", referencedColumnName = "id") - @NonNull private Metadata metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 1f24e6ad4..798dde66d 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -1,9 +1,12 @@ package org.finos.vuu.layoutserver.model; -import jakarta.persistence.*; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.NonNull; import java.util.Date; import java.util.UUID; @@ -17,8 +20,7 @@ public class Metadata { @GeneratedValue(strategy = GenerationType.UUID) private UUID id; - @OneToOne(mappedBy = "metadata", cascade = CascadeType.ALL) - @NonNull + @OneToOne(mappedBy = "metadata") private Layout layout; private String name; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java new file mode 100644 index 000000000..03f81b108 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java @@ -0,0 +1,10 @@ +package org.finos.vuu.layoutserver.repository; + +import org.finos.vuu.layoutserver.model.Metadata; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface MetadataRepository extends CrudRepository {} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index 77451e0e8..b73c440de 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -4,7 +4,9 @@ import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; +import org.finos.vuu.layoutserver.repository.MetadataRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; @@ -15,6 +17,7 @@ public class LayoutService { private final LayoutRepository layoutRepository; + private final MetadataRepository metadataRepository; public Layout getLayout(UUID id) { return layoutRepository.findById(id).orElseThrow(); @@ -26,16 +29,16 @@ public List getMetadata() { return metadata; } + @Transactional public UUID createLayout(Layout layout) { + Metadata metadata = metadataRepository.save(layout.getMetadata()); + metadata.setLayout(layout); + layout.setMetadata(metadata); return layoutRepository.save(layout).getId(); } - public void updateLayout(UUID id, Layout updatedLayout) { - Layout oldLayout = getLayout(id); - oldLayout.setDefinition(updatedLayout.getDefinition()); - oldLayout.setMetadata(updatedLayout.getMetadata()); - - layoutRepository.save(oldLayout); + public void updateLayout(Layout updatedLayout) { + layoutRepository.save(updatedLayout); } public void deleteLayout(UUID id) { From a7ed2bdb69715f0331addab73c9b88d3bd793314 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 4 Oct 2023 17:29:42 +0100 Subject: [PATCH 11/69] VUU25: Pluralise layout endpoint resource --- .../org/finos/vuu/layoutserver/controller/LayoutController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 7032d868f..9c6347246 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -23,7 +23,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/layout") +@RequestMapping("/layouts") public class LayoutController { private final LayoutService layoutService; From ed7f15172a8485f08b67d062aee80457f7f1f6d8 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 4 Oct 2023 17:45:14 +0100 Subject: [PATCH 12/69] VUU25: Amend annotations, javadocs, and updateLayout signature in the Controller --- .../controller/LayoutController.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 9c6347246..7125e75ed 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -57,29 +57,28 @@ public List getMetadata() { /** * Creates a new layout * - * @return the ID of the new layout + * @param layoutToCreate the layout to be created + * @return the generated ID of the new layout */ @ResponseStatus(HttpStatus.CREATED) @PostMapping public UUID createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { Layout layout = mapper.map(layoutToCreate, Layout.class); - // TODO: Layout already created, updating instead - return layoutService.createLayout(layout); } /** * Updates the specified layout * - * @param id ID of the layout to update - * @param layoutToUpdate the updated layout + * @param id ID of the layout to update + * @param newLayout the new layout */ - @ResponseStatus(HttpStatus.OK) + @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") - public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO layoutToUpdate) { + public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO newLayout) { Layout layout = layoutService.getLayout(id); - mapper.map(layoutToUpdate, layout); + mapper.map(newLayout, layout); layoutService.updateLayout(layout); } @@ -89,7 +88,7 @@ public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO la * * @param id ID of the layout to delete */ - @ResponseStatus(HttpStatus.OK) + @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping("/{id}") public void deleteLayout(@PathVariable UUID id) { layoutService.deleteLayout(id); From 7d72461f88149d20eb03d295f870a28d2bde424d Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 4 Oct 2023 17:57:02 +0100 Subject: [PATCH 13/69] VUU25: Add javadocs to definition field in LayoutDTOs --- .../finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java | 4 ++++ .../vuu/layoutserver/dto/response/LayoutResponseDTO.java | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java index a3001f4f7..dafce63b5 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java @@ -5,6 +5,10 @@ @Data public class LayoutRequestDTO { + /** + * The definition of the layout as a string (e.g. stringified JSON structure containing components) + */ private String definition; + private MetadataRequestDTO metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java index 835bcbdd3..9e1077063 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java @@ -8,6 +8,11 @@ public class LayoutResponseDTO { private UUID id; + + /** + * The definition of the layout as a string (e.g. stringified JSON structure containing components) + */ private String definition; + private MetadataResponseDTO metadata; } From 2653ab1fcd17aa423b591c842e7a31d4b23bb70d Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 4 Oct 2023 18:21:59 +0100 Subject: [PATCH 14/69] VUU25: Remove no args constructor on entities --- .../src/main/java/org/finos/vuu/layoutserver/model/Layout.java | 1 - .../src/main/java/org/finos/vuu/layoutserver/model/Metadata.java | 1 - 2 files changed, 2 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 54c93224a..6d1f3aee6 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -13,7 +13,6 @@ import java.util.UUID; @Data -@NoArgsConstructor @Entity public class Layout { diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 798dde66d..531b31ddb 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -12,7 +12,6 @@ import java.util.UUID; @Data -@NoArgsConstructor @Entity public class Metadata { From 29a71ce5c7cf764df7c18b62d8803fbfc1965007 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 4 Oct 2023 18:29:52 +0100 Subject: [PATCH 15/69] VUU25: Replace layoutRepository with metadataRepository in LayoutService.getMetadata --- .../org/finos/vuu/layoutserver/service/LayoutService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index b73c440de..ae4411ba6 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -25,7 +25,9 @@ public Layout getLayout(UUID id) { public List getMetadata() { List metadata = new ArrayList<>(); - layoutRepository.findAll().forEach(layout -> metadata.add(layout.getMetadata())); + + metadataRepository.findAll().forEach(metadata::add); + return metadata; } From 42f48634974416a1642b6eb62ed252c62c977b51 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 4 Oct 2023 18:44:33 +0100 Subject: [PATCH 16/69] VUU25: Add date to create layout HTTP response body --- .../controller/LayoutController.java | 7 +++++-- .../vuu/layoutserver/dto/MappingConfig.java | 6 ++++++ .../dto/response/CreateLayoutResponseDTO.java | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/CreateLayoutResponseDTO.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 7125e75ed..77efd8fcb 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; +import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; @@ -62,10 +63,12 @@ public List getMetadata() { */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public UUID createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { + public CreateLayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { Layout layout = mapper.map(layoutToCreate, Layout.class); - return layoutService.createLayout(layout); + Layout createdLayout = layoutService.getLayout(layoutService.createLayout(layout)); + + return mapper.map(createdLayout, CreateLayoutResponseDTO.class); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java index f20a93003..5d4f8ec34 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java @@ -2,6 +2,7 @@ import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -19,6 +20,11 @@ public ModelMapper modelMapper() { // LayoutRequestDTO to Layout mapper.typeMap(LayoutRequestDTO.class, Layout.class).addMappings(m -> m.skip(Layout::setId)); + // Layout to CreateLayoutResponseDTO + mapper.typeMap(Layout.class, CreateLayoutResponseDTO.class) + .addMappings(m -> m.map(layout -> layout.getMetadata().getCreated(), + CreateLayoutResponseDTO::setCreated)); + // Metadata to MetadataResponseDTO mapper.typeMap(Metadata.class, MetadataResponseDTO.class) .addMappings(m -> m.map(metadata -> metadata.getLayout().getId(), MetadataResponseDTO::setLayoutId)); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/CreateLayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/CreateLayoutResponseDTO.java new file mode 100644 index 000000000..8ff792674 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/CreateLayoutResponseDTO.java @@ -0,0 +1,17 @@ +package org.finos.vuu.layoutserver.dto.response; + +import lombok.Data; + +import java.util.Date; +import java.util.UUID; + +@Data +public class CreateLayoutResponseDTO { + + private UUID id; + + /** + * The generated creation date of the created layout + */ + private Date created; +} From 10e53ad4a75a47e1ab3f9e7c259aa01e2f6d99ea Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 4 Oct 2023 18:53:13 +0100 Subject: [PATCH 17/69] VUU25: Introduce MetadataService --- .../controller/LayoutController.java | 2 ++ .../layoutserver/service/LayoutService.java | 12 ++------ .../layoutserver/service/MetadataService.java | 28 +++++++++++++++++++ 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 77efd8fcb..cc7814e80 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -80,7 +80,9 @@ public CreateLayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layout @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO newLayout) { + // TODO I'm not sure about this, I think layoutService should be doing more logic, not the mapper here Layout layout = layoutService.getLayout(id); + mapper.map(newLayout, layout); layoutService.updateLayout(layout); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index ae4411ba6..5aed4a625 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -4,11 +4,9 @@ import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; -import org.finos.vuu.layoutserver.repository.MetadataRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -17,23 +15,19 @@ public class LayoutService { private final LayoutRepository layoutRepository; - private final MetadataRepository metadataRepository; + private final MetadataService metadataService; public Layout getLayout(UUID id) { return layoutRepository.findById(id).orElseThrow(); } public List getMetadata() { - List metadata = new ArrayList<>(); - - metadataRepository.findAll().forEach(metadata::add); - - return metadata; + return metadataService.getMetadata(); } @Transactional public UUID createLayout(Layout layout) { - Metadata metadata = metadataRepository.save(layout.getMetadata()); + Metadata metadata = metadataService.createMetadata(layout.getMetadata()); metadata.setLayout(layout); layout.setMetadata(metadata); return layoutRepository.save(layout).getId(); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java new file mode 100644 index 000000000..0103e3a4a --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java @@ -0,0 +1,28 @@ +package org.finos.vuu.layoutserver.service; + +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.MetadataRepository; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Service +public class MetadataService { + + private final MetadataRepository metadataRepository; + + public List getMetadata() { + List metadata = new ArrayList<>(); + + metadataRepository.findAll().forEach(metadata::add); + + return metadata; + } + + public Metadata createMetadata(Metadata metadata) { + return metadataRepository.save(metadata); + } +} From 715cbacccb943b6060f06d1d5464cc61451635cc Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 3 Oct 2023 15:10:14 +0100 Subject: [PATCH 18/69] VUU25: Create layout controller unit tests (backend) --- .../controller/LayoutController.java | 5 +- .../dto/request/LayoutRequestDTO.java | 6 + .../dto/request/MetadataRequestDTO.java | 4 + .../vuu/layoutserver/model/Metadata.java | 2 + .../layoutserver/service/LayoutService.java | 2 + .../controller/LayoutControllerTest.java | 158 ++++++++++++++++++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index cc7814e80..d6c8000a8 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,5 +1,6 @@ package org.finos.vuu.layoutserver.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; @@ -63,7 +64,7 @@ public List getMetadata() { */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public CreateLayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { + public CreateLayoutResponseDTO createLayout(@RequestBody @Valid LayoutRequestDTO layoutToCreate) { Layout layout = mapper.map(layoutToCreate, Layout.class); Layout createdLayout = layoutService.getLayout(layoutService.createLayout(layout)); @@ -79,7 +80,7 @@ public CreateLayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layout */ @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") - public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO newLayout) { + public void updateLayout(@PathVariable UUID id, @RequestBody @Valid LayoutRequestDTO newLayout) { // TODO I'm not sure about this, I think layoutService should be doing more logic, not the mapper here Layout layout = layoutService.getLayout(id); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java index dafce63b5..79455260b 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java @@ -1,5 +1,7 @@ package org.finos.vuu.layoutserver.dto.request; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; import lombok.Data; @Data @@ -8,7 +10,11 @@ public class LayoutRequestDTO { /** * The definition of the layout as a string (e.g. stringified JSON structure containing components) */ + @JsonProperty(value = "definition", required = true) + @NotNull(message = "Please provide a valid definition") private String definition; + @JsonProperty(value = "metadata", required = true) + @NotNull(message = "Please provide valid metadata") private MetadataRequestDTO metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java index cef3205e5..d78ef7c36 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java @@ -1,6 +1,7 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.Date; @@ -8,7 +9,10 @@ @Data public class MetadataRequestDTO { + @JsonProperty(value = "name", required = true) + @NotNull(message = "Please provide a valid name") private String name; + private String group; private String screenshot; private String user; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 531b31ddb..e9bf280c8 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -7,6 +7,7 @@ import jakarta.persistence.OneToOne; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.ToString; import java.util.Date; import java.util.UUID; @@ -20,6 +21,7 @@ public class Metadata { private UUID id; @OneToOne(mappedBy = "metadata") + @ToString.Exclude private Layout layout; private String name; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index 5aed4a625..c0185ed18 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -21,6 +21,8 @@ public Layout getLayout(UUID id) { return layoutRepository.findById(id).orElseThrow(); } + + // TODO Refactor to use a metadatarepository.findAll method public List getMetadata() { return metadataService.getMetadata(); } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java new file mode 100644 index 000000000..aed588352 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -0,0 +1,158 @@ +package org.finos.vuu.layoutserver.controller; + +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.LayoutRepository; +import org.finos.vuu.layoutserver.service.LayoutService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.modelmapper.ModelMapper; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LayoutControllerTest { + + @Mock + private LayoutService layoutService; + @Mock + private LayoutRepository layoutRepository; + // TODO Should modelmapper be mocked out? + @Spy + private ModelMapper modelMapper; + @InjectMocks + private LayoutController layoutController; + + private UUID validLayoutId; + private String invalidLayoutId; + private UUID doesNotExistLayoutId; + private Layout layout; + private Metadata metadata; + private LayoutRequestDTO layoutRequest; + private LayoutResponseDTO expectedLayoutResponse; + private List expectedMetadataResponse; + + @BeforeEach + public void setup() { + validLayoutId = UUID.randomUUID(); + invalidLayoutId = "invalidId"; + doesNotExistLayoutId = UUID.randomUUID(); + UUID metadataId = UUID.randomUUID(); + String layoutDefinition = "Test Definition"; + + metadata = new Metadata(); + metadata.setId(metadataId); + metadata.setName("Test Layout"); + metadata.setUser("Test User"); + metadata.setGroup("Test Group"); + metadata.setScreenshot("Test Screenshot"); + + layout = new Layout(); + layout.setId(validLayoutId); + layout.setDefinition(layoutDefinition); + layout.setMetadata(metadata); + metadata.setLayout(layout); + + layoutRequest = new LayoutRequestDTO(); + MetadataRequestDTO metadataRequestDTO = new MetadataRequestDTO(); + metadataRequestDTO.setName(metadata.getName()); + metadataRequestDTO.setUser(metadata.getUser()); + metadataRequestDTO.setGroup(metadata.getGroup()); + metadataRequestDTO.setScreenshot(metadata.getScreenshot()); + layoutRequest.setDefinition(layout.getDefinition()); + layoutRequest.setMetadata(metadataRequestDTO); + + expectedLayoutResponse = new LayoutResponseDTO(); + expectedLayoutResponse.setId(layout.getId()); + expectedLayoutResponse.setDefinition(layout.getDefinition()); + + MetadataResponseDTO metadataResponse = getMetadataResponseDTO(); + expectedLayoutResponse.setMetadata(metadataResponse); + + expectedMetadataResponse = new ArrayList<>(); + expectedMetadataResponse.add(metadataResponse); + } + + + @Test + void getLayout_validIdAndLayoutExists_returnsLayout() { + when(layoutService.getLayout(validLayoutId)).thenReturn(layout); + assertThat(layoutController.getLayout(validLayoutId)).isEqualTo(expectedLayoutResponse); + } + + @Test + void getLayout_layoutDoesNotExist_throwsNotFoundAndReturns404() { + when(layoutService.getLayout(doesNotExistLayoutId)).thenThrow(NoSuchElementException.class); + assertThrows(NoSuchElementException.class, () -> { + layoutController.getLayout(doesNotExistLayoutId); + }); + } + + @Test + void getMetadata_metadataExists_returnsMetadata() { + when(layoutService.getMetadata()).thenReturn(List.of(metadata)); + assertThat(layoutController.getMetadata()).isEqualTo(expectedMetadataResponse); + } + + @Test + void getMetadata_noMetadataExists_returnsEmptyArray() { + when(layoutService.getMetadata()).thenReturn(List.of()); + assertThat(layoutController.getMetadata()).isEmpty(); + } + + @Test + void createLayout_validLayout_createsLayout() { + when(layoutService.createLayout(any(Layout.class))).thenReturn(layout.getId()); + assertThat(layoutController.createLayout(layoutRequest)).isEqualTo(layout.getId()); + } + + // TODO I don't think this is a valid / worthwhile unit test? + @Test + void updateLayout_validLayout_doesNothing() { + } + + @Test + void updateLayout_layoutDoesNotExist_returnsInvalidRequest() { + when(layoutService.getLayout(layout.getId())).thenThrow(NoSuchElementException.class); + assertThrows(NoSuchElementException.class, () -> layoutController.updateLayout(layout.getId(), layoutRequest)); + } + + // TODO I don't think this is a valid / worthwhile unit test? + @Test + void deleteLayout_validId_returnsSuccess() { + } + + // TODO I don't think this is a valid / worthwhile unit test? + @Test + void deleteLayout_layoutDoesNotExist_doesNothing() { + } + + private MetadataResponseDTO getMetadataResponseDTO() { + MetadataResponseDTO metadataResponse = new MetadataResponseDTO(); + metadataResponse.setLayoutId(layout.getId()); + metadataResponse.setName(layout.getMetadata().getName()); + metadataResponse.setUser(layout.getMetadata().getUser()); + metadataResponse.setGroup(layout.getMetadata().getGroup()); + metadataResponse.setScreenshot(layout.getMetadata().getScreenshot()); + metadataResponse.setCreated(layout.getMetadata().getCreated()); + metadataResponse.setUpdated(layout.getMetadata().getUpdated()); + return metadataResponse; + } +} From 9895e100ddee422ada0c4f29afeb2d9784330ca1 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 5 Oct 2023 15:25:05 +0100 Subject: [PATCH 19/69] VUU25: Create layout integration tests (backend) --- .../controller/LayoutController.java | 5 +- .../layoutserver/service/LayoutService.java | 9 +- .../controller/LayoutControllerTest.java | 24 +- .../integration/LayoutIntegrationTest.java | 279 ++++++++++++++++++ .../resources/application-test.properties | 5 + 5 files changed, 302 insertions(+), 20 deletions(-) create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java create mode 100644 layout-server/src/test/resources/application-test.properties diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index d6c8000a8..62d18841a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,6 +1,8 @@ package org.finos.vuu.layoutserver.controller; import jakarta.validation.Valid; +import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; @@ -20,9 +22,6 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import java.util.List; -import java.util.UUID; - @RequiredArgsConstructor @RestController @RequestMapping("/layouts") diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index c0185ed18..eef37d39c 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -1,5 +1,8 @@ package org.finos.vuu.layoutserver.service; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -7,9 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.UUID; - @RequiredArgsConstructor @Service public class LayoutService { @@ -18,7 +18,8 @@ public class LayoutService { private final MetadataService metadataService; public Layout getLayout(UUID id) { - return layoutRepository.findById(id).orElseThrow(); + return layoutRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("Layout with ID '" + id + "' not found")); } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index aed588352..27a8489fe 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -1,5 +1,14 @@ package org.finos.vuu.layoutserver.controller; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; @@ -17,16 +26,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.modelmapper.ModelMapper; -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class LayoutControllerTest { @@ -41,7 +40,6 @@ class LayoutControllerTest { private LayoutController layoutController; private UUID validLayoutId; - private String invalidLayoutId; private UUID doesNotExistLayoutId; private Layout layout; private Metadata metadata; @@ -52,7 +50,6 @@ class LayoutControllerTest { @BeforeEach public void setup() { validLayoutId = UUID.randomUUID(); - invalidLayoutId = "invalidId"; doesNotExistLayoutId = UUID.randomUUID(); UUID metadataId = UUID.randomUUID(); String layoutDefinition = "Test Definition"; @@ -131,7 +128,8 @@ void updateLayout_validLayout_doesNothing() { @Test void updateLayout_layoutDoesNotExist_returnsInvalidRequest() { when(layoutService.getLayout(layout.getId())).thenThrow(NoSuchElementException.class); - assertThrows(NoSuchElementException.class, () -> layoutController.updateLayout(layout.getId(), layoutRequest)); + assertThrows(NoSuchElementException.class, + () -> layoutController.updateLayout(layout.getId(), layoutRequest)); } // TODO I don't think this is a valid / worthwhile unit test? diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java new file mode 100644 index 000000000..1efd18082 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -0,0 +1,279 @@ +package org.finos.vuu.layoutserver.integration; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Date; +import java.util.UUID; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.LayoutRepository; +import org.finos.vuu.layoutserver.repository.MetadataRepository; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +public class LayoutIntegrationTest { + + private static String defaultDefinition; + private static String defaultName; + private static String defaultGroup; + private static String defaultScreenshot; + private static String defaultUser; + private final ObjectMapper objectMapper = new ObjectMapper(); + @Autowired + private MockMvc mockMvc; + @Autowired + private LayoutRepository layoutRepository; + @Autowired + private MetadataRepository metadataRepository; + + @BeforeAll + public static void setup() { + defaultDefinition = "Default layout definition"; + defaultName = "Default layout name"; + defaultGroup = "Default layout group"; + defaultScreenshot = "Default layout screenshot"; + defaultUser = "Default layout user"; + } + + @Test + void getLayout_validIDAndLayoutExists_returns200WithLayout() throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + + mockMvc.perform(get("/layouts/{id}", layout.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.definition", is(layout.getDefinition()))) + .andExpect(jsonPath("$.metadata.name", is(layout.getMetadata().getName()))) + .andExpect(jsonPath("$.metadata.group", is(layout.getMetadata().getGroup()))) + .andExpect(jsonPath("$.metadata.screenshot", is(layout.getMetadata().getScreenshot()))) + .andExpect(jsonPath("$.metadata.user", is(layout.getMetadata().getUser()))); + } + + @Test + void getLayout_validIDButLayoutDoesNotExist_returns404() throws Exception { + UUID layoutID = UUID.randomUUID(); + + mockMvc.perform(get("/layouts/{id}", layoutID)).andExpect(status().isNotFound()); + } + + @Test + void getLayout_invalidId_returns400() throws Exception { + String layoutID = "invalidUUID"; + + mockMvc.perform(get("/layouts/{id}", layoutID)).andExpect(status().isBadRequest()); + } + + @Test + void getMetadata_metadataExists_returnsMetadata() throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + + mockMvc.perform(get("/layouts/metadata")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", is(layout.getMetadata().getName()))) + .andExpect(jsonPath("$[0].group", is(layout.getMetadata().getGroup()))) + .andExpect(jsonPath("$[0].screenshot", is(layout.getMetadata().getScreenshot()))) + .andExpect(jsonPath("$[0].user", is(layout.getMetadata().getUser()))); + } + + @Test + void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { + mockMvc.perform(get("/layouts/metadata")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isEmpty()); + } + + @Test + void createLayout_validLayout_returnsLayoutCreatedWithIDAndCreatedDate() throws Exception { + MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); + metadataRequest.setName(defaultName); + metadataRequest.setGroup(defaultGroup); + metadataRequest.setScreenshot(defaultScreenshot); + metadataRequest.setUser(defaultUser); + + LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); + layoutRequest.setDefinition(defaultDefinition); + layoutRequest.setMetadata(metadataRequest); + + mockMvc.perform(post("/layouts") + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id").isNotEmpty()) + .andExpect(jsonPath("$.created").isNotEmpty()); + } + + @Test + void createLayout_invalidLayout_returns400() throws Exception { + String invalidLayout = "invalidLayout"; + + mockMvc.perform(post("/layouts") + .content(invalidLayout) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + void updateLayout_validIDAndValidRequest_returns204AndLayoutHasChanged() throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + + mockMvc.perform(put("/layouts/{id}", layout.getId()) + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + Layout updatedLayout = layoutRepository.findById(layout.getId()).orElseThrow(); + + assertThat(updatedLayout.getDefinition()) + .isEqualTo(layoutRequest.getDefinition()); + assertThat(updatedLayout.getMetadata().getName()) + .isEqualTo(layoutRequest.getMetadata().getName()); + assertThat(updatedLayout.getMetadata().getGroup()) + .isEqualTo(layoutRequest.getMetadata().getGroup()); + assertThat(updatedLayout.getMetadata().getScreenshot()) + .isEqualTo(layoutRequest.getMetadata().getScreenshot()); + assertThat(updatedLayout.getMetadata().getUser()) + .isEqualTo(layoutRequest.getMetadata().getUser()); + } + + @Test + void updateLayout_invalidRequestBodyDefinitionIsBlankAndMetadataIsNull_returns400AndLayoutDoesNotChange() + throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + + LayoutRequestDTO request = new LayoutRequestDTO(); + request.setDefinition(""); + request.setMetadata(null); + + mockMvc.perform(put("/layouts/{id}", layout.getId()) + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + } + + @Test + void updateLayout_invalidRequestBodyUnexpectedFormat_returns400AndLayoutDoesNotChange() + throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + String request = "invalidRequest"; + + mockMvc.perform(put("/layouts/{id}", layout.getId()) + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + } + + // TODO Update layout, invalid ID, returns 400 + @Test + void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exception { + UUID layoutID = UUID.randomUUID(); + LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + + mockMvc.perform(put("/layouts/{id}", layoutID) + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void updateLayout_invalidId_returns400() throws Exception { + String layoutID = "invalidUUID"; + LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + + mockMvc.perform(put("/layouts/{id}", layoutID) + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + void deleteLayout_validIDLayoutExists_returnsSuccessAndLayoutIsDeleted() throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + + mockMvc.perform(get("/layouts/{id}", layout.getId())) + .andExpect(status().isOk()); + + mockMvc.perform(delete("/layouts/{id}", layout.getId())) + .andExpect(status().isNoContent()); + + mockMvc.perform(get("/layouts/{id}", layout.getId())) + .andExpect(status().isNotFound()); + } + + @Test + void deleteLayout_validIDLayoutDoesNotExist_returnsNotFound() throws Exception { + UUID layoutID = UUID.randomUUID(); + + mockMvc.perform(delete("/layouts/{id}", layoutID)) + .andExpect(status().isNotFound()); + } + + @Test + void deleteLayout_invalidId_returns400() throws Exception { + String layoutID = "invalidUUID"; + + mockMvc.perform(delete("/layouts/{id}", layoutID)) + .andExpect(status().isBadRequest()); + } + + private Layout createDefaultLayoutInDatabase() { + Layout layout = new Layout(); + Metadata metadata = new Metadata(); + + layout.setDefinition(defaultDefinition); + layout.setMetadata(metadata); + + metadata.setLayout(layout); + metadata.setName(defaultName); + metadata.setGroup(defaultGroup); + metadata.setScreenshot(defaultScreenshot); + metadata.setUser(defaultUser); + + metadataRepository.save(metadata); + Layout createdLayout = layoutRepository.save(layout); + + assertThat(layoutRepository.findById(createdLayout.getId()).orElseThrow()) + .isEqualTo(layout); + + return createdLayout; + } + + private LayoutRequestDTO createValidUpdateRequest() { + MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); + metadataRequest.setName("Updated name"); + metadataRequest.setGroup("Updated group"); + metadataRequest.setScreenshot("Updated screenshot"); + metadataRequest.setUser("Updated user"); + + LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); + layoutRequest.setDefinition("Updated definition"); + layoutRequest.setMetadata(metadataRequest); + return layoutRequest; + } + +} diff --git a/layout-server/src/test/resources/application-test.properties b/layout-server/src/test/resources/application-test.properties new file mode 100644 index 000000000..2722b4aca --- /dev/null +++ b/layout-server/src/test/resources/application-test.properties @@ -0,0 +1,5 @@ +spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=GROUP,USER +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect \ No newline at end of file From 399040841ec43b6c88a03df74e5afed9dd3abac3 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 5 Oct 2023 17:26:43 +0100 Subject: [PATCH 20/69] VUU25: Add global exception handling to give appropriate HTTP responses --- layout-server/pom.xml | 4 ++++ .../controller/GlobalExceptionHandler.java | 23 +++++++++++++++++++ .../controller/LayoutController.java | 2 ++ .../dto/request/LayoutRequestDTO.java | 6 +++-- 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java diff --git a/layout-server/pom.xml b/layout-server/pom.xml index ace79292f..490ac4a5e 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -27,6 +27,10 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-validation + org.springframework.boot spring-boot-starter-web diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java new file mode 100644 index 000000000..a41a76ffa --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java @@ -0,0 +1,23 @@ +package org.finos.vuu.layoutserver.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import java.util.NoSuchElementException; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNotFound(NoSuchElementException ex) { + return new ResponseEntity<>(ex.getMessage(), org.springframework.http.HttpStatus.NOT_FOUND); + } + + @ExceptionHandler({MethodArgumentNotValidException.class, MethodArgumentTypeMismatchException.class}) + public ResponseEntity handleBadRequest(MethodArgumentNotValidException ex) { + return new ResponseEntity<>(ex.getMessage(), org.springframework.http.HttpStatus.BAD_REQUEST); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 62d18841a..eb70be1b5 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -12,6 +12,7 @@ import org.finos.vuu.layoutserver.service.LayoutService; import org.modelmapper.ModelMapper; import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -25,6 +26,7 @@ @RequiredArgsConstructor @RestController @RequestMapping("/layouts") +@Validated public class LayoutController { private final LayoutService layoutService; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java index 79455260b..a91f9baac 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java @@ -1,6 +1,7 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -8,10 +9,11 @@ public class LayoutRequestDTO { /** - * The definition of the layout as a string (e.g. stringified JSON structure containing components) + * The definition of the layout as a string (e.g. stringified JSON structure containing + * components) */ @JsonProperty(value = "definition", required = true) - @NotNull(message = "Please provide a valid definition") + @NotBlank(message = "Please provide a valid definition") private String definition; @JsonProperty(value = "metadata", required = true) From 13142bb7b0d0d7ba0927faef753cfc8b2defb9ce Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 5 Oct 2023 20:36:11 +0100 Subject: [PATCH 21/69] VUU25: Rename LayoutResponseDTO to GetLayoutResponseDTO --- .../finos/vuu/layoutserver/controller/LayoutController.java | 6 +++--- .../{LayoutResponseDTO.java => GetLayoutResponseDTO.java} | 2 +- .../vuu/layoutserver/controller/LayoutControllerTest.java | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/{LayoutResponseDTO.java => GetLayoutResponseDTO.java} (89%) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index eb70be1b5..c24bd8d71 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -6,7 +6,7 @@ import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; -import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; +import org.finos.vuu.layoutserver.dto.response.GetLayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.service.LayoutService; @@ -39,8 +39,8 @@ public class LayoutController { * @return the layout */ @GetMapping("/{id}") - public LayoutResponseDTO getLayout(@PathVariable UUID id) { - return mapper.map(layoutService.getLayout(id), LayoutResponseDTO.class); + public GetLayoutResponseDTO getLayout(@PathVariable UUID id) { + return mapper.map(layoutService.getLayout(id), GetLayoutResponseDTO.class); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/GetLayoutResponseDTO.java similarity index 89% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/GetLayoutResponseDTO.java index 9e1077063..e07d77565 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/GetLayoutResponseDTO.java @@ -5,7 +5,7 @@ import java.util.UUID; @Data -public class LayoutResponseDTO { +public class GetLayoutResponseDTO { private UUID id; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 27a8489fe..7acb32067 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -11,7 +11,7 @@ import java.util.UUID; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; -import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; +import org.finos.vuu.layoutserver.dto.response.GetLayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -44,7 +44,7 @@ class LayoutControllerTest { private Layout layout; private Metadata metadata; private LayoutRequestDTO layoutRequest; - private LayoutResponseDTO expectedLayoutResponse; + private GetLayoutResponseDTO expectedLayoutResponse; private List expectedMetadataResponse; @BeforeEach @@ -76,7 +76,7 @@ public void setup() { layoutRequest.setDefinition(layout.getDefinition()); layoutRequest.setMetadata(metadataRequestDTO); - expectedLayoutResponse = new LayoutResponseDTO(); + expectedLayoutResponse = new GetLayoutResponseDTO(); expectedLayoutResponse.setId(layout.getId()); expectedLayoutResponse.setDefinition(layout.getDefinition()); From 681dd938ecf1a110e1063f99e8c563ef54d66c42 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 5 Oct 2023 20:46:12 +0100 Subject: [PATCH 22/69] VUU25: Reformatting --- .../layoutserver/controller/LayoutController.java | 6 +++--- .../finos/vuu/layoutserver/dto/MappingConfig.java | 13 ++++++++----- .../org/finos/vuu/layoutserver/model/Layout.java | 4 +--- .../org/finos/vuu/layoutserver/model/Metadata.java | 6 ++---- .../layoutserver/repository/LayoutRepository.java | 3 +-- .../layoutserver/repository/MetadataRepository.java | 3 +-- .../vuu/layoutserver/service/MetadataService.java | 5 ++--- 7 files changed, 18 insertions(+), 22 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index c24bd8d71..90068d5fd 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -52,9 +52,9 @@ public GetLayoutResponseDTO getLayout(@PathVariable UUID id) { public List getMetadata() { return layoutService.getMetadata() - .stream() - .map(metadata -> mapper.map(metadata, MetadataResponseDTO.class)) - .toList(); + .stream() + .map(metadata -> mapper.map(metadata, MetadataResponseDTO.class)) + .toList(); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java index 5d4f8ec34..ff69a08f8 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java @@ -18,19 +18,22 @@ public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); // LayoutRequestDTO to Layout - mapper.typeMap(LayoutRequestDTO.class, Layout.class).addMappings(m -> m.skip(Layout::setId)); + mapper.typeMap(LayoutRequestDTO.class, Layout.class) + .addMappings(m -> m.skip(Layout::setId)); // Layout to CreateLayoutResponseDTO mapper.typeMap(Layout.class, CreateLayoutResponseDTO.class) - .addMappings(m -> m.map(layout -> layout.getMetadata().getCreated(), - CreateLayoutResponseDTO::setCreated)); + .addMappings(m -> m.map(layout -> layout.getMetadata().getCreated(), + CreateLayoutResponseDTO::setCreated)); // Metadata to MetadataResponseDTO mapper.typeMap(Metadata.class, MetadataResponseDTO.class) - .addMappings(m -> m.map(metadata -> metadata.getLayout().getId(), MetadataResponseDTO::setLayoutId)); + .addMappings(m -> m.map(metadata -> metadata.getLayout().getId(), + MetadataResponseDTO::setLayoutId)); // MetadataRequestDTO to Metadata - mapper.typeMap(MetadataRequestDTO.class, Metadata.class).addMappings(m -> m.skip(Metadata::setId)); + mapper.typeMap(MetadataRequestDTO.class, Metadata.class) + .addMappings(m -> m.skip(Metadata::setId)); return mapper; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 6d1f3aee6..3fc6a6dcc 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -7,10 +7,8 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; -import lombok.Data; -import lombok.NoArgsConstructor; - import java.util.UUID; +import lombok.Data; @Data @Entity diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index e9bf280c8..b5ae0bec2 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -5,12 +5,10 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToOne; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.ToString; - import java.util.Date; import java.util.UUID; +import lombok.Data; +import lombok.ToString; @Data @Entity diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java index d57af4897..6cbae4025 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java @@ -1,10 +1,9 @@ package org.finos.vuu.layoutserver.repository; +import java.util.UUID; import org.finos.vuu.layoutserver.model.Layout; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; -import java.util.UUID; - @Repository public interface LayoutRepository extends CrudRepository {} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java index 03f81b108..50cbe6288 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java @@ -1,10 +1,9 @@ package org.finos.vuu.layoutserver.repository; +import java.util.UUID; import org.finos.vuu.layoutserver.model.Metadata; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; -import java.util.UUID; - @Repository public interface MetadataRepository extends CrudRepository {} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java index 0103e3a4a..fb9696ac5 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java @@ -1,13 +1,12 @@ package org.finos.vuu.layoutserver.service; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.MetadataRepository; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; - @RequiredArgsConstructor @Service public class MetadataService { From 7b5dc5afba85dffc00559be722a20f36eb5231be Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Thu, 5 Oct 2023 20:46:43 +0100 Subject: [PATCH 23/69] VUU25: Make deleteLayout generate 404 if layout does not exist (backend) --- .../org/finos/vuu/layoutserver/controller/LayoutController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 90068d5fd..cdf67d3b3 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -98,6 +98,8 @@ public void updateLayout(@PathVariable UUID id, @RequestBody @Valid LayoutReques @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping("/{id}") public void deleteLayout(@PathVariable UUID id) { + // Generate a 404 if layout doesn't exist + layoutService.getLayout(id); layoutService.deleteLayout(id); } } From 09b9e73c1c17de5ae87fdbe3c2529433203d2316 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Mon, 9 Oct 2023 12:00:02 +0100 Subject: [PATCH 24/69] VUU25: Downgrade Java 17 -> 11 and Springboot 3 -> 2 --- layout-server/pom.xml | 8 ++++---- .../{dto => config}/MappingConfig.java | 2 +- .../controller/LayoutController.java | 14 +++++++------- .../finos/vuu/layoutserver/model/Layout.java | 18 ++++++++---------- .../finos/vuu/layoutserver/model/Metadata.java | 13 ++++++------- 5 files changed, 26 insertions(+), 29 deletions(-) rename layout-server/src/main/java/org/finos/vuu/layoutserver/{dto => config}/MappingConfig.java (97%) diff --git a/layout-server/pom.xml b/layout-server/pom.xml index ace79292f..11dce5f7c 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.3 + 2.7.16 org.finos.vuu @@ -14,7 +14,7 @@ layout-server layout-server - 17 + 11 @@ -42,8 +42,8 @@ org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.1.0 + springdoc-openapi-ui + 1.6.12 org.projectlombok diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java similarity index 97% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java index 5d4f8ec34..0e08053b4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java @@ -1,4 +1,4 @@ -package org.finos.vuu.layoutserver.dto; +package org.finos.vuu.layoutserver.config; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index cc7814e80..f6a557bbe 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,5 +1,7 @@ package org.finos.vuu.layoutserver.controller; +import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; @@ -19,9 +21,6 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import java.util.List; -import java.util.UUID; - @RequiredArgsConstructor @RestController @RequestMapping("/layouts") @@ -50,9 +49,9 @@ public LayoutResponseDTO getLayout(@PathVariable UUID id) { public List getMetadata() { return layoutService.getMetadata() - .stream() - .map(metadata -> mapper.map(metadata, MetadataResponseDTO.class)) - .toList(); + .stream() + .map(metadata -> mapper.map(metadata, MetadataResponseDTO.class)) + .collect(java.util.stream.Collectors.toList()); } /** @@ -80,7 +79,8 @@ public CreateLayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layout @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO newLayout) { - // TODO I'm not sure about this, I think layoutService should be doing more logic, not the mapper here + // TODO I'm not sure about this, I think layoutService should be doing more logic, not + // the mapper here Layout layout = layoutService.getLayout(id); mapper.map(newLayout, layout); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 6d1f3aee6..7c1be01c4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -1,15 +1,13 @@ package org.finos.vuu.layoutserver.model; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; import lombok.Data; -import lombok.NoArgsConstructor; - import java.util.UUID; @Data @@ -17,7 +15,7 @@ public class Layout { @Id - @GeneratedValue(strategy = GenerationType.UUID) + @GeneratedValue(strategy = GenerationType.AUTO) private UUID id; private String definition; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 531b31ddb..16c1fc61b 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -1,12 +1,11 @@ package org.finos.vuu.layoutserver.model; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToOne; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToOne; import lombok.Data; -import lombok.NoArgsConstructor; import java.util.Date; import java.util.UUID; @@ -16,7 +15,7 @@ public class Metadata { @Id - @GeneratedValue(strategy = GenerationType.UUID) + @GeneratedValue(strategy = GenerationType.AUTO) private UUID id; @OneToOne(mappedBy = "metadata") From dd5c120fe54a0aafb7fffd4a75af0b22eff11094 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Mon, 9 Oct 2023 12:30:48 +0100 Subject: [PATCH 25/69] VUU25: Change DB persistence from file to in-memory --- layout-server/src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index 81e7a32e3..2cf2389e5 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1,7 +1,7 @@ server.port=8081 server.servlet.contextPath=/api springdoc.swagger-ui.path=/swagger -spring.datasource.url=jdbc:h2:file:/data/layout;NON_KEYWORDS=GROUP,USER +spring.datasource.url=jdbc:h2:mem:layoutdb;NON_KEYWORDS=GROUP,USER spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password From 07c179a4500a185fae471cfc90048aa447e51332 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Mon, 9 Oct 2023 12:50:53 +0100 Subject: [PATCH 26/69] VUU25: Fix issues caused by downgrading Java & Springboot --- layout-server/pom.xml | 6 ++++++ .../vuu/layoutserver/controller/LayoutController.java | 10 +++++----- .../vuu/layoutserver/dto/request/LayoutRequestDTO.java | 4 ++-- .../layoutserver/dto/request/MetadataRequestDTO.java | 2 +- .../java/org/finos/vuu/layoutserver/model/Layout.java | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/layout-server/pom.xml b/layout-server/pom.xml index 458c7cd69..7d9eab55a 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -58,6 +58,12 @@ modelmapper 3.1.0 + + org.jetbrains + annotations + 13.0 + compile + diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 10d50fdc2..7e5f8c5a5 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -2,9 +2,7 @@ import java.util.List; import java.util.UUID; -import jakarta.validation.Valid; -import java.util.List; -import java.util.UUID; +import javax.validation.Valid; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; @@ -67,7 +65,8 @@ public List getMetadata() { */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public CreateLayoutResponseDTO createLayout(@RequestBody @Valid LayoutRequestDTO layoutToCreate) { + public CreateLayoutResponseDTO createLayout( + @RequestBody @Valid LayoutRequestDTO layoutToCreate) { Layout layout = mapper.map(layoutToCreate, Layout.class); Layout createdLayout = layoutService.getLayout(layoutService.createLayout(layout)); @@ -83,7 +82,8 @@ public CreateLayoutResponseDTO createLayout(@RequestBody @Valid LayoutRequestDTO */ @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") - public void updateLayout(@PathVariable UUID id, @RequestBody @Valid LayoutRequestDTO newLayout) { + public void updateLayout(@PathVariable UUID id, + @RequestBody @Valid LayoutRequestDTO newLayout) { // TODO I'm not sure about this, I think layoutService should be doing more logic, not // the mapper here Layout layout = layoutService.getLayout(id); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java index a91f9baac..8b93e6343 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java @@ -1,8 +1,8 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import lombok.Data; @Data diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java index d78ef7c36..01e62128f 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java @@ -1,7 +1,7 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.validation.constraints.NotNull; +import javax.validation.constraints.NotNull; import lombok.Data; import java.util.Date; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 7c1be01c4..0178e82b8 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -1,5 +1,6 @@ package org.finos.vuu.layoutserver.model; +import java.util.UUID; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.GeneratedValue; @@ -8,7 +9,6 @@ import javax.persistence.JoinColumn; import javax.persistence.OneToOne; import lombok.Data; -import java.util.UUID; @Data @Entity From 5fcb3283a88c0b7d5dd35fb480aa87c8612edf85 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 10 Oct 2023 10:42:04 +0100 Subject: [PATCH 27/69] VUU25: Write description for layout-server pom.xml --- layout-server/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layout-server/pom.xml b/layout-server/pom.xml index 11dce5f7c..f617a4999 100644 --- a/layout-server/pom.xml +++ b/layout-server/pom.xml @@ -12,7 +12,7 @@ layout-server 0.0.1-SNAPSHOT layout-server - layout-server + A remote server to persist layouts for the Vuu client 11 From 1c05d6be05804b403b965cc7fab68e6d8ef046d4 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 10 Oct 2023 10:57:00 +0100 Subject: [PATCH 28/69] VUU25: Implement interface for Metadata DTOs --- .../vuu/layoutserver/dto/MetadataDTO.java | 21 +++++++++++++++++++ .../dto/request/MetadataRequestDTO.java | 3 ++- .../dto/response/MetadataResponseDTO.java | 6 +++--- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java new file mode 100644 index 000000000..23f82a691 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java @@ -0,0 +1,21 @@ +package org.finos.vuu.layoutserver.dto; + +public interface MetadataDTO { + + String getName(); + + void setName(String name); + + String getGroup(); + + void setGroup(String group); + + String getScreenshot(); + + void setScreenshot(String screenshot); + + String getUser(); + + void setUser(String user); + +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java index cef3205e5..26c31ca83 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java @@ -4,9 +4,10 @@ import lombok.Data; import java.util.Date; +import org.finos.vuu.layoutserver.dto.MetadataDTO; @Data -public class MetadataRequestDTO { +public class MetadataRequestDTO implements MetadataDTO { private String name; private String group; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java index 79ecf6c6c..21dc8082d 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java @@ -1,12 +1,12 @@ package org.finos.vuu.layoutserver.dto.response; -import lombok.Data; - import java.util.Date; import java.util.UUID; +import lombok.Data; +import org.finos.vuu.layoutserver.dto.MetadataDTO; @Data -public class MetadataResponseDTO { +public class MetadataResponseDTO implements MetadataDTO { private UUID layoutId; private String name; From 3c32c6ac47e410230b36d4835f56ee04f71c33b5 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 10 Oct 2023 18:12:06 +0100 Subject: [PATCH 29/69] VUU25: Create layout service unit tests (backend) --- .../layoutserver/service/LayoutService.java | 2 - .../service/LayoutServiceTest.java | 93 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index eef37d39c..ac7d354c9 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -22,8 +22,6 @@ public Layout getLayout(UUID id) { .orElseThrow(() -> new NoSuchElementException("Layout with ID '" + id + "' not found")); } - - // TODO Refactor to use a metadatarepository.findAll method public List getMetadata() { return metadataService.getMetadata(); } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java new file mode 100644 index 000000000..734c7efe9 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java @@ -0,0 +1,93 @@ +package org.finos.vuu.layoutserver.service; + +import java.util.Date; +import java.util.List; +import org.finos.vuu.layoutserver.model.Metadata; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.UUID; +import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.repository.LayoutRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LayoutServiceTest { + + @Mock + private LayoutRepository layoutRepository; + + @Mock + private MetadataService metadataService; + + @InjectMocks + private LayoutService layoutService; + + private Layout layout; + private Metadata metadata; + private UUID layoutId; + + @BeforeEach + public void setup() { + layoutId = UUID.randomUUID(); + UUID metadataId = UUID.randomUUID(); + layout = new Layout(); + metadata = new Metadata(); + layout.setId(layoutId); + layout.setDefinition(""); + layout.setMetadata(metadata); + metadata.setId(metadataId); + metadata.setLayout(layout); + metadata.setName(""); + metadata.setGroup(""); + metadata.setScreenshot(""); + metadata.setUser(""); + metadata.setCreated(new Date()); + metadata.setUpdated(new Date()); + } + + @Test + void getLayout_returnsLayout() { + when(layoutRepository.findById(layoutId)).thenReturn(Optional.of(layout)); + + assertThat(layoutService.getLayout(layoutId)).isEqualTo(layout); + } + + @Test + void getMetadata_returnsMetadata() { + when(metadataService.getMetadata()).thenReturn(List.of(metadata)); + + assertThat(layoutService.getMetadata()).isEqualTo(List.of(metadata)); + } + + @Test + void createLayout() { + when(metadataService.createMetadata(metadata)).thenReturn(metadata); + when(layoutRepository.save(layout)).thenReturn(layout); + + assertThat(layoutService.createLayout(layout)).isEqualTo(layoutId); + } + + @Test + void updateLayout_returnsNothing() { + layoutService.updateLayout(layout); + + verify(layoutRepository, times(1)).save(layout); + } + + @Test + void deleteLayout_returnsNothing() { + layoutService.deleteLayout(layoutId); + + verify(layoutRepository, times(1)).deleteById(layoutId); + } +} \ No newline at end of file From 3d5697b5a8967f951b9981af0b3a5c54c312960f Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 10 Oct 2023 18:38:23 +0100 Subject: [PATCH 30/69] VUU25: Add test for transactional nature of LayoutService.createLayout() --- .../integration/LayoutIntegrationTest.java | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index 1efd18082..d3725e5f7 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -1,6 +1,6 @@ package org.finos.vuu.layoutserver.integration; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -10,7 +10,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.Date; import java.util.UUID; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; @@ -104,15 +103,7 @@ void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { @Test void createLayout_validLayout_returnsLayoutCreatedWithIDAndCreatedDate() throws Exception { - MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); - metadataRequest.setName(defaultName); - metadataRequest.setGroup(defaultGroup); - metadataRequest.setScreenshot(defaultScreenshot); - metadataRequest.setUser(defaultUser); - - LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); - layoutRequest.setDefinition(defaultDefinition); - layoutRequest.setMetadata(metadataRequest); + LayoutRequestDTO layoutRequest = createValidCreateRequest(); mockMvc.perform(post("/layouts") .content(objectMapper.writeValueAsString(layoutRequest)) @@ -122,6 +113,7 @@ void createLayout_validLayout_returnsLayoutCreatedWithIDAndCreatedDate() throws .andExpect(jsonPath("$.created").isNotEmpty()); } + @Test void createLayout_invalidLayout_returns400() throws Exception { String invalidLayout = "invalidLayout"; @@ -132,6 +124,20 @@ void createLayout_invalidLayout_returns400() throws Exception { .andExpect(status().isBadRequest()); } + @Test + void createLayout_validLayoutButInvalidMetadata_returns400AndDoesNotCreateLayout() + throws Exception { + LayoutRequestDTO layoutRequest = createValidCreateRequest(); + layoutRequest.setMetadata(null); + + mockMvc.perform(post("/layouts") + .content(objectMapper.writeValueAsString(layoutRequest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + assertThat(layoutRepository.findAll()).isEmpty(); + } + @Test void updateLayout_validIDAndValidRequest_returns204AndLayoutHasChanged() throws Exception { Layout layout = createDefaultLayoutInDatabase(); @@ -215,30 +221,25 @@ void updateLayout_invalidId_returns400() throws Exception { void deleteLayout_validIDLayoutExists_returnsSuccessAndLayoutIsDeleted() throws Exception { Layout layout = createDefaultLayoutInDatabase(); - mockMvc.perform(get("/layouts/{id}", layout.getId())) - .andExpect(status().isOk()); + mockMvc.perform(get("/layouts/{id}", layout.getId())).andExpect(status().isOk()); - mockMvc.perform(delete("/layouts/{id}", layout.getId())) - .andExpect(status().isNoContent()); + mockMvc.perform(delete("/layouts/{id}", layout.getId())).andExpect(status().isNoContent()); - mockMvc.perform(get("/layouts/{id}", layout.getId())) - .andExpect(status().isNotFound()); + mockMvc.perform(get("/layouts/{id}", layout.getId())).andExpect(status().isNotFound()); } @Test void deleteLayout_validIDLayoutDoesNotExist_returnsNotFound() throws Exception { UUID layoutID = UUID.randomUUID(); - mockMvc.perform(delete("/layouts/{id}", layoutID)) - .andExpect(status().isNotFound()); + mockMvc.perform(delete("/layouts/{id}", layoutID)).andExpect(status().isNotFound()); } @Test void deleteLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; - mockMvc.perform(delete("/layouts/{id}", layoutID)) - .andExpect(status().isBadRequest()); + mockMvc.perform(delete("/layouts/{id}", layoutID)).andExpect(status().isBadRequest()); } private Layout createDefaultLayoutInDatabase() { @@ -276,4 +277,16 @@ private LayoutRequestDTO createValidUpdateRequest() { return layoutRequest; } + private LayoutRequestDTO createValidCreateRequest() { + MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); + metadataRequest.setName(defaultName); + metadataRequest.setGroup(defaultGroup); + metadataRequest.setScreenshot(defaultScreenshot); + metadataRequest.setUser(defaultUser); + + LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); + layoutRequest.setDefinition(defaultDefinition); + layoutRequest.setMetadata(metadataRequest); + return layoutRequest; + } } From 3d25ff86252091fc903a3bf9d130ae69531310f3 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 10 Oct 2023 18:49:54 +0100 Subject: [PATCH 31/69] VUU25: Remove unnecessary LayoutController unit tests --- .../controller/LayoutControllerTest.java | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 7acb32067..e2274a214 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -15,7 +15,6 @@ import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; -import org.finos.vuu.layoutserver.repository.LayoutRepository; import org.finos.vuu.layoutserver.service.LayoutService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,8 +30,6 @@ class LayoutControllerTest { @Mock private LayoutService layoutService; - @Mock - private LayoutRepository layoutRepository; // TODO Should modelmapper be mocked out? @Spy private ModelMapper modelMapper; @@ -97,9 +94,8 @@ void getLayout_validIdAndLayoutExists_returnsLayout() { @Test void getLayout_layoutDoesNotExist_throwsNotFoundAndReturns404() { when(layoutService.getLayout(doesNotExistLayoutId)).thenThrow(NoSuchElementException.class); - assertThrows(NoSuchElementException.class, () -> { - layoutController.getLayout(doesNotExistLayoutId); - }); + assertThrows(NoSuchElementException.class, + () -> layoutController.getLayout(doesNotExistLayoutId)); } @Test @@ -117,12 +113,8 @@ void getMetadata_noMetadataExists_returnsEmptyArray() { @Test void createLayout_validLayout_createsLayout() { when(layoutService.createLayout(any(Layout.class))).thenReturn(layout.getId()); - assertThat(layoutController.createLayout(layoutRequest)).isEqualTo(layout.getId()); - } - - // TODO I don't think this is a valid / worthwhile unit test? - @Test - void updateLayout_validLayout_doesNothing() { + when(layoutService.getLayout(layout.getId())).thenReturn(layout); + assertThat(layoutController.createLayout(layoutRequest).getId()).isEqualTo(layout.getId()); } @Test @@ -132,16 +124,6 @@ void updateLayout_layoutDoesNotExist_returnsInvalidRequest() { () -> layoutController.updateLayout(layout.getId(), layoutRequest)); } - // TODO I don't think this is a valid / worthwhile unit test? - @Test - void deleteLayout_validId_returnsSuccess() { - } - - // TODO I don't think this is a valid / worthwhile unit test? - @Test - void deleteLayout_layoutDoesNotExist_doesNothing() { - } - private MetadataResponseDTO getMetadataResponseDTO() { MetadataResponseDTO metadataResponse = new MetadataResponseDTO(); metadataResponse.setLayoutId(layout.getId()); From b9ec7caf0e1ffc74fb6aa0a4b797c71133f65896 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Tue, 10 Oct 2023 19:09:08 +0100 Subject: [PATCH 32/69] VUU25: Mock out model mapper in LayoutController tests --- .../controller/LayoutControllerTest.java | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index e2274a214..309de1ab7 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import java.util.ArrayList; @@ -11,6 +10,7 @@ import java.util.UUID; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.GetLayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; @@ -21,7 +21,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.modelmapper.ModelMapper; @@ -30,9 +29,10 @@ class LayoutControllerTest { @Mock private LayoutService layoutService; - // TODO Should modelmapper be mocked out? - @Spy + + @Mock private ModelMapper modelMapper; + @InjectMocks private LayoutController layoutController; @@ -88,6 +88,8 @@ public void setup() { @Test void getLayout_validIdAndLayoutExists_returnsLayout() { when(layoutService.getLayout(validLayoutId)).thenReturn(layout); + when(modelMapper.map(layout, GetLayoutResponseDTO.class)).thenReturn( + expectedLayoutResponse); assertThat(layoutController.getLayout(validLayoutId)).isEqualTo(expectedLayoutResponse); } @@ -101,6 +103,8 @@ void getLayout_layoutDoesNotExist_throwsNotFoundAndReturns404() { @Test void getMetadata_metadataExists_returnsMetadata() { when(layoutService.getMetadata()).thenReturn(List.of(metadata)); + when(modelMapper.map(metadata, MetadataResponseDTO.class)).thenReturn( + getMetadataResponseDTO()); assertThat(layoutController.getMetadata()).isEqualTo(expectedMetadataResponse); } @@ -112,9 +116,23 @@ void getMetadata_noMetadataExists_returnsEmptyArray() { @Test void createLayout_validLayout_createsLayout() { - when(layoutService.createLayout(any(Layout.class))).thenReturn(layout.getId()); + Layout layoutWithoutIds = layout; + layoutWithoutIds.setId(null); + layoutWithoutIds.getMetadata().setId(null); + + CreateLayoutResponseDTO expectedResponse = new CreateLayoutResponseDTO(); + expectedResponse.setId(layout.getId()); + expectedResponse.setCreated(layout.getMetadata().getCreated()); + + when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layoutWithoutIds); + when(layoutService.createLayout(layoutWithoutIds)).thenReturn(layout.getId()); when(layoutService.getLayout(layout.getId())).thenReturn(layout); - assertThat(layoutController.createLayout(layoutRequest).getId()).isEqualTo(layout.getId()); + when(modelMapper.map(layout, CreateLayoutResponseDTO.class)).thenReturn(expectedResponse); + + assertThat(layoutController.createLayout(layoutRequest).getId()) + .isEqualTo(layout.getId()); + assertThat(layoutController.createLayout(layoutRequest).getCreated()) + .isEqualTo(layout.getMetadata().getCreated()); } @Test From bee7f43cf0a01babcebd9af99937307f3a7e02b4 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 11 Oct 2023 11:18:38 +0100 Subject: [PATCH 33/69] VUU25: Remove TODO --- .../vuu/layoutserver/integration/LayoutIntegrationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index d3725e5f7..73185d84a 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -194,7 +194,6 @@ void updateLayout_invalidRequestBodyUnexpectedFormat_returns400AndLayoutDoesNotC assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); } - // TODO Update layout, invalid ID, returns 400 @Test void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exception { UUID layoutID = UUID.randomUUID(); From 6629880471319bf2abcf0ca11d2d973f77500d7f Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 11 Oct 2023 11:29:53 +0100 Subject: [PATCH 34/69] VUU25: Move 'updated date' logic from DTO to service for update layout requests --- .../vuu/layoutserver/dto/request/MetadataRequestDTO.java | 6 ------ .../org/finos/vuu/layoutserver/service/LayoutService.java | 7 ++++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java index 26c31ca83..35137c4d7 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java @@ -1,9 +1,6 @@ package org.finos.vuu.layoutserver.dto.request; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; - -import java.util.Date; import org.finos.vuu.layoutserver.dto.MetadataDTO; @Data @@ -13,7 +10,4 @@ public class MetadataRequestDTO implements MetadataDTO { private String group; private String screenshot; private String user; - - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private Date updated = new Date(); } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index 5aed4a625..3ee2f12d7 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -1,5 +1,8 @@ package org.finos.vuu.layoutserver.service; +import java.util.Date; +import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -7,9 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.UUID; - @RequiredArgsConstructor @Service public class LayoutService { @@ -34,6 +34,7 @@ public UUID createLayout(Layout layout) { } public void updateLayout(Layout updatedLayout) { + updatedLayout.getMetadata().setUpdated(new Date()); layoutRepository.save(updatedLayout); } From 90545b0e60677fdf1c6834154f5f807cec0983df Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 11 Oct 2023 11:39:15 +0100 Subject: [PATCH 35/69] VUU25: Move updateLayout logic from mapper in controller to setters in service --- .../layoutserver/controller/LayoutController.java | 12 ++++-------- .../vuu/layoutserver/service/LayoutService.java | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index f6a557bbe..abd8dc88a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -74,18 +74,14 @@ public CreateLayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layout * Updates the specified layout * * @param id ID of the layout to update - * @param newLayout the new layout + * @param layout the new layout */ @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") - public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO newLayout) { - // TODO I'm not sure about this, I think layoutService should be doing more logic, not - // the mapper here - Layout layout = layoutService.getLayout(id); + public void updateLayout(@PathVariable UUID id, @RequestBody LayoutRequestDTO layout) { + Layout newLayout = mapper.map(layout, Layout.class); - mapper.map(newLayout, layout); - - layoutService.updateLayout(layout); + layoutService.updateLayout(id, newLayout); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index 3ee2f12d7..6f77417a5 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -33,9 +33,18 @@ public UUID createLayout(Layout layout) { return layoutRepository.save(layout).getId(); } - public void updateLayout(Layout updatedLayout) { - updatedLayout.getMetadata().setUpdated(new Date()); - layoutRepository.save(updatedLayout); + public void updateLayout(UUID layoutId, Layout newLayout) { + Layout layoutToUpdate = getLayout(layoutId); + layoutToUpdate.setDefinition(newLayout.getDefinition()); + + Metadata metadataToUpdate = layoutToUpdate.getMetadata(); + metadataToUpdate.setName(newLayout.getMetadata().getName()); + metadataToUpdate.setGroup(newLayout.getMetadata().getGroup()); + metadataToUpdate.setScreenshot(newLayout.getMetadata().getScreenshot()); + metadataToUpdate.setUser(newLayout.getMetadata().getUser()); + metadataToUpdate.setUpdated(new Date()); + + layoutRepository.save(layoutToUpdate); } public void deleteLayout(UUID id) { From 6f97b5d678fff71e7b2a6c7e34b08370684cdc30 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 11 Oct 2023 11:56:07 +0100 Subject: [PATCH 36/69] VUU25: Fix tests for recent changes in layout server - Update and delete layout in LayoutControllerTest and LayoutServiceTest --- .../controller/LayoutControllerTest.java | 22 +++++++++++++++---- .../service/LayoutServiceTest.java | 17 +++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 309de1ab7..2e69498ad 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.ArrayList; @@ -136,10 +137,23 @@ void createLayout_validLayout_createsLayout() { } @Test - void updateLayout_layoutDoesNotExist_returnsInvalidRequest() { - when(layoutService.getLayout(layout.getId())).thenThrow(NoSuchElementException.class); - assertThrows(NoSuchElementException.class, - () -> layoutController.updateLayout(layout.getId(), layoutRequest)); + void updateLayout_callsLayoutService() { + layout.setId(null); + layout.getMetadata().setId(null); + + when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layout); + + layoutController.updateLayout(validLayoutId, layoutRequest); + + verify(layoutService).updateLayout(validLayoutId, layout); + } + + @Test + void deleteLayout_callsLayoutService() { + layoutController.deleteLayout(validLayoutId); + + verify(layoutService).getLayout(validLayoutId); + verify(layoutService).deleteLayout(validLayoutId); } private MetadataResponseDTO getMetadataResponseDTO() { diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java index 734c7efe9..0edcc1086 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java @@ -2,9 +2,11 @@ import java.util.Date; import java.util.List; +import java.util.NoSuchElementException; import org.finos.vuu.layoutserver.model.Metadata; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -78,14 +80,23 @@ void createLayout() { } @Test - void updateLayout_returnsNothing() { - layoutService.updateLayout(layout); + void updateLayout_layoutExists_callsRepository() { + when(layoutRepository.findById(layoutId)).thenReturn(Optional.of(layout)); + + layoutService.updateLayout(layoutId, layout); verify(layoutRepository, times(1)).save(layout); } @Test - void deleteLayout_returnsNothing() { + void updateLayout_layoutDoesNotExist_throwsNoSuchElementException() { + when(layoutRepository.findById(layoutId)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, () -> layoutService.updateLayout(layoutId, layout)); + } + + @Test + void deleteLayout_callsRepository() { layoutService.deleteLayout(layoutId); verify(layoutRepository, times(1)).deleteById(layoutId); From 6116b28af6ed884f96db81485613c556b2f3c318 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 11 Oct 2023 12:06:30 +0100 Subject: [PATCH 37/69] VUU25: Remove unnecessary autogenerated content - maven wrapper files - gitignore --- layout-server/.gitignore | 33 -- layout-server/.mvn/wrapper/maven-wrapper.jar | Bin 62547 -> 0 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 - layout-server/mvnw | 308 ------------------ layout-server/mvnw.cmd | 205 ------------ 5 files changed, 548 deletions(-) delete mode 100644 layout-server/.gitignore delete mode 100644 layout-server/.mvn/wrapper/maven-wrapper.jar delete mode 100644 layout-server/.mvn/wrapper/maven-wrapper.properties delete mode 100644 layout-server/mvnw delete mode 100644 layout-server/mvnw.cmd diff --git a/layout-server/.gitignore b/layout-server/.gitignore deleted file mode 100644 index 549e00a2a..000000000 --- a/layout-server/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -HELP.md -target/ -!.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ diff --git a/layout-server/.mvn/wrapper/maven-wrapper.jar b/layout-server/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index cb28b0e37c7d206feb564310fdeec0927af4123a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62547 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 A?*IS* diff --git a/layout-server/.mvn/wrapper/maven-wrapper.properties b/layout-server/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 2e76e189d..000000000 --- a/layout-server/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/layout-server/mvnw b/layout-server/mvnw deleted file mode 100644 index 66df28542..000000000 --- a/layout-server/mvnw +++ /dev/null @@ -1,308 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.2.0 -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir -# -# Optional ENV vars -# ----------------- -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files -# ---------------------------------------------------------------------------- - -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "$(uname)" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME - else - JAVA_HOME="/Library/Java/Home"; export JAVA_HOME - fi - fi - ;; -esac - -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=$(java-config --jre-home) - fi -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --unix "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --unix "$CLASSPATH") -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && - JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="$(which javac)" - if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=$(which readlink) - if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then - if $darwin ; then - javaHome="$(dirname "\"$javaExecutable\"")" - javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" - else - javaExecutable="$(readlink -f "\"$javaExecutable\"")" - fi - javaHome="$(dirname "\"$javaExecutable\"")" - javaHome=$(expr "$javaHome" : '\(.*\)/bin') - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - else - JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 - fi - - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=$(cd "$wdir/.." || exit 1; pwd) - fi - # end of workaround - done - printf '%s' "$(cd "$basedir" || exit 1; pwd)" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - # Remove \r in case we run on Windows within Git Bash - # and check out the repository with auto CRLF management - # enabled. Otherwise, we may read lines that are delimited with - # \r\n and produce $'-Xarg\r' rather than -Xarg due to word - # splitting rules. - tr -s '\r\n' ' ' < "$1" - fi -} - -log() { - if [ "$MVNW_VERBOSE" = true ]; then - printf '%s\n' "$1" - fi -} - -BASE_DIR=$(find_maven_basedir "$(dirname "$0")") -if [ -z "$BASE_DIR" ]; then - exit 1; -fi - -MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR -log "$MAVEN_PROJECTBASEDIR" - -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" -if [ -r "$wrapperJarPath" ]; then - log "Found $wrapperJarPath" -else - log "Couldn't find $wrapperJarPath, downloading it ..." - - if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - fi - while IFS="=" read -r key value; do - # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) - safeValue=$(echo "$value" | tr -d '\r') - case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; - esac - done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" - log "Downloading from: $wrapperUrl" - - if $cygwin; then - wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") - fi - - if command -v wget > /dev/null; then - log "Found wget ... using wget" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - log "Found curl ... using curl" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - else - curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - fi - else - log "Falling back to using Java to download" - javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" - javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaSource=$(cygpath --path --windows "$javaSource") - javaClass=$(cygpath --path --windows "$javaClass") - fi - if [ -e "$javaSource" ]; then - if [ ! -e "$javaClass" ]; then - log " - Compiling MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/javac" "$javaSource") - fi - if [ -e "$javaClass" ]; then - log " - Running MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" - fi - fi - fi -fi -########################################################################################## -# End of extension -########################################################################################## - -# If specified, validate the SHA-256 sum of the Maven wrapper jar file -wrapperSha256Sum="" -while IFS="=" read -r key value; do - case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; - esac -done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" -if [ -n "$wrapperSha256Sum" ]; then - wrapperSha256Result=false - if command -v sha256sum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then - wrapperSha256Result=true - fi - elif command -v shasum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then - wrapperSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." - echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." - exit 1 - fi - if [ $wrapperSha256Result = false ]; then - echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 - echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 - echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 - exit 1 - fi -fi - -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --windows "$CLASSPATH") - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") -fi - -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -# shellcheck disable=SC2086 # safe args -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/layout-server/mvnw.cmd b/layout-server/mvnw.cmd deleted file mode 100644 index 95ba6f54a..000000000 --- a/layout-server/mvnw.cmd +++ /dev/null @@ -1,205 +0,0 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM https://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.2.0 -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %WRAPPER_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file -SET WRAPPER_SHA_256_SUM="" -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B -) -IF NOT %WRAPPER_SHA_256_SUM%=="" ( - powershell -Command "&{"^ - "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ - "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ - " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ - " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ - " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ - " exit 1;"^ - "}"^ - "}" - if ERRORLEVEL 1 goto error -) - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% From 461310346c9eb8be663a25bf7b221b1ccd2c36b2 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Wed, 11 Oct 2023 12:45:31 +0100 Subject: [PATCH 38/69] VUU25: Make create layout (with a valid layout) integration test more robust --- .../src/main/resources/application.properties | 3 -- .../integration/LayoutIntegrationTest.java | 29 +++++++++++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index 2cf2389e5..afee88372 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -7,6 +7,3 @@ spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.h2.console.enabled=true -spring.jpa.hibernate.ddl-auto=update -spring.jpa.generate-ddl=true -spring.jpa.show-sql=true diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index 73185d84a..30e2786a3 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -10,6 +10,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; import java.util.UUID; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; @@ -25,6 +26,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.annotation.Transactional; @SpringBootTest @@ -102,15 +104,36 @@ void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { } @Test - void createLayout_validLayout_returnsLayoutCreatedWithIDAndCreatedDate() throws Exception { + void createLayout_validLayout_returnsLayoutCreatedWithIDAndCreatedDateAndLayoutIsCreated() + throws Exception { LayoutRequestDTO layoutRequest = createValidCreateRequest(); - mockMvc.perform(post("/layouts") + MvcResult result = mockMvc.perform(post("/layouts") .content(objectMapper.writeValueAsString(layoutRequest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").isNotEmpty()) - .andExpect(jsonPath("$.created").isNotEmpty()); + .andExpect(jsonPath("$.created").isNotEmpty()) + .andReturn(); + + UUID createdLayoutId = UUID.fromString(JsonPath.read(result.getResponse().getContentAsString(), "$.id")); + Layout createdLayout = layoutRepository.findById(createdLayoutId).orElseThrow(); + Metadata createdMetadata = metadataRepository.findById(createdLayout.getMetadata().getId()).orElseThrow(); + + // Check that the one-to-one relationship isn't causing duplicate/unexpected entries in the DB + assertThat(layoutRepository.findAll()).containsExactly(createdLayout); + assertThat(metadataRepository.findAll()).containsExactly(createdMetadata); + + assertThat(createdLayout.getDefinition()) + .isEqualTo(layoutRequest.getDefinition()); + assertThat(createdMetadata.getName()) + .isEqualTo(layoutRequest.getMetadata().getName()); + assertThat(createdMetadata.getGroup()) + .isEqualTo(layoutRequest.getMetadata().getGroup()); + assertThat(createdMetadata.getScreenshot()) + .isEqualTo(layoutRequest.getMetadata().getScreenshot()); + assertThat(createdMetadata.getUser()) + .isEqualTo(layoutRequest.getMetadata().getUser()); } From 6c0de9aac12db0a245e6176ff7aeeaaec41a6c94 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Mon, 16 Oct 2023 10:56:17 +0100 Subject: [PATCH 39/69] VUU25: Increase max length of screenshot column --- .../org/finos/vuu/layoutserver/model/Metadata.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 16c1fc61b..c94d90a97 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -1,5 +1,8 @@ package org.finos.vuu.layoutserver.model; +import java.util.Date; +import java.util.UUID; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -7,9 +10,6 @@ import javax.persistence.OneToOne; import lombok.Data; -import java.util.Date; -import java.util.UUID; - @Data @Entity public class Metadata { @@ -22,9 +22,15 @@ public class Metadata { private Layout layout; private String name; + private String group; + + @Column(length = 16384) private String screenshot; + private String user; + private Date created = new Date(); + private Date updated; } From ae2d2cc2d1193c7fece1847655a8b22e8c462e09 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Mon, 16 Oct 2023 12:20:19 +0100 Subject: [PATCH 40/69] VUU25: Change create layout endpoint to return whole layout object --- .../vuu/layoutserver/config/MappingConfig.java | 11 ++--------- .../controller/LayoutController.java | 7 +++---- .../dto/response/CreateLayoutResponseDTO.java | 17 ----------------- 3 files changed, 5 insertions(+), 30 deletions(-) delete mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/CreateLayoutResponseDTO.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java index 0e08053b4..66311db40 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java @@ -2,7 +2,6 @@ import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; -import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -17,19 +16,13 @@ public class MappingConfig { public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); - // LayoutRequestDTO to Layout + // Layout mapper.typeMap(LayoutRequestDTO.class, Layout.class).addMappings(m -> m.skip(Layout::setId)); - // Layout to CreateLayoutResponseDTO - mapper.typeMap(Layout.class, CreateLayoutResponseDTO.class) - .addMappings(m -> m.map(layout -> layout.getMetadata().getCreated(), - CreateLayoutResponseDTO::setCreated)); - - // Metadata to MetadataResponseDTO + // Metadata mapper.typeMap(Metadata.class, MetadataResponseDTO.class) .addMappings(m -> m.map(metadata -> metadata.getLayout().getId(), MetadataResponseDTO::setLayoutId)); - // MetadataRequestDTO to Metadata mapper.typeMap(MetadataRequestDTO.class, Metadata.class).addMappings(m -> m.skip(Metadata::setId)); return mapper; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index abd8dc88a..0576af68a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -4,7 +4,6 @@ import java.util.UUID; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; @@ -58,16 +57,16 @@ public List getMetadata() { * Creates a new layout * * @param layoutToCreate the layout to be created - * @return the generated ID of the new layout + * @return the layout that has been created */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public CreateLayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { + public LayoutResponseDTO createLayout(@RequestBody LayoutRequestDTO layoutToCreate) { Layout layout = mapper.map(layoutToCreate, Layout.class); Layout createdLayout = layoutService.getLayout(layoutService.createLayout(layout)); - return mapper.map(createdLayout, CreateLayoutResponseDTO.class); + return mapper.map(createdLayout, LayoutResponseDTO.class); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/CreateLayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/CreateLayoutResponseDTO.java deleted file mode 100644 index 8ff792674..000000000 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/CreateLayoutResponseDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.finos.vuu.layoutserver.dto.response; - -import lombok.Data; - -import java.util.Date; -import java.util.UUID; - -@Data -public class CreateLayoutResponseDTO { - - private UUID id; - - /** - * The generated creation date of the created layout - */ - private Date created; -} From 4d463716c85061da10b1879730f7d9d05f960ead Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Mon, 16 Oct 2023 12:39:35 +0100 Subject: [PATCH 41/69] VUU25: Fix tests for new create layout response --- .../controller/LayoutController.java | 6 ++--- ...esponseDTO.java => LayoutResponseDTO.java} | 2 +- .../controller/LayoutControllerTest.java | 23 +++++++------------ .../integration/LayoutIntegrationTest.java | 14 +++++++---- 4 files changed, 21 insertions(+), 24 deletions(-) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/{GetLayoutResponseDTO.java => LayoutResponseDTO.java} (89%) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index d74f95c24..aeaa409da 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -5,7 +5,7 @@ import javax.validation.Valid; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.response.GetLayoutResponseDTO; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.service.LayoutService; @@ -38,8 +38,8 @@ public class LayoutController { * @return the layout */ @GetMapping("/{id}") - public GetLayoutResponseDTO getLayout(@PathVariable UUID id) { - return mapper.map(layoutService.getLayout(id), GetLayoutResponseDTO.class); + public LayoutResponseDTO getLayout(@PathVariable UUID id) { + return mapper.map(layoutService.getLayout(id), LayoutResponseDTO.class); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/GetLayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java similarity index 89% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/GetLayoutResponseDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java index e07d77565..9e1077063 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/GetLayoutResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java @@ -5,7 +5,7 @@ import java.util.UUID; @Data -public class GetLayoutResponseDTO { +public class LayoutResponseDTO { private UUID id; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 2e69498ad..14fd1dd24 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -11,8 +11,7 @@ import java.util.UUID; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; -import org.finos.vuu.layoutserver.dto.response.CreateLayoutResponseDTO; -import org.finos.vuu.layoutserver.dto.response.GetLayoutResponseDTO; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; @@ -42,7 +41,7 @@ class LayoutControllerTest { private Layout layout; private Metadata metadata; private LayoutRequestDTO layoutRequest; - private GetLayoutResponseDTO expectedLayoutResponse; + private LayoutResponseDTO expectedLayoutResponse; private List expectedMetadataResponse; @BeforeEach @@ -74,7 +73,7 @@ public void setup() { layoutRequest.setDefinition(layout.getDefinition()); layoutRequest.setMetadata(metadataRequestDTO); - expectedLayoutResponse = new GetLayoutResponseDTO(); + expectedLayoutResponse = new LayoutResponseDTO(); expectedLayoutResponse.setId(layout.getId()); expectedLayoutResponse.setDefinition(layout.getDefinition()); @@ -89,7 +88,7 @@ public void setup() { @Test void getLayout_validIdAndLayoutExists_returnsLayout() { when(layoutService.getLayout(validLayoutId)).thenReturn(layout); - when(modelMapper.map(layout, GetLayoutResponseDTO.class)).thenReturn( + when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn( expectedLayoutResponse); assertThat(layoutController.getLayout(validLayoutId)).isEqualTo(expectedLayoutResponse); } @@ -116,24 +115,18 @@ void getMetadata_noMetadataExists_returnsEmptyArray() { } @Test - void createLayout_validLayout_createsLayout() { + void createLayout_validLayout_returnsCreatedLayout() { Layout layoutWithoutIds = layout; layoutWithoutIds.setId(null); layoutWithoutIds.getMetadata().setId(null); - CreateLayoutResponseDTO expectedResponse = new CreateLayoutResponseDTO(); - expectedResponse.setId(layout.getId()); - expectedResponse.setCreated(layout.getMetadata().getCreated()); - when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layoutWithoutIds); when(layoutService.createLayout(layoutWithoutIds)).thenReturn(layout.getId()); when(layoutService.getLayout(layout.getId())).thenReturn(layout); - when(modelMapper.map(layout, CreateLayoutResponseDTO.class)).thenReturn(expectedResponse); + when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn(expectedLayoutResponse); - assertThat(layoutController.createLayout(layoutRequest).getId()) - .isEqualTo(layout.getId()); - assertThat(layoutController.createLayout(layoutRequest).getCreated()) - .isEqualTo(layout.getMetadata().getCreated()); + assertThat(layoutController.createLayout(layoutRequest)) + .isEqualTo(expectedLayoutResponse); } @Test diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index 30e2786a3..c9bd1ae8e 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -104,16 +104,20 @@ void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { } @Test - void createLayout_validLayout_returnsLayoutCreatedWithIDAndCreatedDateAndLayoutIsCreated() + void createLayout_validLayout_returnsCreatedLayoutAndLayoutIsPersisted() throws Exception { - LayoutRequestDTO layoutRequest = createValidCreateRequest(); + LayoutRequestDTO layoutRequest = createValidCreateLayoutRequest(); MvcResult result = mockMvc.perform(post("/layouts") .content(objectMapper.writeValueAsString(layoutRequest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").isNotEmpty()) - .andExpect(jsonPath("$.created").isNotEmpty()) + .andExpect(jsonPath("$.definition", is(layoutRequest.getDefinition()))) + .andExpect(jsonPath("$.metadata.name", is(layoutRequest.getMetadata().getName()))) + .andExpect(jsonPath("$.metadata.group", is(layoutRequest.getMetadata().getGroup()))) + .andExpect(jsonPath("$.metadata.screenshot", is(layoutRequest.getMetadata().getScreenshot()))) + .andExpect(jsonPath("$.metadata.user", is(layoutRequest.getMetadata().getUser()))) .andReturn(); UUID createdLayoutId = UUID.fromString(JsonPath.read(result.getResponse().getContentAsString(), "$.id")); @@ -150,7 +154,7 @@ void createLayout_invalidLayout_returns400() throws Exception { @Test void createLayout_validLayoutButInvalidMetadata_returns400AndDoesNotCreateLayout() throws Exception { - LayoutRequestDTO layoutRequest = createValidCreateRequest(); + LayoutRequestDTO layoutRequest = createValidCreateLayoutRequest(); layoutRequest.setMetadata(null); mockMvc.perform(post("/layouts") @@ -299,7 +303,7 @@ private LayoutRequestDTO createValidUpdateRequest() { return layoutRequest; } - private LayoutRequestDTO createValidCreateRequest() { + private LayoutRequestDTO createValidCreateLayoutRequest() { MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); metadataRequest.setName(defaultName); metadataRequest.setGroup(defaultGroup); From 8a6d60a12595af9ea228cbec8dfc8e55d5884716 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Mon, 16 Oct 2023 12:44:33 +0100 Subject: [PATCH 42/69] VUU25: Simplify LayoutControllerTest test name - `getLayout_validIdAndLayoutExists_returnsLayout` -> `getLayout_layoutExists_returnsLayout` --- .../finos/vuu/layoutserver/controller/LayoutControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 14fd1dd24..7c9dd764f 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -86,7 +86,7 @@ public void setup() { @Test - void getLayout_validIdAndLayoutExists_returnsLayout() { + void getLayout_layoutExists_returnsLayout() { when(layoutService.getLayout(validLayoutId)).thenReturn(layout); when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn( expectedLayoutResponse); From d0ca229223e56e469611aa655cd3e95af0bd6a11 Mon Sep 17 00:00:00 2001 From: cfisher-scottlogic Date: Mon, 16 Oct 2023 17:15:26 +0100 Subject: [PATCH 43/69] VUU25: Fix tests for changed unidirectional Layout<->Metadata relationship --- .../org/finos/vuu/layoutserver/config/MappingConfig.java | 7 ------- .../main/java/org/finos/vuu/layoutserver/model/Layout.java | 2 ++ .../java/org/finos/vuu/layoutserver/model/Metadata.java | 7 +------ .../org/finos/vuu/layoutserver/service/LayoutService.java | 5 ----- .../finos/vuu/layoutserver/service/MetadataService.java | 4 ---- .../vuu/layoutserver/controller/LayoutControllerTest.java | 1 - .../layoutserver/integration/LayoutIntegrationTest.java | 1 - .../finos/vuu/layoutserver/service/LayoutServiceTest.java | 2 -- 8 files changed, 3 insertions(+), 26 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java index 1809371d1..fad55680c 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java @@ -2,7 +2,6 @@ import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.modelmapper.ModelMapper; @@ -16,15 +15,9 @@ public class MappingConfig { public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); - // Layout mapper.typeMap(LayoutRequestDTO.class, Layout.class) .addMappings(m -> m.skip(Layout::setId)); - // Metadata - mapper.typeMap(Metadata.class, MetadataResponseDTO.class) - .addMappings(m -> m.map(metadata -> metadata.getLayout().getId(), - MetadataResponseDTO::setLayoutId)); - mapper.typeMap(MetadataRequestDTO.class, Metadata.class) .addMappings(m -> m.skip(Metadata::setId)); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java index 0178e82b8..6251cbf2a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Layout.java @@ -2,6 +2,7 @@ import java.util.UUID; import javax.persistence.CascadeType; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -16,6 +17,7 @@ public class Layout { @Id @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "BINARY(16)") private UUID id; private String definition; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 8d5d9cd9e..7e81b9030 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -5,12 +5,10 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; -import javax.persistence.OneToOne; import java.util.Date; import java.util.UUID; import lombok.Data; -import lombok.ToString; @Data @Entity @@ -18,12 +16,9 @@ public class Metadata { @Id @GeneratedValue(strategy = GenerationType.AUTO) + @Column(columnDefinition = "BINARY(16)") private UUID id; - @OneToOne(mappedBy = "metadata") - @ToString.Exclude - private Layout layout; - private String name; private String group; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index 127333aed..42bb4d3f3 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -9,7 +9,6 @@ import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service @@ -27,11 +26,7 @@ public List getMetadata() { return metadataService.getMetadata(); } - @Transactional public UUID createLayout(Layout layout) { - Metadata metadata = metadataService.createMetadata(layout.getMetadata()); - metadata.setLayout(layout); - layout.setMetadata(metadata); return layoutRepository.save(layout).getId(); } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java index fb9696ac5..de3eb095e 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java @@ -20,8 +20,4 @@ public List getMetadata() { return metadata; } - - public Metadata createMetadata(Metadata metadata) { - return metadataRepository.save(metadata); - } } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 7c9dd764f..03a9cf640 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -62,7 +62,6 @@ public void setup() { layout.setId(validLayoutId); layout.setDefinition(layoutDefinition); layout.setMetadata(metadata); - metadata.setLayout(layout); layoutRequest = new LayoutRequestDTO(); MetadataRequestDTO metadataRequestDTO = new MetadataRequestDTO(); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index c9bd1ae8e..d0297ad5f 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -275,7 +275,6 @@ private Layout createDefaultLayoutInDatabase() { layout.setDefinition(defaultDefinition); layout.setMetadata(metadata); - metadata.setLayout(layout); metadata.setName(defaultName); metadata.setGroup(defaultGroup); metadata.setScreenshot(defaultScreenshot); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java index 0edcc1086..079347059 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java @@ -48,7 +48,6 @@ public void setup() { layout.setDefinition(""); layout.setMetadata(metadata); metadata.setId(metadataId); - metadata.setLayout(layout); metadata.setName(""); metadata.setGroup(""); metadata.setScreenshot(""); @@ -73,7 +72,6 @@ void getMetadata_returnsMetadata() { @Test void createLayout() { - when(metadataService.createMetadata(metadata)).thenReturn(metadata); when(layoutRepository.save(layout)).thenReturn(layout); assertThat(layoutService.createLayout(layout)).isEqualTo(layoutId); From e097743401338eef686eb1a873d69ac4991c99f1 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Fri, 13 Oct 2023 15:03:53 +0100 Subject: [PATCH 44/69] VUU-70: Create resource for application layouts --- .../ApplicationLayoutController.java | 71 +++++++++ .../dto/ApplicationLayoutDto.java | 25 +++ .../layoutserver/model/ApplicationLayout.java | 31 ++++ .../ApplicationLayoutRepository.java | 9 ++ .../service/ApplicationLayoutService.java | 82 ++++++++++ .../src/main/resources/defaultLayout.json | 22 +++ .../service/ApplicationLayoutServiceTest.java | 145 ++++++++++++++++++ 7 files changed, 385 insertions(+) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java create mode 100644 layout-server/src/main/resources/defaultLayout.json create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java new file mode 100644 index 000000000..540234630 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -0,0 +1,71 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/application-layouts") +@Validated +public class ApplicationLayoutController { + + private final ApplicationLayoutService service; + + /** + * Gets the application layout for the requesting user. Returns a default layout if none exists. + * + * @return the application layout + */ + @GetMapping + public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String username) { + return service.getApplicationLayout(username); + } + + /** + * Creates a new application layout for the requesting user. + * + * @param layoutDefinition JSON representation of the application layout to be created + * @param username the user making the request + */ + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public void createLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + service.createApplicationLayout(username, layoutDefinition); + } + + /** + * Updates the application layout for the requesting user. + * + * @param layoutDefinition JSON representation of the application layout to be created + * @param username the user making the request + */ + @ResponseStatus(HttpStatus.NO_CONTENT) + @PutMapping + public void updateLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + service.updateApplicationLayout(username, layoutDefinition); + } + + /** + * Deletes the application layout for the requesting user. + * + * @param username the user making the request + */ + @ResponseStatus(HttpStatus.NO_CONTENT) + @DeleteMapping + public void deleteLayout(@RequestHeader("user") String username) { + service.deleteApplicationLayout(username); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java new file mode 100644 index 000000000..52e9455f1 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java @@ -0,0 +1,25 @@ +package org.finos.vuu.layoutserver.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.model.ApplicationLayout; + +@Data +@RequiredArgsConstructor +@AllArgsConstructor +@Builder +public class ApplicationLayoutDto { + private String user; + private JsonNode definition; + + public static ApplicationLayoutDto fromEntity(ApplicationLayout entity) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode definition = objectMapper.readTree(entity.extractDefinition()); + return new ApplicationLayoutDto(entity.getUsername(), definition); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java new file mode 100644 index 000000000..192680359 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java @@ -0,0 +1,31 @@ +package org.finos.vuu.layoutserver.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Data +@Entity +@RequiredArgsConstructor +@AllArgsConstructor +public class ApplicationLayout { + @Id + private String username; + + @Column(columnDefinition = "JSON") + private String definition; + + public String extractDefinition() { + String extractedDefinition = definition; + + if (extractedDefinition.startsWith("\"") && extractedDefinition.endsWith("\"")) { + extractedDefinition = extractedDefinition.substring(1, extractedDefinition.length() - 1); + } + + return extractedDefinition.replaceAll("\\\\", ""); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java new file mode 100644 index 000000000..c553e7751 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/ApplicationLayoutRepository.java @@ -0,0 +1,9 @@ +package org.finos.vuu.layoutserver.repository; + +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ApplicationLayoutRepository extends CrudRepository { +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java new file mode 100644 index 000000000..f20833a32 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -0,0 +1,82 @@ +package org.finos.vuu.layoutserver.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class ApplicationLayoutService { + + private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); + private static ApplicationLayoutDto defaultLayout; + private final ApplicationLayoutRepository repository; + + public void createApplicationLayout(String username, JsonNode layoutDefinition) { + repository.save(new ApplicationLayout(username, layoutDefinition.toString())); + } + + public ApplicationLayoutDto getApplicationLayout(String username) { + Optional layout = repository.findById(username); + + if (layout.isEmpty()) { + logger.info("No application layout for user, returning default"); + return getDefaultLayout(); + } + + try { + return ApplicationLayoutDto.fromEntity(layout.get()); + } catch (JsonProcessingException e) { + logger.warn("Failed to read user's application layout, returning default"); + return getDefaultLayout(); + } + } + + public void updateApplicationLayout(String username, JsonNode layoutDefinition) { + createApplicationLayout(username, layoutDefinition); + } + + public void deleteApplicationLayout(String username) { + try { + repository.deleteById(username); + } catch (EmptyResultDataAccessException e) { + throw new NoSuchElementException("No layout found for user: " + username); + } + } + + private ApplicationLayoutDto getDefaultLayout() { + if (defaultLayout == null) { + loadDefaultLayout(); + } + return defaultLayout; + } + + private void loadDefaultLayout() { + JsonNode definition = loadJsonFile(); + defaultLayout = ApplicationLayoutDto.builder().definition(definition).build(); + } + + private JsonNode loadJsonFile() { + ObjectMapper objectMapper = new ObjectMapper(); + try { + ClassPathResource resource = new ClassPathResource("defaultLayout.json"); + return objectMapper.readTree(resource.getInputStream()); + } catch (IOException e) { + logger.warn("Failed to read default application layout, returning empty node"); + return objectMapper.createObjectNode(); + } + } +} diff --git a/layout-server/src/main/resources/defaultLayout.json b/layout-server/src/main/resources/defaultLayout.json new file mode 100644 index 000000000..871b11b44 --- /dev/null +++ b/layout-server/src/main/resources/defaultLayout.json @@ -0,0 +1,22 @@ +{ + "id": "main-tabs", + "type": "Stack", + "props": { + "className": "vuuShell-mainTabs", + "TabstripProps": { + "allowAddTab": true, + "allowRenameTab": true, + "animateSelectionThumb": false, + "className": "vuuShellMainTabstrip", + "location": "main-tab" + }, + "preserve": true, + "active": 0 + }, + "children": [ + { + "type": "Placeholder", + "title": "Page 1" + } + ] +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java new file mode 100644 index 000000000..b3fe52f5a --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -0,0 +1,145 @@ +package org.finos.vuu.layoutserver.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.dao.EmptyResultDataAccessException; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ApplicationLayoutServiceTest { + + private static ApplicationLayoutRepository mockRepo; + private static ApplicationLayoutService service; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final String defaultLayout = + "{" + + " \"id\": \"main-tabs\"," + + " \"type\": \"Stack\"," + + " \"props\": {" + + " \"className\": \"vuuShell-mainTabs\"," + + " \"TabstripProps\": {" + + " \"allowAddTab\": true," + + " \"allowRenameTab\": true," + + " \"animateSelectionThumb\": false," + + " \"className\": \"vuuShellMainTabstrip\"," + + " \"location\": \"main-tab\"" + + " }," + + " \"preserve\": true," + + " \"active\": 0" + + " }," + + " \"children\": [" + + " {" + + " \"type\": \"Placeholder\"," + + " \"title\": \"Page 1\"" + + " }" + + " ]" + + "}"; + + @BeforeEach + public void setup() { + mockRepo = Mockito.mock(ApplicationLayoutRepository.class); + service = new ApplicationLayoutService(mockRepo); + } + + @Test + public void getApplicationLayout_noLayout_returnsDefault() { + when(mockRepo.findById(anyString())).thenReturn(Optional.empty()); + + ApplicationLayoutDto actualLayout = service.getApplicationLayout("new user"); + + assertThat(actualLayout.getUser()).isNull(); + assertThat(actualLayout.getDefinition().toString()).isEqualToIgnoringWhitespace(defaultLayout); + } + + @Test + public void getApplicationLayout_layoutExists_returnsLayout() { + String expectedDefinition = "{\"id\":\"main-tabs\"}"; + String user = "user"; + ApplicationLayout expectedLayout = new ApplicationLayout(user, expectedDefinition); + + when(mockRepo.findById(user)).thenReturn(Optional.of(expectedLayout)); + + ApplicationLayoutDto actualLayout = service.getApplicationLayout(user); + + assertThat(actualLayout.getUser()).isEqualTo(user); + assertThat(actualLayout.getDefinition().toString()).isEqualToIgnoringWhitespace(expectedDefinition); + } + + @Test + public void createApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { + String definition = "{\"id\":\"main-tabs\"}"; + String user = "user"; + + service.createApplicationLayout(user, objectMapper.readTree(definition)); + + ApplicationLayout expectedLayout = new ApplicationLayout(user, definition); + verify(mockRepo, times(1)).save(expectedLayout); + } + + @Test + public void createApplicationLayout_invalidDefinition_throwsJsonException() { + String definition = "invalid JSON"; + + assertThrows(JsonProcessingException.class, () -> + service.createApplicationLayout("user", objectMapper.readTree(definition)) + ); + } + + @Test + public void updateApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { + String definition = "{\"id\":\"main-tabs\"}"; + String user = "user"; + + service.updateApplicationLayout(user, objectMapper.readTree(definition)); + + ApplicationLayout expectedLayout = new ApplicationLayout(user, definition); + verify(mockRepo, times(1)).save(expectedLayout); + } + + @Test + public void updateApplicationLayout_invalidDefinition_throwsJsonException() { + String definition = "invalid JSON"; + + assertThrows(JsonProcessingException.class, () -> + service.updateApplicationLayout("user", objectMapper.readTree(definition)) + ); + } + + @Test + public void deleteApplicationLayout_entryExists_callsRepoDelete() { + String user = "user"; + + service.deleteApplicationLayout(user); + + verify(mockRepo, times(1)).deleteById(user); + } + + @Test + public void deleteApplicationLayout_deleteFails_throwsException() { + String user = "user"; + + doThrow(EmptyResultDataAccessException.class).when(mockRepo).deleteById(user); + + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> + service.deleteApplicationLayout(user) + ); + + assertThat(exception.getMessage()).isEqualTo("No layout found for user: " + user); + } +} From b5e0476093ea6850ba023ab81fbf4e215838b1f7 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Mon, 16 Oct 2023 15:42:29 +0100 Subject: [PATCH 45/69] VUU-70: Add integration tests --- .../ApplicationLayoutController.java | 8 +- .../{ => response}/ApplicationLayoutDto.java | 2 +- .../service/ApplicationLayoutService.java | 2 +- .../ApplicationLayoutIntegrationTest.java | 184 ++++++++++++++++++ .../service/ApplicationLayoutServiceTest.java | 2 +- .../src/test/resources/defaultLayout.json | 3 + 6 files changed, 194 insertions(+), 7 deletions(-) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/{ => response}/ApplicationLayoutDto.java (94%) create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java create mode 100644 layout-server/src/test/resources/defaultLayout.json diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index 540234630..95d3d2637 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.service.ApplicationLayoutService; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; @@ -42,7 +42,7 @@ public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String u */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public void createLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + public void createApplicationLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { service.createApplicationLayout(username, layoutDefinition); } @@ -54,7 +54,7 @@ public void createLayout(@RequestHeader("user") String username, @RequestBody Js */ @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping - public void updateLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + public void updateApplicationLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { service.updateApplicationLayout(username, layoutDefinition); } @@ -65,7 +65,7 @@ public void updateLayout(@RequestHeader("user") String username, @RequestBody Js */ @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping - public void deleteLayout(@RequestHeader("user") String username) { + public void deleteApplicationLayout(@RequestHeader("user") String username) { service.deleteApplicationLayout(username); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java similarity index 94% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java index 52e9455f1..929781ee4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/ApplicationLayoutDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java @@ -1,4 +1,4 @@ -package org.finos.vuu.layoutserver.dto; +package org.finos.vuu.layoutserver.dto.response; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index f20833a32..d76e61806 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.slf4j.Logger; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java new file mode 100644 index 000000000..9143649cb --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -0,0 +1,184 @@ +package org.finos.vuu.layoutserver.integration; + +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +public class ApplicationLayoutIntegrationTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ApplicationLayoutRepository repository; + + @Test + public void getApplicationLayout_noLayoutExists_returns200WithDefaultLayout() throws Exception { + mockMvc.perform(get("/application-layouts").header("user", "new user")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.user", nullValue())) + // Expecting application layout as defined in /test/resources/defaultLayout.json + .andExpect(jsonPath("$.definition.defaultLayoutKey", is("default-layout-value"))); + } + + @Test + public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() throws Exception { + String user = "user"; + + Map definition = new HashMap<>(); + definition.put("defKey", "defVal"); + + persistApplicationLayout(user, definition); + + mockMvc.perform(get("/application-layouts").header("user", user)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.user", is(user))) + .andExpect(jsonPath("$.definition", is(definition))); + } + + @Test + public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() throws Exception { + String user = "user"; + String definition = "{\"key\":\"value\"}"; + + mockMvc.perform(post("/application-layouts") + .header("user", user) + .content(definition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$").doesNotExist()); + + ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); + + assertThat(persistedLayout.getUsername()).isEqualTo(user); + assertThat(persistedLayout.extractDefinition()).isEqualTo(definition); + } + + @Test + public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() throws Exception { + String user = "user"; + + Map initialDefinition = new HashMap<>(); + initialDefinition.put("initial-key", "initial-value"); + + persistApplicationLayout(user, initialDefinition); + + String newDefinition = "{\"new-key\":\"new-value\"}"; + + mockMvc.perform(post("/application-layouts") + .header("user", user) + .content(newDefinition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(1); + + ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); + + assertThat(retrievedLayout.getUsername()).isEqualTo(user); + assertThat(retrievedLayout.extractDefinition()).isEqualTo(newDefinition); + } + + @Test + public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() throws Exception { + String user = "user"; + String definition = "{\"key\":\"value\"}"; + + mockMvc.perform(put("/application-layouts") + .header("user", user) + .content(definition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); + + assertThat(persistedLayout.getUsername()).isEqualTo(user); + assertThat(persistedLayout.extractDefinition()).isEqualTo(definition); + } + + @Test + public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() throws Exception { + String user = "user"; + + Map initialDefinition = new HashMap<>(); + initialDefinition.put("initial-key", "initial-value"); + + persistApplicationLayout(user, initialDefinition); + + String newDefinition = "{\"new-key\":\"new-value\"}"; + + mockMvc.perform(put("/application-layouts") + .header("user", user) + .content(newDefinition) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(1); + + ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); + + assertThat(retrievedLayout.getUsername()).isEqualTo(user); + assertThat(retrievedLayout.extractDefinition()).isEqualTo(newDefinition); + } + + @Test + public void deleteApplicationLayout_noLayoutExists_returns404() throws Exception { + String user = "user"; + + String response = mockMvc.perform(delete("/application-layouts") + .header("user", user)) + .andExpect(status().isNotFound()) + .andReturn().getResponse().getContentAsString(); + + assertThat(response).isEqualTo("No layout found for user: " + user); + } + + @Test + public void deleteApplicationLayout_layoutExists_returns204AndDeletesLayout() throws Exception { + String user = "user"; + + Map initialDefinition = new HashMap<>(); + initialDefinition.put("initial-key", "initial-value"); + + persistApplicationLayout(user, initialDefinition); + + mockMvc.perform(delete("/application-layouts") + .header("user", user)) + .andExpect(status().isNoContent()) + .andExpect(jsonPath("$").doesNotExist()); + + assertThat(repository.findAll()).hasSize(0); + } + + private void persistApplicationLayout(String user, Map definition) { + StringBuilder defBuilder = new StringBuilder("{"); + definition.forEach((k, v) -> defBuilder.append("\"").append(k).append("\":\"").append(v).append("\"")); + defBuilder.append("}"); + + ApplicationLayout appLayout = new ApplicationLayout(user, defBuilder.toString()); + repository.save(appLayout); + } +} diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java index b3fe52f5a..3dbc39706 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.finos.vuu.layoutserver.dto.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.junit.jupiter.api.BeforeEach; diff --git a/layout-server/src/test/resources/defaultLayout.json b/layout-server/src/test/resources/defaultLayout.json new file mode 100644 index 000000000..87a79e544 --- /dev/null +++ b/layout-server/src/test/resources/defaultLayout.json @@ -0,0 +1,3 @@ +{ + "defaultLayoutKey": "default-layout-value" +} From 957148b12bfefa8ff4baf3757a2fed970675ee2c Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 17 Oct 2023 09:45:54 +0100 Subject: [PATCH 46/69] VUU-70: Add unit tests for controller --- .../ApplicationLayoutControllerTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java new file mode 100644 index 000000000..6c51e2f2c --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -0,0 +1,71 @@ +package org.finos.vuu.layoutserver.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ApplicationLayoutControllerTest { + private static ApplicationLayoutService mockService; + private static ApplicationLayoutController controller; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + public void setup() { + mockService = Mockito.mock(ApplicationLayoutService.class); + controller = new ApplicationLayoutController(mockService); + } + + @Test + public void getApplicationLayout_validUser_returnsLayoutFromService() throws JsonProcessingException { + String user = "user"; + + ApplicationLayoutDto expectedDto = ApplicationLayoutDto.builder() + .user(user) + .definition(objectMapper.readTree("{\"id\":\"main-tabs\"}")) + .build(); + + when(mockService.getApplicationLayout(user)).thenReturn(expectedDto); + + assertThat(controller.getApplicationLayout(user)).isEqualTo(expectedDto); + verify(mockService, times(1)).getApplicationLayout(user); + } + + @Test + public void createApplicationLayout_validUser_callsService() throws JsonProcessingException { + String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); + + controller.createApplicationLayout(user, definition); + + verify(mockService, times(1)).createApplicationLayout(user, definition); + } + + @Test + public void updateApplicationLayout_validUser_callsService() throws JsonProcessingException { + String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); + + controller.updateApplicationLayout(user, definition); + + verify(mockService, times(1)).updateApplicationLayout(user, definition); + } + + @Test + public void deleteApplicationLayout_validUser_callsService() { + String user = "user"; + + controller.deleteApplicationLayout(user); + + verify(mockService, times(1)).deleteApplicationLayout(user); + } +} From fe5c279aadf58e6c16582660bfb65313439ebdee Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 17 Oct 2023 11:09:30 +0100 Subject: [PATCH 47/69] VUU-70: Fix service test --- .../ApplicationLayoutController.java | 1 + .../service/ApplicationLayoutServiceTest.java | 31 +++---------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index 95d3d2637..b7095db61 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -29,6 +29,7 @@ public class ApplicationLayoutController { * * @return the application layout */ + @ResponseStatus(HttpStatus.OK) @GetMapping public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String username) { return service.getApplicationLayout(username); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java index 3dbc39706..8dd7d1626 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -1,6 +1,7 @@ package org.finos.vuu.layoutserver.service; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; @@ -27,30 +28,6 @@ class ApplicationLayoutServiceTest { private static ApplicationLayoutService service; private static final ObjectMapper objectMapper = new ObjectMapper(); - private static final String defaultLayout = - "{" + - " \"id\": \"main-tabs\"," + - " \"type\": \"Stack\"," + - " \"props\": {" + - " \"className\": \"vuuShell-mainTabs\"," + - " \"TabstripProps\": {" + - " \"allowAddTab\": true," + - " \"allowRenameTab\": true," + - " \"animateSelectionThumb\": false," + - " \"className\": \"vuuShellMainTabstrip\"," + - " \"location\": \"main-tab\"" + - " }," + - " \"preserve\": true," + - " \"active\": 0" + - " }," + - " \"children\": [" + - " {" + - " \"type\": \"Placeholder\"," + - " \"title\": \"Page 1\"" + - " }" + - " ]" + - "}"; - @BeforeEach public void setup() { mockRepo = Mockito.mock(ApplicationLayoutRepository.class); @@ -58,13 +35,15 @@ public void setup() { } @Test - public void getApplicationLayout_noLayout_returnsDefault() { + public void getApplicationLayout_noLayout_returnsDefault() throws JsonProcessingException { when(mockRepo.findById(anyString())).thenReturn(Optional.empty()); ApplicationLayoutDto actualLayout = service.getApplicationLayout("new user"); + // Expecting application layout as defined in /test/resources/defaultLayout.json + JsonNode expectedDefinition = objectMapper.readTree("{\"defaultLayoutKey\":\"default-layout-value\"}"); assertThat(actualLayout.getUser()).isNull(); - assertThat(actualLayout.getDefinition().toString()).isEqualToIgnoringWhitespace(defaultLayout); + assertThat(actualLayout.getDefinition()).isEqualTo(expectedDefinition); } @Test From fc8e069383b59b877865b5072364b9b7836e399a Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 17 Oct 2023 11:25:56 +0100 Subject: [PATCH 48/69] VUU-70: Extract constant for default layout file --- .../vuu/layoutserver/service/ApplicationLayoutService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index d76e61806..e6fdbc549 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -22,6 +22,7 @@ public class ApplicationLayoutService { private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); + private static final String DEFAULT_LAYOUT_FILE = "defaultLayout.json"; private static ApplicationLayoutDto defaultLayout; private final ApplicationLayoutRepository repository; @@ -72,7 +73,7 @@ private void loadDefaultLayout() { private JsonNode loadJsonFile() { ObjectMapper objectMapper = new ObjectMapper(); try { - ClassPathResource resource = new ClassPathResource("defaultLayout.json"); + ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); return objectMapper.readTree(resource.getInputStream()); } catch (IOException e) { logger.warn("Failed to read default application layout, returning empty node"); From 108b087e974ab0206dfff6e205e9b29cc09c486d Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 17 Oct 2023 11:26:13 +0100 Subject: [PATCH 49/69] VUU-70: Adjust whitespace --- .../ApplicationLayoutIntegrationTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index 9143649cb..2a983b156 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -59,7 +59,7 @@ public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() th @Test public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() throws Exception { String user = "user"; - String definition = "{\"key\":\"value\"}"; + String definition = "{\"key\": \"value\"}"; mockMvc.perform(post("/application-layouts") .header("user", user) @@ -71,7 +71,7 @@ public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.extractDefinition()).isEqualTo(definition); + assertThat(persistedLayout.extractDefinition()).isEqualToIgnoringWhitespace(definition); } @Test @@ -83,7 +83,7 @@ public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() persistApplicationLayout(user, initialDefinition); - String newDefinition = "{\"new-key\":\"new-value\"}"; + String newDefinition = "{\"new-key\": \"new-value\"}"; mockMvc.perform(post("/application-layouts") .header("user", user) @@ -97,13 +97,13 @@ public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.extractDefinition()).isEqualTo(newDefinition); + assertThat(retrievedLayout.extractDefinition()).isEqualToIgnoringWhitespace(newDefinition); } @Test public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() throws Exception { String user = "user"; - String definition = "{\"key\":\"value\"}"; + String definition = "{\"key\": \"value\"}"; mockMvc.perform(put("/application-layouts") .header("user", user) @@ -115,7 +115,7 @@ public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.extractDefinition()).isEqualTo(definition); + assertThat(persistedLayout.extractDefinition()).isEqualToIgnoringWhitespace(definition); } @Test @@ -127,7 +127,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() persistApplicationLayout(user, initialDefinition); - String newDefinition = "{\"new-key\":\"new-value\"}"; + String newDefinition = "{\"new-key\": \"new-value\"}"; mockMvc.perform(put("/application-layouts") .header("user", user) @@ -141,7 +141,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.extractDefinition()).isEqualTo(newDefinition); + assertThat(retrievedLayout.extractDefinition()).isEqualToIgnoringWhitespace(newDefinition); } @Test From fe796e64432e5491459b4604417e10e8c5a49fb9 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Wed, 18 Oct 2023 11:35:05 +0100 Subject: [PATCH 50/69] VUU-70: Use AttributeConverter for JsonNode --- .../config/JsonNodeConverter.java | 43 +++++++++++++++++++ .../ApplicationLayoutController.java | 4 +- .../dto/response/ApplicationLayoutDto.java | 11 +---- .../layoutserver/model/ApplicationLayout.java | 16 +++---- .../service/ApplicationLayoutService.java | 19 +++----- .../ApplicationLayoutControllerTest.java | 16 ++++--- .../ApplicationLayoutIntegrationTest.java | 22 +++++----- .../service/ApplicationLayoutServiceTest.java | 32 +++++++------- 8 files changed, 93 insertions(+), 70 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java new file mode 100644 index 000000000..728e7635a --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java @@ -0,0 +1,43 @@ +package org.finos.vuu.layoutserver.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.AttributeConverter; +import java.io.IOException; + +public class JsonNodeConverter implements AttributeConverter { + private static final Logger logger = LoggerFactory.getLogger(JsonNodeConverter.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(JsonNode definition) { + try { + return objectMapper.writeValueAsString(definition); + } catch (final JsonProcessingException e) { + logger.error("JSON writing error", e); + return null; + } + } + + @Override + public JsonNode convertToEntityAttribute(String definition) { + try { + return objectMapper.readValue(extractDefinition(definition), new TypeReference<>() {}); + } catch (final IOException e) { + logger.error("JSON reading error", e); + return null; + } + } + + private String extractDefinition(String definition) { + if (definition.startsWith("\"") && definition.endsWith("\"")) { + definition = definition.substring(1, definition.length() - 1); + } + return definition.replaceAll("\\\\", ""); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index b7095db61..dae53281d 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.service.ApplicationLayoutService; +import org.modelmapper.ModelMapper; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -23,6 +24,7 @@ public class ApplicationLayoutController { private final ApplicationLayoutService service; + private final ModelMapper mapper; /** * Gets the application layout for the requesting user. Returns a default layout if none exists. @@ -32,7 +34,7 @@ public class ApplicationLayoutController { @ResponseStatus(HttpStatus.OK) @GetMapping public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String username) { - return service.getApplicationLayout(username); + return mapper.map(service.getApplicationLayout(username), ApplicationLayoutDto.class); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java index 929781ee4..7d271959f 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java @@ -1,25 +1,16 @@ package org.finos.vuu.layoutserver.dto.response; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.model.ApplicationLayout; @Data @RequiredArgsConstructor @AllArgsConstructor @Builder public class ApplicationLayoutDto { - private String user; + private String username; private JsonNode definition; - - public static ApplicationLayoutDto fromEntity(ApplicationLayout entity) throws JsonProcessingException { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode definition = objectMapper.readTree(entity.extractDefinition()); - return new ApplicationLayoutDto(entity.getUsername(), definition); - } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java index 192680359..7c69de7a7 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java @@ -1,10 +1,13 @@ package org.finos.vuu.layoutserver.model; +import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.config.JsonNodeConverter; import javax.persistence.Column; +import javax.persistence.Convert; import javax.persistence.Entity; import javax.persistence.Id; @@ -16,16 +19,7 @@ public class ApplicationLayout { @Id private String username; + @Convert(converter = JsonNodeConverter.class) @Column(columnDefinition = "JSON") - private String definition; - - public String extractDefinition() { - String extractedDefinition = definition; - - if (extractedDefinition.startsWith("\"") && extractedDefinition.endsWith("\"")) { - extractedDefinition = extractedDefinition.substring(1, extractedDefinition.length() - 1); - } - - return extractedDefinition.replaceAll("\\\\", ""); - } + private JsonNode definition; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index e6fdbc549..f141cc88a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -1,10 +1,8 @@ package org.finos.vuu.layoutserver.service; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.slf4j.Logger; @@ -23,14 +21,14 @@ public class ApplicationLayoutService { private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); private static final String DEFAULT_LAYOUT_FILE = "defaultLayout.json"; - private static ApplicationLayoutDto defaultLayout; + private static ApplicationLayout defaultLayout; private final ApplicationLayoutRepository repository; public void createApplicationLayout(String username, JsonNode layoutDefinition) { - repository.save(new ApplicationLayout(username, layoutDefinition.toString())); + repository.save(new ApplicationLayout(username, layoutDefinition)); } - public ApplicationLayoutDto getApplicationLayout(String username) { + public ApplicationLayout getApplicationLayout(String username) { Optional layout = repository.findById(username); if (layout.isEmpty()) { @@ -38,12 +36,7 @@ public ApplicationLayoutDto getApplicationLayout(String username) { return getDefaultLayout(); } - try { - return ApplicationLayoutDto.fromEntity(layout.get()); - } catch (JsonProcessingException e) { - logger.warn("Failed to read user's application layout, returning default"); - return getDefaultLayout(); - } + return layout.get(); } public void updateApplicationLayout(String username, JsonNode layoutDefinition) { @@ -58,7 +51,7 @@ public void deleteApplicationLayout(String username) { } } - private ApplicationLayoutDto getDefaultLayout() { + private ApplicationLayout getDefaultLayout() { if (defaultLayout == null) { loadDefaultLayout(); } @@ -67,7 +60,7 @@ private ApplicationLayoutDto getDefaultLayout() { private void loadDefaultLayout() { JsonNode definition = loadJsonFile(); - defaultLayout = ApplicationLayoutDto.builder().definition(definition).build(); + defaultLayout = new ApplicationLayout(null, definition); } private JsonNode loadJsonFile() { diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java index 6c51e2f2c..2fba75110 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -4,10 +4,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; +import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.service.ApplicationLayoutService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.modelmapper.ModelMapper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.times; @@ -17,26 +19,26 @@ class ApplicationLayoutControllerTest { private static ApplicationLayoutService mockService; private static ApplicationLayoutController controller; + private static final ModelMapper modelMapper = new ModelMapper(); private static final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach public void setup() { mockService = Mockito.mock(ApplicationLayoutService.class); - controller = new ApplicationLayoutController(mockService); + controller = new ApplicationLayoutController(mockService, modelMapper); } @Test public void getApplicationLayout_validUser_returnsLayoutFromService() throws JsonProcessingException { String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - ApplicationLayoutDto expectedDto = ApplicationLayoutDto.builder() - .user(user) - .definition(objectMapper.readTree("{\"id\":\"main-tabs\"}")) - .build(); + when(mockService.getApplicationLayout(user)) + .thenReturn(new ApplicationLayout(user, definition)); - when(mockService.getApplicationLayout(user)).thenReturn(expectedDto); + assertThat(controller.getApplicationLayout(user)) + .isEqualTo(new ApplicationLayoutDto(user, definition)); - assertThat(controller.getApplicationLayout(user)).isEqualTo(expectedDto); verify(mockService, times(1)).getApplicationLayout(user); } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index 2a983b156..a0c5837f2 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -1,5 +1,7 @@ package org.finos.vuu.layoutserver.integration; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.junit.jupiter.api.Test; @@ -27,6 +29,7 @@ @AutoConfigureMockMvc @ActiveProfiles("test") public class ApplicationLayoutIntegrationTest { + public static final ObjectMapper objectMapper = new ObjectMapper(); @Autowired private MockMvc mockMvc; @Autowired @@ -36,7 +39,7 @@ public class ApplicationLayoutIntegrationTest { public void getApplicationLayout_noLayoutExists_returns200WithDefaultLayout() throws Exception { mockMvc.perform(get("/application-layouts").header("user", "new user")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.user", nullValue())) + .andExpect(jsonPath("$.username", nullValue())) // Expecting application layout as defined in /test/resources/defaultLayout.json .andExpect(jsonPath("$.definition.defaultLayoutKey", is("default-layout-value"))); } @@ -52,7 +55,7 @@ public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() th mockMvc.perform(get("/application-layouts").header("user", user)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.user", is(user))) + .andExpect(jsonPath("$.username", is(user))) .andExpect(jsonPath("$.definition", is(definition))); } @@ -71,7 +74,7 @@ public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.extractDefinition()).isEqualToIgnoringWhitespace(definition); + assertThat(persistedLayout.getDefinition()).isEqualTo(objectMapper.readTree(definition)); } @Test @@ -97,7 +100,7 @@ public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.extractDefinition()).isEqualToIgnoringWhitespace(newDefinition); + assertThat(retrievedLayout.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); } @Test @@ -115,7 +118,7 @@ public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.extractDefinition()).isEqualToIgnoringWhitespace(definition); + assertThat(persistedLayout.getDefinition()).isEqualTo(objectMapper.readTree(definition)); } @Test @@ -141,7 +144,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.extractDefinition()).isEqualToIgnoringWhitespace(newDefinition); + assertThat(retrievedLayout.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); } @Test @@ -174,11 +177,6 @@ public void deleteApplicationLayout_layoutExists_returns204AndDeletesLayout() th } private void persistApplicationLayout(String user, Map definition) { - StringBuilder defBuilder = new StringBuilder("{"); - definition.forEach((k, v) -> defBuilder.append("\"").append(k).append("\":\"").append(v).append("\"")); - defBuilder.append("}"); - - ApplicationLayout appLayout = new ApplicationLayout(user, defBuilder.toString()); - repository.save(appLayout); + repository.save(new ApplicationLayout(user, objectMapper.convertValue(definition, JsonNode.class))); } } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java index 8dd7d1626..297b917e4 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.finos.vuu.layoutserver.dto.response.ApplicationLayoutDto; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.junit.jupiter.api.BeforeEach; @@ -38,37 +37,38 @@ public void setup() { public void getApplicationLayout_noLayout_returnsDefault() throws JsonProcessingException { when(mockRepo.findById(anyString())).thenReturn(Optional.empty()); - ApplicationLayoutDto actualLayout = service.getApplicationLayout("new user"); + ApplicationLayout actualLayout = service.getApplicationLayout("new user"); + // Expecting application layout as defined in /test/resources/defaultLayout.json JsonNode expectedDefinition = objectMapper.readTree("{\"defaultLayoutKey\":\"default-layout-value\"}"); - assertThat(actualLayout.getUser()).isNull(); + assertThat(actualLayout.getUsername()).isNull(); assertThat(actualLayout.getDefinition()).isEqualTo(expectedDefinition); } @Test - public void getApplicationLayout_layoutExists_returnsLayout() { - String expectedDefinition = "{\"id\":\"main-tabs\"}"; + public void getApplicationLayout_layoutExists_returnsLayout() throws JsonProcessingException { String user = "user"; + + JsonNode expectedDefinition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); ApplicationLayout expectedLayout = new ApplicationLayout(user, expectedDefinition); when(mockRepo.findById(user)).thenReturn(Optional.of(expectedLayout)); - ApplicationLayoutDto actualLayout = service.getApplicationLayout(user); + ApplicationLayout actualLayout = service.getApplicationLayout(user); - assertThat(actualLayout.getUser()).isEqualTo(user); - assertThat(actualLayout.getDefinition().toString()).isEqualToIgnoringWhitespace(expectedDefinition); + assertThat(actualLayout).isEqualTo(expectedLayout); } @Test public void createApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { - String definition = "{\"id\":\"main-tabs\"}"; String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - service.createApplicationLayout(user, objectMapper.readTree(definition)); + service.createApplicationLayout(user, definition); - ApplicationLayout expectedLayout = new ApplicationLayout(user, definition); - verify(mockRepo, times(1)).save(expectedLayout); + verify(mockRepo, times(1)) + .save(new ApplicationLayout(user, definition)); } @Test @@ -82,13 +82,13 @@ public void createApplicationLayout_invalidDefinition_throwsJsonException() { @Test public void updateApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { - String definition = "{\"id\":\"main-tabs\"}"; String user = "user"; + JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - service.updateApplicationLayout(user, objectMapper.readTree(definition)); + service.updateApplicationLayout(user, definition); - ApplicationLayout expectedLayout = new ApplicationLayout(user, definition); - verify(mockRepo, times(1)).save(expectedLayout); + verify(mockRepo, times(1)) + .save(new ApplicationLayout(user, definition)); } @Test From c231404038f2bd87cecb9152a8d9e79ca000c580 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Thu, 19 Oct 2023 14:46:56 +0100 Subject: [PATCH 51/69] VUU-70: Remove DTO annotations --- .../vuu/layoutserver/dto/response/ApplicationLayoutDto.java | 6 ------ .../controller/ApplicationLayoutControllerTest.java | 6 ++++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java index 7d271959f..2b34c6151 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ApplicationLayoutDto.java @@ -1,15 +1,9 @@ package org.finos.vuu.layoutserver.dto.response; import com.fasterxml.jackson.databind.JsonNode; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; -import lombok.RequiredArgsConstructor; @Data -@RequiredArgsConstructor -@AllArgsConstructor -@Builder public class ApplicationLayoutDto { private String username; private JsonNode definition; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java index 2fba75110..2209f2a3a 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -36,8 +36,10 @@ public void getApplicationLayout_validUser_returnsLayoutFromService() throws Jso when(mockService.getApplicationLayout(user)) .thenReturn(new ApplicationLayout(user, definition)); - assertThat(controller.getApplicationLayout(user)) - .isEqualTo(new ApplicationLayoutDto(user, definition)); + ApplicationLayoutDto response = controller.getApplicationLayout(user); + + assertThat(response.getUsername()).isEqualTo(user); + assertThat(response.getDefinition()).isEqualTo(definition); verify(mockService, times(1)).getApplicationLayout(user); } From cc13be9d167dc8dd9bb745825a146c3315bcc7e5 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Thu, 19 Oct 2023 14:50:51 +0100 Subject: [PATCH 52/69] VUU-70: Rename method --- .../vuu/layoutserver/service/ApplicationLayoutService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index f141cc88a..58bffb111 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -59,11 +59,11 @@ private ApplicationLayout getDefaultLayout() { } private void loadDefaultLayout() { - JsonNode definition = loadJsonFile(); + JsonNode definition = loadDefaultLayoutJsonFile(); defaultLayout = new ApplicationLayout(null, definition); } - private JsonNode loadJsonFile() { + private JsonNode loadDefaultLayoutJsonFile() { ObjectMapper objectMapper = new ObjectMapper(); try { ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); From 306dbec1d11aef09635528334370a82c0c0e6cca Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Fri, 20 Oct 2023 09:30:54 +0100 Subject: [PATCH 53/69] VUU-70: Refactor getApplicationLayout --- .../layoutserver/service/ApplicationLayoutService.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index 58bffb111..8f019f0cb 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -13,7 +13,6 @@ import java.io.IOException; import java.util.NoSuchElementException; -import java.util.Optional; @RequiredArgsConstructor @Service @@ -29,14 +28,10 @@ public void createApplicationLayout(String username, JsonNode layoutDefinition) } public ApplicationLayout getApplicationLayout(String username) { - Optional layout = repository.findById(username); - - if (layout.isEmpty()) { + return repository.findById(username).orElseGet(() -> { logger.info("No application layout for user, returning default"); return getDefaultLayout(); - } - - return layout.get(); + }); } public void updateApplicationLayout(String username, JsonNode layoutDefinition) { From c547efad380a3ed1c0478f670a5c538121c951e7 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Fri, 20 Oct 2023 14:58:38 +0100 Subject: [PATCH 54/69] VUU-70: Standardise DTO casing --- .../layoutserver/config/MappingConfig.java | 8 +-- .../controller/LayoutController.java | 29 ++++++----- .../{MetadataDTO.java => MetadataDto.java} | 2 +- ...tRequestDTO.java => LayoutRequestDto.java} | 7 +-- ...equestDTO.java => MetadataRequestDto.java} | 7 +-- ...esponseDTO.java => LayoutResponseDto.java} | 4 +- ...ponseDTO.java => MetadataResponseDto.java} | 7 +-- .../controller/LayoutControllerTest.java | 51 ++++++++++--------- .../integration/LayoutIntegrationTest.java | 49 +++++++++--------- 9 files changed, 85 insertions(+), 79 deletions(-) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/{MetadataDTO.java => MetadataDto.java} (90%) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/{LayoutRequestDTO.java => LayoutRequestDto.java} (89%) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/{MetadataRequestDTO.java => MetadataRequestDto.java} (78%) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/{LayoutResponseDTO.java => LayoutResponseDto.java} (78%) rename layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/{MetadataResponseDTO.java => MetadataResponseDto.java} (73%) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java index fad55680c..5b3482f85 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java @@ -1,7 +1,7 @@ package org.finos.vuu.layoutserver.config; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.modelmapper.ModelMapper; @@ -15,10 +15,10 @@ public class MappingConfig { public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); - mapper.typeMap(LayoutRequestDTO.class, Layout.class) + mapper.typeMap(LayoutRequestDto.class, Layout.class) .addMappings(m -> m.skip(Layout::setId)); - mapper.typeMap(MetadataRequestDTO.class, Metadata.class) + mapper.typeMap(MetadataRequestDto.class, Metadata.class) .addMappings(m -> m.skip(Metadata::setId)); return mapper; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index aeaa409da..4430687df 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -1,12 +1,9 @@ package org.finos.vuu.layoutserver.controller; -import java.util.List; -import java.util.UUID; -import javax.validation.Valid; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDto; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.service.LayoutService; import org.modelmapper.ModelMapper; @@ -22,6 +19,10 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import javax.validation.Valid; +import java.util.List; +import java.util.UUID; + @RequiredArgsConstructor @RestController @RequestMapping("/layouts") @@ -38,8 +39,8 @@ public class LayoutController { * @return the layout */ @GetMapping("/{id}") - public LayoutResponseDTO getLayout(@PathVariable UUID id) { - return mapper.map(layoutService.getLayout(id), LayoutResponseDTO.class); + public LayoutResponseDto getLayout(@PathVariable UUID id) { + return mapper.map(layoutService.getLayout(id), LayoutResponseDto.class); } /** @@ -48,11 +49,11 @@ public LayoutResponseDTO getLayout(@PathVariable UUID id) { * @return the metadata */ @GetMapping("/metadata") - public List getMetadata() { + public List getMetadata() { return layoutService.getMetadata() .stream() - .map(metadata -> mapper.map(metadata, MetadataResponseDTO.class)) + .map(metadata -> mapper.map(metadata, MetadataResponseDto.class)) .collect(java.util.stream.Collectors.toList()); } @@ -64,13 +65,13 @@ public List getMetadata() { */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public LayoutResponseDTO createLayout( - @RequestBody @Valid LayoutRequestDTO layoutToCreate) { + public LayoutResponseDto createLayout( + @RequestBody @Valid LayoutRequestDto layoutToCreate) { Layout layout = mapper.map(layoutToCreate, Layout.class); Layout createdLayout = layoutService.getLayout(layoutService.createLayout(layout)); - return mapper.map(createdLayout, LayoutResponseDTO.class); + return mapper.map(createdLayout, LayoutResponseDto.class); } /** @@ -82,7 +83,7 @@ public LayoutResponseDTO createLayout( @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping("/{id}") public void updateLayout(@PathVariable UUID id, - @RequestBody @Valid LayoutRequestDTO layout) { + @RequestBody @Valid LayoutRequestDto layout) { Layout newLayout = mapper.map(layout, Layout.class); layoutService.updateLayout(id, newLayout); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDto.java similarity index 90% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDto.java index 23f82a691..f3f2d7b1a 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/MetadataDto.java @@ -1,6 +1,6 @@ package org.finos.vuu.layoutserver.dto; -public interface MetadataDTO { +public interface MetadataDto { String getName(); diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java similarity index 89% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java index 8b93e6343..4ad9c64fd 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; -import lombok.Data; @Data -public class LayoutRequestDTO { +public class LayoutRequestDto { /** * The definition of the layout as a string (e.g. stringified JSON structure containing @@ -18,5 +19,5 @@ public class LayoutRequestDTO { @JsonProperty(value = "metadata", required = true) @NotNull(message = "Please provide valid metadata") - private MetadataRequestDTO metadata; + private MetadataRequestDto metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java similarity index 78% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java index ee01f1f3c..d0d0b4292 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.dto.request; import com.fasterxml.jackson.annotation.JsonProperty; -import javax.validation.constraints.NotNull; import lombok.Data; -import org.finos.vuu.layoutserver.dto.MetadataDTO; +import org.finos.vuu.layoutserver.dto.MetadataDto; + +import javax.validation.constraints.NotNull; @Data -public class MetadataRequestDTO implements MetadataDTO { +public class MetadataRequestDto implements MetadataDto { @JsonProperty(value = "name", required = true) @NotNull(message = "Please provide a valid name") diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java similarity index 78% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java index 9e1077063..0a7f01fe0 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java @@ -5,7 +5,7 @@ import java.util.UUID; @Data -public class LayoutResponseDTO { +public class LayoutResponseDto { private UUID id; @@ -14,5 +14,5 @@ public class LayoutResponseDTO { */ private String definition; - private MetadataResponseDTO metadata; + private MetadataResponseDto metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java similarity index 73% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java index 21dc8082d..0e20e9066 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDTO.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.dto.response; +import lombok.Data; +import org.finos.vuu.layoutserver.dto.MetadataDto; + import java.util.Date; import java.util.UUID; -import lombok.Data; -import org.finos.vuu.layoutserver.dto.MetadataDTO; @Data -public class MetadataResponseDTO implements MetadataDTO { +public class MetadataResponseDto implements MetadataDto { private UUID layoutId; private String name; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 03a9cf640..4ba654e25 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -1,18 +1,9 @@ package org.finos.vuu.layoutserver.controller; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.UUID; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; -import org.finos.vuu.layoutserver.dto.response.LayoutResponseDTO; -import org.finos.vuu.layoutserver.dto.response.MetadataResponseDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; +import org.finos.vuu.layoutserver.dto.response.LayoutResponseDto; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.service.LayoutService; @@ -24,6 +15,16 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.modelmapper.ModelMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class LayoutControllerTest { @@ -40,9 +41,9 @@ class LayoutControllerTest { private UUID doesNotExistLayoutId; private Layout layout; private Metadata metadata; - private LayoutRequestDTO layoutRequest; - private LayoutResponseDTO expectedLayoutResponse; - private List expectedMetadataResponse; + private LayoutRequestDto layoutRequest; + private LayoutResponseDto expectedLayoutResponse; + private List expectedMetadataResponse; @BeforeEach public void setup() { @@ -63,8 +64,8 @@ public void setup() { layout.setDefinition(layoutDefinition); layout.setMetadata(metadata); - layoutRequest = new LayoutRequestDTO(); - MetadataRequestDTO metadataRequestDTO = new MetadataRequestDTO(); + layoutRequest = new LayoutRequestDto(); + MetadataRequestDto metadataRequestDTO = new MetadataRequestDto(); metadataRequestDTO.setName(metadata.getName()); metadataRequestDTO.setUser(metadata.getUser()); metadataRequestDTO.setGroup(metadata.getGroup()); @@ -72,11 +73,11 @@ public void setup() { layoutRequest.setDefinition(layout.getDefinition()); layoutRequest.setMetadata(metadataRequestDTO); - expectedLayoutResponse = new LayoutResponseDTO(); + expectedLayoutResponse = new LayoutResponseDto(); expectedLayoutResponse.setId(layout.getId()); expectedLayoutResponse.setDefinition(layout.getDefinition()); - MetadataResponseDTO metadataResponse = getMetadataResponseDTO(); + MetadataResponseDto metadataResponse = getMetadataResponseDTO(); expectedLayoutResponse.setMetadata(metadataResponse); expectedMetadataResponse = new ArrayList<>(); @@ -87,7 +88,7 @@ public void setup() { @Test void getLayout_layoutExists_returnsLayout() { when(layoutService.getLayout(validLayoutId)).thenReturn(layout); - when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn( + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn( expectedLayoutResponse); assertThat(layoutController.getLayout(validLayoutId)).isEqualTo(expectedLayoutResponse); } @@ -102,7 +103,7 @@ void getLayout_layoutDoesNotExist_throwsNotFoundAndReturns404() { @Test void getMetadata_metadataExists_returnsMetadata() { when(layoutService.getMetadata()).thenReturn(List.of(metadata)); - when(modelMapper.map(metadata, MetadataResponseDTO.class)).thenReturn( + when(modelMapper.map(metadata, MetadataResponseDto.class)).thenReturn( getMetadataResponseDTO()); assertThat(layoutController.getMetadata()).isEqualTo(expectedMetadataResponse); } @@ -122,7 +123,7 @@ void createLayout_validLayout_returnsCreatedLayout() { when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layoutWithoutIds); when(layoutService.createLayout(layoutWithoutIds)).thenReturn(layout.getId()); when(layoutService.getLayout(layout.getId())).thenReturn(layout); - when(modelMapper.map(layout, LayoutResponseDTO.class)).thenReturn(expectedLayoutResponse); + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn(expectedLayoutResponse); assertThat(layoutController.createLayout(layoutRequest)) .isEqualTo(expectedLayoutResponse); @@ -148,8 +149,8 @@ void deleteLayout_callsLayoutService() { verify(layoutService).deleteLayout(validLayoutId); } - private MetadataResponseDTO getMetadataResponseDTO() { - MetadataResponseDTO metadataResponse = new MetadataResponseDTO(); + private MetadataResponseDto getMetadataResponseDTO() { + MetadataResponseDto metadataResponse = new MetadataResponseDto(); metadataResponse.setLayoutId(layout.getId()); metadataResponse.setName(layout.getMetadata().getName()); metadataResponse.setUser(layout.getMetadata().getUser()); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index d0297ad5f..15ea622a6 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -1,19 +1,9 @@ package org.finos.vuu.layoutserver.integration; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -import java.util.UUID; -import org.finos.vuu.layoutserver.dto.request.LayoutRequestDTO; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDTO; +import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; +import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; @@ -29,6 +19,17 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.annotation.Transactional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @SpringBootTest @AutoConfigureMockMvc @Transactional @@ -106,7 +107,7 @@ void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { @Test void createLayout_validLayout_returnsCreatedLayoutAndLayoutIsPersisted() throws Exception { - LayoutRequestDTO layoutRequest = createValidCreateLayoutRequest(); + LayoutRequestDto layoutRequest = createValidCreateLayoutRequest(); MvcResult result = mockMvc.perform(post("/layouts") .content(objectMapper.writeValueAsString(layoutRequest)) @@ -154,7 +155,7 @@ void createLayout_invalidLayout_returns400() throws Exception { @Test void createLayout_validLayoutButInvalidMetadata_returns400AndDoesNotCreateLayout() throws Exception { - LayoutRequestDTO layoutRequest = createValidCreateLayoutRequest(); + LayoutRequestDto layoutRequest = createValidCreateLayoutRequest(); layoutRequest.setMetadata(null); mockMvc.perform(post("/layouts") @@ -168,7 +169,7 @@ void createLayout_validLayoutButInvalidMetadata_returns400AndDoesNotCreateLayout @Test void updateLayout_validIDAndValidRequest_returns204AndLayoutHasChanged() throws Exception { Layout layout = createDefaultLayoutInDatabase(); - LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + LayoutRequestDto layoutRequest = createValidUpdateRequest(); mockMvc.perform(put("/layouts/{id}", layout.getId()) .content(objectMapper.writeValueAsString(layoutRequest)) @@ -195,7 +196,7 @@ void updateLayout_invalidRequestBodyDefinitionIsBlankAndMetadataIsNull_returns40 throws Exception { Layout layout = createDefaultLayoutInDatabase(); - LayoutRequestDTO request = new LayoutRequestDTO(); + LayoutRequestDto request = new LayoutRequestDto(); request.setDefinition(""); request.setMetadata(null); @@ -224,7 +225,7 @@ void updateLayout_invalidRequestBodyUnexpectedFormat_returns400AndLayoutDoesNotC @Test void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exception { UUID layoutID = UUID.randomUUID(); - LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + LayoutRequestDto layoutRequest = createValidUpdateRequest(); mockMvc.perform(put("/layouts/{id}", layoutID) .content(objectMapper.writeValueAsString(layoutRequest)) @@ -235,7 +236,7 @@ void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exceptio @Test void updateLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; - LayoutRequestDTO layoutRequest = createValidUpdateRequest(); + LayoutRequestDto layoutRequest = createValidUpdateRequest(); mockMvc.perform(put("/layouts/{id}", layoutID) .content(objectMapper.writeValueAsString(layoutRequest)) @@ -289,27 +290,27 @@ private Layout createDefaultLayoutInDatabase() { return createdLayout; } - private LayoutRequestDTO createValidUpdateRequest() { - MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); + private LayoutRequestDto createValidUpdateRequest() { + MetadataRequestDto metadataRequest = new MetadataRequestDto(); metadataRequest.setName("Updated name"); metadataRequest.setGroup("Updated group"); metadataRequest.setScreenshot("Updated screenshot"); metadataRequest.setUser("Updated user"); - LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); + LayoutRequestDto layoutRequest = new LayoutRequestDto(); layoutRequest.setDefinition("Updated definition"); layoutRequest.setMetadata(metadataRequest); return layoutRequest; } - private LayoutRequestDTO createValidCreateLayoutRequest() { - MetadataRequestDTO metadataRequest = new MetadataRequestDTO(); + private LayoutRequestDto createValidCreateLayoutRequest() { + MetadataRequestDto metadataRequest = new MetadataRequestDto(); metadataRequest.setName(defaultName); metadataRequest.setGroup(defaultGroup); metadataRequest.setScreenshot(defaultScreenshot); metadataRequest.setUser(defaultUser); - LayoutRequestDTO layoutRequest = new LayoutRequestDTO(); + LayoutRequestDto layoutRequest = new LayoutRequestDto(); layoutRequest.setDefinition(defaultDefinition); layoutRequest.setMetadata(metadataRequest); return layoutRequest; From 5fbc41b92df9743f8f7168b58ed49aa5e0e1af2e Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Fri, 20 Oct 2023 17:14:35 +0100 Subject: [PATCH 55/69] VUU-70: Return 500 on failed JSON read --- .../controller/GlobalExceptionHandler.java | 14 ++++++++++---- .../service/ApplicationLayoutService.java | 3 +-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java index a41a76ffa..e3c9a0345 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package org.finos.vuu.layoutserver.controller; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -12,12 +13,17 @@ public class GlobalExceptionHandler { @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNotFound(NoSuchElementException ex) { - return new ResponseEntity<>(ex.getMessage(), org.springframework.http.HttpStatus.NOT_FOUND); + public ResponseEntity handleNotFound(Exception ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); } @ExceptionHandler({MethodArgumentNotValidException.class, MethodArgumentTypeMismatchException.class}) - public ResponseEntity handleBadRequest(MethodArgumentNotValidException ex) { - return new ResponseEntity<>(ex.getMessage(), org.springframework.http.HttpStatus.BAD_REQUEST); + public ResponseEntity handleBadRequest(Exception ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleInternalServerError(Exception ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index 8f019f0cb..915717803 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -64,8 +64,7 @@ private JsonNode loadDefaultLayoutJsonFile() { ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); return objectMapper.readTree(resource.getInputStream()); } catch (IOException e) { - logger.warn("Failed to read default application layout, returning empty node"); - return objectMapper.createObjectNode(); + throw new RuntimeException("Failed to read default application layout"); } } } From 27ceb94cecbec612875802b2249237e9fc77999f Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Mon, 23 Oct 2023 11:41:17 +0100 Subject: [PATCH 56/69] VUU-70: Adjust exception handling --- .../vuu/layoutserver/controller/GlobalExceptionHandler.java | 5 ----- layout-server/src/main/resources/application.properties | 1 + layout-server/src/test/resources/application-test.properties | 1 + 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java index e3c9a0345..830163c2f 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java @@ -21,9 +21,4 @@ public ResponseEntity handleNotFound(Exception ex) { public ResponseEntity handleBadRequest(Exception ex) { return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); } - - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleInternalServerError(Exception ex) { - return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); - } } diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index afee88372..621ba95d8 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1,3 +1,4 @@ +server.error.include-message=always server.port=8081 server.servlet.contextPath=/api springdoc.swagger-ui.path=/swagger diff --git a/layout-server/src/test/resources/application-test.properties b/layout-server/src/test/resources/application-test.properties index 2722b4aca..13ca6c535 100644 --- a/layout-server/src/test/resources/application-test.properties +++ b/layout-server/src/test/resources/application-test.properties @@ -1,3 +1,4 @@ +server.error.include-message=always spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=GROUP,USER spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa From 8602b7035ee0a347cfac05f6956df3317553ddb0 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Mon, 23 Oct 2023 15:57:58 +0100 Subject: [PATCH 57/69] VUU-70: Implement custom error responses --- .../controller/GlobalExceptionHandler.java | 22 ++++++++++++++---- .../dto/response/ErrorResponse.java | 23 +++++++++++++++++++ .../InternalServerErrorException.java | 7 ++++++ .../service/ApplicationLayoutService.java | 5 ++-- .../ApplicationLayoutIntegrationTest.java | 6 ++--- 5 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java index 830163c2f..e3aa1bcd7 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java @@ -1,5 +1,7 @@ package org.finos.vuu.layoutserver.controller; +import org.finos.vuu.layoutserver.dto.response.ErrorResponse; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -7,18 +9,30 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import javax.servlet.http.HttpServletRequest; import java.util.NoSuchElementException; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNotFound(Exception ex) { - return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); + public ResponseEntity handleNotFound(HttpServletRequest request, Exception ex) { + return generateResponse(request, ex, HttpStatus.NOT_FOUND); } @ExceptionHandler({MethodArgumentNotValidException.class, MethodArgumentTypeMismatchException.class}) - public ResponseEntity handleBadRequest(Exception ex) { - return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); + public ResponseEntity handleBadRequest(Exception ex, HttpServletRequest request) { + return generateResponse(request, ex, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(InternalServerErrorException.class) + public ResponseEntity handleInternalServerError(HttpServletRequest request, Exception ex) { + return generateResponse(request, ex, HttpStatus.INTERNAL_SERVER_ERROR); + } + + private ResponseEntity generateResponse(HttpServletRequest request, + Exception ex, + HttpStatus status) { + return new ResponseEntity<>(new ErrorResponse(request, ex, status), status); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java new file mode 100644 index 000000000..fb2576489 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/ErrorResponse.java @@ -0,0 +1,23 @@ +package org.finos.vuu.layoutserver.dto.response; + +import lombok.Data; +import org.springframework.http.HttpStatus; + +import javax.servlet.http.HttpServletRequest; +import java.util.Date; + +@Data +public class ErrorResponse { + private Date timestamp = new Date(); + private int status; + private String error; + private String message; + private String path; + + public ErrorResponse(HttpServletRequest request, Exception ex, HttpStatus status) { + this.status = status.value(); + this.error = status.getReasonPhrase(); + this.path = request.getRequestURI(); + this.message = ex.getMessage(); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java new file mode 100644 index 000000000..b1164eab6 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/InternalServerErrorException.java @@ -0,0 +1,7 @@ +package org.finos.vuu.layoutserver.exceptions; + +public class InternalServerErrorException extends RuntimeException { + public InternalServerErrorException(String message) { + super(message); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index 915717803..52ca46c03 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; import org.slf4j.Logger; @@ -60,11 +61,11 @@ private void loadDefaultLayout() { private JsonNode loadDefaultLayoutJsonFile() { ObjectMapper objectMapper = new ObjectMapper(); + ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); try { - ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); return objectMapper.readTree(resource.getInputStream()); } catch (IOException e) { - throw new RuntimeException("Failed to read default application layout"); + throw new InternalServerErrorException("Failed to read default application layout"); } } } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index a0c5837f2..11eeaffb5 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -151,12 +151,10 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() public void deleteApplicationLayout_noLayoutExists_returns404() throws Exception { String user = "user"; - String response = mockMvc.perform(delete("/application-layouts") + mockMvc.perform(delete("/application-layouts") .header("user", user)) .andExpect(status().isNotFound()) - .andReturn().getResponse().getContentAsString(); - - assertThat(response).isEqualTo("No layout found for user: " + user); + .andExpect(jsonPath("$.message", is("No layout found for user: " + user))); } @Test From 4a8b62e4ee3305735d03edf5880702707c1667ee Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Mon, 23 Oct 2023 17:12:14 +0100 Subject: [PATCH 58/69] VUU-70: Update javadocs --- .../controller/ApplicationLayoutController.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index dae53281d..b0b4d67f9 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -27,7 +27,9 @@ public class ApplicationLayoutController { private final ModelMapper mapper; /** - * Gets the application layout for the requesting user. Returns a default layout if none exists. + * Gets the persisted application layout for the requesting user. If the requesting user does not have an + * application layout persisted, a default layout with a null username is returned instead. No more than one + * application layout can be persisted for a given user. * * @return the application layout */ @@ -38,7 +40,8 @@ public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String u } /** - * Creates a new application layout for the requesting user. + * Creates a new application layout for the requesting user. If the requesting user already has an application + * layout persisted, this will be identical to the PUT method, {@link #updateApplicationLayout(String, JsonNode)}. * * @param layoutDefinition JSON representation of the application layout to be created * @param username the user making the request @@ -50,7 +53,8 @@ public void createApplicationLayout(@RequestHeader("user") String username, @Req } /** - * Updates the application layout for the requesting user. + * Updates the application layout for the requesting user. If the requesting user does not have an application + * layout persisted, this will be identical to the POST method, {@link #createApplicationLayout(String, JsonNode)}. * * @param layoutDefinition JSON representation of the application layout to be created * @param username the user making the request @@ -62,7 +66,8 @@ public void updateApplicationLayout(@RequestHeader("user") String username, @Req } /** - * Deletes the application layout for the requesting user. + * Deletes the application layout for the requesting user. A 404 will be returned if there is no existing + * application layout. * * @param username the user making the request */ From 3772c26c031330de51b2078c8f59a7360aed6a02 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 24 Oct 2023 08:49:43 +0100 Subject: [PATCH 59/69] VUU-70: Revert application properties --- layout-server/src/main/resources/application.properties | 1 - layout-server/src/test/resources/application-test.properties | 1 - 2 files changed, 2 deletions(-) diff --git a/layout-server/src/main/resources/application.properties b/layout-server/src/main/resources/application.properties index 621ba95d8..afee88372 100644 --- a/layout-server/src/main/resources/application.properties +++ b/layout-server/src/main/resources/application.properties @@ -1,4 +1,3 @@ -server.error.include-message=always server.port=8081 server.servlet.contextPath=/api springdoc.swagger-ui.path=/swagger diff --git a/layout-server/src/test/resources/application-test.properties b/layout-server/src/test/resources/application-test.properties index 13ca6c535..2722b4aca 100644 --- a/layout-server/src/test/resources/application-test.properties +++ b/layout-server/src/test/resources/application-test.properties @@ -1,4 +1,3 @@ -server.error.include-message=always spring.datasource.url=jdbc:h2:mem:testdb;NON_KEYWORDS=GROUP,USER spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa From 3cfbe20f631039c5a164bfdc53fe53fceedba811 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 24 Oct 2023 11:41:02 +0100 Subject: [PATCH 60/69] VUU-70: Add DefaultApplicationLayoutLoader - Introduce DefaultApplicationLayoutLoader to lazily load JSON from a static file - Refactor service to use loader - Add integration test case for failure to load default - Rename static JSON files - Move JsonNodeConverter to utils package --- .../layoutserver/model/ApplicationLayout.java | 2 +- .../service/ApplicationLayoutService.java | 32 ++------------- .../utils/DefaultApplicationLayoutLoader.java | 40 +++++++++++++++++++ .../{config => utils}/JsonNodeConverter.java | 2 +- ...out.json => defaultApplicationLayout.json} | 0 .../ApplicationLayoutIntegrationTest.java | 22 +++++++++- .../service/ApplicationLayoutServiceTest.java | 6 ++- ...out.json => defaultApplicationLayout.json} | 0 8 files changed, 70 insertions(+), 34 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java rename layout-server/src/main/java/org/finos/vuu/layoutserver/{config => utils}/JsonNodeConverter.java (97%) rename layout-server/src/main/resources/{defaultLayout.json => defaultApplicationLayout.json} (100%) rename layout-server/src/test/resources/{defaultLayout.json => defaultApplicationLayout.json} (100%) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java index 7c69de7a7..a8cef5ad6 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/ApplicationLayout.java @@ -4,7 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.config.JsonNodeConverter; +import org.finos.vuu.layoutserver.utils.JsonNodeConverter; import javax.persistence.Column; import javax.persistence.Convert; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index 52ca46c03..4a008ccdd 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -1,18 +1,15 @@ package org.finos.vuu.layoutserver.service; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.core.io.ClassPathResource; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Service; -import java.io.IOException; import java.util.NoSuchElementException; @RequiredArgsConstructor @@ -20,9 +17,8 @@ public class ApplicationLayoutService { private static final Logger logger = LoggerFactory.getLogger(ApplicationLayoutService.class); - private static final String DEFAULT_LAYOUT_FILE = "defaultLayout.json"; - private static ApplicationLayout defaultLayout; private final ApplicationLayoutRepository repository; + private final DefaultApplicationLayoutLoader defaultLoader; public void createApplicationLayout(String username, JsonNode layoutDefinition) { repository.save(new ApplicationLayout(username, layoutDefinition)); @@ -31,7 +27,7 @@ public void createApplicationLayout(String username, JsonNode layoutDefinition) public ApplicationLayout getApplicationLayout(String username) { return repository.findById(username).orElseGet(() -> { logger.info("No application layout for user, returning default"); - return getDefaultLayout(); + return defaultLoader.getDefaultLayout(); }); } @@ -46,26 +42,4 @@ public void deleteApplicationLayout(String username) { throw new NoSuchElementException("No layout found for user: " + username); } } - - private ApplicationLayout getDefaultLayout() { - if (defaultLayout == null) { - loadDefaultLayout(); - } - return defaultLayout; - } - - private void loadDefaultLayout() { - JsonNode definition = loadDefaultLayoutJsonFile(); - defaultLayout = new ApplicationLayout(null, definition); - } - - private JsonNode loadDefaultLayoutJsonFile() { - ObjectMapper objectMapper = new ObjectMapper(); - ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); - try { - return objectMapper.readTree(resource.getInputStream()); - } catch (IOException e) { - throw new InternalServerErrorException("Failed to read default application layout"); - } - } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java new file mode 100644 index 000000000..0921acebf --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/DefaultApplicationLayoutLoader.java @@ -0,0 +1,40 @@ +package org.finos.vuu.layoutserver.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; +import org.finos.vuu.layoutserver.model.ApplicationLayout; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class DefaultApplicationLayoutLoader { + private static final String DEFAULT_LAYOUT_FILE = "defaultApplicationLayout.json"; + private static ApplicationLayout defaultLayout; + + @Bean + public ApplicationLayout getDefaultLayout() { + if (defaultLayout == null) { + loadDefaultLayout(); + } + return defaultLayout; + } + + private void loadDefaultLayout() { + JsonNode definition = loadDefaultLayoutJsonFile(); + defaultLayout = new ApplicationLayout(null, definition); + } + + private JsonNode loadDefaultLayoutJsonFile() { + ObjectMapper objectMapper = new ObjectMapper(); + ClassPathResource resource = new ClassPathResource(DEFAULT_LAYOUT_FILE); + try { + return objectMapper.readTree(resource.getInputStream()); + } catch (IOException e) { + throw new InternalServerErrorException("Failed to read default application layout"); + } + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/JsonNodeConverter.java similarity index 97% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/utils/JsonNodeConverter.java index 728e7635a..1984aa56b 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/JsonNodeConverter.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/utils/JsonNodeConverter.java @@ -1,4 +1,4 @@ -package org.finos.vuu.layoutserver.config; +package org.finos.vuu.layoutserver.utils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; diff --git a/layout-server/src/main/resources/defaultLayout.json b/layout-server/src/main/resources/defaultApplicationLayout.json similarity index 100% rename from layout-server/src/main/resources/defaultLayout.json rename to layout-server/src/main/resources/defaultApplicationLayout.json diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index 11eeaffb5..cf72a3432 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -2,12 +2,15 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; @@ -18,6 +21,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -34,16 +39,31 @@ public class ApplicationLayoutIntegrationTest { private MockMvc mockMvc; @Autowired private ApplicationLayoutRepository repository; + @MockBean + private DefaultApplicationLayoutLoader mockLoader; + private final DefaultApplicationLayoutLoader realLoader = new DefaultApplicationLayoutLoader(); @Test public void getApplicationLayout_noLayoutExists_returns200WithDefaultLayout() throws Exception { + when(mockLoader.getDefaultLayout()).thenReturn(realLoader.getDefaultLayout()); + mockMvc.perform(get("/application-layouts").header("user", "new user")) .andExpect(status().isOk()) .andExpect(jsonPath("$.username", nullValue())) - // Expecting application layout as defined in /test/resources/defaultLayout.json + // Expecting application layout as defined in /test/resources/defaultApplicationLayout.json .andExpect(jsonPath("$.definition.defaultLayoutKey", is("default-layout-value"))); } + @Test + public void getApplicationLayout_defaultFailsToLoad_returns500() throws Exception { + String errorMessage = "Failed to read default application layout"; + doThrow(new InternalServerErrorException(errorMessage)).when(mockLoader).getDefaultLayout(); + + mockMvc.perform(get("/application-layouts").header("user", "new user")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message", is(errorMessage))); + } + @Test public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() throws Exception { String user = "user"; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java index 297b917e4..8d8559678 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.finos.vuu.layoutserver.model.ApplicationLayout; import org.finos.vuu.layoutserver.repository.ApplicationLayoutRepository; +import org.finos.vuu.layoutserver.utils.DefaultApplicationLayoutLoader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -25,12 +26,13 @@ class ApplicationLayoutServiceTest { private static ApplicationLayoutRepository mockRepo; private static ApplicationLayoutService service; + private static final DefaultApplicationLayoutLoader defaultLoader = new DefaultApplicationLayoutLoader(); private static final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach public void setup() { mockRepo = Mockito.mock(ApplicationLayoutRepository.class); - service = new ApplicationLayoutService(mockRepo); + service = new ApplicationLayoutService(mockRepo, defaultLoader); } @Test @@ -39,7 +41,7 @@ public void getApplicationLayout_noLayout_returnsDefault() throws JsonProcessing ApplicationLayout actualLayout = service.getApplicationLayout("new user"); - // Expecting application layout as defined in /test/resources/defaultLayout.json + // Expecting application layout as defined in /test/resources/defaultApplicationLayout.json JsonNode expectedDefinition = objectMapper.readTree("{\"defaultLayoutKey\":\"default-layout-value\"}"); assertThat(actualLayout.getUsername()).isNull(); diff --git a/layout-server/src/test/resources/defaultLayout.json b/layout-server/src/test/resources/defaultApplicationLayout.json similarity index 100% rename from layout-server/src/test/resources/defaultLayout.json rename to layout-server/src/test/resources/defaultApplicationLayout.json From 4c2b809e654f8cc6818f5e911c465b4ff96f3155 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Tue, 24 Oct 2023 11:53:21 +0100 Subject: [PATCH 61/69] VUU-70: Rename controller test methods --- .../controller/ApplicationLayoutControllerTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java index 2209f2a3a..7aac7e75e 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -29,7 +29,7 @@ public void setup() { } @Test - public void getApplicationLayout_validUser_returnsLayoutFromService() throws JsonProcessingException { + public void getApplicationLayout_anyUsername_returnsLayoutFromService() throws JsonProcessingException { String user = "user"; JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); @@ -45,7 +45,7 @@ public void getApplicationLayout_validUser_returnsLayoutFromService() throws Jso } @Test - public void createApplicationLayout_validUser_callsService() throws JsonProcessingException { + public void createApplicationLayout_anyInput_callsService() throws JsonProcessingException { String user = "user"; JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); @@ -55,7 +55,7 @@ public void createApplicationLayout_validUser_callsService() throws JsonProcessi } @Test - public void updateApplicationLayout_validUser_callsService() throws JsonProcessingException { + public void updateApplicationLayout_anyInput_callsService() throws JsonProcessingException { String user = "user"; JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); @@ -65,7 +65,7 @@ public void updateApplicationLayout_validUser_callsService() throws JsonProcessi } @Test - public void deleteApplicationLayout_validUser_callsService() { + public void deleteApplicationLayout_anyUsername_callsService() { String user = "user"; controller.deleteApplicationLayout(user); From d9b27dae69c9871f44e773d6be5151dfe9ef7938 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Thu, 26 Oct 2023 11:37:13 +0100 Subject: [PATCH 62/69] VUU-70: Rename header key for username - Rename header key from 'user' to 'username' - Add integration tests for missing username --- .../ApplicationLayoutController.java | 8 +-- .../ApplicationLayoutIntegrationTest.java | 64 ++++++++++++++----- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index b0b4d67f9..e071813c6 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -35,7 +35,7 @@ public class ApplicationLayoutController { */ @ResponseStatus(HttpStatus.OK) @GetMapping - public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String username) { + public ApplicationLayoutDto getApplicationLayout(@RequestHeader("username") String username) { return mapper.map(service.getApplicationLayout(username), ApplicationLayoutDto.class); } @@ -48,7 +48,7 @@ public ApplicationLayoutDto getApplicationLayout(@RequestHeader("user") String u */ @ResponseStatus(HttpStatus.CREATED) @PostMapping - public void createApplicationLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + public void createApplicationLayout(@RequestHeader("username") String username, @RequestBody JsonNode layoutDefinition) { service.createApplicationLayout(username, layoutDefinition); } @@ -61,7 +61,7 @@ public void createApplicationLayout(@RequestHeader("user") String username, @Req */ @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping - public void updateApplicationLayout(@RequestHeader("user") String username, @RequestBody JsonNode layoutDefinition) { + public void updateApplicationLayout(@RequestHeader("username") String username, @RequestBody JsonNode layoutDefinition) { service.updateApplicationLayout(username, layoutDefinition); } @@ -73,7 +73,7 @@ public void updateApplicationLayout(@RequestHeader("user") String username, @Req */ @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping - public void deleteApplicationLayout(@RequestHeader("user") String username) { + public void deleteApplicationLayout(@RequestHeader("username") String username) { service.deleteApplicationLayout(username); } } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index cf72a3432..7977b6f9f 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -35,6 +35,10 @@ @ActiveProfiles("test") public class ApplicationLayoutIntegrationTest { public static final ObjectMapper objectMapper = new ObjectMapper(); + public static final String BASE_URL = "/application-layouts"; + public static final String MISSING_USERNAME_ERROR_MESSAGE = + "Required request header 'username' for method parameter type String is not present"; + @Autowired private MockMvc mockMvc; @Autowired @@ -47,7 +51,7 @@ public class ApplicationLayoutIntegrationTest { public void getApplicationLayout_noLayoutExists_returns200WithDefaultLayout() throws Exception { when(mockLoader.getDefaultLayout()).thenReturn(realLoader.getDefaultLayout()); - mockMvc.perform(get("/application-layouts").header("user", "new user")) + mockMvc.perform(get(BASE_URL).header("username", "new user")) .andExpect(status().isOk()) .andExpect(jsonPath("$.username", nullValue())) // Expecting application layout as defined in /test/resources/defaultApplicationLayout.json @@ -59,11 +63,20 @@ public void getApplicationLayout_defaultFailsToLoad_returns500() throws Exceptio String errorMessage = "Failed to read default application layout"; doThrow(new InternalServerErrorException(errorMessage)).when(mockLoader).getDefaultLayout(); - mockMvc.perform(get("/application-layouts").header("user", "new user")) + mockMvc.perform(get(BASE_URL).header("username", "new user")) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("$.message", is(errorMessage))); } + @Test + public void getApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(get(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + @Test public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() throws Exception { String user = "user"; @@ -73,7 +86,7 @@ public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() th persistApplicationLayout(user, definition); - mockMvc.perform(get("/application-layouts").header("user", user)) + mockMvc.perform(get(BASE_URL).header("username", user)) .andExpect(status().isOk()) .andExpect(jsonPath("$.username", is(user))) .andExpect(jsonPath("$.definition", is(definition))); @@ -84,8 +97,7 @@ public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() String user = "user"; String definition = "{\"key\": \"value\"}"; - mockMvc.perform(post("/application-layouts") - .header("user", user) + mockMvc.perform(post(BASE_URL).header("username", user) .content(definition) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) @@ -108,8 +120,7 @@ public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() String newDefinition = "{\"new-key\": \"new-value\"}"; - mockMvc.perform(post("/application-layouts") - .header("user", user) + mockMvc.perform(post(BASE_URL).header("username", user) .content(newDefinition) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) @@ -123,13 +134,21 @@ public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() assertThat(retrievedLayout.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); } + @Test + public void createApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(post(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + @Test public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() throws Exception { String user = "user"; String definition = "{\"key\": \"value\"}"; - mockMvc.perform(put("/application-layouts") - .header("user", user) + mockMvc.perform(put(BASE_URL).header("username", user) .content(definition) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()) @@ -152,8 +171,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() String newDefinition = "{\"new-key\": \"new-value\"}"; - mockMvc.perform(put("/application-layouts") - .header("user", user) + mockMvc.perform(put(BASE_URL).header("username", user) .content(newDefinition) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()) @@ -167,12 +185,20 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() assertThat(retrievedLayout.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); } + @Test + public void updateApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(put(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + @Test public void deleteApplicationLayout_noLayoutExists_returns404() throws Exception { String user = "user"; - mockMvc.perform(delete("/application-layouts") - .header("user", user)) + mockMvc.perform(delete(BASE_URL).header("username", user)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message", is("No layout found for user: " + user))); } @@ -186,14 +212,22 @@ public void deleteApplicationLayout_layoutExists_returns204AndDeletesLayout() th persistApplicationLayout(user, initialDefinition); - mockMvc.perform(delete("/application-layouts") - .header("user", user)) + mockMvc.perform(delete(BASE_URL).header("username", user)) .andExpect(status().isNoContent()) .andExpect(jsonPath("$").doesNotExist()); assertThat(repository.findAll()).hasSize(0); } + @Test + public void deleteApplicationLayout_noUserInHeader_returns400() throws Exception { + String actualError = mockMvc.perform(delete(BASE_URL)) + .andExpect(status().isBadRequest()) + .andReturn().getResponse().getErrorMessage(); + + assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); + } + private void persistApplicationLayout(String user, Map definition) { repository.save(new ApplicationLayout(user, objectMapper.convertValue(definition, JsonNode.class))); } From 8f15b2855f541f605daeb1cbc5b06d31f12bc129 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Thu, 26 Oct 2023 14:55:10 +0100 Subject: [PATCH 63/69] VUU-70: Remove POST endpoint --- .../ApplicationLayoutController.java | 21 +------ .../service/ApplicationLayoutService.java | 6 +- .../ApplicationLayoutControllerTest.java | 16 +---- .../ApplicationLayoutIntegrationTest.java | 62 ++----------------- .../service/ApplicationLayoutServiceTest.java | 24 +------ 5 files changed, 14 insertions(+), 115 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index e071813c6..e012883a8 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -9,7 +9,6 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; @@ -40,29 +39,15 @@ public ApplicationLayoutDto getApplicationLayout(@RequestHeader("username") Stri } /** - * Creates a new application layout for the requesting user. If the requesting user already has an application - * layout persisted, this will be identical to the PUT method, {@link #updateApplicationLayout(String, JsonNode)}. + * Creates or updates the unique application layout for the requesting user. * * @param layoutDefinition JSON representation of the application layout to be created * @param username the user making the request */ @ResponseStatus(HttpStatus.CREATED) - @PostMapping - public void createApplicationLayout(@RequestHeader("username") String username, @RequestBody JsonNode layoutDefinition) { - service.createApplicationLayout(username, layoutDefinition); - } - - /** - * Updates the application layout for the requesting user. If the requesting user does not have an application - * layout persisted, this will be identical to the POST method, {@link #createApplicationLayout(String, JsonNode)}. - * - * @param layoutDefinition JSON representation of the application layout to be created - * @param username the user making the request - */ - @ResponseStatus(HttpStatus.NO_CONTENT) @PutMapping - public void updateApplicationLayout(@RequestHeader("username") String username, @RequestBody JsonNode layoutDefinition) { - service.updateApplicationLayout(username, layoutDefinition); + public void persistApplicationLayout(@RequestHeader("username") String username, @RequestBody JsonNode layoutDefinition) { + service.persistApplicationLayout(username, layoutDefinition); } /** diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java index 4a008ccdd..8c9a2c622 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/ApplicationLayoutService.java @@ -20,7 +20,7 @@ public class ApplicationLayoutService { private final ApplicationLayoutRepository repository; private final DefaultApplicationLayoutLoader defaultLoader; - public void createApplicationLayout(String username, JsonNode layoutDefinition) { + public void persistApplicationLayout(String username, JsonNode layoutDefinition) { repository.save(new ApplicationLayout(username, layoutDefinition)); } @@ -31,10 +31,6 @@ public ApplicationLayout getApplicationLayout(String username) { }); } - public void updateApplicationLayout(String username, JsonNode layoutDefinition) { - createApplicationLayout(username, layoutDefinition); - } - public void deleteApplicationLayout(String username) { try { repository.deleteById(username); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java index 7aac7e75e..ea689bd53 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutControllerTest.java @@ -45,23 +45,13 @@ public void getApplicationLayout_anyUsername_returnsLayoutFromService() throws J } @Test - public void createApplicationLayout_anyInput_callsService() throws JsonProcessingException { + public void persistApplicationLayout_anyInput_callsService() throws JsonProcessingException { String user = "user"; JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - controller.createApplicationLayout(user, definition); + controller.persistApplicationLayout(user, definition); - verify(mockService, times(1)).createApplicationLayout(user, definition); - } - - @Test - public void updateApplicationLayout_anyInput_callsService() throws JsonProcessingException { - String user = "user"; - JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - - controller.updateApplicationLayout(user, definition); - - verify(mockService, times(1)).updateApplicationLayout(user, definition); + verify(mockService, times(1)).persistApplicationLayout(user, definition); } @Test diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index 7977b6f9f..e2a95d747 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -25,7 +25,6 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -93,65 +92,14 @@ public void getApplicationLayout_layoutExists_returns200WithPersistedLayout() th } @Test - public void createApplicationLayout_noLayoutExists_returns201AndPersistsLayout() throws Exception { - String user = "user"; - String definition = "{\"key\": \"value\"}"; - - mockMvc.perform(post(BASE_URL).header("username", user) - .content(definition) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$").doesNotExist()); - - ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); - - assertThat(persistedLayout.getUsername()).isEqualTo(user); - assertThat(persistedLayout.getDefinition()).isEqualTo(objectMapper.readTree(definition)); - } - - @Test - public void createApplicationLayout_layoutExists_returns201AndOverwritesLayout() throws Exception { - String user = "user"; - - Map initialDefinition = new HashMap<>(); - initialDefinition.put("initial-key", "initial-value"); - - persistApplicationLayout(user, initialDefinition); - - String newDefinition = "{\"new-key\": \"new-value\"}"; - - mockMvc.perform(post(BASE_URL).header("username", user) - .content(newDefinition) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$").doesNotExist()); - - assertThat(repository.findAll()).hasSize(1); - - ApplicationLayout retrievedLayout = repository.findById(user).orElseThrow(); - - assertThat(retrievedLayout.getUsername()).isEqualTo(user); - assertThat(retrievedLayout.getDefinition()).isEqualTo(objectMapper.readTree(newDefinition)); - } - - @Test - public void createApplicationLayout_noUserInHeader_returns400() throws Exception { - String actualError = mockMvc.perform(post(BASE_URL)) - .andExpect(status().isBadRequest()) - .andReturn().getResponse().getErrorMessage(); - - assertThat(actualError).isEqualTo(MISSING_USERNAME_ERROR_MESSAGE); - } - - @Test - public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() throws Exception { + public void persistApplicationLayout_noLayoutExists_returns201AndPersistsLayout() throws Exception { String user = "user"; String definition = "{\"key\": \"value\"}"; mockMvc.perform(put(BASE_URL).header("username", user) .content(definition) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNoContent()) + .andExpect(status().isCreated()) .andExpect(jsonPath("$").doesNotExist()); ApplicationLayout persistedLayout = repository.findById(user).orElseThrow(); @@ -161,7 +109,7 @@ public void updateApplicationLayout_noLayoutExists_returns204AndPersistsLayout() } @Test - public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() throws Exception { + public void persistApplicationLayout_layoutExists_returns201AndOverwritesLayout() throws Exception { String user = "user"; Map initialDefinition = new HashMap<>(); @@ -174,7 +122,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() mockMvc.perform(put(BASE_URL).header("username", user) .content(newDefinition) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNoContent()) + .andExpect(status().isCreated()) .andExpect(jsonPath("$").doesNotExist()); assertThat(repository.findAll()).hasSize(1); @@ -186,7 +134,7 @@ public void updateApplicationLayout_layoutExists_returns204AndOverwritesLayout() } @Test - public void updateApplicationLayout_noUserInHeader_returns400() throws Exception { + public void persistApplicationLayout_noUserInHeader_returns400() throws Exception { String actualError = mockMvc.perform(put(BASE_URL)) .andExpect(status().isBadRequest()) .andReturn().getResponse().getErrorMessage(); diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java index 8d8559678..d6d245b8b 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/ApplicationLayoutServiceTest.java @@ -67,7 +67,7 @@ public void createApplicationLayout_validDefinition_callsRepoSave() throws JsonP String user = "user"; JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - service.createApplicationLayout(user, definition); + service.persistApplicationLayout(user, definition); verify(mockRepo, times(1)) .save(new ApplicationLayout(user, definition)); @@ -78,27 +78,7 @@ public void createApplicationLayout_invalidDefinition_throwsJsonException() { String definition = "invalid JSON"; assertThrows(JsonProcessingException.class, () -> - service.createApplicationLayout("user", objectMapper.readTree(definition)) - ); - } - - @Test - public void updateApplicationLayout_validDefinition_callsRepoSave() throws JsonProcessingException { - String user = "user"; - JsonNode definition = objectMapper.readTree("{\"id\":\"main-tabs\"}"); - - service.updateApplicationLayout(user, definition); - - verify(mockRepo, times(1)) - .save(new ApplicationLayout(user, definition)); - } - - @Test - public void updateApplicationLayout_invalidDefinition_throwsJsonException() { - String definition = "invalid JSON"; - - assertThrows(JsonProcessingException.class, () -> - service.updateApplicationLayout("user", objectMapper.readTree(definition)) + service.persistApplicationLayout("user", objectMapper.readTree(definition)) ); } From 7ab6d308e1ad7d5d8dc4dfddf798b1266acab16e Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Wed, 1 Nov 2023 12:08:06 +0000 Subject: [PATCH 64/69] VUU-70: Remove @Validated annotation --- .../layoutserver/controller/ApplicationLayoutController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java index e012883a8..c78dfaafa 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/ApplicationLayoutController.java @@ -6,7 +6,6 @@ import org.finos.vuu.layoutserver.service.ApplicationLayoutService; import org.modelmapper.ModelMapper; import org.springframework.http.HttpStatus; -import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -19,7 +18,6 @@ @RequiredArgsConstructor @RestController @RequestMapping("/application-layouts") -@Validated public class ApplicationLayoutController { private final ApplicationLayoutService service; From 5ad3a79993786c23d04b9b21d47a2f8574c8b459 Mon Sep 17 00:00:00 2001 From: Peter Ling Date: Wed, 1 Nov 2023 12:43:01 +0000 Subject: [PATCH 65/69] VUU-70: Make integration test constants private --- .../integration/ApplicationLayoutIntegrationTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java index e2a95d747..80de9ab63 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/ApplicationLayoutIntegrationTest.java @@ -33,9 +33,9 @@ @AutoConfigureMockMvc @ActiveProfiles("test") public class ApplicationLayoutIntegrationTest { - public static final ObjectMapper objectMapper = new ObjectMapper(); - public static final String BASE_URL = "/application-layouts"; - public static final String MISSING_USERNAME_ERROR_MESSAGE = + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String BASE_URL = "/application-layouts"; + private static final String MISSING_USERNAME_ERROR_MESSAGE = "Required request header 'username' for method parameter type String is not present"; @Autowired From 6c93a6a1917cb0cbac00eb2eaeef39252561566e Mon Sep 17 00:00:00 2001 From: vferraro-scottlogic Date: Mon, 6 Nov 2023 13:53:07 +0000 Subject: [PATCH 66/69] merge main in VUU-70 --- .github/workflows/test-ui.yml | 39 + .gitignore | 1 + .semgrepignore | 2 + .../finos/vuu/layoutserver/CorsConfig.java | 15 + .../layoutserver/config/MappingConfig.java | 13 +- .../controller/LayoutController.java | 14 +- .../dto/request/LayoutRequestDto.java | 4 +- .../dto/request/MetadataRequestDto.java | 17 +- .../dto/response/LayoutResponseDto.java | 3 +- .../dto/response/MetadataResponseDto.java | 19 +- .../GlobalExceptionHandler.java | 23 +- .../vuu/layoutserver/model/BaseMetadata.java | 19 + .../vuu/layoutserver/model/Metadata.java | 30 +- .../repository/LayoutRepository.java | 8 +- .../repository/MetadataRepository.java | 3 +- .../layoutserver/service/LayoutService.java | 33 +- .../layoutserver/service/MetadataService.java | 5 +- .../controller/LayoutControllerTest.java | 102 +- .../integration/LayoutIntegrationTest.java | 346 ++++-- .../service/LayoutServiceTest.java | 97 +- .../service/MetadataServiceTest.java | 37 + vuu-ui/cypress.config.ts | 1 + .../e2e/layout-management/screenshot.cy.js | 4 +- ...st but one will be overflowed (failed).png | Bin 82772 -> 0 bytes vuu-ui/cypress/support/component/index.css | 2 +- .../cypress/support/component/index.css.map | 7 + vuu-ui/cypress/support/e2e/constants.ts | 2 +- vuu-ui/global.d.ts | 3 + vuu-ui/package-lock.json | 1029 +++++++++++++---- vuu-ui/package.json | 7 +- .../src/codemirror-basic-setup.ts | 16 +- vuu-ui/packages/vuu-codemirror/tsconfig.json | 6 + .../packages/vuu-data-ag-grid/tsconfig.json | 6 + .../src/hooks/useTypeaheadSuggestions.ts | 46 +- .../src/hooks/useVuuMenuActions.ts | 3 +- vuu-ui/packages/vuu-data-react/tsconfig.json | 6 + vuu-ui/packages/vuu-data-test/package.json | 15 + .../vuu-data-test/src}/UpdateGenerator.ts | 51 +- .../src/basket/basket-schemas.ts | 125 ++ .../data-generators/basket-generator.ts | 10 + .../basketConstituent-generator.ts | 13 + .../basketTrading-generator.ts | 13 + .../basketTradingConstituent-generator.ts | 13 + .../src/basket/data-generators/index.ts | 4 + .../src/basket/reference-data/basket.ts | 22 + .../reference-data/basketConstituent.ts | 43 + .../basket/reference-data/basketTrading.ts | 45 + .../basketTradingConstituent.ts | 16 + .../src/basket/reference-data/ftse100.ts | 102 ++ .../src/basket/reference-data/index.ts | 13 + .../vuu-data-test/src/generatorTemplate.ts | 44 + vuu-ui/packages/vuu-data-test/src/index.ts | 2 + .../vuu-data-test/src}/rowUpdates.ts | 3 +- vuu-ui/packages/vuu-data-test/src/schemas.ts | 29 + .../data-generators}/child-order-generator.ts | 19 +- .../data-generators/generate-data-utils.ts | 60 + .../simul/data-generators}/generatedData.ts | 0 .../src/simul/data-generators/index.ts | 6 + .../data-generators}/instrument-generator.ts | 23 +- .../instrument-prices-generator.ts | 25 +- .../simul/data-generators}/order-generator.ts | 17 +- .../parent-order-generator.ts | 19 +- .../data-generators}/prices-generator.ts | 25 +- .../packages/vuu-data-test/src/simul/index.ts | 0 .../src/simul}/reference-data/currencies.ts | 0 .../src/simul}/reference-data/index.ts | 4 - .../reference-data/instrument-prices.ts | 0 .../src/simul}/reference-data/instruments.ts | 0 .../src/simul}/reference-data/locations.ts | 0 .../src/simul}/reference-data/lotsizes.ts | 0 .../simul}/reference-data/priceStrategies.ts | 4 +- .../src/simul}/reference-data/prices.ts | 0 .../src/simul}/reference-data/utils.ts | 6 + .../vuu-data-test/src/simul/simul-schemas.ts | 137 +++ .../vuu-data-test/src/vuu-row-generator.ts | 110 ++ vuu-ui/packages/vuu-data-test/tsconfig.json | 6 + vuu-ui/packages/vuu-data-types/tsconfig.json | 6 + .../array-data-source/array-data-source.ts | 101 +- .../vuu-data/src/connection-manager.ts | 19 + vuu-ui/packages/vuu-data/src/data-source.ts | 21 +- .../packages/vuu-data/src/json-data-source.ts | 40 +- .../vuu-data/src/remote-data-source.ts | 37 +- .../array-backed-moving-window.ts | 27 +- .../vuu-data/src/server-proxy/server-proxy.ts | 4 +- .../vuu-data/src/server-proxy/viewport.ts | 4 +- .../vuu-data/src/vuuUIMessageTypes.ts | 2 + .../vuu-data/src/websocket-connection.ts | 73 +- vuu-ui/packages/vuu-data/src/worker.ts | 17 +- .../vuu-data/test/server-proxy.test.ts | 26 +- vuu-ui/packages/vuu-data/test/test-utils.ts | 5 +- .../test/websocket-connection.test.ts | 109 ++ vuu-ui/packages/vuu-data/tsconfig.json | 6 + vuu-ui/packages/vuu-datagrid-types/index.d.ts | 5 +- .../packages/vuu-datagrid-types/tsconfig.json | 6 + .../vuu-datagrid/src/ColumnBearer.tsx | 4 +- vuu-ui/packages/vuu-datagrid/src/Viewport.tsx | 2 +- .../src/cell-renderers/progress-cell.tsx | 2 +- .../build-context-menu-descriptors.ts | 2 +- .../src/context-menu/useContextMenu.ts | 2 +- .../src/grid-cells/HeaderCell.tsx | 2 +- .../src/grid-cells/filter-indicator.tsx | 2 +- .../vuu-datagrid/src/grid-context.tsx | 2 +- .../src/grid-hooks/use-data-source.ts | 2 +- .../src/grid-hooks/useGridActionDispatcher.ts | 2 +- vuu-ui/packages/vuu-datagrid/tsconfig.json | 6 + vuu-ui/packages/vuu-datatable/package.json | 3 + .../configurable-table/ConfigurableTable.tsx | 2 +- .../src/filter-table/FilterTable.css | 1 + .../src/json-table/JsonTable.tsx | 51 +- vuu-ui/packages/vuu-datatable/tsconfig.json | 6 + .../packages/vuu-filter-parser/tsconfig.json | 6 + .../packages/vuu-filter-types/tsconfig.json | 6 + .../vuu-filters/src/filter-bar/FilterBar.css | 9 +- .../vuu-filters/src/filter-bar/FilterBar.tsx | 42 +- .../src/filter-bar/useFilterBar.ts | 117 +- .../vuu-filters/src/filter-bar/useFilters.ts | 2 +- .../filter-builder-menu/FilterBuilderMenu.tsx | 9 +- .../src/filter-clause/CloseButton.css | 23 - .../src/filter-clause/CloseButton.tsx | 16 - .../src/filter-clause/ExpandoCombobox.tsx | 107 +- .../src/filter-clause/FilterClauseEditor.tsx | 41 +- .../src/filter-clause/TextInput.tsx | 41 +- .../src/filter-clause/filterClauseTypes.ts | 2 +- .../filter-clause/useFilterClauseEditor.ts | 90 +- .../src/filter-input/FilterInput.css | 2 +- .../vuu-filters/src/filter-input/theme.ts | 1 + .../src/filter-pill/FilterPill.css | 9 +- .../src/filter-pill/FilterPill.tsx | 2 +- vuu-ui/packages/vuu-filters/tsconfig.json | 6 + vuu-ui/packages/vuu-icons/index.css | 40 +- vuu-ui/packages/vuu-layout/package.json | 3 + .../OverflowContainer.cy.tsx | 29 +- .../packages/vuu-layout/src/common-types.ts | 9 - .../vuu-layout/src/drag-drop/BoxModel.ts | 16 +- .../vuu-layout/src/drag-drop/Draggable.ts | 4 +- .../vuu-layout/src/drag-drop/DropTarget.ts | 2 +- .../vuu-layout/src/drag-drop/dragDropTypes.ts | 4 +- vuu-ui/packages/vuu-layout/src/index.ts | 1 - .../LayoutPersistenceManager.ts | 28 +- .../LocalLayoutPersistenceManager.ts | 222 +++- .../RemoteLayoutPersistenceManager.ts | 146 +++ .../vuu-layout/src/layout-persistence/data.ts | 43 + .../src/layout-persistence/index.ts | 7 +- .../useLayoutContextMenuItems.tsx | 78 ++ .../src/layout-provider/LayoutProvider.tsx | 7 +- .../src/layout-reducer/flexUtils.ts | 17 +- .../layout-reducer/insert-layout-element.ts | 9 +- .../src/layout-reducer/layoutTypes.ts | 4 +- .../src/layout-reducer/layoutUtils.ts | 4 +- .../layout-reducer/resize-flex-children.ts | 2 +- .../src/layout-reducer/wrap-layout-element.ts | 5 +- .../measured-container/MeasuredContainer.css | 2 + .../measured-container/MeasuredContainer.tsx | 6 + .../useMeasuredContainer.ts | 19 +- .../measured-container/useResizeObserver.ts | 18 +- .../overflow-container/OverflowContainer.css | 27 +- .../overflow-container/OverflowContainer.tsx | 9 +- .../src/overflow-container/overflow-utils.ts | 16 +- .../useOverflowContainer.ts | 30 +- .../vuu-layout/src/palette/Palette.tsx | 19 +- .../packages/vuu-layout/src/stack/Stack.tsx | 3 +- .../vuu-layout/src/toolbar/Toolbar.css | 17 +- .../vuu-layout/src/toolbar/Toolbar.tsx | 44 +- ...trip-dom-utils.ts => toolbar-dom-utils.ts} | 5 +- .../src/toolbar/useKeyboardNavigation.ts | 165 ++- .../vuu-layout/src/toolbar/useToolbar.ts | 19 +- .../packages/vuu-layout/test/global-mocks.ts | 23 + .../LocalLayoutPersistenceManager.test.ts | 473 ++++++++ .../RemoteLayoutPersistenceManager.test.ts | 304 +++++ .../test/layout-persistence/utils.ts | 8 + vuu-ui/packages/vuu-layout/tsconfig.json | 6 + vuu-ui/packages/vuu-popups/package.json | 6 +- .../src/dialog-header/DialogHeader.css | 25 + .../src/dialog-header/DialogHeader.tsx | 34 + .../vuu-popups/src/dialog-header/index.ts | 1 + .../packages/vuu-popups/src/dialog/Dialog.css | 30 - .../packages/vuu-popups/src/dialog/Dialog.tsx | 39 +- .../packages/vuu-popups/src/dialog/index.ts | 1 + .../vuu-popups/src/dialog/useDialog.tsx | 34 + vuu-ui/packages/vuu-popups/src/index.ts | 2 + .../vuu-popups/src/menu/ContextMenu.css | 7 - .../vuu-popups/src/menu/ContextMenu.tsx | 77 +- .../packages/vuu-popups/src/menu/MenuList.css | 2 +- .../packages/vuu-popups/src/menu/MenuList.tsx | 19 +- .../vuu-popups/src/menu/list-dom-utils.ts | 7 +- .../vuu-popups/src/menu/use-cascade.ts | 45 +- .../src/menu/use-keyboard-navigation.ts | 7 +- .../vuu-popups/src/menu/useContextMenu.tsx | 39 +- vuu-ui/packages/vuu-popups/src/menu/utils.ts | 2 +- .../notifications/NotificationsProvider.tsx | 124 ++ .../vuu-popups/src/notifications/index.ts | 1 + .../src/notifications/notifications.css | 82 ++ .../vuu-popups/src/popup-menu/PopupMenu.css | 10 +- .../vuu-popups/src/popup-menu/PopupMenu.tsx | 32 +- .../packages/vuu-popups/src/popup/Popup.tsx | 32 +- .../vuu-popups/src/popup/popup-service.ts | 82 +- .../src/popup/useAnchoredPosition.ts | 110 +- .../portal-deprecated/PortalDeprecated.tsx | 58 - .../vuu-popups/src/portal-deprecated/index.ts | 2 - .../src/portal-deprecated/portal-utils.ts | 9 - .../packages/vuu-popups/src/portal/Portal.css | 13 +- .../packages/vuu-popups/src/portal/Portal.tsx | 31 +- .../packages/vuu-popups/src/prompt/Prompt.tsx | 4 +- vuu-ui/packages/vuu-popups/tsconfig.json | 6 + .../packages/vuu-protocol-types/tsconfig.json | 6 + vuu-ui/packages/vuu-shell/package.json | 5 + .../src/feature-list/FeatureList.css | 12 +- .../src/feature-list/FeatureList.tsx | 39 +- vuu-ui/packages/vuu-shell/src/index.ts | 1 - .../vuu-shell/src/layout-config/index.ts | 1 - .../src/layout-config/local-config.ts | 39 - .../src/layout-config/remote-config.ts | 50 - .../src/layout-config/use-layout-config.ts | 61 - .../src/layout-management/LayoutList.css | 6 + .../src/layout-management/LayoutList.tsx | 119 +- .../src/layout-management/SaveLayoutPanel.tsx | 77 +- .../src/layout-management/layoutTypes.ts | 18 +- .../layout-management/useLayoutManager.tsx | 146 ++- .../vuu-shell/src/left-nav/LeftNav.css | 2 +- .../vuu-shell/src/left-nav/LeftNav.tsx | 9 +- vuu-ui/packages/vuu-shell/src/shell.css | 4 + vuu-ui/packages/vuu-shell/src/shell.tsx | 39 +- vuu-ui/packages/vuu-shell/src/shellTypes.ts | 12 +- .../src/theme-provider/ThemeProvider.tsx | 22 +- vuu-ui/packages/vuu-shell/tsconfig.json | 6 + .../background-cell/BackgroundCell.tsx | 2 - .../src/cell-renderers-next/index.ts | 1 + .../progress-cell/ProgressCell.tsx | 36 +- .../ColumnExpressionInput.css | 25 +- .../ColumnExpressionInput.tsx | 40 +- .../column-function-descriptors.ts | 43 +- .../ColumnExpressionLanguage.ts | 1 + .../ColumnExpressionTreeWalker.ts | 5 +- .../generated/column-parser.js | 8 +- .../grammar/column.grammar | 7 +- .../functionDocInfo.ts | 4 +- .../column-expression-input/highlighting.ts | 4 + .../src/column-expression-input/theme.ts | 9 + .../useColumnAutoComplete.ts | 10 +- .../useColumnExpressionEditor.ts | 25 +- .../useColumnExpressionSuggestionProvider.ts | 2 +- .../ColumnExpressionPanel.tsx | 55 +- .../useColumnExpression.ts | 84 +- .../ColumnFormattingPanel.tsx | 15 +- .../NumericFormattingSettings.tsx | 6 +- .../src/column-formatting-settings/index.ts | 1 + .../src/column-list/ColumnList.css | 6 + .../src/column-list/ColumnList.tsx | 31 +- .../src/column-settings/ColumnNameLabel.css | 13 + .../src/column-settings/ColumnNameLabel.tsx | 45 + .../column-settings/ColumnSettingsPanel.css | 3 + .../column-settings/ColumnSettingsPanel.tsx | 74 +- .../src/column-settings/useColumnSettings.ts | 62 +- vuu-ui/packages/vuu-table-extras/src/index.ts | 1 + .../src/table-settings/TableSettingsPanel.tsx | 9 +- .../src/useTableAndColumnSettings.ts | 25 + .../test/ColumnExpressionTreeWalker.test.ts | 14 + .../test/column-parser.test.ts | 8 + .../packages/vuu-table-extras/tsconfig.json | 6 + vuu-ui/packages/vuu-table/package.json | 4 + vuu-ui/packages/vuu-table/src/index.ts | 1 + .../packages/vuu-table/src/table-next/Row.css | 2 +- .../packages/vuu-table/src/table-next/Row.tsx | 12 +- .../vuu-table/src/table-next/TableNext.css | 4 + .../vuu-table/src/table-next/TableNext.tsx | 66 +- .../dropdown-cell/DropdownCell.tsx | 4 +- .../column-header-pill/ColumnHeaderPill.css | 3 +- .../column-header-pill/ColumnHeaderPill.tsx | 2 +- .../column-header-pill/GroupColumnPill.tsx | 2 +- .../src/table-next/column-menu/ColumnMenu.css | 12 +- .../src/table-next/column-menu/ColumnMenu.tsx | 1 - .../column-resizing/ColumnResizer.css | 10 +- .../header-cell/GroupHeaderCellNext.tsx | 25 +- .../src/table-next/header-cell/HeaderCell.css | 18 +- .../vuu-table/src/table-next/index.ts | 1 + .../vuu-table/src/table-next/moving-window.ts | 19 +- .../src/table-next/table-cell/TableCell.css | 2 +- .../src/table-next/table-cell/TableCell.tsx | 21 +- .../vuu-table/src/table-next/useDataSource.ts | 23 +- .../src/table-next/useKeyboardNavigation.ts | 51 +- .../src/table-next/useMeasuredContainer.ts | 147 --- .../src/table-next/useTableContextMenu.ts | 19 +- .../vuu-table/src/table-next/useTableModel.ts | 7 +- .../vuu-table/src/table-next/useTableNext.ts | 68 +- vuu-ui/packages/vuu-table/src/table/Table.tsx | 10 +- .../vuu-table/src/table/dataTableTypes.ts | 28 +- .../vuu-table/src/table/useDataSource.ts | 15 +- .../vuu-table/src/table/useSelection.ts | 10 +- .../packages/vuu-table/src/table/useTable.ts | 4 - .../vuu-table/src/table/useTableModel.ts | 12 +- vuu-ui/packages/vuu-table/tsconfig.json | 6 + .../vuu-theme/css/components/button.css | 17 +- .../vuu-theme/css/components/checkbox.css | 23 +- .../vuu-theme/css/components/input.css | 7 +- .../vuu-theme/css/components/switch.css | 16 +- .../vuu-theme/css/foundations/color.css | 7 +- .../vuu-theme/css/foundations/typography.css | 2 +- vuu-ui/packages/vuu-theme/css/global.css | 12 - .../vuu-theme/css/palette/interact.css | 12 +- .../packages/vuu-theme/fonts/NunitoSans.css | 58 + .../vuu-theme/fonts/NunitoSansv15.woff2 | Bin 0 -> 22144 bytes .../vuu-theme/fonts/NunitoSansv15Latin.woff2 | Bin 0 -> 49876 bytes .../packages/vuu-theme/fonts/SomeTypeMono.css | 8 + .../vuu-theme/fonts/SomeTypeMonov1-500.woff2 | Bin 0 -> 10044 bytes vuu-ui/packages/vuu-theme/index.css | 2 + .../src/combo-box/ComboBox.tsx | 115 +- .../src/combo-box/useCombobox.ts | 402 +++---- .../src/common-hooks/collectionTypes.ts | 28 +- .../src/common-hooks/navigationTypes.ts | 12 +- .../src/common-hooks/selectionTypes.ts | 111 +- .../src/common-hooks/useCollectionItems.ts | 103 +- .../src/common-hooks/useSelection.ts | 70 +- .../src/drag-drop/DragDropProvider.tsx | 105 +- .../src/drag-drop/DragDropState.ts | 36 + .../src/drag-drop/Draggable.tsx | 33 +- .../src/drag-drop/DropIndicator.tsx | 6 +- .../src/drag-drop/drag-utils.ts | 218 ---- .../src/drag-drop/dragDropTypesNext.ts | 35 +- .../src/drag-drop/drop-target-utils.ts | 22 +- .../src/drag-drop/useDragDisplacers.ts | 60 +- .../src/drag-drop/useDragDropIndicator.tsx | 45 +- .../useDragDropNaturalMovementNext.tsx | 149 +-- .../src/drag-drop/useDragDropNext.tsx | 363 ++++-- .../src/drag-drop/useDragSpacers.ts | 128 -- .../src/drag-drop/useGlobalDragDrop.ts | 89 ++ .../vuu-ui-controls/src/dropdown/Dropdown.tsx | 78 +- .../src/dropdown/DropdownBase.tsx | 11 +- .../src/dropdown/dropdownTypes.ts | 34 +- .../src/dropdown/useClickAway.ts | 7 +- .../src/dropdown/useDropdown.ts | 61 +- .../src/dropdown/useDropdownBase.ts | 39 +- .../src/editable-label/EditableLabel.css | 2 +- .../src/expando-input/ExpandoInput.css | 2 + vuu-ui/packages/vuu-ui-controls/src/index.ts | 2 + .../instrument-picker/InstrumentPicker.css | 19 + .../instrument-picker/InstrumentPicker.tsx | 116 ++ .../src/instrument-picker/SearchCell.css | 14 + .../src/instrument-picker/SearchCell.tsx | 29 + .../src/instrument-picker/index.ts | 1 + .../src/instrument-picker/moving-window.ts | 68 ++ .../src/instrument-picker/useDataSource.ts | 58 + .../instrument-picker/useInstrumentPicker.ts | 110 ++ .../instrument-search/InstrumentSearch.css | 1 - .../instrument-search/InstrumentSearch.tsx | 75 +- .../src/instrument-search/SearchCell.tsx | 12 +- .../src/instrument-search/moving-window.ts | 2 +- .../src/instrument-search/useDataSource.ts | 2 +- .../vuu-ui-controls/src/list/List.css | 6 +- .../vuu-ui-controls/src/list/List.tsx | 39 +- .../src/list/VirtualizedList.tsx | 19 +- .../common-hooks/useKeyboardNavigation.ts | 86 +- .../src/list/common-hooks/utils/isSelected.ts | 8 +- .../vuu-ui-controls/src/list/listTypes.ts | 82 +- .../vuu-ui-controls/src/list/useList.ts | 161 +-- .../vuu-ui-controls/src/list/useListDrop.ts | 113 ++ .../vuu-ui-controls/src/list/useListHeight.ts | 1 - .../src/list/useVirtualization.ts | 2 +- .../src/price-ticker/PriceTicker.css | 41 + .../src/price-ticker/PriceTicker.tsx | 60 + .../vuu-ui-controls/src/price-ticker/index.ts | 1 + .../vuu-ui-controls/src/tabstrip/Tab.tsx | 4 +- .../vuu-ui-controls/src/tabstrip/TabMenu.tsx | 2 +- .../src/tabstrip/TabMenuOptions.ts | 2 +- .../vuu-ui-controls/src/tabstrip/TabsTypes.ts | 6 +- .../vuu-ui-controls/src/tabstrip/Tabstrip.css | 2 + .../vuu-ui-controls/src/tabstrip/Tabstrip.tsx | 19 +- .../src/vuu-input/VuuInput.tsx | 47 +- vuu-ui/packages/vuu-ui-controls/tsconfig.json | 6 + vuu-ui/packages/vuu-utils/src/array-utils.ts | 18 + vuu-ui/packages/vuu-utils/src/box-utils.ts | 15 + vuu-ui/packages/vuu-utils/src/column-utils.ts | 53 +- vuu-ui/packages/vuu-utils/src/data-utils.ts | 5 +- .../packages/vuu-utils/src/event-emitter.ts | 9 + vuu-ui/packages/vuu-utils/src/filter-utils.ts | 5 +- vuu-ui/packages/vuu-utils/src/index.ts | 3 +- .../packages/vuu-utils/src/selection-utils.ts | 2 + .../vuu-utils/test/filter-utils.test.ts | 18 + .../vuu-utils/test/json-utils.test.ts | 73 ++ vuu-ui/packages/vuu-utils/tsconfig.json | 6 + .../app-vuu-basket-trader/index.tsx | 10 +- .../app-vuu-basket-trader/login.css | 1 - .../app-vuu-basket-trader/public/demo.html | 3 - .../app-vuu-basket-trader/public/index.html | 3 - .../app-vuu-basket-trader/public/login.html | 3 - .../app-vuu-basket-trader/src/App.css | 15 +- .../app-vuu-basket-trader/src/App.tsx | 71 +- .../src/defaultLayout.ts | 24 - .../app-vuu-basket-trader/src/useFeatures.ts | 30 +- .../src/useRpcResponseHandler.tsx | 73 ++ vuu-ui/sample-apps/app-vuu-example/index.tsx | 10 +- vuu-ui/sample-apps/app-vuu-example/login.css | 1 - .../sample-apps/app-vuu-example/src/App.css | 7 + .../sample-apps/app-vuu-example/src/App.tsx | 39 +- .../app-vuu-example/src/AppStack.tsx | 22 - .../app-vuu-example/src/createPlaceholder.tsx | 13 + .../feature-basket-trading/index.ts | 4 + .../feature-basket-trading/package.json | 21 +- .../src/VuuBasketTradingFeature.tsx | 179 ++- .../src/basket-selector/BasketSelector.css | 93 +- .../src/basket-selector/BasketSelector.tsx | 105 +- .../src/basket-selector/BasketSelectorRow.css | 41 + .../src/basket-selector/BasketSelectorRow.tsx | 58 + .../src/basket-selector/useBasketSelector.ts | 141 +++ .../src/basket-table-edit/BasketTableEdit.tsx | 12 +- .../src/basket-table-live/BasketTableLive.css | 5 + .../src/basket-table-live/BasketTableLive.tsx | 153 ++- .../src/basket-toolbar/BasketMenu.css | 5 + .../src/basket-toolbar/BasketMenu.tsx | 68 ++ .../src/basket-toolbar/BasketToolbar.css | 47 + .../src/basket-toolbar/BasketToolbar.tsx | 116 +- .../src/basket-toolbar/TabMenuOptions.ts | 29 + .../src/cell-renderers/index.ts | 3 + .../progress-cell/ProgressCell.css | 20 + .../progress-cell/ProgressCell.tsx | 68 ++ .../cell-renderers/spread-cell/SpreadCell.css | 35 + .../cell-renderers/spread-cell/SpreadCell.tsx | 63 + .../cell-renderers/status-cell/StatusCell.css | 38 + .../cell-renderers/status-cell/StatusCell.tsx | 33 + .../empty-baskets-panel/EmptyBasketsPanel.css | 21 + .../empty-baskets-panel/EmptyBasketsPanel.tsx | 30 + .../src/empty-baskets-panel/index.ts | 1 + .../src/new-basket-panel/NewBasketPanel.css | 23 + .../src/new-basket-panel/NewBasketPanel.tsx | 114 ++ .../src/new-basket-panel/index.ts | 1 + .../src/new-basket-panel/useNewBasketPanel.ts | 113 ++ .../src/useBasketTabMenu.ts | 8 +- .../src/useBasketTrading.tsx | 69 ++ .../src/useBasketTradingDatasources.ts | 99 ++ .../feature-basket-trading/tsconfig.json | 6 + .../src/VuuFilterTableFeature.css | 10 + .../src/VuuFilterTableFeature.tsx | 276 +---- .../src/useFilterTable.tsx | 231 ++++ .../src/useSessionDataSource.ts | 91 ++ .../feature-instrument-tiles/package.json | 5 +- .../src/InstrumentTile.tsx | 2 +- .../src/PriceTicker.css | 32 - .../src/PriceTicker.tsx | 43 - .../src/VuuInstrumentTilesFeature.tsx | 4 +- .../src/moving-window.ts | 2 +- .../src/useDataSource.ts | 2 +- .../src/ConfigurableDataTable.tsx | 2 +- vuu-ui/scripts/build-all-type-defs.mjs | 1 + vuu-ui/scripts/build-all.mjs | 1 + vuu-ui/scripts/launch-app.mjs | 7 +- vuu-ui/scripts/publish.mjs | 2 + vuu-ui/showcase/package.json | 1 + vuu-ui/showcase/scripts/build-file-list.mjs | 18 + vuu-ui/showcase/scripts/build.mjs | 23 +- .../TableSettings/TableSettings.examples.tsx | 91 -- .../src/examples/AppComponents/index.ts | 1 - .../src/examples/Apps/NewTheme.examples.css | 16 +- .../src/examples/Apps/NewTheme.examples.tsx | 195 +--- .../src/examples/DataGrid/Grid.examples.tsx | 21 +- .../DataTable/FilterTable.examples.tsx | 57 +- .../JsonTable.examples.tsx | 62 +- .../showcase/src/examples/DataTable/index.ts | 1 + .../Filters/FilterBar/FilterBar.examples.tsx | 75 +- .../FilterBar/FilterClause.examples.tsx | 82 +- .../src/examples/Filters/FilterBar/index.ts | 2 +- .../Layout/LayoutsBrowser.examples.tsx | 14 +- .../Layout/OverflowContainer.examples.css | 4 + .../Layout/OverflowContainer.examples.tsx | 82 +- .../examples/Layout/SavePanel.examples.tsx | 24 +- .../src/examples/Layout/Toolbar.examples.css | 7 +- .../src/examples/Layout/Toolbar.examples.tsx | 12 +- vuu-ui/showcase/src/examples/Layout/index.ts | 1 - .../Performance/Performance.examples.tsx | 2 +- .../examples/Popups/ContextMenu.examples.tsx | 7 +- .../{Layout => Popups}/Dialog.examples.tsx | 0 .../Notifications/Notifications.examples.tsx | 117 ++ .../examples/Popups/Notifications/index.ts | 1 + .../examples/Popups/PopupMenu.examples.tsx | 53 + .../src/examples/Popups/Tooltip.examples.css | 1 + vuu-ui/showcase/src/examples/Popups/index.ts | 2 + .../Shell/ConnectionMetrics.examples.tsx | 5 +- .../src/examples/Shell/Feature.examples.tsx | 4 +- .../src/examples/Shell/LeftNav.examples.tsx | 8 + .../Shell/SessionTableEditing.examples.tsx | 3 +- .../src/examples/Shell/Shell.examples.tsx | 57 - .../examples/Shell/ThemeProvider.examples.tsx | 2 +- vuu-ui/showcase/src/examples/Shell/index.ts | 1 + .../ShowcaseControls/Tree.examples.tsx | 2 +- .../examples/Table/BasketTables.examples.tsx | 126 ++ .../Table/TableArrayData.examples.tsx | 102 +- .../src/examples/Table/TableList.examples.tsx | 2 +- .../src/examples/Table/TableNext.examples.tsx | 230 +++- .../examples/Table/TableVuuData.examples.tsx | 19 +- vuu-ui/showcase/src/examples/Table/index.ts | 2 +- .../ColumnExpressionInput.examples.tsx | 47 +- .../ColumnExpressionPanel.examples.tsx | 23 +- .../ColumnSettings.examples.tsx | 158 +++ .../TableExtras/ColumnSettings/index.ts | 1 + .../TableSettings/TableSettings.examples.tsx | 170 +++ .../TableSettings/index.ts | 0 .../src/examples/TableExtras/index.ts | 2 + .../examples/UiControls/Combobox.examples.tsx | 170 ++- .../examples/UiControls/DragDrop.examples.tsx | 126 ++ .../examples/UiControls/Dropdown.examples.tsx | 38 +- .../UiControls/InstrumentPicker.examples.tsx | 100 ++ .../UiControls/InstrumentSearch.examples.tsx | 25 +- .../src/examples/UiControls/List.examples.tsx | 141 ++- .../showcase/src/examples/UiControls/index.ts | 2 + .../VuuFeatures/BasketSelector.examples.tsx | 38 + .../VuuFeatures/BasketToolbar.examples.tsx | 90 ++ .../BasketTradingFeature.examples.tsx | 179 ++- .../FilterTableFeature.examples.tsx | 88 +- .../InstrumentTilesFeature.examples.tsx | 28 +- .../VuuFeatures/NewBasketPanel.examples.tsx | 29 + .../VuuFeatures/TableNextFeature.examples.tsx | 85 -- .../src/examples/VuuFeatures/index.ts | 4 +- .../src/examples/html/HtmlTable.examples.tsx | 12 +- .../html/LayoutExperiments.examples.tsx | 1 - ...hTranslateInlineScrollbarsCssVariables.tsx | 1 + .../vuu-table/VuuTable.tsx | 16 +- .../vuu-table/useTable.ts | 20 +- vuu-ui/showcase/src/examples/index.ts | 1 - .../src/examples/popups/Tooltip.examples.tsx | 6 +- .../examples/utils/TickingArrayDataSource.ts | 27 +- .../examples/utils/basket-design-generator.ts | 44 - .../examples/utils/createArrayDataSource.ts | 26 + .../src/examples/utils/generate-data-utils.ts | 63 - vuu-ui/showcase/src/examples/utils/index.ts | 2 - .../showcase/src/examples/utils/mock-data.ts | 4 +- .../utils/reference-data/basket-design.ts | 97 -- .../showcase/src/examples/utils/useColumns.ts | 46 - .../showcase/src/examples/utils/useSchemas.ts | 190 --- .../src/examples/utils/useTableConfig.ts | 5 +- .../src/examples/utils/vuu-row-generator.ts | 129 --- .../src/features/BasketTrading.feature.tsx | 58 +- .../BasketTradingNoBaskets.feature.tsx | 53 + .../BasketTradingOneBasket.feature.tsx | 53 + .../src/features/FilterTable.feature.tsx | 25 +- .../src/features/InstrumentTiles.feature.tsx | 1 - .../src/features/TableNext.feature.css | 10 - .../src/features/TableNext.feature.tsx | 130 --- vuu-ui/showcase/src/index.css | 13 +- vuu-ui/showcase/src/index.tsx | 31 +- vuu-ui/showcase/templates/index-preview.html | 3 - vuu-ui/showcase/templates/index.html | 3 - vuu-ui/showcase/vite.config.js | 2 + vuu-ui/tools/electron/package-lock.json | 1026 ++++++---------- vuu-ui/tools/electron/package.json | 2 +- vuu-ui/tsconfig-typecheck.json | 31 + vuu-ui/tsconfig.json | 35 +- vuu-ui/vitest.config.js | 3 +- .../runconfigurations/SimulMain.run.xml | 4 +- .../static/{ftse.csv => ftse100.csv} | 0 vuu/src/main/resources/static/hsi.csv | 81 ++ vuu/src/main/resources/static/nasdaq100.csv | 102 ++ vuu/src/main/resources/static/sp500.csv | 504 ++++++++ .../main/scala/org/finos/vuu/SimulMain.scala | 8 +- .../scala/org/finos/vuu/core/VuuServer.scala | 3 +- .../finos/vuu/core/module/ModuleFactory.scala | 56 +- .../vuu/core/module/TableDefContainer.scala | 23 + .../org/finos/vuu/core/module/VsModule.scala | 2 + .../vuu/core/module/authn/AuthNModule.scala | 4 +- .../core/module/auths/PermissionModule.scala | 4 +- .../auths/provider/PermissionsProvider.scala | 6 +- .../core/module/basket/BasketConstants.scala | 10 + .../vuu/core/module/basket/BasketModule.scala | 193 ++++ .../module/basket/csv/CsvStaticLoader.scala | 87 ++ .../module/basket/provider/AlgoProvider.scala | 34 + .../provider/BasketConstituentProvider.scala | 54 + .../basket/provider/BasketProvider.scala | 29 + .../module/basket/provider/NullProvider.scala | 8 + .../provider/PriceStrategyProvider.scala | 34 + .../module/basket/service/BasketService.scala | 80 ++ .../BasketTradingConstituentService.scala | 71 ++ .../core/module/editable/EditableModule.scala | 4 +- .../core/module/metrics/MetricsModule.scala | 4 +- .../vuu/core/module/price/PriceModule.scala | 34 + .../core/module/simul/SimulationModule.scala | 39 +- .../simul/provider/OrdersSimulProvider.scala | 10 +- .../module/typeahead/TypeAheadModule.scala | 4 +- .../vuu/core/module/vui/VuiStateModule.scala | 4 +- .../org/finos/vuu/core/table/Column.scala | 4 +- .../org/finos/vuu/provider/Provider.scala | 13 + .../vuu/provider/VuuJoinTableProvider.scala | 3 + .../vuu/core/module/ModuleSyntaxTest.scala | 1 + .../finos/vuu/core/module/TestModule.scala | 2 +- .../finos/vuu/core/module/TestModule2.scala | 10 +- .../core/module/authn/AuthNServerTest.scala | 2 + .../vuu/core/module/core/CoreModuleTest.scala | 2 + .../modulefrommodule/InstrumentModule.scala | 33 + .../module/modulefrommodule/JoinModule.scala | 32 + .../ModuleConstructorTest.scala | 60 + .../module/modulefrommodule/PriceModule.scala | 29 + .../core/table/{ => join}/JoinTableTest.scala | 3 +- .../{ => join}/JoinsOfJoinsTableTest.scala | 3 +- .../table/{ => join}/MultiJoinTableTest.scala | 7 +- .../finos/vuu/net/rest/RestServiceTest.scala | 2 + .../org/finos/vuu/net/rpc/RpcModuleTest.scala | 3 +- .../BasketConstituentProviderTest.scala | 102 ++ .../vuu/provider/BasketProviderTest.scala | 55 + .../viewport/CreateViewPortScenarioTest.scala | 6 +- .../AbstractSessionTestCase.scala | 3 +- .../SessionTableViewportTest.scala | 10 +- 597 files changed, 16014 insertions(+), 6875 deletions(-) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java rename layout-server/src/main/java/org/finos/vuu/layoutserver/{controller => exceptions}/GlobalExceptionHandler.java (61%) create mode 100644 layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java create mode 100644 layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java delete mode 100644 vuu-ui/cypress/screenshots/vuu-ui-controls/src/__tests__/__e2e__/tabstrip/Tabstrip.cy.tsx/WHEN initial size is sufficient to display all contents -- WHEN resized such that space is sufficient for only 4 tabs (LAST tab selected) -- THEN as last tab is selected, last but one will be overflowed (failed).png create mode 100644 vuu-ui/cypress/support/component/index.css.map create mode 100644 vuu-ui/global.d.ts create mode 100644 vuu-ui/packages/vuu-codemirror/tsconfig.json create mode 100644 vuu-ui/packages/vuu-data-ag-grid/tsconfig.json create mode 100644 vuu-ui/packages/vuu-data-react/tsconfig.json create mode 100644 vuu-ui/packages/vuu-data-test/package.json rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src}/UpdateGenerator.ts (53%) create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/ftse100.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/generatorTemplate.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/index.ts rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src}/rowUpdates.ts (90%) create mode 100644 vuu-ui/packages/vuu-data-test/src/schemas.ts rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul/data-generators}/child-order-generator.ts (78%) create mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul/data-generators}/generatedData.ts (100%) create mode 100644 vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul/data-generators}/instrument-generator.ts (74%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul/data-generators}/instrument-prices-generator.ts (70%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul/data-generators}/order-generator.ts (83%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul/data-generators}/parent-order-generator.ts (77%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul/data-generators}/prices-generator.ts (59%) create mode 100644 vuu-ui/packages/vuu-data-test/src/simul/index.ts rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/currencies.ts (100%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/index.ts (77%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/instrument-prices.ts (100%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/instruments.ts (100%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/locations.ts (100%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/lotsizes.ts (100%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/priceStrategies.ts (71%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/prices.ts (100%) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src/simul}/reference-data/utils.ts (53%) create mode 100644 vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts create mode 100644 vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts create mode 100644 vuu-ui/packages/vuu-data-test/tsconfig.json create mode 100644 vuu-ui/packages/vuu-data-types/tsconfig.json create mode 100644 vuu-ui/packages/vuu-data/test/websocket-connection.test.ts create mode 100644 vuu-ui/packages/vuu-data/tsconfig.json create mode 100644 vuu-ui/packages/vuu-datagrid-types/tsconfig.json create mode 100644 vuu-ui/packages/vuu-datagrid/tsconfig.json create mode 100644 vuu-ui/packages/vuu-datatable/tsconfig.json create mode 100644 vuu-ui/packages/vuu-filter-parser/tsconfig.json create mode 100644 vuu-ui/packages/vuu-filter-types/tsconfig.json delete mode 100644 vuu-ui/packages/vuu-filters/src/filter-clause/CloseButton.css delete mode 100644 vuu-ui/packages/vuu-filters/src/filter-clause/CloseButton.tsx create mode 100644 vuu-ui/packages/vuu-filters/tsconfig.json delete mode 100644 vuu-ui/packages/vuu-layout/src/common-types.ts create mode 100644 vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts create mode 100644 vuu-ui/packages/vuu-layout/src/layout-persistence/data.ts create mode 100644 vuu-ui/packages/vuu-layout/src/layout-persistence/useLayoutContextMenuItems.tsx rename vuu-ui/packages/vuu-layout/src/toolbar/{tabstrip-dom-utils.ts => toolbar-dom-utils.ts} (85%) create mode 100644 vuu-ui/packages/vuu-layout/test/global-mocks.ts create mode 100644 vuu-ui/packages/vuu-layout/test/layout-persistence/LocalLayoutPersistenceManager.test.ts create mode 100644 vuu-ui/packages/vuu-layout/test/layout-persistence/RemoteLayoutPersistenceManager.test.ts create mode 100644 vuu-ui/packages/vuu-layout/test/layout-persistence/utils.ts create mode 100644 vuu-ui/packages/vuu-layout/tsconfig.json create mode 100644 vuu-ui/packages/vuu-popups/src/dialog-header/DialogHeader.css create mode 100644 vuu-ui/packages/vuu-popups/src/dialog-header/DialogHeader.tsx create mode 100644 vuu-ui/packages/vuu-popups/src/dialog-header/index.ts create mode 100644 vuu-ui/packages/vuu-popups/src/dialog/useDialog.tsx delete mode 100644 vuu-ui/packages/vuu-popups/src/menu/ContextMenu.css create mode 100644 vuu-ui/packages/vuu-popups/src/notifications/NotificationsProvider.tsx create mode 100644 vuu-ui/packages/vuu-popups/src/notifications/index.ts create mode 100644 vuu-ui/packages/vuu-popups/src/notifications/notifications.css delete mode 100644 vuu-ui/packages/vuu-popups/src/portal-deprecated/PortalDeprecated.tsx delete mode 100644 vuu-ui/packages/vuu-popups/src/portal-deprecated/portal-utils.ts create mode 100644 vuu-ui/packages/vuu-popups/tsconfig.json create mode 100644 vuu-ui/packages/vuu-protocol-types/tsconfig.json delete mode 100644 vuu-ui/packages/vuu-shell/src/layout-config/index.ts delete mode 100644 vuu-ui/packages/vuu-shell/src/layout-config/local-config.ts delete mode 100644 vuu-ui/packages/vuu-shell/src/layout-config/remote-config.ts delete mode 100644 vuu-ui/packages/vuu-shell/src/layout-config/use-layout-config.ts create mode 100644 vuu-ui/packages/vuu-shell/tsconfig.json create mode 100644 vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.css create mode 100644 vuu-ui/packages/vuu-table-extras/src/column-settings/ColumnNameLabel.tsx create mode 100644 vuu-ui/packages/vuu-table-extras/tsconfig.json delete mode 100644 vuu-ui/packages/vuu-table/src/table-next/useMeasuredContainer.ts create mode 100644 vuu-ui/packages/vuu-table/tsconfig.json create mode 100644 vuu-ui/packages/vuu-theme/fonts/NunitoSans.css create mode 100644 vuu-ui/packages/vuu-theme/fonts/NunitoSansv15.woff2 create mode 100644 vuu-ui/packages/vuu-theme/fonts/NunitoSansv15Latin.woff2 create mode 100644 vuu-ui/packages/vuu-theme/fonts/SomeTypeMono.css create mode 100644 vuu-ui/packages/vuu-theme/fonts/SomeTypeMonov1-500.woff2 create mode 100644 vuu-ui/packages/vuu-ui-controls/src/drag-drop/DragDropState.ts delete mode 100644 vuu-ui/packages/vuu-ui-controls/src/drag-drop/drag-utils.ts delete mode 100644 vuu-ui/packages/vuu-ui-controls/src/drag-drop/useDragSpacers.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/drag-drop/useGlobalDragDrop.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.css create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/InstrumentPicker.tsx create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/SearchCell.css create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/SearchCell.tsx create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/index.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/moving-window.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useDataSource.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/instrument-picker/useInstrumentPicker.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/list/useListDrop.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/src/price-ticker/PriceTicker.css create mode 100644 vuu-ui/packages/vuu-ui-controls/src/price-ticker/PriceTicker.tsx create mode 100644 vuu-ui/packages/vuu-ui-controls/src/price-ticker/index.ts create mode 100644 vuu-ui/packages/vuu-ui-controls/tsconfig.json create mode 100644 vuu-ui/packages/vuu-utils/src/box-utils.ts create mode 100644 vuu-ui/packages/vuu-utils/tsconfig.json delete mode 100644 vuu-ui/sample-apps/app-vuu-basket-trader/src/defaultLayout.ts create mode 100644 vuu-ui/sample-apps/app-vuu-basket-trader/src/useRpcResponseHandler.tsx delete mode 100644 vuu-ui/sample-apps/app-vuu-example/src/AppStack.tsx create mode 100644 vuu-ui/sample-apps/app-vuu-example/src/createPlaceholder.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.css create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/BasketSelectorRow.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/basket-selector/useBasketSelector.ts create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketMenu.css create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/BasketMenu.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/basket-toolbar/TabMenuOptions.ts create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/index.ts create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/progress-cell/ProgressCell.css create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/progress-cell/ProgressCell.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/spread-cell/SpreadCell.css create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/spread-cell/SpreadCell.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/status-cell/StatusCell.css create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/cell-renderers/status-cell/StatusCell.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.css create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/EmptyBasketsPanel.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/empty-baskets-panel/index.ts create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.css create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/NewBasketPanel.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/index.ts create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/new-basket-panel/useNewBasketPanel.ts create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/useBasketTrading.tsx create mode 100644 vuu-ui/sample-apps/feature-basket-trading/src/useBasketTradingDatasources.ts create mode 100644 vuu-ui/sample-apps/feature-basket-trading/tsconfig.json create mode 100644 vuu-ui/sample-apps/feature-filter-table/src/useFilterTable.tsx create mode 100644 vuu-ui/sample-apps/feature-filter-table/src/useSessionDataSource.ts delete mode 100644 vuu-ui/sample-apps/feature-instrument-tiles/src/PriceTicker.css delete mode 100644 vuu-ui/sample-apps/feature-instrument-tiles/src/PriceTicker.tsx create mode 100644 vuu-ui/showcase/scripts/build-file-list.mjs delete mode 100644 vuu-ui/showcase/src/examples/AppComponents/TableSettings/TableSettings.examples.tsx delete mode 100644 vuu-ui/showcase/src/examples/AppComponents/index.ts rename vuu-ui/showcase/src/examples/{Table => DataTable}/JsonTable.examples.tsx (56%) rename vuu-ui/showcase/src/examples/{Layout => Popups}/Dialog.examples.tsx (100%) create mode 100644 vuu-ui/showcase/src/examples/Popups/Notifications/Notifications.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/Popups/Notifications/index.ts create mode 100644 vuu-ui/showcase/src/examples/Shell/LeftNav.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/Table/BasketTables.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/ColumnSettings.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/TableExtras/ColumnSettings/index.ts create mode 100644 vuu-ui/showcase/src/examples/TableExtras/TableSettings/TableSettings.examples.tsx rename vuu-ui/showcase/src/examples/{AppComponents => TableExtras}/TableSettings/index.ts (100%) create mode 100644 vuu-ui/showcase/src/examples/UiControls/DragDrop.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/UiControls/InstrumentPicker.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/VuuFeatures/BasketSelector.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/VuuFeatures/BasketToolbar.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/VuuFeatures/NewBasketPanel.examples.tsx delete mode 100644 vuu-ui/showcase/src/examples/utils/basket-design-generator.ts create mode 100644 vuu-ui/showcase/src/examples/utils/createArrayDataSource.ts delete mode 100644 vuu-ui/showcase/src/examples/utils/generate-data-utils.ts delete mode 100644 vuu-ui/showcase/src/examples/utils/reference-data/basket-design.ts delete mode 100644 vuu-ui/showcase/src/examples/utils/useColumns.ts delete mode 100644 vuu-ui/showcase/src/examples/utils/useSchemas.ts delete mode 100644 vuu-ui/showcase/src/examples/utils/vuu-row-generator.ts create mode 100644 vuu-ui/showcase/src/features/BasketTradingNoBaskets.feature.tsx create mode 100644 vuu-ui/showcase/src/features/BasketTradingOneBasket.feature.tsx delete mode 100644 vuu-ui/showcase/src/features/TableNext.feature.css delete mode 100644 vuu-ui/showcase/src/features/TableNext.feature.tsx create mode 100644 vuu-ui/tsconfig-typecheck.json rename vuu/src/main/resources/static/{ftse.csv => ftse100.csv} (100%) create mode 100644 vuu/src/main/resources/static/hsi.csv create mode 100644 vuu/src/main/resources/static/nasdaq100.csv create mode 100644 vuu/src/main/resources/static/sp500.csv create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/TableDefContainer.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/BasketConstants.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/BasketModule.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/csv/CsvStaticLoader.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/provider/AlgoProvider.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/provider/BasketConstituentProvider.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/provider/BasketProvider.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/provider/NullProvider.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/provider/PriceStrategyProvider.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/service/BasketService.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/basket/service/BasketTradingConstituentService.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/core/module/price/PriceModule.scala create mode 100644 vuu/src/test/scala/org/finos/vuu/core/module/modulefrommodule/InstrumentModule.scala create mode 100644 vuu/src/test/scala/org/finos/vuu/core/module/modulefrommodule/JoinModule.scala create mode 100644 vuu/src/test/scala/org/finos/vuu/core/module/modulefrommodule/ModuleConstructorTest.scala create mode 100644 vuu/src/test/scala/org/finos/vuu/core/module/modulefrommodule/PriceModule.scala rename vuu/src/test/scala/org/finos/vuu/core/table/{ => join}/JoinTableTest.scala (99%) rename vuu/src/test/scala/org/finos/vuu/core/table/{ => join}/JoinsOfJoinsTableTest.scala (97%) rename vuu/src/test/scala/org/finos/vuu/core/table/{ => join}/MultiJoinTableTest.scala (98%) create mode 100644 vuu/src/test/scala/org/finos/vuu/provider/BasketConstituentProviderTest.scala create mode 100644 vuu/src/test/scala/org/finos/vuu/provider/BasketProviderTest.scala diff --git a/.github/workflows/test-ui.yml b/.github/workflows/test-ui.yml index 690606c8b..df80c1ce7 100644 --- a/.github/workflows/test-ui.yml +++ b/.github/workflows/test-ui.yml @@ -23,6 +23,45 @@ jobs: run: cd ./vuu-ui && npm install - run: cd ./vuu-ui && npm run test:vite + cypress-e2e: + # As a third party action, cypress-io is pinned to a full length commit SHA for security purposes. + # This is also a requirement for the semgrep (static code analysis) scan to pass. + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: "16" + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/.npm + key: npm-${{ hashFiles('package-lock.json') }} + restore-keys: npm- + - name: Install dependencies + run: cd ./vuu-ui && npm install + - name: Run end-to-end tests in Chrome + uses: cypress-io/github-action@bd9dda317ed2d4fbffc808ba6cdcd27823b2a13b + with: + install: false + working-directory: ./vuu-ui + browser: chrome + build: npm run build + start: npm run showcase + wait-on: "http://localhost:5173" + - name: Run end-to-end tests in Edge + uses: cypress-io/github-action@bd9dda317ed2d4fbffc808ba6cdcd27823b2a13b + with: + install: false + working-directory: ./vuu-ui + browser: edge + build: npm run build + start: npm run showcase + wait-on: "http://localhost:5173" + # ensure the vuu example and showcase still build vuu-and-showcase-build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index ac02b5aa4..0f5a6d690 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ yarn-debug.log* yarn-error.log* /vuu-ui/showcase/src/examples/**/*.js +/vuu-ui/cypress/screenshots deployed_apps dist diff --git a/.semgrepignore b/.semgrepignore index 93e9d090f..5b374e053 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -4,10 +4,12 @@ vuu/src/main/resources/www/ws-example.html vuu/src/main/scala/org/finos/vuu/provider/simulation/SimulatedBigInstrumentsProvider.scala vuu-ui/packages/vuu-data/src/array-data-source/group-utils.ts vuu-ui/packages/vuu-datagrid-extras/src/column-expression-input/column-language-parser/walkExpressionTree.ts +vuu-ui/packages/vuu-layout/src/layout-persistence/RemoteLayoutPersistenceManager.ts vuu-ui/packages/vuu-popups/src/menu/useContextMenu.tsx vuu-ui/packages/vuu-table-extras/src/cell-edit-validators/PatternValidator.ts vuu-ui/packages/vuu-ui-controls/src/list/Highlighter.tsx vuu-ui/packages/vuu-ui-controls/src/list/common-hooks/utils/filter-utils.ts +vuu-ui/showcase/scripts/build-file-list.mjs vuu-ui/showcase/src/index.tsx vuu-ui/showcase/src/examples/Layout/Menu.examples.tsx vuu-ui/tools/websocket-test.html diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java new file mode 100644 index 000000000..7950487b4 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/CorsConfig.java @@ -0,0 +1,15 @@ +package org.finos.vuu.layoutserver; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://127.0.0.1:5173") + .allowedMethods("GET", "POST", "PUT", "DELETE"); + } +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java index 5b3482f85..6fa9ab1de 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/config/MappingConfig.java @@ -1,16 +1,21 @@ package org.finos.vuu.layoutserver.config; +import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; -import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; +import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.service.LayoutService; import org.modelmapper.ModelMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +@RequiredArgsConstructor @Configuration public class MappingConfig { + private final LayoutService layoutService; + @Bean public ModelMapper modelMapper() { ModelMapper mapper = new ModelMapper(); @@ -18,8 +23,10 @@ public ModelMapper modelMapper() { mapper.typeMap(LayoutRequestDto.class, Layout.class) .addMappings(m -> m.skip(Layout::setId)); - mapper.typeMap(MetadataRequestDto.class, Metadata.class) - .addMappings(m -> m.skip(Metadata::setId)); + mapper.typeMap(Metadata.class, MetadataResponseDto.class) + .addMappings(m -> m.map( + metadata -> layoutService.getLayoutByMetadataId(metadata.getId()), + MetadataResponseDto::setLayoutId)); return mapper; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java index 4430687df..9877a88c0 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/LayoutController.java @@ -9,15 +9,7 @@ import org.modelmapper.ModelMapper; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; @@ -38,6 +30,7 @@ public class LayoutController { * @param id ID of the layout to get * @return the layout */ + @ResponseStatus(HttpStatus.OK) @GetMapping("/{id}") public LayoutResponseDto getLayout(@PathVariable UUID id) { return mapper.map(layoutService.getLayout(id), LayoutResponseDto.class); @@ -48,6 +41,7 @@ public LayoutResponseDto getLayout(@PathVariable UUID id) { * * @return the metadata */ + @ResponseStatus(HttpStatus.OK) @GetMapping("/metadata") public List getMetadata() { @@ -97,8 +91,6 @@ public void updateLayout(@PathVariable UUID id, @ResponseStatus(HttpStatus.NO_CONTENT) @DeleteMapping("/{id}") public void deleteLayout(@PathVariable UUID id) { - // Generate a 404 if layout doesn't exist - layoutService.getLayout(id); layoutService.deleteLayout(id); } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java index 4ad9c64fd..6255692b1 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/LayoutRequestDto.java @@ -14,10 +14,10 @@ public class LayoutRequestDto { * components) */ @JsonProperty(value = "definition", required = true) - @NotBlank(message = "Please provide a valid definition") + @NotBlank(message = "Definition must not be blank") private String definition; @JsonProperty(value = "metadata", required = true) - @NotNull(message = "Please provide valid metadata") + @NotNull(message = "Metadata must not be null") private MetadataRequestDto metadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java index d0d0b4292..abbf99430 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/request/MetadataRequestDto.java @@ -1,19 +1,12 @@ package org.finos.vuu.layoutserver.dto.request; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import lombok.Data; -import org.finos.vuu.layoutserver.dto.MetadataDto; - -import javax.validation.constraints.NotNull; +import org.finos.vuu.layoutserver.model.BaseMetadata; @Data -public class MetadataRequestDto implements MetadataDto { - - @JsonProperty(value = "name", required = true) - @NotNull(message = "Please provide a valid name") - private String name; +public class MetadataRequestDto { - private String group; - private String screenshot; - private String user; + @JsonUnwrapped + BaseMetadata baseMetadata; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java index 0a7f01fe0..9c689833b 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/LayoutResponseDto.java @@ -10,7 +10,8 @@ public class LayoutResponseDto { private UUID id; /** - * The definition of the layout as a string (e.g. stringified JSON structure containing components) + * The definition of the layout as a string (e.g. stringified JSON structure containing + * components) */ private String definition; diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java index 0e20e9066..178636558 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/dto/response/MetadataResponseDto.java @@ -1,19 +1,20 @@ package org.finos.vuu.layoutserver.dto.response; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import lombok.Data; -import org.finos.vuu.layoutserver.dto.MetadataDto; +import org.finos.vuu.layoutserver.model.BaseMetadata; -import java.util.Date; +import java.time.LocalDate; import java.util.UUID; @Data -public class MetadataResponseDto implements MetadataDto { +public class MetadataResponseDto { private UUID layoutId; - private String name; - private String group; - private String screenshot; - private String user; - private Date created; - private Date updated; + + @JsonUnwrapped + BaseMetadata baseMetadata; + + private LocalDate created; + private LocalDate updated; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java similarity index 61% rename from layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java rename to layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java index e3aa1bcd7..782a15752 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/controller/GlobalExceptionHandler.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/exceptions/GlobalExceptionHandler.java @@ -1,16 +1,18 @@ -package org.finos.vuu.layoutserver.controller; +package org.finos.vuu.layoutserver.exceptions; import org.finos.vuu.layoutserver.dto.response.ErrorResponse; -import org.finos.vuu.layoutserver.exceptions.InternalServerErrorException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import javax.servlet.http.HttpServletRequest; +import java.util.List; import java.util.NoSuchElementException; +import java.util.stream.Collectors; @ControllerAdvice public class GlobalExceptionHandler { @@ -20,11 +22,23 @@ public ResponseEntity handleNotFound(HttpServletRequest request, Excepti return generateResponse(request, ex, HttpStatus.NOT_FOUND); } - @ExceptionHandler({MethodArgumentNotValidException.class, MethodArgumentTypeMismatchException.class}) - public ResponseEntity handleBadRequest(Exception ex, HttpServletRequest request) { + @ExceptionHandler({ + HttpMessageNotReadableException.class, + MethodArgumentTypeMismatchException.class}) + public ResponseEntity handleBadRequest(HttpServletRequest request, Exception ex) { + return generateResponse(request, ex, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { + List errors = ex.getFieldErrors() + .stream() + .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()) + .collect(Collectors.toList()); + return new ResponseEntity<>(errors.toString(), + org.springframework.http.HttpStatus.BAD_REQUEST); + } @ExceptionHandler(InternalServerErrorException.class) public ResponseEntity handleInternalServerError(HttpServletRequest request, Exception ex) { return generateResponse(request, ex, HttpStatus.INTERNAL_SERVER_ERROR); @@ -35,4 +49,5 @@ private ResponseEntity generateResponse(HttpServletRequest request, HttpStatus status) { return new ResponseEntity<>(new ErrorResponse(request, ex, status), status); } + } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java new file mode 100644 index 000000000..2500eb247 --- /dev/null +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/BaseMetadata.java @@ -0,0 +1,19 @@ +package org.finos.vuu.layoutserver.model; + +import lombok.Data; + +import javax.persistence.Embeddable; +import javax.persistence.Lob; + +@Data +@Embeddable +public class BaseMetadata { + + private String name; + private String group; + + @Lob + private String screenshot; + + private String user; +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java index 7e81b9030..90c64f70b 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/model/Metadata.java @@ -1,16 +1,18 @@ package org.finos.vuu.layoutserver.model; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; -import java.util.Date; +import javax.persistence.*; +import java.time.LocalDate; import java.util.UUID; -import lombok.Data; @Data +@Builder +@AllArgsConstructor +@NoArgsConstructor @Entity public class Metadata { @@ -19,16 +21,10 @@ public class Metadata { @Column(columnDefinition = "BINARY(16)") private UUID id; - private String name; - - private String group; - - @Column(length = 16384) - private String screenshot; - - private String user; + @Embedded + private BaseMetadata baseMetadata; - private Date created = new Date(); + private final LocalDate created = LocalDate.now(); - private Date updated; + private LocalDate updated; } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java index 6cbae4025..b99ef0ae9 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/LayoutRepository.java @@ -1,9 +1,13 @@ package org.finos.vuu.layoutserver.repository; -import java.util.UUID; import org.finos.vuu.layoutserver.model.Layout; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.UUID; + @Repository -public interface LayoutRepository extends CrudRepository {} +public interface LayoutRepository extends CrudRepository { + + Layout findLayoutByMetadataId(UUID id); +} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java index 50cbe6288..03f81b108 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/repository/MetadataRepository.java @@ -1,9 +1,10 @@ package org.finos.vuu.layoutserver.repository; -import java.util.UUID; import org.finos.vuu.layoutserver.model.Metadata; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.UUID; + @Repository public interface MetadataRepository extends CrudRepository {} diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java index 42bb4d3f3..22ed963aa 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/LayoutService.java @@ -1,15 +1,16 @@ package org.finos.vuu.layoutserver.service; -import java.util.Date; -import java.util.List; -import java.util.UUID; -import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; import org.springframework.stereotype.Service; +import java.time.LocalDate; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; + @RequiredArgsConstructor @Service public class LayoutService { @@ -25,6 +26,9 @@ public Layout getLayout(UUID id) { public List getMetadata() { return metadataService.getMetadata(); } + public Layout getLayoutByMetadataId(UUID id) { + return layoutRepository.findLayoutByMetadataId(id); + } public UUID createLayout(Layout layout) { return layoutRepository.save(layout).getId(); @@ -32,19 +36,24 @@ public UUID createLayout(Layout layout) { public void updateLayout(UUID layoutId, Layout newLayout) { Layout layoutToUpdate = getLayout(layoutId); - layoutToUpdate.setDefinition(newLayout.getDefinition()); + Metadata newMetadata = newLayout.getMetadata(); - Metadata metadataToUpdate = layoutToUpdate.getMetadata(); - metadataToUpdate.setName(newLayout.getMetadata().getName()); - metadataToUpdate.setGroup(newLayout.getMetadata().getGroup()); - metadataToUpdate.setScreenshot(newLayout.getMetadata().getScreenshot()); - metadataToUpdate.setUser(newLayout.getMetadata().getUser()); - metadataToUpdate.setUpdated(new Date()); + Metadata updatedMetadata = Metadata.builder() + .baseMetadata(newMetadata.getBaseMetadata()) + .updated(LocalDate.now()) + .build(); + + layoutToUpdate.setDefinition(newLayout.getDefinition()); + layoutToUpdate.setMetadata(updatedMetadata); layoutRepository.save(layoutToUpdate); } public void deleteLayout(UUID id) { - layoutRepository.deleteById(id); + try { + layoutRepository.deleteById(id); + } catch (Exception e) { + throw new NoSuchElementException("Layout with ID '" + id + "' not found"); + } } } diff --git a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java index de3eb095e..08398edc4 100644 --- a/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java +++ b/layout-server/src/main/java/org/finos/vuu/layoutserver/service/MetadataService.java @@ -1,12 +1,13 @@ package org.finos.vuu.layoutserver.service; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.MetadataRepository; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; + @RequiredArgsConstructor @Service public class MetadataService { diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java index 4ba654e25..3a21b9e42 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/controller/LayoutControllerTest.java @@ -4,9 +4,11 @@ import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; import org.finos.vuu.layoutserver.dto.response.LayoutResponseDto; import org.finos.vuu.layoutserver.dto.response.MetadataResponseDto; +import org.finos.vuu.layoutserver.model.BaseMetadata; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.service.LayoutService; +import org.finos.vuu.layoutserver.service.MetadataService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,7 +17,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.modelmapper.ModelMapper; -import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.UUID; @@ -28,89 +29,95 @@ @ExtendWith(MockitoExtension.class) class LayoutControllerTest { + private static final String LAYOUT_DEFINITION = "Test Definition"; + private static final String LAYOUT_GROUP = "Test Group"; + private static final String LAYOUT_NAME = "Test Layout"; + private static final String LAYOUT_SCREENSHOT = "Test Screenshot"; + private static final String LAYOUT_USER = "Test User"; + private static final UUID VALID_LAYOUT_ID = UUID.randomUUID(); + private static final UUID VALID_METADATA_ID = UUID.randomUUID(); + private static final UUID DOES_NOT_EXIST_LAYOUT_ID = UUID.randomUUID(); + @Mock private LayoutService layoutService; + @Mock + private MetadataService metadataService; + @Mock private ModelMapper modelMapper; @InjectMocks private LayoutController layoutController; - private UUID validLayoutId; - private UUID doesNotExistLayoutId; private Layout layout; private Metadata metadata; + private BaseMetadata baseMetadata; private LayoutRequestDto layoutRequest; private LayoutResponseDto expectedLayoutResponse; - private List expectedMetadataResponse; + private MetadataResponseDto metadataResponse; @BeforeEach public void setup() { - validLayoutId = UUID.randomUUID(); - doesNotExistLayoutId = UUID.randomUUID(); - UUID metadataId = UUID.randomUUID(); - String layoutDefinition = "Test Definition"; - - metadata = new Metadata(); - metadata.setId(metadataId); - metadata.setName("Test Layout"); - metadata.setUser("Test User"); - metadata.setGroup("Test Group"); - metadata.setScreenshot("Test Screenshot"); + baseMetadata = new BaseMetadata(); + baseMetadata.setName(LAYOUT_NAME); + baseMetadata.setUser(LAYOUT_USER); + baseMetadata.setGroup(LAYOUT_GROUP); + baseMetadata.setScreenshot(LAYOUT_SCREENSHOT); + + metadata = Metadata.builder().id(VALID_METADATA_ID).baseMetadata(baseMetadata).build(); layout = new Layout(); - layout.setId(validLayoutId); - layout.setDefinition(layoutDefinition); + layout.setId(VALID_LAYOUT_ID); + layout.setDefinition(LAYOUT_DEFINITION); layout.setMetadata(metadata); layoutRequest = new LayoutRequestDto(); MetadataRequestDto metadataRequestDTO = new MetadataRequestDto(); - metadataRequestDTO.setName(metadata.getName()); - metadataRequestDTO.setUser(metadata.getUser()); - metadataRequestDTO.setGroup(metadata.getGroup()); - metadataRequestDTO.setScreenshot(metadata.getScreenshot()); + metadataRequestDTO.setBaseMetadata(baseMetadata); layoutRequest.setDefinition(layout.getDefinition()); layoutRequest.setMetadata(metadataRequestDTO); + metadataResponse = getMetadataResponseDTO(); + expectedLayoutResponse = new LayoutResponseDto(); expectedLayoutResponse.setId(layout.getId()); expectedLayoutResponse.setDefinition(layout.getDefinition()); - - MetadataResponseDto metadataResponse = getMetadataResponseDTO(); expectedLayoutResponse.setMetadata(metadataResponse); - expectedMetadataResponse = new ArrayList<>(); - expectedMetadataResponse.add(metadataResponse); } @Test void getLayout_layoutExists_returnsLayout() { - when(layoutService.getLayout(validLayoutId)).thenReturn(layout); - when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn( - expectedLayoutResponse); - assertThat(layoutController.getLayout(validLayoutId)).isEqualTo(expectedLayoutResponse); + when(layoutService.getLayout(VALID_LAYOUT_ID)).thenReturn(layout); + when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn(expectedLayoutResponse); + assertThat(layoutController.getLayout(VALID_LAYOUT_ID)).isEqualTo(expectedLayoutResponse); } @Test - void getLayout_layoutDoesNotExist_throwsNotFoundAndReturns404() { - when(layoutService.getLayout(doesNotExistLayoutId)).thenThrow(NoSuchElementException.class); + void getLayout_layoutDoesNotExist_throwsNoSuchElementException() { + when(layoutService.getLayout(DOES_NOT_EXIST_LAYOUT_ID)) + .thenThrow(NoSuchElementException.class); + assertThrows(NoSuchElementException.class, - () -> layoutController.getLayout(doesNotExistLayoutId)); + () -> layoutController.getLayout(DOES_NOT_EXIST_LAYOUT_ID)); } @Test void getMetadata_metadataExists_returnsMetadata() { - when(layoutService.getMetadata()).thenReturn(List.of(metadata)); - when(modelMapper.map(metadata, MetadataResponseDto.class)).thenReturn( - getMetadataResponseDTO()); - assertThat(layoutController.getMetadata()).isEqualTo(expectedMetadataResponse); + List metadataList = List.of(metadata); + + when(layoutService.getMetadata()).thenReturn(metadataList); + when(modelMapper.map(metadata, MetadataResponseDto.class)) + .thenReturn(metadataResponse); + + assertThat(layoutController.getMetadata()).isEqualTo(List.of(metadataResponse)); } @Test void getMetadata_noMetadataExists_returnsEmptyArray() { - when(layoutService.getMetadata()).thenReturn(List.of()); +// when(metadataService.getMetadata()).thenReturn(List.of()); assertThat(layoutController.getMetadata()).isEmpty(); } @@ -125,37 +132,32 @@ void createLayout_validLayout_returnsCreatedLayout() { when(layoutService.getLayout(layout.getId())).thenReturn(layout); when(modelMapper.map(layout, LayoutResponseDto.class)).thenReturn(expectedLayoutResponse); - assertThat(layoutController.createLayout(layoutRequest)) - .isEqualTo(expectedLayoutResponse); + assertThat(layoutController.createLayout(layoutRequest)).isEqualTo(expectedLayoutResponse); } @Test - void updateLayout_callsLayoutService() { + void updateLayout_validLayout_callsLayoutService() { layout.setId(null); layout.getMetadata().setId(null); when(modelMapper.map(layoutRequest, Layout.class)).thenReturn(layout); - layoutController.updateLayout(validLayoutId, layoutRequest); + layoutController.updateLayout(VALID_LAYOUT_ID, layoutRequest); - verify(layoutService).updateLayout(validLayoutId, layout); + verify(layoutService).updateLayout(VALID_LAYOUT_ID, layout); } @Test - void deleteLayout_callsLayoutService() { - layoutController.deleteLayout(validLayoutId); + void deleteLayout__validId_callsLayoutService() { + layoutController.deleteLayout(VALID_LAYOUT_ID); - verify(layoutService).getLayout(validLayoutId); - verify(layoutService).deleteLayout(validLayoutId); + verify(layoutService).deleteLayout(VALID_LAYOUT_ID); } private MetadataResponseDto getMetadataResponseDTO() { MetadataResponseDto metadataResponse = new MetadataResponseDto(); metadataResponse.setLayoutId(layout.getId()); - metadataResponse.setName(layout.getMetadata().getName()); - metadataResponse.setUser(layout.getMetadata().getUser()); - metadataResponse.setGroup(layout.getMetadata().getGroup()); - metadataResponse.setScreenshot(layout.getMetadata().getScreenshot()); + metadataResponse.setBaseMetadata(baseMetadata); metadataResponse.setCreated(layout.getMetadata().getCreated()); metadataResponse.setUpdated(layout.getMetadata().getUpdated()); return metadataResponse; diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java index 15ea622a6..ac7b36a48 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/integration/LayoutIntegrationTest.java @@ -4,11 +4,12 @@ import com.jayway.jsonpath.JsonPath; import org.finos.vuu.layoutserver.dto.request.LayoutRequestDto; import org.finos.vuu.layoutserver.dto.request.MetadataRequestDto; +import org.finos.vuu.layoutserver.model.BaseMetadata; import org.finos.vuu.layoutserver.model.Layout; import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; import org.finos.vuu.layoutserver.repository.MetadataRepository; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -23,25 +24,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc -@Transactional @ActiveProfiles("test") public class LayoutIntegrationTest { - private static String defaultDefinition; - private static String defaultName; - private static String defaultGroup; - private static String defaultScreenshot; - private static String defaultUser; + private static final String DEFAULT_LAYOUT_DEFINITION = "Default layout definition"; + private static final String DEFAULT_LAYOUT_NAME = "Default layout name"; + private static final String DEFAULT_LAYOUT_GROUP = "Default layout group"; + private static final String DEFAULT_LAYOUT_SCREENSHOT = "Default layout screenshot"; + private static final String DEFAULT_LAYOUT_USER = "Default layout user"; + private final ObjectMapper objectMapper = new ObjectMapper(); + @Autowired private MockMvc mockMvc; @Autowired @@ -49,26 +47,29 @@ public class LayoutIntegrationTest { @Autowired private MetadataRepository metadataRepository; - @BeforeAll - public static void setup() { - defaultDefinition = "Default layout definition"; - defaultName = "Default layout name"; - defaultGroup = "Default layout group"; - defaultScreenshot = "Default layout screenshot"; - defaultUser = "Default layout user"; + @BeforeEach + void tearDown() { + layoutRepository.deleteAll(); + metadataRepository.deleteAll(); } @Test void getLayout_validIDAndLayoutExists_returns200WithLayout() throws Exception { Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); mockMvc.perform(get("/layouts/{id}", layout.getId())) .andExpect(status().isOk()) - .andExpect(jsonPath("$.definition", is(layout.getDefinition()))) - .andExpect(jsonPath("$.metadata.name", is(layout.getMetadata().getName()))) - .andExpect(jsonPath("$.metadata.group", is(layout.getMetadata().getGroup()))) - .andExpect(jsonPath("$.metadata.screenshot", is(layout.getMetadata().getScreenshot()))) - .andExpect(jsonPath("$.metadata.user", is(layout.getMetadata().getUser()))); + .andExpect(jsonPath("$.definition", + is(layout.getDefinition()))) + .andExpect(jsonPath("$.metadata.name", + is(layout.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$.metadata.group", + is(layout.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$.metadata.screenshot", + is(layout.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$.metadata.user", + is(layout.getMetadata().getBaseMetadata().getUser()))); } @Test @@ -82,19 +83,63 @@ void getLayout_validIDButLayoutDoesNotExist_returns404() throws Exception { void getLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; - mockMvc.perform(get("/layouts/{id}", layoutID)).andExpect(status().isBadRequest()); + mockMvc.perform(get("/layouts/{id}", layoutID)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message", + is("Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); } @Test - void getMetadata_metadataExists_returnsMetadata() throws Exception { + void getMetadata_singleMetadataExists_returnsMetadata() throws Exception { Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + + mockMvc.perform(get("/layouts/metadata")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].name", + is(layout.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[0].group", + is(layout.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[0].screenshot", + is(layout.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[0].user", + is(layout.getMetadata().getBaseMetadata().getUser()))); + } + + @Test + void getMetadata_multipleMetadataExists_returnsAllMetadata() throws Exception { + Layout layout1 = createDefaultLayoutInDatabase(); + Layout layout2 = createDefaultLayoutInDatabase(); + layout2.setDefinition("Different definition"); + layout2.getMetadata().getBaseMetadata().setName("Different name"); + layout2.getMetadata().getBaseMetadata().setGroup("Different group"); + layout2.getMetadata().getBaseMetadata().setScreenshot("Different screenshot"); + layout2.getMetadata().getBaseMetadata().setUser("Different user"); + layoutRepository.save(layout2); + + assertThat(layoutRepository.findById(layout1.getId()).orElseThrow()).isEqualTo(layout1); + assertThat(layoutRepository.findById(layout2.getId()).orElseThrow()).isEqualTo(layout2); mockMvc.perform(get("/layouts/metadata")) .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].name", is(layout.getMetadata().getName()))) - .andExpect(jsonPath("$[0].group", is(layout.getMetadata().getGroup()))) - .andExpect(jsonPath("$[0].screenshot", is(layout.getMetadata().getScreenshot()))) - .andExpect(jsonPath("$[0].user", is(layout.getMetadata().getUser()))); + .andExpect(jsonPath("$[0].name", + is(layout1.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[0].group", + is(layout1.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[0].screenshot", + is(layout1.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[0].user", + is(layout1.getMetadata().getBaseMetadata().getUser()))) + .andExpect(jsonPath("$[1].name", + is(layout2.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$[1].group", + is(layout2.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$[1].screenshot", + is(layout2.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$[1].user", + is(layout2.getMetadata().getBaseMetadata().getUser()))); } @Test @@ -105,9 +150,9 @@ void getMetadata_metadataDoesNotExist_returnsEmptyList() throws Exception { } @Test - void createLayout_validLayout_returnsCreatedLayoutAndLayoutIsPersisted() + void createLayout_validRequest_returnsCreatedLayoutAndLayoutIsPersisted() throws Exception { - LayoutRequestDto layoutRequest = createValidCreateLayoutRequest(); + LayoutRequestDto layoutRequest = createValidLayoutRequest(); MvcResult result = mockMvc.perform(post("/layouts") .content(objectMapper.writeValueAsString(layoutRequest)) @@ -115,95 +160,158 @@ void createLayout_validLayout_returnsCreatedLayoutAndLayoutIsPersisted() .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").isNotEmpty()) .andExpect(jsonPath("$.definition", is(layoutRequest.getDefinition()))) - .andExpect(jsonPath("$.metadata.name", is(layoutRequest.getMetadata().getName()))) - .andExpect(jsonPath("$.metadata.group", is(layoutRequest.getMetadata().getGroup()))) - .andExpect(jsonPath("$.metadata.screenshot", is(layoutRequest.getMetadata().getScreenshot()))) - .andExpect(jsonPath("$.metadata.user", is(layoutRequest.getMetadata().getUser()))) + .andExpect(jsonPath("$.metadata.name", + is(layoutRequest.getMetadata().getBaseMetadata().getName()))) + .andExpect(jsonPath("$.metadata.group", + is(layoutRequest.getMetadata().getBaseMetadata().getGroup()))) + .andExpect(jsonPath("$.metadata.screenshot", + is(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()))) + .andExpect(jsonPath("$.metadata.user", + is(layoutRequest.getMetadata().getBaseMetadata().getUser()))) .andReturn(); - UUID createdLayoutId = UUID.fromString(JsonPath.read(result.getResponse().getContentAsString(), "$.id")); + UUID createdLayoutId = UUID.fromString( + JsonPath.read(result.getResponse().getContentAsString(), "$.id")); Layout createdLayout = layoutRepository.findById(createdLayoutId).orElseThrow(); - Metadata createdMetadata = metadataRepository.findById(createdLayout.getMetadata().getId()).orElseThrow(); + Metadata createdMetadata = metadataRepository.findById(createdLayout.getMetadata().getId()) + .orElseThrow(); - // Check that the one-to-one relationship isn't causing duplicate/unexpected entries in the DB + // Check that the one-to-one relationship isn't causing duplicate/unexpected entries in + // the DB assertThat(layoutRepository.findAll()).containsExactly(createdLayout); assertThat(metadataRepository.findAll()).containsExactly(createdMetadata); assertThat(createdLayout.getDefinition()) .isEqualTo(layoutRequest.getDefinition()); - assertThat(createdMetadata.getName()) - .isEqualTo(layoutRequest.getMetadata().getName()); - assertThat(createdMetadata.getGroup()) - .isEqualTo(layoutRequest.getMetadata().getGroup()); - assertThat(createdMetadata.getScreenshot()) - .isEqualTo(layoutRequest.getMetadata().getScreenshot()); - assertThat(createdMetadata.getUser()) - .isEqualTo(layoutRequest.getMetadata().getUser()); + assertThat(createdMetadata.getBaseMetadata().getName()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName()); + assertThat(createdMetadata.getBaseMetadata().getGroup()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup()); + assertThat(createdMetadata.getBaseMetadata().getScreenshot()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()); + assertThat(createdMetadata.getBaseMetadata().getUser()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser()); } - @Test - void createLayout_invalidLayout_returns400() throws Exception { - String invalidLayout = "invalidLayout"; + void createLayout_invalidRequestBodyDefinitionsIsBlank_returns400AndDoesNotCreateLayout() + throws Exception { + LayoutRequestDto layoutRequest = createValidLayoutRequest(); + layoutRequest.setDefinition(""); mockMvc.perform(post("/layouts") - .content(invalidLayout) + .content(objectMapper.writeValueAsString(layoutRequest)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()) + .andExpect(content().string("[definition: Definition must not be blank]")); + + assertThat(layoutRepository.findAll()).isEmpty(); + assertThat(metadataRepository.findAll()).isEmpty(); } @Test - void createLayout_validLayoutButInvalidMetadata_returns400AndDoesNotCreateLayout() + void createLayout_invalidRequestBodyMetadataIsNull_returns400AndDoesNotCreateLayout() throws Exception { - LayoutRequestDto layoutRequest = createValidCreateLayoutRequest(); + LayoutRequestDto layoutRequest = createValidLayoutRequest(); layoutRequest.setMetadata(null); mockMvc.perform(post("/layouts") .content(objectMapper.writeValueAsString(layoutRequest)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()) + .andExpect(content().string( + "[metadata: Metadata must not be null]")); assertThat(layoutRepository.findAll()).isEmpty(); + assertThat(metadataRepository.findAll()).isEmpty(); } + @Test - void updateLayout_validIDAndValidRequest_returns204AndLayoutHasChanged() throws Exception { - Layout layout = createDefaultLayoutInDatabase(); - LayoutRequestDto layoutRequest = createValidUpdateRequest(); + void createLayout_invalidRequestBodyUnexpectedFormat_returns400() throws Exception { + String invalidLayout = "invalidLayout"; - mockMvc.perform(put("/layouts/{id}", layout.getId()) + mockMvc.perform(post("/layouts") + .content(invalidLayout) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message", is( + "JSON parse error: Unrecognized token 'invalidLayout': was expecting (JSON " + + "String, Number, Array, Object or token 'null', 'true' or 'false'); nested " + + "exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized " + + "token 'invalidLayout': was expecting (JSON String, Number, Array, Object " + + "or token 'null', 'true' or 'false')\n" + + " at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream);" + + " line: 1, column: 14]"))); + } + + @Test + void updateLayout_validIdAndValidRequest_returns204AndLayoutHasChanged() throws Exception { + Layout initialLayout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(initialLayout.getId()).orElseThrow()).isEqualTo( + initialLayout); + + LayoutRequestDto layoutRequest = createValidLayoutRequest(); + layoutRequest.setDefinition("Updated definition"); + layoutRequest.getMetadata().getBaseMetadata().setName("Updated name"); + layoutRequest.getMetadata().getBaseMetadata().setGroup("Updated group"); + layoutRequest.getMetadata().getBaseMetadata().setScreenshot("Updated screenshot"); + layoutRequest.getMetadata().getBaseMetadata().setUser("Updated user"); + + mockMvc.perform(put("/layouts/{id}", initialLayout.getId()) .content(objectMapper.writeValueAsString(layoutRequest)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()) .andExpect(jsonPath("$").doesNotExist()); - Layout updatedLayout = layoutRepository.findById(layout.getId()).orElseThrow(); + Layout updatedLayout = layoutRepository.findById(initialLayout.getId()).orElseThrow(); assertThat(updatedLayout.getDefinition()) .isEqualTo(layoutRequest.getDefinition()); - assertThat(updatedLayout.getMetadata().getName()) - .isEqualTo(layoutRequest.getMetadata().getName()); - assertThat(updatedLayout.getMetadata().getGroup()) - .isEqualTo(layoutRequest.getMetadata().getGroup()); - assertThat(updatedLayout.getMetadata().getScreenshot()) - .isEqualTo(layoutRequest.getMetadata().getScreenshot()); - assertThat(updatedLayout.getMetadata().getUser()) - .isEqualTo(layoutRequest.getMetadata().getUser()); + assertThat(updatedLayout.getMetadata().getBaseMetadata().getName()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getName()); + assertThat(updatedLayout.getMetadata().getBaseMetadata().getGroup()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getGroup()); + assertThat(updatedLayout.getMetadata().getBaseMetadata().getScreenshot()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getScreenshot()); + assertThat(updatedLayout.getMetadata().getBaseMetadata().getUser()) + .isEqualTo(layoutRequest.getMetadata().getBaseMetadata().getUser()); + + assertThat(updatedLayout).isNotEqualTo(initialLayout); } @Test - void updateLayout_invalidRequestBodyDefinitionIsBlankAndMetadataIsNull_returns400AndLayoutDoesNotChange() + void updateLayout_invalidRequestBodyDefinitionIsBlank_returns400AndLayoutDoesNotChange() throws Exception { Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); - LayoutRequestDto request = new LayoutRequestDto(); + LayoutRequestDto request = createValidLayoutRequest(); request.setDefinition(""); + + mockMvc.perform(put("/layouts/{id}", layout.getId()) + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(content().string("[definition: Definition must not be blank]")); + + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + } + + @Test + void updateLayout_invalidRequestBodyMetadataIsNull_returns400AndLayoutDoesNotChange() + throws Exception { + Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + + LayoutRequestDto request = createValidLayoutRequest(); request.setMetadata(null); mockMvc.perform(put("/layouts/{id}", layout.getId()) .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()) + .andExpect(content().string("[metadata: Metadata must not be null]")); assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); } @@ -212,20 +320,34 @@ void updateLayout_invalidRequestBodyDefinitionIsBlankAndMetadataIsNull_returns40 void updateLayout_invalidRequestBodyUnexpectedFormat_returns400AndLayoutDoesNotChange() throws Exception { Layout layout = createDefaultLayoutInDatabase(); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + String request = "invalidRequest"; mockMvc.perform(put("/layouts/{id}", layout.getId()) .content(objectMapper.writeValueAsString(request)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); - - assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message", is( + "JSON parse error: Cannot construct instance of `org.finos.vuu.layoutserver.dto" + + ".request.LayoutRequestDto` (although at least one Creator exists): no " + + "String-argument constructor/factory method to deserialize from String " + + "value ('invalidRequest'); nested exception is com.fasterxml.jackson" + + ".databind.exc.MismatchedInputException: Cannot construct instance of `org" + + ".finos.vuu.layoutserver.dto.request.LayoutRequestDto` (although at least " + + "one Creator exists): no String-argument constructor/factory method to " + + "deserialize from String value ('invalidRequest')\n" + + " at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream);" + + " line: 1, column: 1]"))); + + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo( + layout); } @Test void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exception { UUID layoutID = UUID.randomUUID(); - LayoutRequestDto layoutRequest = createValidUpdateRequest(); + LayoutRequestDto layoutRequest = createValidLayoutRequest(); mockMvc.perform(put("/layouts/{id}", layoutID) .content(objectMapper.writeValueAsString(layoutRequest)) @@ -236,27 +358,30 @@ void updateLayout_validIdButLayoutDoesNotExist_returnsNotFound() throws Exceptio @Test void updateLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; - LayoutRequestDto layoutRequest = createValidUpdateRequest(); + LayoutRequestDto layoutRequest = createValidLayoutRequest(); mockMvc.perform(put("/layouts/{id}", layoutID) .content(objectMapper.writeValueAsString(layoutRequest)) .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message", is( + "Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); } @Test - void deleteLayout_validIDLayoutExists_returnsSuccessAndLayoutIsDeleted() throws Exception { + void deleteLayout_validIdLayoutExists_returnsSuccessAndLayoutIsDeleted() throws Exception { Layout layout = createDefaultLayoutInDatabase(); - - mockMvc.perform(get("/layouts/{id}", layout.getId())).andExpect(status().isOk()); + assertThat(layoutRepository.findById(layout.getId()).orElseThrow()).isEqualTo(layout); mockMvc.perform(delete("/layouts/{id}", layout.getId())).andExpect(status().isNoContent()); - mockMvc.perform(get("/layouts/{id}", layout.getId())).andExpect(status().isNotFound()); + assertThat(layoutRepository.findById(layout.getId())).isEmpty(); } @Test - void deleteLayout_validIDLayoutDoesNotExist_returnsNotFound() throws Exception { + void deleteLayout_validIdLayoutDoesNotExist_returnsNotFound() throws Exception { UUID layoutID = UUID.randomUUID(); mockMvc.perform(delete("/layouts/{id}", layoutID)).andExpect(status().isNotFound()); @@ -266,53 +391,46 @@ void deleteLayout_validIDLayoutDoesNotExist_returnsNotFound() throws Exception { void deleteLayout_invalidId_returns400() throws Exception { String layoutID = "invalidUUID"; - mockMvc.perform(delete("/layouts/{id}", layoutID)).andExpect(status().isBadRequest()); + mockMvc.perform(delete("/layouts/{id}", layoutID)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message", is( + "Failed to convert value of type 'java.lang.String' to required type 'java.util" + + ".UUID'; nested exception is java.lang.IllegalArgumentException: Invalid " + + "UUID string: invalidUUID"))); } private Layout createDefaultLayoutInDatabase() { Layout layout = new Layout(); Metadata metadata = new Metadata(); + BaseMetadata baseMetadata = new BaseMetadata(); - layout.setDefinition(defaultDefinition); - layout.setMetadata(metadata); - - metadata.setName(defaultName); - metadata.setGroup(defaultGroup); - metadata.setScreenshot(defaultScreenshot); - metadata.setUser(defaultUser); + baseMetadata.setName(DEFAULT_LAYOUT_NAME); + baseMetadata.setGroup(DEFAULT_LAYOUT_GROUP); + baseMetadata.setScreenshot(DEFAULT_LAYOUT_SCREENSHOT); + baseMetadata.setUser(DEFAULT_LAYOUT_USER); - metadataRepository.save(metadata); - Layout createdLayout = layoutRepository.save(layout); + metadata.setBaseMetadata(baseMetadata); - assertThat(layoutRepository.findById(createdLayout.getId()).orElseThrow()) - .isEqualTo(layout); + layout.setDefinition(DEFAULT_LAYOUT_DEFINITION); + layout.setMetadata(metadata); - return createdLayout; + return layoutRepository.save(layout); } - private LayoutRequestDto createValidUpdateRequest() { - MetadataRequestDto metadataRequest = new MetadataRequestDto(); - metadataRequest.setName("Updated name"); - metadataRequest.setGroup("Updated group"); - metadataRequest.setScreenshot("Updated screenshot"); - metadataRequest.setUser("Updated user"); + private LayoutRequestDto createValidLayoutRequest() { + BaseMetadata baseMetadata = new BaseMetadata(); + baseMetadata.setName(DEFAULT_LAYOUT_NAME); + baseMetadata.setGroup(DEFAULT_LAYOUT_GROUP); + baseMetadata.setScreenshot(DEFAULT_LAYOUT_SCREENSHOT); + baseMetadata.setUser(DEFAULT_LAYOUT_USER); - LayoutRequestDto layoutRequest = new LayoutRequestDto(); - layoutRequest.setDefinition("Updated definition"); - layoutRequest.setMetadata(metadataRequest); - return layoutRequest; - } - - private LayoutRequestDto createValidCreateLayoutRequest() { MetadataRequestDto metadataRequest = new MetadataRequestDto(); - metadataRequest.setName(defaultName); - metadataRequest.setGroup(defaultGroup); - metadataRequest.setScreenshot(defaultScreenshot); - metadataRequest.setUser(defaultUser); + metadataRequest.setBaseMetadata(baseMetadata); LayoutRequestDto layoutRequest = new LayoutRequestDto(); - layoutRequest.setDefinition(defaultDefinition); + layoutRequest.setDefinition(DEFAULT_LAYOUT_DEFINITION); layoutRequest.setMetadata(metadataRequest); + return layoutRequest; } } diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java index 079347059..b5f31af85 100644 --- a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/LayoutServiceTest.java @@ -1,19 +1,8 @@ package org.finos.vuu.layoutserver.service; -import java.util.Date; -import java.util.List; -import java.util.NoSuchElementException; -import org.finos.vuu.layoutserver.model.Metadata; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Optional; -import java.util.UUID; +import org.finos.vuu.layoutserver.model.BaseMetadata; import org.finos.vuu.layoutserver.model.Layout; +import org.finos.vuu.layoutserver.model.Metadata; import org.finos.vuu.layoutserver.repository.LayoutRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -21,82 +10,102 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.EmptyResultDataAccessException; + +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class LayoutServiceTest { @Mock - private LayoutRepository layoutRepository; + private MetadataService metadataService; + private static final UUID LAYOUT_ID = UUID.randomUUID(); + public static final UUID METADATA_ID = UUID.randomUUID(); @Mock - private MetadataService metadataService; + private LayoutRepository layoutRepository; @InjectMocks private LayoutService layoutService; private Layout layout; - private Metadata metadata; - private UUID layoutId; @BeforeEach public void setup() { - layoutId = UUID.randomUUID(); - UUID metadataId = UUID.randomUUID(); + BaseMetadata baseMetadata = new BaseMetadata(); + baseMetadata.setName("Test Name"); + baseMetadata.setGroup("Test Group"); + baseMetadata.setScreenshot("Test Screenshot"); + baseMetadata.setUser("Test User"); + + Metadata metadata = Metadata.builder().id(METADATA_ID).baseMetadata(baseMetadata).build(); + layout = new Layout(); - metadata = new Metadata(); - layout.setId(layoutId); + layout.setId(LAYOUT_ID); layout.setDefinition(""); layout.setMetadata(metadata); - metadata.setId(metadataId); - metadata.setName(""); - metadata.setGroup(""); - metadata.setScreenshot(""); - metadata.setUser(""); - metadata.setCreated(new Date()); - metadata.setUpdated(new Date()); } @Test - void getLayout_returnsLayout() { - when(layoutRepository.findById(layoutId)).thenReturn(Optional.of(layout)); + void getLayout_layoutExists_returnsLayout() { + when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.of(layout)); - assertThat(layoutService.getLayout(layoutId)).isEqualTo(layout); + assertThat(layoutService.getLayout(LAYOUT_ID)).isEqualTo(layout); } @Test - void getMetadata_returnsMetadata() { - when(metadataService.getMetadata()).thenReturn(List.of(metadata)); + void getLayout_noLayoutsExist_throwsNotFoundException() { + when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.empty()); - assertThat(layoutService.getMetadata()).isEqualTo(List.of(metadata)); + assertThrows(NoSuchElementException.class, + () -> layoutService.getLayout(LAYOUT_ID)); } @Test - void createLayout() { + void createLayout_anyLayout_returnsLayoutId() { when(layoutRepository.save(layout)).thenReturn(layout); - assertThat(layoutService.createLayout(layout)).isEqualTo(layoutId); + assertThat(layoutService.createLayout(layout)).isEqualTo(LAYOUT_ID); } @Test - void updateLayout_layoutExists_callsRepository() { - when(layoutRepository.findById(layoutId)).thenReturn(Optional.of(layout)); + void updateLayout_layoutExists_callsRepositorySave() { + when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.of(layout)); - layoutService.updateLayout(layoutId, layout); + layoutService.updateLayout(LAYOUT_ID, layout); verify(layoutRepository, times(1)).save(layout); } @Test void updateLayout_layoutDoesNotExist_throwsNoSuchElementException() { - when(layoutRepository.findById(layoutId)).thenReturn(Optional.empty()); + when(layoutRepository.findById(LAYOUT_ID)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, + () -> layoutService.updateLayout(LAYOUT_ID, layout)); + } - assertThrows(NoSuchElementException.class, () -> layoutService.updateLayout(layoutId, layout)); + @Test + void deleteLayout_anyUUID_callsRepositoryDeleteById() { + layoutService.deleteLayout(LAYOUT_ID); + + verify(layoutRepository, times(1)).deleteById(LAYOUT_ID); } @Test - void deleteLayout_callsRepository() { - layoutService.deleteLayout(layoutId); + void deleteLayout_noLayoutExists_throwsNoSuchElementException() { + doThrow(new EmptyResultDataAccessException(1)) + .when(layoutRepository).deleteById(LAYOUT_ID); + + assertThrows(NoSuchElementException.class, + () -> layoutService.deleteLayout(LAYOUT_ID)); - verify(layoutRepository, times(1)).deleteById(layoutId); + verify(layoutRepository, times(1)).deleteById(LAYOUT_ID); } } \ No newline at end of file diff --git a/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java new file mode 100644 index 000000000..1539a9237 --- /dev/null +++ b/layout-server/src/test/java/org/finos/vuu/layoutserver/service/MetadataServiceTest.java @@ -0,0 +1,37 @@ +package org.finos.vuu.layoutserver.service; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.finos.vuu.layoutserver.model.Metadata; +import org.finos.vuu.layoutserver.repository.MetadataRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class MetadataServiceTest { + + @Mock + private MetadataRepository metadataRepository; + + @InjectMocks + private MetadataService metadataService; + + @Test + void getMetadata_metadataExists_returnsMetadata() { + Metadata metadata = Metadata.builder().build(); + + when(metadataRepository.findAll()).thenReturn(List.of(metadata)); + assertThat(metadataService.getMetadata()).isEqualTo(List.of(metadata)); + } + + @Test + void getMetadata_noMetadataExists_returnsEmptyList() { + when(metadataRepository.findAll()).thenReturn(List.of()); + assertThat(metadataService.getMetadata()).isEqualTo(List.of()); + } +} \ No newline at end of file diff --git a/vuu-ui/cypress.config.ts b/vuu-ui/cypress.config.ts index a2cb479a5..5d93ecd14 100644 --- a/vuu-ui/cypress.config.ts +++ b/vuu-ui/cypress.config.ts @@ -69,6 +69,7 @@ export default defineConfig({ viteConfig, }, specPattern: "packages/**/src/**/*.cy.{js,ts,jsx,tsx}", + indexHtmlFile: "cypress/support/component/component-index.html", }, e2e: { diff --git a/vuu-ui/cypress/e2e/layout-management/screenshot.cy.js b/vuu-ui/cypress/e2e/layout-management/screenshot.cy.js index 97f2933af..db03b177b 100644 --- a/vuu-ui/cypress/e2e/layout-management/screenshot.cy.js +++ b/vuu-ui/cypress/e2e/layout-management/screenshot.cy.js @@ -9,7 +9,7 @@ context("Screenshot", () => { // TODO (#VUU24): Improve test alignment with the user flow it("Takes a screenshot of the current layout and displays it in the save layout dialog", () => { // TODO (#VUU24): Improve selector - cy.findByRole("tab", { name: "My Instruments" }).then((tab) => { + cy.get("#tab1-tab").then((tab) => { cy.wrap(tab).findByRole("button").click(); }); @@ -17,7 +17,7 @@ context("Screenshot", () => { cy.findByRole("menuitem", { name: "Save Layout" }).click(); // TODO (#VUU24): Don't find by classname, use an accessible selector - cy.get(".vuuSaveLayoutPanel").then((dialog) => { + cy.get(".saveLayoutPanel-panelContainer").then((dialog) => { cy.wrap(dialog) .find("img") .should("be.visible") diff --git a/vuu-ui/cypress/screenshots/vuu-ui-controls/src/__tests__/__e2e__/tabstrip/Tabstrip.cy.tsx/WHEN initial size is sufficient to display all contents -- WHEN resized such that space is sufficient for only 4 tabs (LAST tab selected) -- THEN as last tab is selected, last but one will be overflowed (failed).png b/vuu-ui/cypress/screenshots/vuu-ui-controls/src/__tests__/__e2e__/tabstrip/Tabstrip.cy.tsx/WHEN initial size is sufficient to display all contents -- WHEN resized such that space is sufficient for only 4 tabs (LAST tab selected) -- THEN as last tab is selected, last but one will be overflowed (failed).png deleted file mode 100644 index f3e3fa91d47ab34cbc39ca834c0c90e5ebb0bca0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82772 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 z 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⪚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');--svg-alert-circle-filled: url('data:image/svg+xml;utf8,');--svg-arrow-thin: url('data:image/svg+xml;utf8,');--svg-box: url('data:image/svg+xml;utf8,');--svg-caret-right: url('data:image/svg+xml;utf8,');--svg-caret-down: url('data:image/svg+xml;utf8,');--svg-checkbox: url('data:image/svg+xml;utf8,');--svg-checkbox-checked: url('data:image/svg+xml;utf8,');--svg-close: url('data:image/svg+xml;utf8,');--svg-close-circle: url('data:image/svg+xml;utf8,');--svg-checkbox-small: url('data:image/svg+xml;utf8,');--svg-chevron-down: url('data:image/svg+xml;utf8,');--svg-chevron-double-left: url('data:image/svg+xml;utf8,');--svg-chevron-double-right: url('data:image/svg+xml;utf8,');--svg-column-2A: url('data:image/svg+xml;utf8,');--svg-column-2B: url('data:image/svg+xml;utf8,');--svg-corner-triangle: url('data:image/svg+xml;utf8,');--svg-filter: url('data:image/svg+xml;utf8,');--svg-folder-closed: url('data:image/svg+xml;utf8,');--svg-folder-open: url('data:image/svg+xml;utf8,');--svg-function: url('data:image/svg+xml;utf8,');--svg-logout: url('data:image/svg+xml;utf8,');--svg-rings: url('data:image/svg+xml;utf8,');--svg-settings: url('data:image/svg+xml;utf8,');--svg-sort-order-down: url('data:image/svg+xml;utf8,');--svg-sorted-asc: url('data:image/svg+xml;utf8,');--svg-sorted-dsc: url('data:image/svg+xml;utf8,');--svg-user: url('data:image/svg+xml;utf8,');--svg-grab-handle: url('data:image/svg+xml;utf8, ');--svg-open-in: url('data:image/svg+xml;utf8,');--svg-sort-up: url('data:image/svg+xml;utf8,');--svg-tree-node-collapse: url('data:image/svg+xml;utf8,');--svg-tree-node-expand: url('data:image/svg+xml;utf8,');--svg-triangle-right: url('data:image/svg+xml;utf8,');--svg-triangle-down: url('data:image/svg+xml;utf8,');--svg-plus-box: url('data:image/svg+xml;utf8,');--svg-minus-box: url('data:image/svg+xml;utf8,');--vuu-icon-size: 12px;--svg-active-status: url('data:image/svg+xml;utf8,');--svg-connecting-status: url('data:image/svg+xml;utf8,');--svg-disconnected-status: url('data:image/svg+xml;utf8,');--vuu-svg-chevron-left: url('data:image/svg+xml;utf8,');--vuu-svg-chevron-right: url('data:image/svg+xml;utf8,');--vuu-svg-more-vert: url('data:image/svg+xml;utf8,');--vuu-svg-triangle-right: url('data:image/svg+xml;utf8,')}span[data-icon]{display:inline-block;height:var(--vuu-icon-height, var(--vuu-icon-size,18px));position:relative;width:var(--vuu-icon-width, var(--vuu-icon-size, 18px))}[data-icon]:after{content:"";background-color:var(--vuu-icon-color, var(--saltIcon-color, var(--salt-text-secondary-foreground)));left:var(--vuu-icon-left, auto);height:var(--vuu-icon-height, var(--vuu-icon-size, 12px));-webkit-mask:var(--vuu-icon-svg) center center/var(--vuu-icon-size) var(--vuu-icon-size);mask:var(--vuu-icon-svg) center center/var(--vuu-icon-size) var(--vuu-icon-size);mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;position:absolute;top:var(--vuu-icon-top, auto);width:var(--vuu-icon-width, var(--vuu-icon-size, 12px))}[data-icon=sort-up]{--vuu-icon-svg: var(--svg-sort-up)}[data-icon=add]{--vuu-icon-svg: var(--svg-add)}[data-icon=arrow-thin-left]{--vuu-icon-svg: var(--svg-arrow-thin)}[data-icon=arrow-thin-right]{--vuu-icon-svg: var(--svg-arrow-thin);transform:rotate(180deg)}[data-icon=arrow-thin-up]{--vuu-icon-svg: var(--svg-arrow-thin);transform:rotate(90deg)}[data-icon=arrow-thin-down]{--vuu-icon-svg: var(--svg-arrow-thin);transform:rotate(270deg)}[data-icon=box]{--vuu-icon-svg: var(--svg-box)}[data-icon=chevron-left]{--vuu-icon-svg: var(--vuu-svg-chevron-left)}[data-icon=chevron-right]{--vuu-icon-svg: var(--vuu-svg-chevron-right)}[data-icon=close]{--vuu-icon-svg: var(--vuu-close-icon-svg, var(--svg-close))}[data-icon=error]{--vuu-icon-color: var(--salt-status-error-foreground);--vuu-icon-svg: var(--svg-alert-circle-filled)}[data-icon=filter]{--vuu-icon-svg: var(--svg-filter)}[data-icon=rings]{--vuu-icon-svg: var(--svg-rings)}[data-icon=open-in]{--vuu-icon-svg: var(--svg-open-in)}[data-icon=close-circle]{--vuu-icon-svg: var(--svg-close-circle)}[data-icon=chevron-double-left]{--vuu-icon-svg: var(--svg-chevron-double-left)}[data-icon=chevron-double-right]{--vuu-icon-svg: var(--svg-chevron-double-right)}[data-icon=column-2A]{--vuu-icon-svg: var(--svg-column-2A)}[data-icon=column-2B]{--vuu-icon-svg: var(--svg-column-2B)}:is([data-icon="folder"],[data-icon="folder-closed"]){--vuu-icon-svg: var(--svg-folder-closed)}[data-icon=plus-box]{--vuu-icon-svg: var(--svg-plus-box)}[data-icon=minus-box]{--vuu-icon-svg: var(--svg-minus-box)}[data-icon=more-vert]{--vuu-icon-svg: var(--vuu-svg-more-vert)}[data-icon=settings]{--vuu-icon-svg: var(--svg-settings)}[data-icon=sorted-asc]{--vuu-icon-svg: var(--svg-sorted-asc)}[data-icon=sorted-dsc]{--vuu-icon-svg: var(--svg-sorted-dsc)}[data-icon=triangle-right]{--vuu-icon-svg: var(--svg-triangle-right)}[data-icon=user]{--vuu-icon-svg: var(--svg-user)}[data-icon=active-status]{--svg-icon: var(--svg-active-status)}[data-icon=connecting-status]{--svg-icon: var(--svg-connecting-status)}[data-icon=disconnected-status]{--svg-icon: var(--svg-disconnected-status)}[data-icon=vuu-triangle-right]{--vuu-icon-svg: var(--vuu-svg-triangle-right)} +@font-face{font-family:Nunito Sans;font-style:normal;font-weight:300;font-stretch:100%;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:400;font-stretch:100%;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:500;font-stretch:100%;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:600;font-stretch:100%;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:700;font-stretch:100%;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:800;font-stretch:100%;font-display:swap;src:url(data:font/woff2;base64,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) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}.vuu-theme{color:var(--salt-text-primary-foreground);font-family:var(--salt-typography-fontFamily);font-size:var(--salt-text-fontSize);letter-spacing:var(--salt-text-letterSpacing);line-height:var(--salt-text-lineHeight)}::selection{background:var(--salt-text-background-selected);color:var(--salt-text-color-selected, inherit)}.vuu-theme[data-mode=light]{color-scheme:light}.vuu-theme[data-mode=dark]{color-scheme:dark}*,*:before,*:after{box-sizing:border-box}.salt-visuallyHidden{position:absolute;height:1px;width:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.vuu-density-touch,.vuu-density-low,.vuu-density-medium,.vuu-density-high{--salt-animation-opacity-start: 0;--salt-animation-opacity-end: 1;--salt-animation-scale-start: 0;--salt-animation-scale-end: 1;--salt-animation-transform-start: 100%;--salt-animation-transform-end: 0;--salt-animation-duration: .3s;--salt-animation-timing-function: ease-in;--salt-animation-slide-in-top: slide-in-top var(--salt-animation-duration) var(--salt-animation-timing-function);--salt-animation-slide-in-left: slide-in-left var(--salt-animation-duration) var(--salt-animation-timing-function);--salt-animation-slide-in-right: slide-in-right var(--salt-animation-duration) var(--salt-animation-timing-function);--salt-animation-slide-in-bottom: slide-in-bottom var(--salt-animation-duration) var(--salt-animation-timing-function);--salt-animation-slide-out-top: slide-out-top var(--salt-animation-duration) var(--salt-animation-timing-function) both;--salt-animation-slide-out-left: slide-out-left var(--salt-animation-duration) var(--salt-animation-timing-function) both;--salt-animation-slide-out-right: slide-out-right var(--salt-animation-duration) var(--salt-animation-timing-function) both;--salt-animation-slide-out-bottom: slide-out-bottom var(--salt-animation-duration) var(--salt-animation-timing-function) both;--salt-animation-fade-in-back: fade-in-back var(--salt-animation-duration) var(--salt-animation-timing-function);--salt-animation-fade-in-forward: fade-in-forward var(--salt-animation-duration) var(--salt-animation-timing-function);--salt-animation-fade-in-center: fade-in-center var(--salt-animation-duration) var(--salt-animation-timing-function);--salt-animation-fade-out-back: fade-out-back var(--salt-animation-duration) ease-out both}@keyframes slide-in-top{0%{opacity:var(--salt-animation-opacity-start);transform:translateY(var(--salt-animation-transform-start))}to{opacity:var(--salt-animation-opacity-end);transform:translateY(var(--salt-animation-transform-end))}}@keyframes slide-out-top{0%{opacity:var(--salt-animation-opacity-end);transform:translateY(var(--salt-animation-transform-end))}to{opacity:var(--salt-animation-opacity-start);transform:translateY(var(--salt-animation-transform-start))}}@keyframes slide-in-left{0%{opacity:var(--salt-animation-opacity-start);transform:translate(calc(-1 * var(--salt-animation-transform-start)))}to{opacity:var(--salt-animation-opacity-end);transform:translate(var(--salt-animation-transform-end))}}@keyframes slide-out-left{0%{opacity:var(--salt-animation-opacity-end);transform:translate(var(--salt-animation-transform-end))}to{opacity:var(--salt-animation-opacity-start);transform:translate(calc(-1 * var(--salt-animation-transform-start)))}}@keyframes slide-in-right{0%{opacity:var(--salt-animation-opacity-start);transform:translate(var(--salt-animation-transform-start))}to{opacity:var(--salt-animation-opacity-end);transform:translate(var(--salt-animation-transform-end))}}@keyframes slide-out-right{0%{opacity:var(--salt-animation-opacity-end);transform:translate(var(--salt-animation-transform-end))}to{opacity:var(--salt-animation-opacity-start);transform:translate(var(--salt-animation-transform-start))}}@keyframes slide-in-bottom{0%{opacity:var(--salt-animation-opacity-start);transform:translateY(calc(-1 * var(--salt-animation-transform-start)))}to{opacity:var(--salt-animation-opacity-end);transform:translateY(var(--salt-animation-transform-end))}}@keyframes slide-out-bottom{0%{opacity:var(--salt-animation-opacity-end);transform:translateY(var(--salt-animation-transform-end))}to{opacity:var(--salt-animation-opacity-start);transform:translateY(calc(-1 * var(--salt-animation-transform-start)))}}@keyframes fade-in-back{0%{--salt-animation-scale-start: 1.4;opacity:var(--salt-animation-opacity-start);transform:scale(var(--salt-animation-scale-start))}to{opacity:var(--salt-animation-opacity-end);transform:scale(var(--salt-animation-scale-end))}}@keyframes fade-in-forward{0%{--salt-animation-scale-start: .6;opacity:var(--salt-animation-opacity-start);transform:scale(var(--salt-animation-scale-start))}to{opacity:var(--salt-animation-opacity-end);transform:scale(var(--salt-animation-scale-end))}}@keyframes fade-in-center{0%{opacity:var(--salt-animation-opacity-start)}to{opacity:var(--salt-animation-opacity-end)}}@keyframes fade-out-back{0%{opacity:var(--salt-animation-opacity-end)}to{opacity:var(--salt-animation-opacity-start)}}.vuu-theme{--vuu-fade-light: .13;--vuu-color-transparent: transparent;--vuu-color-black: black;--vuu-color-white: white;--vuu-color-blue-40: rgb(164, 213, 244);--vuu-color-purple-10: rgb(109,24,189);--vuu-color-purple-10-fade-light: rgba(109,24,189, var(--vuu-fade-light));--vuu-color-pink-10: rgb(234, 120, 128);--vuu-color-gray-05: rgb(222, 222, 222);--vuu-color-gray-10: rgb(228, 227, 231);--vuu-color-gray-20: rgb(245, 242, 248);--vuu-color-gray-25: rgb(244, 244, 244) ;--vuu-color-gray-28: rgb(249, 249, 251);--vuu-color-gray-30: rgb(214, 215, 218);--vuu-color-gray-35: rgb(155, 158, 168);--vuu-color-gray-40: rgb(169, 170, 173);--vuu-color-gray-42: rgb(135, 139, 158);--vuu-color-gray-45: rgb(119, 124, 148);--vuu-color-gray-50: rgb(96, 100, 119);--vuu-color-gray-80: rgb(21, 23, 27);--vuu-color-green-50: rgb(102, 174, 90);--vuu-color-green-60: rgb(36, 137, 19);--vuu-color-green-60-fade-30: rgba(36, 137, 19, .3);--vuu-color-red-50: rgb(226, 52, 52);--vuu-color-yellow-20: rgb(244, 202, 51);--salt-color-white: rgb(255, 255, 255);--salt-color-black: rgb(0, 0, 0);--salt-color-red-10: rgb(255, 227, 224);--salt-color-red-20: rgb(255, 207, 201);--salt-color-red-30: rgb(255, 187, 178);--salt-color-red-40: rgb(255, 167, 156);--salt-color-red-50: rgb(255, 148, 133);--salt-color-red-100: rgb(255, 128, 111);--salt-color-red-200: rgb(255, 108, 88);--salt-color-red-300: rgb(255, 89, 66);--salt-color-red-400: rgb(237, 65, 42);--salt-color-red-500: rgb(227, 43, 22);--salt-color-red-600: rgb(196, 32, 16);--salt-color-red-700: rgb(166, 21, 11);--salt-color-red-800: rgb(136, 10, 5);--salt-color-red-900: rgb(65, 37, 34);--salt-color-orange-10: rgb(255, 232, 191);--salt-color-orange-20: rgb(254, 223, 166);--salt-color-orange-30: rgb(254, 214, 142);--salt-color-orange-40: rgb(254, 205, 118);--salt-color-orange-50: rgb(254, 197, 94);--salt-color-orange-100: rgb(250, 181, 81);--salt-color-orange-200: rgb(246, 165, 68);--salt-color-orange-300: rgb(242, 149, 56);--salt-color-orange-400: rgb(238, 133, 43);--salt-color-orange-500: rgb(234, 115, 25);--salt-color-orange-600: rgb(224, 101, 25);--salt-color-orange-700: rgb(214, 85, 19);--salt-color-orange-800: rgb(204, 68, 13);--salt-color-orange-900: rgb(54, 44, 36);--salt-color-green-10: rgb(209, 244, 201);--salt-color-green-20: rgb(184, 232, 182);--salt-color-green-30: rgb(160, 221, 164);--salt-color-green-40: rgb(136, 210, 145);--salt-color-green-50: rgb(112, 199, 127);--salt-color-green-100: rgb(93, 189, 116);--salt-color-green-200: rgb(77, 180, 105);--salt-color-green-300: rgb(60, 171, 96);--salt-color-green-400: rgb(48, 156, 90);--salt-color-green-500: rgb(36, 135, 75);--salt-color-green-600: rgb(24, 114, 61);--salt-color-green-700: rgb(12, 93, 46);--salt-color-green-800: rgb(1, 73, 32);--salt-color-green-900: rgb(35, 52, 43);--salt-color-teal-10: rgb(218, 240, 240);--salt-color-teal-20: rgb(199, 232, 232);--salt-color-teal-30: rgb(180, 224, 225);--salt-color-teal-40: rgb(162, 217, 218);--salt-color-teal-50: rgb(141, 205, 209);--salt-color-teal-100: rgb(123, 193, 200);--salt-color-teal-200: rgb(99, 181, 192);--salt-color-teal-300: rgb(73, 160, 172);--salt-color-teal-400: rgb(48, 149, 166);--salt-color-teal-500: rgb(0, 130, 151);--salt-color-teal-600: rgb(27, 107, 133);--salt-color-teal-700: rgb(0, 85, 113);--salt-color-teal-800: rgb(1, 65, 86);--salt-color-teal-900: rgb(0, 49, 76);--salt-color-blue-10: rgb(203, 231, 249);--salt-color-blue-20: rgb(183, 222, 246);--salt-color-blue-30: rgb(164, 213, 244);--salt-color-blue-40: rgb(144, 204, 242);--salt-color-blue-50: rgb(125, 195, 240);--salt-color-blue-100: rgb(100, 177, 228);--salt-color-blue-200: rgb(75, 159, 216);--salt-color-blue-300: rgb(51, 141, 205);--salt-color-blue-400: rgb(46, 132, 198);--salt-color-blue-500: rgb(38, 112, 169);--salt-color-blue-600: rgb(21, 92, 147);--salt-color-blue-700: rgb(0, 71, 123);--salt-color-blue-800: rgb(39, 60, 77);--salt-color-blue-900: rgb(35, 47, 56);--salt-color-purple-10: rgb(249, 224, 247);--salt-color-purple-20: rgb(247, 212, 244);--salt-color-purple-30: rgb(245, 201, 241);--salt-color-purple-40: rgb(243, 189, 238);--salt-color-purple-50: rgb(241, 178, 235);--salt-color-purple-100: rgb(223, 156, 225);--salt-color-purple-200: rgb(205, 135, 215);--salt-color-purple-300: rgb(192, 116, 203);--salt-color-purple-400: rgb(169, 97, 181);--salt-color-purple-500: rgb(150, 78, 162);--salt-color-purple-600: rgb(129, 60, 141);--salt-color-purple-700: rgb(103, 46, 122);--salt-color-purple-800: rgb(83, 37, 109);--salt-color-purple-900: rgb(59, 16, 84);--salt-color-gray-10: rgb(242, 244, 246);--salt-color-gray-20: rgb(234, 237, 239);--salt-color-gray-30: rgb(224, 228, 233);--salt-color-gray-40: rgb(217, 221, 227);--salt-color-gray-50: rgb(206, 210, 217);--salt-color-gray-60: rgb(197, 201, 208);--salt-color-gray-70: rgb(180, 183, 190);--salt-color-gray-80: rgb(159, 163, 170);--salt-color-gray-90: rgb(132, 135, 142);--salt-color-gray-100: rgb(116, 119, 127);--salt-color-gray-200: rgb(97, 101, 110);--salt-color-gray-300: rgb(76, 80, 91);--salt-color-gray-400: rgb(68, 72, 79);--salt-color-gray-500: rgb(59, 63, 70);--salt-color-gray-600: rgb(47, 49, 54);--salt-color-gray-700: rgb(42, 44, 47);--salt-color-gray-800: rgb(36, 37, 38);--salt-color-gray-900: rgb(22, 22, 22)}.vuu-theme{--salt-duration-instant: 0ms;--salt-duration-perceptible: .3s;--salt-duration-notable: 1s;--salt-duration-cutoff: 10s}.vuu-theme{--salt-color-blue-100-fade-foreground: rgba(100, 177, 228, var(--salt-palette-opacity-disabled));--salt-color-blue-500-fade-foreground: rgba(38, 112, 169, var(--salt-palette-opacity-disabled));--salt-color-blue-600-fade-foreground: rgba(21, 92, 147, var(--salt-palette-opacity-disabled));--salt-color-gray-200-fade-foreground: rgba(97, 101, 110, var(--salt-palette-opacity-disabled));--salt-color-gray-70-fade-foreground: rgba(180, 183, 190, var(--salt-palette-opacity-disabled));--salt-color-gray-90-fade-foreground: rgba(132, 135, 142, var(--salt-palette-opacity-disabled));--salt-color-gray-900-fade-foreground: rgba(22, 22, 22, var(--salt-palette-opacity-disabled));--salt-color-green-300-fade-foreground: rgba(60, 171, 96, var(--salt-palette-opacity-disabled));--salt-color-green-400-fade-foreground: rgba(48, 156, 90, var(--salt-palette-opacity-disabled));--salt-color-green-500-fade-foreground: rgba(36, 135, 75, var(--salt-palette-opacity-disabled));--salt-color-green-700-fade-foreground: rgba(12, 93, 46, var(--salt-palette-opacity-disabled));--salt-color-red-300-fade-foreground: rgba(255, 89, 66, var(--salt-palette-opacity-disabled));--salt-color-red-500-fade-foreground: rgba(227, 43, 22, var(--salt-palette-opacity-disabled));--salt-color-red-700-fade-foreground: rgba(166, 21, 11, var(--salt-palette-opacity-disabled));--salt-color-white-fade-foreground: rgba(255, 255, 255, var(--salt-palette-opacity-disabled));--salt-color-blue-500-fade-border: rgba(38, 112, 169, var(--salt-palette-opacity-disabled));--salt-color-gray-60-fade-border: rgba(197, 201, 208, var(--salt-palette-opacity-disabled));--salt-color-gray-90-fade-border: rgba(132, 135, 142, var(--salt-palette-opacity-disabled));--salt-color-gray-200-fade-border: rgba(97, 101, 110, var(--salt-palette-opacity-disabled));--salt-color-gray-300-fade-border: rgba(76, 80, 91, var(--salt-palette-opacity-disabled));--salt-color-green-400-fade-border: rgba(48, 156, 90, var(--salt-palette-opacity-disabled));--salt-color-green-500-fade-border: rgba(36, 135, 75, var(--salt-palette-opacity-disabled));--salt-color-orange-400-fade-border: rgba(238, 133, 43, var(--salt-palette-opacity-disabled));--salt-color-orange-500-fade-border: rgba(234, 115, 25, var(--salt-palette-opacity-disabled));--salt-color-orange-600-fade-border: rgba(224, 101, 25, var(--salt-palette-opacity-disabled));--salt-color-orange-700-fade-border: rgba(214, 85, 19, var(--salt-palette-opacity-disabled));--salt-color-red-500-fade-border: rgba(227, 43, 22, var(--salt-palette-opacity-disabled));--salt-color-gray-90-fade-border-readonly: rgba(132, 135, 142, var(--salt-palette-opacity-border-readonly));--salt-color-gray-200-fade-border-readonly: rgba(97, 101, 110, var(--salt-palette-opacity-border-readonly));--salt-color-blue-30-fade-background: rgba(164, 213, 244, var(--salt-palette-opacity-disabled));--salt-color-blue-500-fade-background: rgba(38, 112, 169, var(--salt-palette-opacity-disabled));--salt-color-blue-600-fade-background: rgba(21, 92, 147, var(--salt-palette-opacity-disabled));--salt-color-blue-700-fade-background: rgba(0, 71, 123, var(--salt-palette-opacity-disabled));--salt-color-gray-20-fade-background: rgba(234, 237, 239, var(--salt-palette-opacity-disabled));--salt-color-gray-60-fade-background: rgba(197, 201, 208, var(--salt-palette-opacity-disabled));--salt-color-gray-70-fade-background: rgba(180, 183, 190, var(--salt-palette-opacity-disabled));--salt-color-gray-200-fade-background: rgba(97, 101, 110, var(--salt-palette-opacity-disabled));--salt-color-gray-300-fade-background: rgba(76, 80, 91, var(--salt-palette-opacity-disabled));--salt-color-gray-600-fade-background: rgba(47, 49, 54, var(--salt-palette-opacity-disabled));--salt-color-gray-800-fade-background: rgba(36, 37, 38, var(--salt-palette-opacity-disabled));--salt-color-white-fade-background: rgba(255, 255, 255, var(--salt-palette-opacity-disabled));--salt-color-white-fade-background-readonly: rgba(255, 255, 255, var(--salt-palette-opacity-background-readonly));--salt-color-gray-20-fade-background-readonly: rgba(234, 237, 239, var(--salt-palette-opacity-background-readonly));--salt-color-gray-600-fade-background-readonly: rgba(47, 49, 54, var(--salt-palette-opacity-background-readonly));--salt-color-gray-800-fade-background-readonly: rgba(36, 37, 38, var(--salt-palette-opacity-background-readonly));--salt-color-black-fade-backdrop: rgba(36, 37, 38, var(--salt-palette-opacity-backdrop));--salt-color-blue-100-fade-fill: rgba(100, 177, 228, var(--salt-palette-opacity-disabled));--salt-color-blue-600-fade-fill: rgba(21, 92, 147, var(--salt-palette-opacity-disabled));--salt-color-white-fade-separatorOpacity-primary: rgba(255, 255, 255, var(--salt-palette-opacity-primary-border));--salt-color-white-fade-separatorOpacity-secondary: rgba(255, 255, 255, var(--salt-palette-opacity-secondary-border));--salt-color-white-fade-separatorOpacity-tertiary: rgba(255, 255, 255, var(--salt-palette-opacity-tertiary-border));--salt-color-black-fade-separatorOpacity-primary: rgba(0, 0, 0, var(--salt-palette-opacity-primary-border));--salt-color-black-fade-separatorOpacity-secondary: rgba(0, 0, 0, var(--salt-palette-opacity-secondary-border));--salt-color-black-fade-separatorOpacity-tertiary: rgba(0, 0, 0, var(--salt-palette-opacity-tertiary-border))}.vuu-density-touch{--salt-icon-size-base: 16px;--salt-icon-size-status-adornment: 12px}.vuu-density-low{--salt-icon-size-base: 14px;--salt-icon-size-status-adornment: 10px}.vuu-density-medium{--salt-icon-size-base: 12px;--salt-icon-size-status-adornment: 8px}.vuu-density-high{--salt-icon-size-base: 10px;--salt-icon-size-status-adornment: 6px}.vuu-theme{--salt-opacity-0: 0;--salt-opacity-8: .08;--salt-opacity-15: .15;--salt-opacity-25: .25;--salt-opacity-40: .4;--salt-opacity-70: .7}.vuu-theme[data-mode=light]{--salt-shadow-1-color: rgba(0, 0, 0, .1);--salt-shadow-2-color: rgba(0, 0, 0, .1);--salt-shadow-3-color: rgba(0, 0, 0, .15);--salt-shadow-4-color: rgba(0, 0, 0, .2);--salt-shadow-5-color: rgba(0, 0, 0, .3)}.vuu-theme[data-mode=dark]{--salt-shadow-1-color: rgba(0, 0, 0, .5);--salt-shadow-2-color: rgba(0, 0, 0, .5);--salt-shadow-3-color: rgba(0, 0, 0, .55);--salt-shadow-4-color: rgba(0, 0, 0, .55);--salt-shadow-5-color: rgba(0, 0, 0, .65)}.vuu-theme{--salt-shadow-0: none;--salt-shadow-1: 0 1px 3px 0 var(--salt-shadow-1-color);--salt-shadow-2: 0 2px 4px 0 var(--salt-shadow-2-color);--salt-shadow-3: 0 4px 8px 0 var(--salt-shadow-3-color);--salt-shadow-4: 0 6px 10px 0 var(--salt-shadow-4-color);--salt-shadow-5: 0 12px 40px 0 var(--salt-shadow-5-color)}.vuu-density-touch,.vuu-density-low,.vuu-density-medium,.vuu-density-high{--salt-size-basis-unit: 4px;--salt-size-adornmentGap: calc(.75 * var(--salt-size-unit));--salt-size-container-spacing: calc(3 * var(--salt-size-unit));--salt-size-separator-strokeWidth: 1px;--salt-size-selectable: calc(var(--salt-size-base) - (1.5 * var(--salt-size-unit)) - (.5 * var(--salt-size-basis-unit)));--salt-size-separator-height: calc(var(--salt-size-compact) + 1.5 * var(--salt-size-basis-unit));--salt-size-sharktooth-height: 5px;--salt-size-sharktooth-width: 10px;--salt-size-stackable: calc(var(--salt-size-base) + var(--salt-size-unit))}.vuu-density-high{--salt-size-unit: calc(var(--salt-size-basis-unit) * 1);--salt-size-compact: calc(var(--salt-size-basis-unit) * 1.5);--salt-size-accent: calc(var(--salt-size-basis-unit) * .5);--salt-size-adornment: 6px;--salt-size-bar: 2px;--salt-size-base: 20px;--salt-size-border: 1px;--salt-size-selectable: 12px;--salt-size-icon: 12px}.vuu-density-medium{--salt-size-unit: calc(var(--salt-size-basis-unit) * 2);--salt-size-compact: calc(var(--salt-size-basis-unit) * 2);--salt-size-accent: calc(var(--salt-size-basis-unit) * 1);--salt-size-adornment: 8px;--salt-size-bar: 4px;--salt-size-base: 28px;--salt-size-border: 1px;--salt-size-selectable: 14px;--salt-size-icon: 12px}.vuu-density-low{--salt-size-unit: calc(var(--salt-size-basis-unit) * 3);--salt-size-compact: calc(var(--salt-size-basis-unit) * 2.5);--salt-size-accent: calc(var(--salt-size-basis-unit) * 1.5);--salt-size-adornment: 10px;--salt-size-bar: 6px;--salt-size-base: 36px;--salt-size-border: 1px;--salt-size-selectable: 16px;--salt-size-icon: 14px}.vuu-density-touch{--salt-size-unit: calc(var(--salt-size-basis-unit) * 4);--salt-size-compact: calc(var(--salt-size-basis-unit) * 3);--salt-size-accent: calc(var(--salt-size-basis-unit) * 2);--salt-size-adornment: 12px;--salt-size-bar: 8px;--salt-size-base: 44px;--salt-size-border: 1px;--salt-size-selectable: 18px;--salt-size-icon: 16px}.vuu-density-touch{--salt-spacing-100: 16px}.vuu-density-low{--salt-spacing-100: 12px}.vuu-density-medium{--salt-spacing-100: 8px}.vuu-density-high{--salt-spacing-100: 4px}.vuu-density-touch,.vuu-density-low,.vuu-density-medium,.vuu-density-high{--salt-spacing-25: calc(.25 * var(--salt-spacing-100));--salt-spacing-50: calc(.5 * var(--salt-spacing-100));--salt-spacing-75: calc(.75 * var(--salt-spacing-100));--salt-spacing-150: calc(1.5 * var(--salt-spacing-100));--salt-spacing-200: calc(2 * var(--salt-spacing-100));--salt-spacing-250: calc(2.5 * var(--salt-spacing-100));--salt-spacing-300: calc(3 * var(--salt-spacing-100));--salt-spacing-350: calc(3.5 * var(--salt-spacing-100));--salt-spacing-400: calc(4 * var(--salt-spacing-100))}.vuu-theme{--salt-typography-fontFamily: "Nunito Sans";--salt-typography-fontFamily-code: "PT Mono";--salt-typography-fontWeight-light: 300;--salt-typography-fontWeight-regular: 400;--salt-typography-fontWeight-medium: 500;--salt-typography-fontWeight-semiBold: 600;--salt-typography-fontWeight-bold: 700;--salt-typography-fontWeight-extraBold: 800}.vuu-density-touch,.vuu-density-low,.vuu-density-medium,.vuu-density-high{--salt-zIndex-default: 1;--salt-zIndex-popout: 1000;--salt-zIndex-docked: 1050;--salt-zIndex-appHeader: 1100;--salt-zIndex-drawer: 1200;--salt-zIndex-modal: 1300;--salt-zIndex-notification: 1400;--salt-zIndex-dragObject: 1420;--salt-zIndex-contextMenu: 1450;--salt-zIndex-flyover: 1500}.vuu-theme[data-mode=light],.vuu-theme[data-mode=dark]{--salt-palette-accent-background: var(--salt-color-blue-500);--salt-palette-accent-background-disabled: var(--salt-color-blue-500-fade-background);--salt-palette-accent-border: var(--salt-color-blue-500);--salt-palette-accent-border-disabled: var(--salt-color-blue-500-fade-border);--salt-palette-accent-foreground: var(--salt-color-white);--salt-palette-accent-foreground-disabled: var(--salt-color-white-fade-foreground)}.vuu-theme[data-mode=light]{--salt-palette-error-background: var(--salt-color-red-10);--salt-palette-error-background-selected: var(--salt-color-red-20);--salt-palette-error-border: var(--salt-color-red-500);--salt-palette-error-foreground: var(--salt-color-red-500)}.vuu-theme[data-mode=dark]{--salt-palette-error-background: var(--salt-color-red-900);--salt-palette-error-background-selected: var(--salt-color-red-900);--salt-palette-error-border: var(--salt-color-red-500);--salt-palette-error-foreground: var(--salt-color-red-500)}.vuu-theme[data-mode=light]{--salt-palette-info-background: var(--salt-color-blue-10);--salt-palette-info-border: var(--salt-color-blue-500);--salt-palette-info-foreground: var(--salt-color-blue-500)}.vuu-theme[data-mode=dark]{--salt-palette-info-background: var(--salt-color-blue-900);--salt-palette-info-border: var(--salt-color-blue-500);--salt-palette-info-foreground: var(--salt-color-blue-500)}.vuu-theme[data-mode=light]{--salt-palette-interact-background: transparent;--salt-palette-interact-background-blurSelected: var(--salt-color-gray-30);--salt-palette-interact-background-hover: var(--vuu-color-gray-10);--salt-palette-interact-background-active: var(--vuu-color-blue-40);--salt-palette-interact-background-disabled: var(--vuu-color-gray-35);--salt-palette-interact-background-activeDisabled: var(--salt-color-blue-30-fade-background);--salt-palette-interact-border: var(--vuu-color-gray-45);--salt-palette-interact-border-active: var(--vuu-color-purple-10);--salt-palette-interact-border-activeDisabled: var(--salt-color-blue-600-fade-fill);--salt-palette-interact-border-disabled: var(--salt-color-gray-200-fade-border);--salt-palette-interact-border-hover: var(--vuu-color-pink-10);--salt-palette-interact-border-readonly: var(--salt-color-gray-200-fade-border-readonly);--salt-palette-interact-foreground: var(--salt-color-gray-200);--salt-palette-interact-foreground-active: var(--vuu-color-purple-10);--salt-palette-interact-foreground-activeDisabled: var(--salt-color-blue-600-fade-foreground);--salt-palette-interact-foreground-disabled: var(--salt-color-gray-200-fade-foreground);--salt-palette-interact-foreground-hover: var(--salt-color-blue-500);--salt-palette-interact-outline: var(--salt-color-blue-600);--salt-palette-interact-cta-background: var(--vuu-color-purple-10);--salt-palette-interact-cta-background-active: var(--vuu-color-purple-10);--salt-palette-interact-cta-background-activeDisabled: var(--salt-color-blue-700-fade-background);--salt-palette-interact-cta-background-disabled: var(--salt-color-blue-600-fade-background);--salt-palette-interact-cta-background-hover: var(--vuu-color-pink-10);--salt-palette-interact-cta-foreground: var(--salt-color-white);--salt-palette-interact-cta-foreground-active: var(--salt-color-white);--salt-palette-interact-cta-foreground-activeDisabled: var(--salt-color-white-fade-foreground);--salt-palette-interact-cta-foreground-disabled: var(--salt-color-white-fade-foreground);--salt-palette-interact-cta-foreground-hover: var(--vuu-color-gray-80);--salt-palette-interact-primary-background: var(--vuu-color-white);--salt-palette-interact-primary-background-active: var(--vuu-color-gray-50);--salt-palette-interact-primary-background-activeDisabled: var(--salt-color-gray-200-fade-background);--salt-palette-interact-primary-background-disabled: var(--salt-color-gray-60-fade-background);--salt-palette-interact-primary-background-hover: var(--vuu-color-pink-10);--salt-palette-interact-primary-foreground: var(--vuu-color-gray-50);--salt-palette-interact-primary-foreground-active: var(--salt-color-white);--salt-palette-interact-primary-foreground-activeDisabled: var(--salt-color-white-fade-foreground);--salt-palette-interact-primary-foreground-disabled: var(--salt-color-gray-900-fade-foreground);--salt-palette-interact-primary-foreground-hover: var(--vuu-color-gray-80);--salt-palette-interact-secondary-background: transparent;--salt-palette-interact-secondary-background-active: var(--vuu-color-purple-10);--salt-palette-interact-secondary-background-activeDisabled: var(--salt-color-gray-200-fade-background);--salt-palette-interact-secondary-background-disabled: transparent;--salt-palette-interact-secondary-background-hover: var(--salt-color-gray-40);--salt-palette-interact-secondary-foreground: var(--salt-color-gray-900);--salt-palette-interact-secondary-foreground-active: var(--salt-color-white);--salt-palette-interact-secondary-foreground-activeDisabled: var(--salt-color-white-fade-foreground);--salt-palette-interact-secondary-foreground-disabled: var(--salt-color-gray-900-fade-foreground);--salt-palette-interact-secondary-foreground-hover: var(--salt-color-gray-900)}.vuu-theme[data-mode=dark]{--salt-palette-interact-background: transparent;--salt-palette-interact-background-active: var(--salt-color-blue-700);--salt-palette-interact-background-blurSelected: var(--salt-color-gray-600);--salt-palette-interact-background-hover: var(--salt-color-blue-800);--salt-palette-interact-background-disabled: transparent;--salt-palette-interact-background-activeDisabled: var(--salt-color-blue-700-fade-background);--salt-palette-interact-border: var(--salt-color-gray-90);--salt-palette-interact-border-active: var(--salt-color-blue-100);--salt-palette-interact-border-activeDisabled: var(--salt-color-blue-100-fade-fill);--salt-palette-interact-border-disabled: var(--salt-color-gray-90-fade-border);--salt-palette-interact-border-hover: var(--salt-color-blue-500);--salt-palette-interact-border-readonly: var(--salt-color-gray-90-fade-border-readonly);--salt-palette-interact-foreground: var(--salt-color-gray-90);--salt-palette-interact-foreground-active: var(--salt-color-blue-100);--salt-palette-interact-foreground-activeDisabled: var(--salt-color-blue-100-fade-foreground);--salt-palette-interact-foreground-disabled: var(--salt-color-gray-90-fade-foreground);--salt-palette-interact-foreground-hover: var(--salt-color-blue-500);--salt-palette-interact-outline: var(--vuu-color-pink-10);--salt-palette-interact-cta-background: var(--salt-color-blue-600);--salt-palette-interact-cta-background-active: var(--salt-color-blue-700);--salt-palette-interact-cta-background-activeDisabled: var(--salt-color-blue-700-fade-background);--salt-palette-interact-cta-background-disabled: var(--salt-color-blue-600-fade-background);--salt-palette-interact-cta-background-hover: var(--salt-color-blue-500);--salt-palette-interact-cta-foreground: var(--salt-color-white);--salt-palette-interact-cta-foreground-active: var(--salt-color-white);--salt-palette-interact-cta-foreground-activeDisabled: var(--salt-color-white-fade-foreground);--salt-palette-interact-cta-foreground-disabled: var(--salt-color-white-fade-foreground);--salt-palette-interact-cta-foreground-hover: var(--salt-color-white);--salt-palette-interact-primary-background: var(--salt-color-gray-300);--salt-palette-interact-primary-background-active: var(--salt-color-gray-70);--salt-palette-interact-primary-background-activeDisabled: var(--salt-color-gray-70-fade-background);--salt-palette-interact-primary-background-disabled: var(--salt-color-gray-300-fade-background);--salt-palette-interact-primary-background-hover: var(--salt-color-gray-200);--salt-palette-interact-primary-foreground: var(--salt-color-white);--salt-palette-interact-primary-foreground-active: var(--salt-color-gray-900);--salt-palette-interact-primary-foreground-activeDisabled: var(--salt-color-gray-900-fade-foreground);--salt-palette-interact-primary-foreground-disabled: var(--salt-color-white-fade-foreground);--salt-palette-interact-primary-foreground-hover: var(--salt-color-white);--salt-palette-interact-secondary-background: transparent;--salt-palette-interact-secondary-background-active: var(--salt-color-gray-70);--salt-palette-interact-secondary-background-activeDisabled: var(--salt-color-gray-70-fade-background);--salt-palette-interact-secondary-background-disabled: transparent;--salt-palette-interact-secondary-background-hover: var(--salt-color-gray-200);--salt-palette-interact-secondary-foreground: var(--salt-color-white);--salt-palette-interact-secondary-foreground-active: var(--salt-color-gray-900);--salt-palette-interact-secondary-foreground-activeDisabled: var(--salt-color-gray-900-fade-foreground);--salt-palette-interact-secondary-foreground-disabled: var(--salt-color-white-fade-foreground);--salt-palette-interact-secondary-foreground-hover: var(--salt-color-white)}.vuu-theme[data-mode=light]{--salt-palette-navigate-primary-background: transparent;--salt-palette-navigate-primary-background-active: transparent;--salt-palette-navigate-primary-background-hover: var(--salt-color-gray-20);--salt-palette-navigate-secondary-background: transparent;--salt-palette-navigate-secondary-background-active: transparent;--salt-palette-navigate-secondary-background-hover: var(--salt-color-gray-30);--salt-palette-navigate-tertiary-background: transparent;--salt-palette-navigate-tertiary-background-active: transparent;--salt-palette-navigate-tertiary-background-hover: var(--salt-color-gray-20);--salt-palette-navigate-foreground-hover: var(--salt-color-blue-600);--salt-palette-navigate-foreground-active: var(--salt-color-blue-700);--salt-palette-navigate-foreground-visited: var(--salt-color-purple-800);--salt-palette-navigate-indicator-hover: var(--salt-color-gray-90);--salt-palette-navigate-indicator-active: var(--vuu-color-purple-10);--salt-palette-navigate-indicator-activeDisabled: var(--salt-color-orange-600-fade-border)}.vuu-theme[data-mode=dark]{--salt-palette-navigate-primary-background: transparent;--salt-palette-navigate-primary-background-active: transparent;--salt-palette-navigate-primary-background-hover: var(--salt-color-gray-700);--salt-palette-navigate-secondary-background: transparent;--salt-palette-navigate-secondary-background-active: transparent;--salt-palette-navigate-secondary-background-hover: var(--salt-color-gray-600);--salt-palette-navigate-tertiary-background: transparent;--salt-palette-navigate-tertiary-background-active: transparent;--salt-palette-navigate-tertiary-background-hover: var(--salt-color-gray-700);--salt-palette-navigate-foreground-hover: var(--salt-color-blue-200);--salt-palette-navigate-foreground-active: var(--salt-color-blue-300);--salt-palette-navigate-foreground-visited: var(--salt-color-purple-100);--salt-palette-navigate-indicator-hover: var(--salt-color-gray-90);--salt-palette-navigate-indicator-active: var(--vuu-color-pink-10);--salt-palette-navigate-indicator-activeDisabled: var(--salt-color-orange-400-fade-border)}.vuu-theme[data-mode=light]{--salt-palette-negative-foreground: var(--salt-color-red-700)}.vuu-theme[data-mode=dark]{--salt-palette-negative-foreground: var(--salt-color-red-300)}.vuu-theme[data-mode=light]{--salt-palette-neutral-primary-background: var(--salt-color-white);--salt-palette-neutral-primary-background-disabled: var(--salt-color-white-fade-background);--salt-palette-neutral-primary-background-readonly: var(--salt-color-white-fade-background-readonly);--salt-palette-neutral-primary-foreground: var(--vuu-color-gray-80);--salt-palette-neutral-primary-foreground-disabled: var(--salt-color-gray-900-fade-foreground);--salt-palette-neutral-primary-separator: var(--salt-color-black-fade-separatorOpacity-primary);--salt-palette-neutral-primary-border: var(--vuu-color-purple-10);--salt-palette-neutral-primary-border-disabled: var(--salt-color-gray-60-fade-border);--salt-palette-neutral-secondary-background: var(--vuu-color-gray-20);--salt-palette-neutral-secondary-background-disabled: var(--salt-color-gray-20-fade-background);--salt-palette-neutral-secondary-background-readonly: var(--salt-color-gray-20-fade-background-readonly);--salt-palette-neutral-secondary-border: var(--salt-color-gray-90);--salt-palette-neutral-secondary-border-disabled: var(--salt-color-gray-90-fade-border);--salt-palette-neutral-secondary-foreground: var(--salt-color-gray-200);--salt-palette-neutral-secondary-foreground-disabled: var(--salt-color-gray-200-fade-foreground);--salt-palette-neutral-backdrop: var(--salt-color-black-fade-backdrop);--salt-palette-neutral-secondary-separator: var(--salt-color-black-fade-separatorOpacity-secondary);--salt-palette-neutral-tertiary-background: transparent;--salt-palette-neutral-tertiary-background-disabled: transparent;--salt-palette-neutral-tertiary-border: transparent;--salt-palette-neutral-tertiary-border-disabled: transparent;--salt-palette-neutral-tertiary-separator: var(--vuu-color-gray-05)}.vuu-theme[data-mode=dark]{--salt-palette-neutral-primary-background: var(--salt-color-gray-800);--salt-palette-neutral-primary-background-disabled: var(--salt-color-gray-800-fade-background);--salt-palette-neutral-primary-background-readonly: var(--salt-color-gray-800-fade-background-readonly);--salt-palette-neutral-primary-border: var(--salt-color-gray-300);--salt-palette-neutral-primary-border-disabled: var(--salt-color-gray-300-fade-border);--salt-palette-neutral-primary-foreground: var(--salt-color-white);--salt-palette-neutral-primary-foreground-disabled: var(--salt-color-white-fade-foreground);--salt-palette-neutral-primary-separator: var(--salt-color-white-fade-separatorOpacity-primary);--salt-palette-neutral-secondary-background: var(--salt-color-gray-600);--salt-palette-neutral-secondary-background-disabled: var(--salt-color-gray-600-fade-background);--salt-palette-neutral-secondary-background-readonly: var(--salt-color-gray-600-fade-background-readonly);--salt-palette-neutral-secondary-border: var(--salt-color-gray-90);--salt-palette-neutral-secondary-border-disabled: var(--salt-color-gray-90-fade-border);--salt-palette-neutral-secondary-foreground: var(--salt-color-gray-70);--salt-palette-neutral-secondary-foreground-disabled: var(--salt-color-gray-70-fade-foreground);--salt-palette-neutral-backdrop: var(--salt-color-black-fade-backdrop);--salt-palette-neutral-secondary-separator: var(--salt-color-white-fade-separatorOpacity-secondary);--salt-palette-neutral-tertiary-background: transparent;--salt-palette-neutral-tertiary-background-disabled: transparent;--salt-palette-neutral-tertiary-border: transparent;--salt-palette-neutral-tertiary-border-disabled: transparent;--salt-palette-neutral-tertiary-separator: var(--salt-color-white-fade-separatorOpacity-tertiary)}.vuu-theme{--salt-palette-opacity-backdrop: var(--salt-opacity-70);--salt-palette-opacity-disabled: var(--salt-opacity-40);--salt-palette-opacity-background-readonly: var(--salt-opacity-0);--salt-palette-opacity-border-readonly: var(--salt-opacity-15);--salt-palette-opacity-primary-border: var(--salt-opacity-40);--salt-palette-opacity-secondary-border: var(--salt-opacity-25);--salt-palette-opacity-tertiary-border: var(--salt-opacity-15)}.vuu-theme[data-mode=light]{--salt-palette-positive-foreground: var(--salt-color-green-700)}.vuu-theme[data-mode=dark]{--salt-palette-positive-foreground: var(--salt-color-green-300)}.vuu-theme[data-mode=light]{--salt-palette-success-background: var(--salt-color-green-10);--salt-palette-success-background-selected: var(--salt-color-green-20);--salt-palette-success-border: var(--salt-color-green-500);--salt-palette-success-foreground: var(--salt-color-green-500)}.vuu-theme[data-mode=dark]{--salt-palette-success-background: var(--salt-color-green-900);--salt-palette-success-background-selected: var(--salt-color-green-900);--salt-palette-success-border: var(--salt-color-green-400);--salt-palette-success-foreground: var(--salt-color-green-400)}.vuu-theme[data-mode=light]{--salt-palette-track-background: var(--salt-color-gray-60);--salt-palette-track-background-disabled: var(--salt-color-gray-60-fade-background);--salt-palette-track-border: var(--salt-color-gray-90);--salt-palette-track-border-disabled: var(--salt-color-gray-90-fade-border)}.vuu-theme[data-mode=dark]{--salt-palette-track-background: var(--salt-color-gray-300);--salt-palette-track-background-disabled: var(--salt-color-gray-300-fade-background);--salt-palette-track-border: var(--salt-color-gray-90);--salt-palette-track-border-disabled: var(--salt-color-gray-90-fade-border)}.vuu-theme[data-mode=light]{--salt-palette-warning-background: var(--salt-color-orange-10);--salt-palette-warning-background-selected: var(--salt-color-orange-20);--salt-palette-warning-border: var(--salt-color-orange-700);--salt-palette-warning-foreground: var(--salt-color-orange-700)}.vuu-theme[data-mode=dark]{--salt-palette-warning-background: var(--salt-color-orange-900);--salt-palette-warning-background-selected: var(--salt-color-orange-900);--salt-palette-warning-border: var(--salt-color-orange-500);--salt-palette-warning-foreground: var(--salt-color-orange-500)}.vuu-density-high{--salt-accent-fontSize: 8px;--salt-accent-lineHeight: 11px}.vuu-density-medium{--salt-accent-fontSize: 10px;--salt-accent-lineHeight: 13px}.vuu-density-low{--salt-accent-fontSize: 12px;--salt-accent-lineHeight: 16px}.vuu-density-touch{--salt-accent-fontSize: 14px;--salt-accent-lineHeight: 18px}.vuu-theme{--salt-accent-background: var(--salt-palette-accent-background);--salt-accent-background-disabled: var(--salt-palette-accent-background-disabled);--salt-accent-borderColor: var(--salt-palette-accent-border);--salt-accent-borderColor-disabled: var(--salt-palette-accent-border-disabled);--salt-accent-foreground: var(--salt-palette-accent-foreground);--salt-accent-foreground-disabled: var(--salt-palette-accent-foreground-disabled);--salt-accent-fontWeight: var(--salt-typography-fontWeight-semiBold)}.vuu-theme{--salt-actionable-cursor-hover: pointer;--salt-actionable-cursor-active: pointer;--salt-actionable-cursor-disabled: not-allowed;--salt-actionable-letterSpacing: .6px;--salt-actionable-textAlign: center;--salt-actionable-textTransform: uppercase;--salt-actionable-primary-foreground: var(--salt-palette-interact-primary-foreground);--salt-actionable-primary-foreground-hover: var(--salt-palette-interact-primary-foreground-hover);--salt-actionable-primary-foreground-active: var(--salt-palette-interact-primary-foreground-active);--salt-actionable-primary-foreground-disabled: var(--salt-palette-interact-primary-foreground-disabled);--salt-actionable-primary-background: var(--salt-palette-interact-primary-background);--salt-actionable-primary-background-hover: var(--salt-palette-interact-primary-background-hover);--salt-actionable-primary-background-active: var(--salt-palette-interact-primary-background-active);--salt-actionable-primary-background-disabled: var(--salt-palette-interact-primary-background-disabled);--salt-actionable-primary-fontWeight: var(--salt-typography-fontWeight-bold);--salt-actionable-cta-foreground: var(--salt-palette-interact-cta-foreground);--salt-actionable-cta-foreground-hover: var(--salt-palette-interact-cta-foreground-hover);--salt-actionable-cta-foreground-active: var(--salt-palette-interact-cta-foreground-active);--salt-actionable-cta-foreground-disabled: var(--salt-palette-interact-cta-foreground-disabled);--salt-actionable-cta-background: var(--salt-palette-interact-cta-background);--salt-actionable-cta-background-hover: var(--salt-palette-interact-cta-background-hover);--salt-actionable-cta-background-active: var(--salt-palette-interact-cta-background-active);--salt-actionable-cta-background-disabled: var(--salt-palette-interact-cta-background-disabled);--salt-actionable-cta-fontWeight: var(--salt-typography-fontWeight-bold);--salt-actionable-secondary-foreground: var(--salt-palette-interact-secondary-foreground);--salt-actionable-secondary-foreground-hover: var(--salt-palette-interact-secondary-foreground-hover);--salt-actionable-secondary-foreground-active: var(--salt-palette-interact-secondary-foreground-active);--salt-actionable-secondary-foreground-disabled: var(--salt-palette-interact-secondary-foreground-disabled);--salt-actionable-secondary-background: var(--salt-palette-interact-secondary-background);--salt-actionable-secondary-background-hover: var(--salt-palette-interact-secondary-background-hover);--salt-actionable-secondary-background-active: var(--salt-palette-interact-secondary-background-active);--salt-actionable-secondary-background-disabled: var(--salt-palette-interact-secondary-background-disabled);--salt-actionable-secondary-fontWeight: var(--salt-typography-fontWeight-semiBold)}.vuu-theme{--salt-container-borderStyle: solid;--salt-container-primary-background: var(--salt-palette-neutral-primary-background);--salt-container-primary-background-disabled: var(--salt-palette-neutral-primary-background-disabled);--salt-container-primary-borderColor: var(--salt-palette-neutral-primary-border);--salt-container-primary-borderColor-disabled: var(--salt-palette-neutral-primary-border-disabled);--salt-container-secondary-background: var(--salt-palette-neutral-secondary-background);--salt-container-secondary-background-disabled: var(--salt-palette-neutral-secondary-background-disabled);--salt-container-secondary-borderColor: var(--salt-palette-neutral-secondary-border);--salt-container-secondary-borderColor-disabled: var(--salt-palette-neutral-secondary-border-disabled);--salt-container-tertiary-background: var(--salt-palette-neutral-tertiary-background);--salt-container-tertiary-background-disabled: var(--salt-palette-neutral-tertiary-background-disabled);--salt-container-tertiary-borderColor: var(--salt-palette-neutral-tertiary-border);--salt-container-tertiary-borderColor-disabled: var(--salt-palette-neutral-tertiary-border-disabled)}.vuu-theme{--salt-draggable-horizontal-cursor-hover: row-resize;--salt-draggable-horizontal-cursor-active: row-resize;--salt-draggable-vertical-cursor-hover: col-resize;--salt-draggable-vertical-cursor-active: col-resize;--salt-draggable-grab-cursor-hover: grab;--salt-draggable-grab-cursor-active: grabbing}.vuu-theme{--salt-target-background-hover: var(--salt-palette-interact-background-hover);--salt-target-borderColor-hover: var(--salt-palette-interact-border-hover);--salt-target-borderStyle: dashed;--salt-target-borderStyle-hover: solid;--salt-target-borderStyle-disabled: dashed;--salt-target-cursor-disabled: not-allowed}.vuu-theme{--salt-editable-cursor-hover: text;--salt-editable-cursor-active: text;--salt-editable-cursor-disabled: not-allowed;--salt-editable-cursor-readonly: text;--salt-editable-borderStyle: solid;--salt-editable-borderStyle-hover: solid;--salt-editable-borderStyle-active: solid;--salt-editable-borderStyle-disabled: solid;--salt-editable-borderStyle-readonly: solid;--salt-editable-borderWidth-active: 2px;--salt-editable-borderColor: var(--salt-palette-interact-border);--salt-editable-borderColor-active: var(--salt-palette-interact-border-active);--salt-editable-borderColor-disabled: var(--salt-palette-interact-border-disabled);--salt-editable-borderColor-hover: var(--salt-palette-interact-border-hover);--salt-editable-borderColor-readonly: var(--salt-palette-interact-border-readonly);--salt-editable-primary-background: var(--salt-palette-neutral-primary-background);--salt-editable-primary-background-active: var(--salt-palette-neutral-primary-background);--salt-editable-primary-background-disabled: var(--salt-palette-neutral-primary-background-disabled);--salt-editable-primary-background-hover: var(--salt-palette-neutral-primary-background);--salt-editable-primary-background-readonly: var(--salt-palette-neutral-primary-background-readonly);--salt-editable-secondary-background: var(--salt-palette-neutral-secondary-background);--salt-editable-secondary-background-active: var(--salt-palette-neutral-secondary-background);--salt-editable-secondary-background-disabled: var(--salt-palette-neutral-secondary-background-disabled);--salt-editable-secondary-background-hover: var(--salt-palette-neutral-secondary-background);--salt-editable-secondary-background-readonly: var(--salt-palette-neutral-secondary-background-readonly);--salt-editable-help-fontStyle: italic}.vuu-theme{--vuu-editable-borderColor-active: var(--editable-border-active, #6D18BD)}.saltInput-focused{border-color:var(--vuu-editable-borderColor-active)!important}.vuu-theme{--salt-navigable-cursor-active: pointer;--salt-navigable-cursor-hover: pointer;--salt-navigable-cursor-disabled: not-allowed;--salt-navigable-cursor-edit: text;--salt-navigable-fontWeight: var(--salt-typography-fontWeight-regular);--salt-navigable-fontWeight-hover: var(--salt-typography-fontWeight-regular);--salt-navigable-fontWeight-active: var(--salt-typography-fontWeight-semiBold);--salt-navigable-fontWeight-edit: var(--salt-typography-fontWeight-regular);--salt-navigable-indicator-hover: var(--salt-palette-navigate-indicator-hover);--salt-navigable-indicator-active: var(--salt-palette-navigate-indicator-active);--salt-navigable-indicator-activeDisabled: var(--salt-palette-navigate-indicator-activeDisabled);--salt-navigable-primary-background: var(--salt-palette-navigate-primary-background);--salt-navigable-primary-background-hover: var(--salt-palette-navigate-primary-background-hover);--salt-navigable-primary-background-active: var(--salt-palette-navigate-primary-background-active);--salt-navigable-secondary-background: var(--salt-palette-navigate-secondary-background);--salt-navigable-secondary-background-hover: var(--salt-palette-navigate-secondary-background-hover);--salt-navigable-secondary-background-active: var(--salt-palette-navigate-secondary-background-active);--salt-navigable-tertiary-background: var(--salt-palette-navigate-tertiary-background);--salt-navigable-tertiary-background-hover: var(--salt-palette-navigate-tertiary-background-hover);--salt-navigable-tertiary-background-active: var(--salt-palette-navigate-tertiary-background-active)}.vuu-theme{--salt-overlayable-shadow-scroll: var(--salt-shadow-1);--salt-overlayable-shadow-borderRegion: var(--salt-shadow-2);--salt-overlayable-shadow: var(--salt-shadow-2);--salt-overlayable-shadow-hover: var(--salt-shadow-3);--salt-overlayable-shadow-popout: var(--salt-shadow-4);--salt-overlayable-shadow-drag: var(--salt-shadow-4);--salt-overlayable-shadow-modal: var(--salt-shadow-5);--salt-overlayable-background: var(--salt-palette-neutral-backdrop)}.vuu-theme{--salt-selectable-cursor-hover: pointer;--salt-selectable-cursor-selected: pointer;--salt-selectable-cursor-blurSelected: pointer;--salt-selectable-cursor-disabled: not-allowed;--salt-selectable-cursor-readonly: not-allowed;--salt-selectable-borderStyle: solid;--salt-selectable-borderStyle-hover: solid;--salt-selectable-borderStyle-selected: solid;--salt-selectable-borderStyle-blurSelected: solid;--salt-selectable-borderColor: var(--salt-palette-interact-border);--salt-selectable-borderColor-hover: var(--salt-palette-interact-border-hover);--salt-selectable-borderColor-selected: var(--salt-palette-interact-border-active);--salt-selectable-borderColor-selectedDisabled: var(--salt-palette-interact-border-activeDisabled);--salt-selectable-borderColor-disabled: var(--salt-palette-interact-border-disabled);--salt-selectable-borderColor-readonly: var(--salt-palette-interact-border-readonly);--salt-selectable-foreground: var(--salt-palette-interact-foreground);--salt-selectable-foreground-disabled: var(--salt-palette-interact-foreground-disabled);--salt-selectable-foreground-hover: var(--salt-palette-interact-foreground-hover);--salt-selectable-foreground-selected: var(--salt-palette-interact-foreground-active);--salt-selectable-foreground-selectedDisabled: var(--salt-palette-interact-foreground-activeDisabled);--salt-selectable-background: var(--salt-palette-interact-background);--salt-selectable-background-hover: var(--salt-palette-interact-background-hover);--salt-selectable-background-selected: var(--salt-palette-interact-background-active);--salt-selectable-background-blurSelected: var(--salt-palette-interact-background-blurSelected);--salt-selectable-background-disabled: var(--salt-palette-interact-background-disabled);--salt-selectable-background-selectedDisabled: var(--salt-palette-interact-background-activeDisabled);--salt-selectable-cta-foreground-hover: var(--salt-palette-interact-cta-foreground-hover);--salt-selectable-cta-foreground-selected: var(--salt-palette-interact-cta-foreground-active);--salt-selectable-cta-foreground-selectedDisabled: var(--salt-palette-interact-cta-foreground-activeDisabled);--salt-selectable-cta-background: var(--salt-palette-interact-background);--salt-selectable-cta-background-disabled: var(--salt-palette-interact-background-disabled);--salt-selectable-cta-background-hover: var(--salt-palette-interact-cta-background-hover);--salt-selectable-cta-background-selected: var(--salt-palette-interact-cta-background-active);--salt-selectable-cta-background-selectedDisabled: var(--salt-palette-interact-cta-background-activeDisabled);--salt-selectable-primary-foreground-hover: var(--salt-palette-interact-primary-foreground-hover);--salt-selectable-primary-foreground-selected: var(--salt-palette-interact-primary-foreground-active);--salt-selectable-primary-foreground-selectedDisabled: var(--salt-palette-interact-primary-foreground-activeDisabled);--salt-selectable-primary-background: var(--salt-palette-interact-background);--salt-selectable-primary-background-disabled: var(--salt-palette-interact-background-disabled);--salt-selectable-primary-background-hover: var(--salt-palette-interact-primary-background-hover);--salt-selectable-primary-background-selected: var(--salt-palette-interact-primary-background-active);--salt-selectable-primary-background-selectedDisabled: var(--salt-palette-interact-primary-background-activeDisabled);--salt-selectable-secondary-foreground-hover: var(--salt-palette-interact-secondary-foreground-hover);--salt-selectable-secondary-foreground-selected: var(--salt-palette-interact-secondary-foreground-active);--salt-selectable-secondary-foreground-selectedDisabled: var(--salt-palette-interact-secondary-foreground-activeDisabled);--salt-selectable-secondary-background: var(--salt-palette-interact-background);--salt-selectable-secondary-background-disabled: var(--salt-palette-interact-background-disabled);--salt-selectable-secondary-background-hover: var(--salt-palette-interact-secondary-background-hover);--salt-selectable-secondary-background-selected: var(--salt-palette-interact-secondary-background-active);--salt-selectable-secondary-background-selectedDisabled: var(--salt-palette-interact-secondary-background-activeDisabled)}.vuu-theme{--salt-separable-borderStyle: solid;--salt-separable-primary-borderColor: var(--salt-palette-neutral-primary-separator);--salt-separable-secondary-borderColor: var(--salt-palette-neutral-secondary-separator);--salt-separable-tertiary-borderColor: var(--salt-palette-neutral-tertiary-separator)}.vuu-theme{--salt-status-info-foreground: var(--salt-palette-info-foreground);--salt-status-success-foreground: var(--salt-palette-success-foreground);--salt-status-warning-foreground: var(--salt-palette-warning-foreground);--salt-status-error-foreground: var(--salt-palette-error-foreground);--salt-status-static-foreground: var(--salt-palette-neutral-secondary-foreground);--salt-status-negative-foreground: var(--salt-palette-negative-foreground);--salt-status-positive-foreground: var(--salt-palette-positive-foreground);--salt-status-info-borderColor: var(--salt-palette-info-border);--salt-status-success-borderColor: var(--salt-palette-success-border);--salt-status-warning-borderColor: var(--salt-palette-warning-border);--salt-status-error-borderColor: var(--salt-palette-error-border);--salt-status-info-background: var(--salt-palette-info-background);--salt-status-success-background: var(--salt-palette-success-background);--salt-status-warning-background: var(--salt-palette-warning-background);--salt-status-error-background: var(--salt-palette-error-background);--salt-status-success-background-selected: var(--salt-palette-success-background-selected);--salt-status-warning-background-selected: var(--salt-palette-warning-background-selected);--salt-status-error-background-selected: var(--salt-palette-error-background-selected)}.vuu-theme{--salt-taggable-cursor-hover: pointer;--salt-taggable-cursor-active: pointer;--salt-taggable-cursor-disabled: not-allowed;--salt-taggable-background: var(--salt-palette-interact-primary-background);--salt-taggable-background-hover: var(--salt-palette-interact-primary-background-hover);--salt-taggable-background-active: var(--salt-palette-interact-primary-background-active);--salt-taggable-background-disabled: var(--salt-palette-interact-primary-background-disabled);--salt-taggable-foreground: var(--salt-palette-interact-primary-foreground);--salt-taggable-foreground-hover: var(--salt-palette-interact-primary-foreground-hover);--salt-taggable-foreground-active: var(--salt-palette-interact-primary-foreground-active);--salt-taggable-foreground-disabled: var(--salt-palette-interact-primary-foreground-disabled)}.vuu-theme{--salt-text-letterSpacing: 0;--salt-text-textAlign: left;--salt-text-textAlign-embedded: center;--salt-text-textDecoration: none;--salt-text-textTransform: none;--salt-text-fontFamily: var(--salt-typography-fontFamily);--salt-text-fontWeight: var(--salt-typography-fontWeight-regular);--salt-text-fontWeight-small: var(--salt-typography-fontWeight-light);--salt-text-fontWeight-strong: var(--salt-typography-fontWeight-semiBold);--salt-text-h1-fontFamily: var(--salt-typography-fontFamily);--salt-text-h1-fontWeight: var(--salt-typography-fontWeight-bold);--salt-text-h1-fontWeight-small: var(--salt-typography-fontWeight-medium);--salt-text-h1-fontWeight-strong: var(--salt-typography-fontWeight-extraBold);--salt-text-h2-fontFamily: var(--salt-typography-fontFamily);--salt-text-h2-fontWeight: var(--salt-typography-fontWeight-semiBold);--salt-text-h2-fontWeight-small: var(--salt-typography-fontWeight-regular);--salt-text-h2-fontWeight-strong: var(--salt-typography-fontWeight-bold);--salt-text-h3-fontFamily: var(--salt-typography-fontFamily);--salt-text-h3-fontWeight: var(--salt-typography-fontWeight-semiBold);--salt-text-h3-fontWeight-small: var(--salt-typography-fontWeight-regular);--salt-text-h3-fontWeight-strong: var(--salt-typography-fontWeight-bold);--salt-text-h4-fontFamily: var(--salt-typography-fontFamily);--salt-text-h4-fontWeight: var(--salt-typography-fontWeight-semiBold);--salt-text-h4-fontWeight-small: var(--salt-typography-fontWeight-regular);--salt-text-h4-fontWeight-strong: var(--salt-typography-fontWeight-bold);--salt-text-label-fontFamily: var(--salt-typography-fontFamily);--salt-text-label-fontWeight: var(--salt-typography-fontWeight-regular);--salt-text-label-fontWeight-small: var(--salt-typography-fontWeight-light);--salt-text-label-fontWeight-strong: var(--salt-typography-fontWeight-semiBold);--salt-text-display1-fontFamily: var(--salt-typography-fontFamily);--salt-text-display1-fontWeight: var(--salt-typography-fontWeight-semiBold);--salt-text-display1-fontWeight-strong: var(--salt-typography-fontWeight-bold);--salt-text-display1-fontWeight-small: var(--salt-typography-fontWeight-regular);--salt-text-display2-fontFamily: var(--salt-typography-fontFamily);--salt-text-display2-fontWeight: var(--salt-typography-fontWeight-semiBold);--salt-text-display2-fontWeight-strong: var(--salt-typography-fontWeight-bold);--salt-text-display2-fontWeight-small: var(--salt-typography-fontWeight-regular);--salt-text-display3-fontFamily: var(--salt-typography-fontFamily);--salt-text-display3-fontWeight: var(--salt-typography-fontWeight-semiBold);--salt-text-display3-fontWeight-strong: var(--salt-typography-fontWeight-bold);--salt-text-display3-fontWeight-small: var(--salt-typography-fontWeight-regular);--salt-text-background-selected: var(--salt-palette-interact-background-active);--salt-text-primary-foreground: var(--salt-palette-neutral-primary-foreground);--salt-text-primary-foreground-disabled: var(--salt-palette-neutral-primary-foreground-disabled);--salt-text-secondary-foreground: var(--salt-palette-neutral-secondary-foreground);--salt-text-secondary-foreground-disabled: var(--salt-palette-neutral-secondary-foreground-disabled);--salt-text-link-foreground-hover: var(--salt-palette-navigate-foreground-hover);--salt-text-link-foreground-active: var(--salt-palette-navigate-foreground-active);--salt-text-link-foreground-visited: var(--salt-palette-navigate-foreground-visited);--salt-text-link-textDecoration: underline;--salt-text-link-textDecoration-hover: none;--salt-text-link-textDecoration-selected: underline;--salt-text-code-fontFamily: var(--salt-typography-fontFamily-code)}.vuu-density-touch{--salt-text-h1-fontSize: 42px;--salt-text-h1-lineHeight: 54px;--salt-text-h2-fontSize: 32px;--salt-text-h2-lineHeight: 42px;--salt-text-h3-fontSize: 24px;--salt-text-h3-lineHeight: 32px;--salt-text-h4-fontSize: 16px;--salt-text-h4-lineHeight: 20px;--salt-text-label-fontSize: 14px;--salt-text-label-lineHeight: 18px;--salt-text-fontSize: 16px;--salt-text-lineHeight: 20px;--salt-text-minHeight: 20px;--salt-text-display1-fontSize: 84px;--salt-text-display1-lineHeight: 109px;--salt-text-display2-fontSize: 58px;--salt-text-display2-lineHeight: 76px;--salt-text-display3-fontSize: 42px;--salt-text-display3-lineHeight: 54px}.vuu-density-low{--salt-text-h1-fontSize: 32px;--salt-text-h1-lineHeight: 42px;--salt-text-h2-fontSize: 24px;--salt-text-h2-lineHeight: 32px;--salt-text-h3-fontSize: 18px;--salt-text-h3-lineHeight: 24px;--salt-text-h4-fontSize: 14px;--salt-text-h4-lineHeight: 18px;--salt-text-label-fontSize: 12px;--salt-text-label-lineHeight: 16px;--salt-text-fontSize: 14px;--salt-text-lineHeight: 18px;--salt-text-minHeight: 18px;--salt-text-display1-fontSize: 68px;--salt-text-display1-lineHeight: 88px;--salt-text-display2-fontSize: 46px;--salt-text-display2-lineHeight: 60px;--salt-text-display3-fontSize: 32px;--salt-text-display3-lineHeight: 42px}.vuu-density-medium{--salt-text-h1-fontSize: 24px;--salt-text-h1-lineHeight: 32px;--salt-text-h2-fontSize: 18px;--salt-text-h2-lineHeight: 24px;--salt-text-h3-fontSize: 14px;--salt-text-h3-lineHeight: 18px;--salt-text-h4-fontSize: 12px;--salt-text-h4-lineHeight: 16px;--salt-text-label-fontSize: 11px;--salt-text-label-lineHeight: 14px;--salt-text-fontSize: 12px;--salt-text-lineHeight: 16px;--salt-text-minHeight: 16px;--salt-text-display1-fontSize: 54px;--salt-text-display1-lineHeight: 70px;--salt-text-display2-fontSize: 36px;--salt-text-display2-lineHeight: 47px;--salt-text-display3-fontSize: 24px;--salt-text-display3-lineHeight: 32px}.vuu-density-high{--salt-text-h1-fontSize: 18px;--salt-text-h1-lineHeight: 24px;--salt-text-h2-fontSize: 14px;--salt-text-h2-lineHeight: 18px;--salt-text-h3-fontSize: 12px;--salt-text-h3-lineHeight: 16px;--salt-text-h4-fontSize: 11px;--salt-text-h4-lineHeight: 14px;--salt-text-label-fontSize: 10px;--salt-text-label-lineHeight: 13px;--salt-text-fontSize: 12px;--salt-text-lineHeight: 14px;--salt-text-minHeight: 14px;--salt-text-display1-fontSize: 42px;--salt-text-display1-lineHeight: 54px;--salt-text-display2-fontSize: 28px;--salt-text-display2-lineHeight: 36px;--salt-text-display3-fontSize: 18px;--salt-text-display3-lineHeight: 24px}.vuu-theme{--salt-track-borderStyle: solid;--salt-track-borderStyle-active: solid;--salt-track-borderStyle-complete: solid;--salt-track-borderStyle-incomplete: dotted;--salt-track-borderWidth: 2px;--salt-track-borderWidth-active: 2px;--salt-track-borderWidth-complete: 2px;--salt-track-borderWidth-incomplete: 2px;--salt-track-fontWeight: var(--salt-typography-fontWeight-semiBold);--salt-track-textAlign: center;--salt-track-background: var(--salt-palette-track-background);--salt-track-background-disabled: var(--salt-palette-track-background-disabled);--salt-track-borderColor: var(--salt-palette-track-border);--salt-track-borderColor-disabled: var(--salt-palette-track-border-disabled)}.vuu-theme{--salt-differential-positive-foreground: var(--salt-palette-positive-foreground);--salt-differential-negative-foreground: var(--salt-palette-negative-foreground);--salt-editable-tertiary-background: var(--salt-palette-neutral-tertiary-background);--salt-editable-tertiary-background-active: var(--salt-palette-neutral-tertiary-background);--salt-editable-tertiary-background-disabled: var(--salt-palette-neutral-tertiary-background-disabled);--salt-editable-tertiary-background-hover: var(--salt-palette-neutral-tertiary-background);--salt-editable-tertiary-background-readonly: var(--salt-palette-neutral-tertiary-background-readonly);--salt-measured-borderStyle: solid;--salt-measured-borderStyle-active: solid;--salt-measured-borderStyle-complete: solid;--salt-measured-borderStyle-incomplete: dotted;--salt-measured-borderWidth: 2px;--salt-measured-borderWidth-active: 2px;--salt-measured-borderWidth-complete: 2px;--salt-measured-borderWidth-incomplete: 2px;--salt-measured-fontWeight: var(--salt-typography-fontWeight-semiBold);--salt-measured-textAlign: center;--salt-measured-background: var(--salt-palette-measured-background);--salt-measured-background-disabled: var(--salt-palette-measured-background-disabled);--salt-measured-borderColor: var(--salt-palette-measured-border);--salt-measured-borderColor-disabled: var(--salt-palette-measured-border-disabled);--salt-measured-fill: var(--salt-palette-measured-fill);--salt-measured-fill-disabled: var(--salt-palette-measured-fill-disabled);--salt-measured-foreground: var(--salt-palette-measured-foreground);--salt-measured-foreground-hover: var(--salt-palette-measured-foreground-active);--salt-measured-foreground-active: var(--salt-palette-measured-foreground-active);--salt-measured-foreground-undo: var(--salt-palette-measured-foreground-active);--salt-measured-foreground-activeDisabled: var(--salt-palette-measured-foreground-activeDisabled);--salt-measured-foreground-disabled: var(--salt-palette-measured-foreground-disabled);--salt-overlayable-shadow-scroll-color: var(--salt-shadow-1-color);--salt-selectable-foreground-partial: var(--salt-palette-interact-foreground-partial);--salt-selectable-foreground-partialDisabled: var(--salt-palette-interact-foreground-partialDisabled);--salt-selectable-cta-foreground: var(--salt-palette-interact-foreground);--salt-selectable-cta-foreground-disabled: var(--salt-palette-interact-foreground-disabled);--salt-selectable-primary-foreground: var(--salt-palette-interact-foreground);--salt-selectable-primary-foreground-disabled: var(--salt-palette-interact-foreground-disabled);--salt-selectable-secondary-foreground: var(--salt-palette-interact-foreground);--salt-selectable-secondary-foreground-disabled: var(--salt-palette-interact-foreground-disabled);--salt-status-info-background-emphasize: var(--salt-status-info-background);--salt-status-success-background-emphasize: var(--salt-status-success-background);--salt-status-warning-background-emphasize: var(--salt-status-warning-background);--salt-status-error-background-emphasize: var(--salt-status-error-background);--salt-status-info-foreground-disabled: var(--salt-palette-info-foreground-disabled);--salt-status-success-foreground-disabled: var(--salt-palette-success-foreground-disabled);--salt-status-warning-foreground-disabled: var(--salt-palette-warning-foreground-disabled);--salt-status-error-foreground-disabled: var(--salt-palette-error-foreground-disabled);--salt-status-static-foreground-disabled: var(--salt-palette-neutral-secondary-foreground-disabled);--salt-status-negative-foreground-disabled: var(--salt-palette-negative-foreground-disabled);--salt-status-positive-foreground-disabled: var(--salt-palette-positive-foreground-disabled);--salt-status-info-borderColor-disabled: var(--salt-palette-info-border-disabled);--salt-status-success-borderColor-disabled: var(--salt-palette-success-border-disabled);--salt-status-warning-borderColor-disabled: var(--salt-palette-warning-border-disabled);--salt-status-error-borderColor-disabled: var(--salt-palette-error-border-disabled)}.vuu-theme{--salt-color-orange-500-fade-foreground: rgba(234, 115, 25, var(--salt-palette-opacity-foreground));--salt-color-orange-700-fade-foreground: rgba(214, 85, 19, var(--salt-palette-opacity-foreground));--salt-color-orange-400-fade-background: rgba(238, 133, 43, var(--salt-palette-opacity-background));--salt-color-orange-600-fade-background: rgba(224, 101, 25, var(--salt-palette-opacity-background));--salt-color-blue-300-fade-fill: rgba(51, 141, 205, var(--salt-palette-opacity-fill));--salt-color-blue-500-fade-fill: rgba(38, 112, 169, var(--salt-palette-opacity-fill))}.vuu-theme{--salt-delay-instant: .1s;--salt-delay-perceptible: .3s;--salt-delay-notable: 1s;--salt-delay-cutoff: 10s;--salt-size-icon-base: var(--salt-icon-size-base);--salt-opacity-1: .15;--salt-opacity-2: .25;--salt-opacity-3: .4;--salt-opacity-4: .7}.vuu-density-touch,.vuu-density-low,.vuu-density-medium,.vuu-density-high{--salt-size-selection: var(--salt-size-selectable);--salt-size-brandBar: 4px;--salt-size-graphic-small: 12px;--salt-size-graphic-medium: 24px;--salt-size-graphic-large: 48px;--salt-size-divider-height: var(--salt-size-separator-height);--salt-size-divider-strokeWidth: var(--salt-size-separator-strokeWidth);--salt-size-detail: var(--salt-size-compact)}.vuu-theme{--salt-palette-error-background-emphasize: var(--salt-palette-error-background);--salt-palette-warning-background-emphasize: var(--salt-palette-warning-background);--salt-palette-success-background-emphasize: var(--salt-palette-success-background);--salt-palette-info-background-emphasize: var(--salt-palette-info-background);--salt-palette-opacity-fill: var(--salt-palette-opacity-disabled);--salt-palette-opacity-stroke: var(--salt-palette-opacity-disabled);--salt-palette-opacity-background: var(--salt-palette-opacity-disabled);--salt-palette-opacity-border: var(--salt-palette-opacity-disabled);--salt-palette-opacity-foreground: var(--salt-palette-opacity-disabled)}.vuu-theme[data-mode=light]{--salt-palette-interact-foreground-partial: var(--salt-color-blue-600);--salt-palette-interact-foreground-partialDisabled: var(--salt-color-blue-600-fade-foreground);--salt-palette-measured-fill: var(--salt-color-blue-500);--salt-palette-measured-fill-disabled: var(--salt-color-blue-500-fade-fill);--salt-palette-measured-foreground: var(--salt-color-gray-90);--salt-palette-measured-foreground-active: var(--salt-color-blue-500);--salt-palette-measured-foreground-disabled: var(--salt-color-gray-90-fade-foreground);--salt-palette-measured-foreground-activeDisabled: var(--salt-color-blue-500-fade-fill);--salt-palette-measured-background: var(--salt-color-gray-60);--salt-palette-measured-background-disabled: var(--salt-color-gray-60-fade-background);--salt-palette-measured-border: var(--salt-color-gray-90);--salt-palette-measured-border-disabled: var(--salt-color-gray-90-fade-border);--salt-palette-neutral-tertiary-background-readonly: transparent;--salt-palette-error-foreground-disabled: var(--salt-color-red-500-fade-foreground);--salt-palette-error-border-disabled: var(--salt-color-red-500-fade-border);--salt-palette-info-border-disabled: var(--salt-color-blue-500-fade-border);--salt-palette-info-foreground-disabled: var(--salt-color-blue-500-fade-foreground);--salt-palette-negative-foreground-disabled: var(--salt-color-red-700-fade-foreground);--salt-palette-positive-foreground-disabled: var(--salt-color-green-700-fade-foreground);--salt-palette-success-border-disabled: var(--salt-color-green-500-fade-border);--salt-palette-success-foreground-disabled: var(--salt-color-green-500-fade-foreground);--salt-palette-warning-foreground-disabled: var(--salt-color-orange-700-fade-foreground);--salt-palette-warning-border-disabled: var(--salt-color-orange-700-fade-border)}.vuu-theme[data-mode=dark]{--salt-palette-interact-foreground-partial: var(--salt-color-blue-100);--salt-palette-interact-foreground-partialDisabled: var(--salt-color-blue-100-fade-foreground);--salt-palette-measured-fill: var(--salt-color-blue-300);--salt-palette-measured-fill-disabled: var(--salt-color-blue-300-fade-fill);--salt-palette-measured-foreground: var(--salt-color-gray-90);--salt-palette-measured-foreground-active: var(--salt-color-blue-300);--salt-palette-measured-foreground-disabled: var(--salt-color-gray-90-fade-foreground);--salt-palette-measured-foreground-activeDisabled: var(--salt-color-blue-300-fade-fill);--salt-palette-measured-background: var(--salt-color-gray-300);--salt-palette-measured-background-disabled: var(--salt-color-gray-300-fade-background);--salt-palette-measured-border: var(--salt-color-gray-90);--salt-palette-measured-border-disabled: var(--salt-color-gray-90-fade-border);--salt-palette-neutral-tertiary-background-readonly: transparent;--salt-palette-error-foreground-disabled: var(--salt-color-red-500-fade-foreground);--salt-palette-error-border-disabled: var(--salt-color-red-500-fade-border);--salt-palette-info-border-disabled: var(--salt-color-blue-500-fade-border);--salt-palette-info-foreground-disabled: var(--salt-color-blue-500-fade-foreground);--salt-palette-negative-foreground-disabled: var(--salt-color-red-300-fade-foreground);--salt-palette-positive-foreground-disabled: var(--salt-color-green-300-fade-foreground);--salt-palette-success-border-disabled: var(--salt-color-green-400-fade-border);--salt-palette-success-foreground-disabled: var(--salt-color-green-400-fade-foreground);--salt-palette-warning-foreground-disabled: var(--salt-color-orange-500-fade-foreground);--salt-palette-warning-border-disabled: var(--salt-color-orange-500-fade-border)}.saltButton{white-space:nowrap}.saltButton-primary{--saltButton-borderColor: var(--salt-actionable-primary-foreground);--saltButton-borderWidth: 1px;--saltButton-borderRadius: 6px;--saltButton-borderStyle: solid;--vuu-icon-color: var(--saltIcon-color)}.saltButton-primary:hover{--saltButton-borderColor: var(--salt-actionable-primary-background-hover) }.saltCheckbox{--vuu-icon-size: 12px;--vuu-icon-left: -1px;--vuu-icon-top: -1px}.saltCheckboxIcon{border-radius:3px;height:12px;width:12px}.saltCheckboxIcon-checked{background-color:var(--vuuCheckboxIcon-background-checked, var(--salt-selectable-background-selected))}.saltCheckboxIcon-checked.saltCheckboxIcon-disabled,.saltCheckbox:hover .saltCheckboxIcon-checked.saltCheckboxIcon-disabled{background-color:var(--salt-selectable-background-disabled);border-color:transparent}.saltCheckboxIcon-checked:after{content:"";background-color:#fff;left:var(--vuu-icon-left, auto);height:var(--vuu-icon-height, var(--vuu-icon-size, 12px));-webkit-mask:var(--vuu-svg-tick) center center/var(--vuu-icon-size) var(--vuu-icon-size);mask:var(--vuu-icon-svg) center center/var(--vuu-icon-size) var(--vuu-icon-size);mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;position:absolute;top:var(--vuu-icon-top, auto);width:var(--vuu-icon-width, var(--vuu-icon-size, 12px))}.saltIcon{display:none}.saltInput-activationIndicator{display:none}.saltInput-primary{--saltInput-height: 24px;border:solid 1px var(--input-borderColor, var(--salt-editable-borderColor));border-radius:6px}.saltInput-focused:hover,.saltInput-focused{--input-borderColor: var(--vuu-color-purple-10)}.vuu-theme .vuuSplitter{--splitter-background: var(--vuu-color-gray-05);--splitter-size: 9px;--splitter-borderColor: white;--splitter-borderStyle: none solid none solid;--splitter-borderWidth: 4px}.vuu-theme .vuuSplitter-column{--splitter-borderStyle: solid none solid none}.saltSwitch{--vuu-icon-left: -1px}.saltSwitch-track,.saltSwitch-track:hover{background-color:var(--vuu-color-gray-45);border:none;border-radius:4px;height:14px;padding:0 2px;width:26px}.saltSwitch-thumb{background-color:var(--vuu-color-white);border:none;border-radius:3px;height:10px;margin:0;width:10px}.saltSwitch-checked .saltSwitch-track{background-color:var(--salt-selectable-background-selected)}.saltSwitch-checked .saltSwitch-thumb,.saltSwitch-checked:hover .saltSwitch-thumb{background-color:#fff;transform:translate(calc(100% + 2px))}.saltSwitch-checked .saltSwitch-thumb:after{background-color:var(--vuu-color-purple-10);content:"";left:var(--vuu-icon-left, auto);height:var(--vuu-icon-height, var(--vuu-icon-size, 12px));-webkit-mask:var(--vuu-svg-tick) center center/var(--vuu-icon-size) var(--vuu-icon-size);mask:var(--vuu-icon-svg) center center/var(--vuu-icon-size) var(--vuu-icon-size);mask-repeat:no-repeat;-webkit-mask-repeat:no-repeat;position:absolute;top:var(--vuu-icon-top, auto);width:var(--vuu-icon-width, var(--vuu-icon-size, 12px))}.saltToggleButtonGroup{border-radius:6px;gap:0;padding:0}.saltToggleButtonGroup-horizontal .saltToggleButton{height:24px}.vuuIconToggleButton{--vuu-icon-size: var(--vuuIconToggleButton-iconSize, 48px);width:48px}.vuuIconToggleButton:first-child{border-radius:4px 0 0 4px}.vuuIconToggleButton:last-child{border-radius:0 4px 4px 0} /*# sourceMappingURL=index.css.map */ diff --git a/vuu-ui/cypress/support/component/index.css.map b/vuu-ui/cypress/support/component/index.css.map new file mode 100644 index 000000000..53d6f6e6f --- /dev/null +++ b/vuu-ui/cypress/support/component/index.css.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../../packages/vuu-theme/fonts/NunitoSans.css", "../../../packages/vuu-theme/css/global.css", "../../../packages/vuu-theme/css/foundations/animation.css", "../../../packages/vuu-theme/css/foundations/color.css", "../../../packages/vuu-theme/css/foundations/duration.css", "../../../packages/vuu-theme/css/foundations/fade.css", "../../../packages/vuu-theme/css/foundations/icon.css", "../../../packages/vuu-theme/css/foundations/opacity.css", "../../../packages/vuu-theme/css/foundations/shadow.css", "../../../packages/vuu-theme/css/foundations/size.css", "../../../packages/vuu-theme/css/foundations/spacing.css", "../../../packages/vuu-theme/css/foundations/typography.css", "../../../packages/vuu-theme/css/foundations/zindex.css", "../../../packages/vuu-theme/css/palette/accent.css", "../../../packages/vuu-theme/css/palette/error.css", "../../../packages/vuu-theme/css/palette/info.css", "../../../packages/vuu-theme/css/palette/interact.css", "../../../packages/vuu-theme/css/palette/navigate.css", "../../../packages/vuu-theme/css/palette/negative.css", "../../../packages/vuu-theme/css/palette/neutral.css", "../../../packages/vuu-theme/css/palette/opacity.css", "../../../packages/vuu-theme/css/palette/positive.css", "../../../packages/vuu-theme/css/palette/success.css", "../../../packages/vuu-theme/css/palette/track.css", "../../../packages/vuu-theme/css/palette/warning.css", "../../../packages/vuu-theme/css/characteristics/accent.css", "../../../packages/vuu-theme/css/characteristics/actionable.css", "../../../packages/vuu-theme/css/characteristics/container.css", "../../../packages/vuu-theme/css/characteristics/draggable.css", "../../../packages/vuu-theme/css/characteristics/target.css", "../../../packages/vuu-theme/css/characteristics/editable.css", "../../../packages/vuu-theme/css/characteristics/focused.css", "../../../packages/vuu-theme/css/characteristics/navigable.css", "../../../packages/vuu-theme/css/characteristics/overlayable.css", "../../../packages/vuu-theme/css/characteristics/selectable.css", "../../../packages/vuu-theme/css/characteristics/separable.css", "../../../packages/vuu-theme/css/characteristics/status.css", "../../../packages/vuu-theme/css/characteristics/taggable.css", "../../../packages/vuu-theme/css/characteristics/text.css", "../../../packages/vuu-theme/css/characteristics/track.css", "../../../packages/vuu-theme/css/deprecated/characteristics.css", "../../../packages/vuu-theme/css/deprecated/fade.css", "../../../packages/vuu-theme/css/deprecated/foundations.css", "../../../packages/vuu-theme/css/deprecated/palette.css", "../../../packages/vuu-theme/css/components/button.css", "../../../packages/vuu-theme/css/components/checkbox.css", "../../../packages/vuu-theme/css/components/icon.css", "../../../packages/vuu-theme/css/components/input.css", "../../../packages/vuu-theme/css/components/splitter.css", "../../../packages/vuu-theme/css/components/switch.css", "../../../packages/vuu-theme/css/components/toggle-button.css"], + "sourcesContent": ["/* latin */\n@font-face {\n font-family: 'Nunito Sans';\n font-style: normal;\n font-weight: 300;\n font-stretch: 100%;\n font-display: swap;\n src: url(./NunitoSansv15.woff2) format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n} /* latin */\n@font-face {\n font-family: 'Nunito Sans';\n font-style: normal;\n font-weight: 400;\n font-stretch: 100%;\n font-display: swap;\n src: url(./NunitoSansv15.woff2) format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n} /* latin */\n@font-face {\n font-family: 'Nunito Sans';\n font-style: normal;\n font-weight: 500;\n font-stretch: 100%;\n font-display: swap;\n src: url(./NunitoSansv15.woff2) format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n /* latin */\n@font-face {\n font-family: 'Nunito Sans';\n font-style: normal;\n font-weight: 600;\n font-stretch: 100%;\n font-display: swap;\n src: url(./NunitoSansv15.woff2) format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n /* latin */\n @font-face {\n font-family: 'Nunito Sans';\n font-style: normal;\n font-weight: 700;\n font-stretch: 100%;\n font-display: swap;\n src: url(./NunitoSansv15.woff2) format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n }\n /* latin */\n @font-face {\n font-family: 'Nunito Sans';\n font-style: normal;\n font-weight: 800;\n font-stretch: 100%;\n font-display: swap;\n src: url(./NunitoSansv15.woff2) format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n }", "\n .vuu-theme {\n color: var(--salt-text-primary-foreground);\n font-family: var(--salt-typography-fontFamily);\n font-size: var(--salt-text-fontSize);\n letter-spacing: var(--salt-text-letterSpacing);\n line-height: var(--salt-text-lineHeight);\n}\n\n::selection {\n background: var(--salt-text-background-selected);\n color: var(--salt-text-color-selected, inherit);\n}\n\n.vuu-theme[data-mode=\"light\"] {\n color-scheme: light;\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n color-scheme: dark;\n}\n\n/* Setting every element's box-sizing to border-box ensures the declared width\nof the element is never exceeded due to padding or border. */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n.salt-visuallyHidden {\n position: absolute;\n height: 1px;\n width: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n", ".vuu-density-touch,\n.vuu-density-low,\n.vuu-density-medium,\n.vuu-density-high {\n --salt-animation-opacity-start: 0;\n --salt-animation-opacity-end: 1;\n --salt-animation-scale-start: 0;\n --salt-animation-scale-end: 1;\n --salt-animation-transform-start: 100%;\n --salt-animation-transform-end: 0;\n --salt-animation-duration: 0.3s;\n --salt-animation-timing-function: ease-in;\n\n /* Slide Animations */\n --salt-animation-slide-in-top: slide-in-top var(--salt-animation-duration) var(--salt-animation-timing-function);\n --salt-animation-slide-in-left: slide-in-left var(--salt-animation-duration) var(--salt-animation-timing-function);\n --salt-animation-slide-in-right: slide-in-right var(--salt-animation-duration) var(--salt-animation-timing-function);\n --salt-animation-slide-in-bottom: slide-in-bottom var(--salt-animation-duration) var(--salt-animation-timing-function);\n\n --salt-animation-slide-out-top: slide-out-top var(--salt-animation-duration) var(--salt-animation-timing-function) both;\n --salt-animation-slide-out-left: slide-out-left var(--salt-animation-duration) var(--salt-animation-timing-function) both;\n --salt-animation-slide-out-right: slide-out-right var(--salt-animation-duration) var(--salt-animation-timing-function) both;\n --salt-animation-slide-out-bottom: slide-out-bottom var(--salt-animation-duration) var(--salt-animation-timing-function) both;\n\n /* Fade Animations */\n --salt-animation-fade-in-back: fade-in-back var(--salt-animation-duration) var(--salt-animation-timing-function);\n --salt-animation-fade-in-forward: fade-in-forward var(--salt-animation-duration) var(--salt-animation-timing-function);\n --salt-animation-fade-in-center: fade-in-center var(--salt-animation-duration) var(--salt-animation-timing-function);\n --salt-animation-fade-out-back: fade-out-back var(--salt-animation-duration) ease-out both;\n}\n\n/*Slide keyframes */\n@keyframes slide-in-top {\n 0% {\n opacity: var(--salt-animation-opacity-start);\n transform: translateY(var(--salt-animation-transform-start));\n }\n 100% {\n opacity: var(--salt-animation-opacity-end);\n transform: translateY(var(--salt-animation-transform-end));\n }\n}\n@keyframes slide-out-top {\n 0% {\n opacity: var(--salt-animation-opacity-end);\n transform: translateY(var(--salt-animation-transform-end));\n }\n 100% {\n opacity: var(--salt-animation-opacity-start);\n transform: translateY(var(--salt-animation-transform-start));\n }\n}\n@keyframes slide-in-left {\n 0% {\n opacity: var(--salt-animation-opacity-start);\n transform: translateX(calc(-1 * var(--salt-animation-transform-start)));\n }\n 100% {\n opacity: var(--salt-animation-opacity-end);\n transform: translateX(var(--salt-animation-transform-end));\n }\n}\n@keyframes slide-out-left {\n 0% {\n opacity: var(--salt-animation-opacity-end);\n transform: translateX(var(--salt-animation-transform-end));\n }\n 100% {\n opacity: var(--salt-animation-opacity-start);\n transform: translateX(calc(-1 * var(--salt-animation-transform-start)));\n }\n}\n@keyframes slide-in-right {\n 0% {\n opacity: var(--salt-animation-opacity-start);\n transform: translateX(var(--salt-animation-transform-start));\n }\n 100% {\n opacity: var(--salt-animation-opacity-end);\n transform: translateX(var(--salt-animation-transform-end));\n }\n}\n@keyframes slide-out-right {\n 0% {\n opacity: var(--salt-animation-opacity-end);\n transform: translateX(var(--salt-animation-transform-end));\n }\n 100% {\n opacity: var(--salt-animation-opacity-start);\n transform: translateX(var(--salt-animation-transform-start));\n }\n}\n@keyframes slide-in-bottom {\n 0% {\n opacity: var(--salt-animation-opacity-start);\n transform: translateY(calc(-1 * var(--salt-animation-transform-start)));\n }\n 100% {\n opacity: var(--salt-animation-opacity-end);\n transform: translateY(var(--salt-animation-transform-end));\n }\n}\n@keyframes slide-out-bottom {\n 0% {\n opacity: var(--salt-animation-opacity-end);\n transform: translateY(var(--salt-animation-transform-end));\n }\n 100% {\n opacity: var(--salt-animation-opacity-start);\n transform: translateY(calc(-1 * var(--salt-animation-transform-start)));\n }\n}\n/* Fade keyframes */\n@keyframes fade-in-back {\n 0% {\n --salt-animation-scale-start: 1.4;\n opacity: var(--salt-animation-opacity-start);\n transform: scale(var(--salt-animation-scale-start));\n }\n\n 100% {\n opacity: var(--salt-animation-opacity-end);\n transform: scale(var(--salt-animation-scale-end));\n }\n}\n@keyframes fade-in-forward {\n 0% {\n --salt-animation-scale-start: 0.6;\n opacity: var(--salt-animation-opacity-start);\n transform: scale(var(--salt-animation-scale-start));\n }\n\n 100% {\n opacity: var(--salt-animation-opacity-end);\n transform: scale(var(--salt-animation-scale-end));\n }\n}\n@keyframes fade-in-center {\n 0% {\n opacity: var(--salt-animation-opacity-start);\n }\n\n 100% {\n opacity: var(--salt-animation-opacity-end);\n }\n}\n\n@keyframes fade-out-back {\n 0% {\n opacity: var(--salt-animation-opacity-end);\n }\n 100% {\n opacity: var(--salt-animation-opacity-start);\n }\n}\n", ".vuu-theme {\n\n --vuu-fade-light : 0.13;\n\n --vuu-color-transparent: transparent;\n --vuu-color-black: black;\n --vuu-color-white: white;\n\n /** text-selection */\n --vuu-color-blue-40: rgb(164, 213, 244); /* #A4D5F4 */\n\n\n --vuu-color-purple-10: rgb(109,24,189); /* #6D18BD */\n --vuu-color-purple-10-fade-light: rgba(109,24,189, var(--vuu-fade-light)); \n\n --vuu-color-pink-10: rgb(234, 120, 128); /* #F37880 */\n\n\n --vuu-color-gray-05: rgb(222, 222, 222); /* #DEDEDE */\n --vuu-color-gray-10: rgb(228, 227, 231); /* #E4E3E7 */\n --vuu-color-gray-20: rgb(245, 242, 248); /* #F5F2F8 */\n --vuu-color-gray-25: rgb(244, 244, 244) ; /* #F4F4F4 */\n --vuu-color-gray-28: rgb(249, 249, 251); /* #F9F9FB */\n --vuu-color-gray-30: rgb(214, 215, 218); /* #D6D7Da */\n --vuu-color-gray-35: rgb(155, 158, 168); /* #9B9EA8; */\n --vuu-color-gray-40: rgb(169, 170, 173); /* #A9AAAD */ \n --vuu-color-gray-42: rgb(135, 139, 158); /* #878b9e */ \n --vuu-color-gray-45: rgb(119, 124, 148); /* #777C94 */ \n --vuu-color-gray-50: rgb(96, 100, 119); /* #606477 */\n --vuu-color-gray-80: rgb(21, 23, 27); /* #15171B */ \n\n--vuu-color-green-50: rgb(102, 174, 90); /* #66AE5A */\n--vuu-color-green-60: rgb(36, 137, 19); /* #248913 */\n--vuu-color-green-60-fade-30: rgba(36, 137, 19, .3); /* #248913 */\n\n--vuu-color-red-50: rgb(226, 52, 52); /* #E23434 */\n\n --vuu-color-yellow-20: rgb(244, 202, 51); /* #F4CA33 */\n\n\n\n /* Color palette will stay the same no matter of theming */\n --salt-color-white: rgb(255, 255, 255);\n --salt-color-black: rgb(0, 0, 0);\n\n --salt-color-red-10: rgb(255, 227, 224);\n --salt-color-red-20: rgb(255, 207, 201);\n --salt-color-red-30: rgb(255, 187, 178);\n --salt-color-red-40: rgb(255, 167, 156);\n --salt-color-red-50: rgb(255, 148, 133);\n --salt-color-red-100: rgb(255, 128, 111);\n --salt-color-red-200: rgb(255, 108, 88);\n --salt-color-red-300: rgb(255, 89, 66);\n --salt-color-red-400: rgb(237, 65, 42);\n --salt-color-red-500: rgb(227, 43, 22);\n --salt-color-red-600: rgb(196, 32, 16);\n --salt-color-red-700: rgb(166, 21, 11);\n --salt-color-red-800: rgb(136, 10, 5);\n --salt-color-red-900: rgb(65, 37, 34);\n\n --salt-color-orange-10: rgb(255, 232, 191);\n --salt-color-orange-20: rgb(254, 223, 166);\n --salt-color-orange-30: rgb(254, 214, 142);\n --salt-color-orange-40: rgb(254, 205, 118);\n --salt-color-orange-50: rgb(254, 197, 94);\n --salt-color-orange-100: rgb(250, 181, 81);\n --salt-color-orange-200: rgb(246, 165, 68);\n --salt-color-orange-300: rgb(242, 149, 56);\n --salt-color-orange-400: rgb(238, 133, 43);\n --salt-color-orange-500: rgb(234, 115, 25);\n --salt-color-orange-600: rgb(224, 101, 25);\n --salt-color-orange-700: rgb(214, 85, 19);\n --salt-color-orange-800: rgb(204, 68, 13);\n --salt-color-orange-900: rgb(54, 44, 36);\n\n --salt-color-green-10: rgb(209, 244, 201);\n --salt-color-green-20: rgb(184, 232, 182);\n --salt-color-green-30: rgb(160, 221, 164);\n --salt-color-green-40: rgb(136, 210, 145);\n --salt-color-green-50: rgb(112, 199, 127);\n --salt-color-green-100: rgb(93, 189, 116);\n --salt-color-green-200: rgb(77, 180, 105);\n --salt-color-green-300: rgb(60, 171, 96);\n --salt-color-green-400: rgb(48, 156, 90);\n --salt-color-green-500: rgb(36, 135, 75);\n --salt-color-green-600: rgb(24, 114, 61);\n --salt-color-green-700: rgb(12, 93, 46);\n --salt-color-green-800: rgb(1, 73, 32);\n --salt-color-green-900: rgb(35, 52, 43);\n\n --salt-color-teal-10: rgb(218, 240, 240);\n --salt-color-teal-20: rgb(199, 232, 232);\n --salt-color-teal-30: rgb(180, 224, 225);\n --salt-color-teal-40: rgb(162, 217, 218);\n --salt-color-teal-50: rgb(141, 205, 209);\n --salt-color-teal-100: rgb(123, 193, 200);\n --salt-color-teal-200: rgb(99, 181, 192);\n --salt-color-teal-300: rgb(73, 160, 172);\n --salt-color-teal-400: rgb(48, 149, 166);\n --salt-color-teal-500: rgb(0, 130, 151);\n --salt-color-teal-600: rgb(27, 107, 133);\n --salt-color-teal-700: rgb(0, 85, 113);\n --salt-color-teal-800: rgb(1, 65, 86);\n --salt-color-teal-900: rgb(0, 49, 76);\n\n --salt-color-blue-10: rgb(203, 231, 249);\n --salt-color-blue-20: rgb(183, 222, 246);\n --salt-color-blue-30: rgb(164, 213, 244);\n --salt-color-blue-40: rgb(144, 204, 242);\n --salt-color-blue-50: rgb(125, 195, 240);\n --salt-color-blue-100: rgb(100, 177, 228);\n --salt-color-blue-200: rgb(75, 159, 216);\n --salt-color-blue-300: rgb(51, 141, 205);\n --salt-color-blue-400: rgb(46, 132, 198);\n --salt-color-blue-500: rgb(38, 112, 169);\n --salt-color-blue-600: rgb(21, 92, 147);\n --salt-color-blue-700: rgb(0, 71, 123);\n --salt-color-blue-800: rgb(39, 60, 77);\n --salt-color-blue-900: rgb(35, 47, 56);\n\n --salt-color-purple-10: rgb(249, 224, 247);\n --salt-color-purple-20: rgb(247, 212, 244);\n --salt-color-purple-30: rgb(245, 201, 241);\n --salt-color-purple-40: rgb(243, 189, 238);\n --salt-color-purple-50: rgb(241, 178, 235);\n --salt-color-purple-100: rgb(223, 156, 225);\n --salt-color-purple-200: rgb(205, 135, 215);\n --salt-color-purple-300: rgb(192, 116, 203);\n --salt-color-purple-400: rgb(169, 97, 181);\n --salt-color-purple-500: rgb(150, 78, 162);\n --salt-color-purple-600: rgb(129, 60, 141);\n --salt-color-purple-700: rgb(103, 46, 122);\n --salt-color-purple-800: rgb(83, 37, 109);\n --salt-color-purple-900: rgb(59, 16, 84);\n\n --salt-color-gray-10: rgb(242, 244, 246);\n --salt-color-gray-20: rgb(234, 237, 239);\n --salt-color-gray-30: rgb(224, 228, 233);\n --salt-color-gray-40: rgb(217, 221, 227);\n --salt-color-gray-50: rgb(206, 210, 217);\n --salt-color-gray-60: rgb(197, 201, 208);\n --salt-color-gray-70: rgb(180, 183, 190);\n --salt-color-gray-80: rgb(159, 163, 170);\n --salt-color-gray-90: rgb(132, 135, 142);\n --salt-color-gray-100: rgb(116, 119, 127);\n --salt-color-gray-200: rgb(97, 101, 110);\n --salt-color-gray-300: rgb(76, 80, 91);\n --salt-color-gray-400: rgb(68, 72, 79);\n --salt-color-gray-500: rgb(59, 63, 70);\n --salt-color-gray-600: rgb(47, 49, 54);\n --salt-color-gray-700: rgb(42, 44, 47);\n --salt-color-gray-800: rgb(36, 37, 38);\n --salt-color-gray-900: rgb(22, 22, 22);\n}\n", ".vuu-theme {\n --salt-duration-instant: 0ms;\n --salt-duration-perceptible: 300ms;\n --salt-duration-notable: 1000ms;\n --salt-duration-cutoff: 10000ms;\n}\n", ".vuu-theme {\n --salt-color-blue-100-fade-foreground: rgba(100, 177, 228, var(--salt-palette-opacity-disabled));\n --salt-color-blue-500-fade-foreground: rgba(38, 112, 169, var(--salt-palette-opacity-disabled));\n --salt-color-blue-600-fade-foreground: rgba(21, 92, 147, var(--salt-palette-opacity-disabled));\n --salt-color-gray-200-fade-foreground: rgba(97, 101, 110, var(--salt-palette-opacity-disabled));\n --salt-color-gray-70-fade-foreground: rgba(180, 183, 190, var(--salt-palette-opacity-disabled));\n --salt-color-gray-90-fade-foreground: rgba(132, 135, 142, var(--salt-palette-opacity-disabled));\n --salt-color-gray-900-fade-foreground: rgba(22, 22, 22, var(--salt-palette-opacity-disabled));\n --salt-color-green-300-fade-foreground: rgba(60, 171, 96, var(--salt-palette-opacity-disabled));\n --salt-color-green-400-fade-foreground: rgba(48, 156, 90, var(--salt-palette-opacity-disabled));\n --salt-color-green-500-fade-foreground: rgba(36, 135, 75, var(--salt-palette-opacity-disabled));\n --salt-color-green-700-fade-foreground: rgba(12, 93, 46, var(--salt-palette-opacity-disabled));\n --salt-color-red-300-fade-foreground: rgba(255, 89, 66, var(--salt-palette-opacity-disabled));\n --salt-color-red-500-fade-foreground: rgba(227, 43, 22, var(--salt-palette-opacity-disabled));\n --salt-color-red-700-fade-foreground: rgba(166, 21, 11, var(--salt-palette-opacity-disabled));\n --salt-color-white-fade-foreground: rgba(255, 255, 255, var(--salt-palette-opacity-disabled));\n\n --salt-color-blue-500-fade-border: rgba(38, 112, 169, var(--salt-palette-opacity-disabled));\n --salt-color-gray-60-fade-border: rgba(197, 201, 208, var(--salt-palette-opacity-disabled));\n --salt-color-gray-90-fade-border: rgba(132, 135, 142, var(--salt-palette-opacity-disabled));\n --salt-color-gray-200-fade-border: rgba(97, 101, 110, var(--salt-palette-opacity-disabled));\n --salt-color-gray-300-fade-border: rgba(76, 80, 91, var(--salt-palette-opacity-disabled));\n --salt-color-green-400-fade-border: rgba(48, 156, 90, var(--salt-palette-opacity-disabled));\n --salt-color-green-500-fade-border: rgba(36, 135, 75, var(--salt-palette-opacity-disabled));\n --salt-color-orange-400-fade-border: rgba(238, 133, 43, var(--salt-palette-opacity-disabled));\n --salt-color-orange-500-fade-border: rgba(234, 115, 25, var(--salt-palette-opacity-disabled));\n --salt-color-orange-600-fade-border: rgba(224, 101, 25, var(--salt-palette-opacity-disabled));\n --salt-color-orange-700-fade-border: rgba(214, 85, 19, var(--salt-palette-opacity-disabled));\n --salt-color-red-500-fade-border: rgba(227, 43, 22, var(--salt-palette-opacity-disabled));\n\n --salt-color-gray-90-fade-border-readonly: rgba(132, 135, 142, var(--salt-palette-opacity-border-readonly));\n --salt-color-gray-200-fade-border-readonly: rgba(97, 101, 110, var(--salt-palette-opacity-border-readonly));\n\n --salt-color-blue-30-fade-background: rgba(164, 213, 244, var(--salt-palette-opacity-disabled));\n --salt-color-blue-500-fade-background: rgba(38, 112, 169, var(--salt-palette-opacity-disabled));\n --salt-color-blue-600-fade-background: rgba(21, 92, 147, var(--salt-palette-opacity-disabled));\n --salt-color-blue-700-fade-background: rgba(0, 71, 123, var(--salt-palette-opacity-disabled));\n --salt-color-gray-20-fade-background: rgba(234, 237, 239, var(--salt-palette-opacity-disabled));\n --salt-color-gray-60-fade-background: rgba(197, 201, 208, var(--salt-palette-opacity-disabled));\n --salt-color-gray-70-fade-background: rgba(180, 183, 190, var(--salt-palette-opacity-disabled));\n --salt-color-gray-200-fade-background: rgba(97, 101, 110, var(--salt-palette-opacity-disabled));\n --salt-color-gray-300-fade-background: rgba(76, 80, 91, var(--salt-palette-opacity-disabled));\n --salt-color-gray-600-fade-background: rgba(47, 49, 54, var(--salt-palette-opacity-disabled));\n --salt-color-gray-800-fade-background: rgba(36, 37, 38, var(--salt-palette-opacity-disabled));\n --salt-color-white-fade-background: rgba(255, 255, 255, var(--salt-palette-opacity-disabled));\n\n --salt-color-white-fade-background-readonly: rgba(255, 255, 255, var(--salt-palette-opacity-background-readonly));\n --salt-color-gray-20-fade-background-readonly: rgba(234, 237, 239, var(--salt-palette-opacity-background-readonly));\n --salt-color-gray-600-fade-background-readonly: rgba(47, 49, 54, var(--salt-palette-opacity-background-readonly));\n --salt-color-gray-800-fade-background-readonly: rgba(36, 37, 38, var(--salt-palette-opacity-background-readonly));\n\n --salt-color-black-fade-backdrop: rgba(36, 37, 38, var(--salt-palette-opacity-backdrop));\n\n --salt-color-blue-100-fade-fill: rgba(100, 177, 228, var(--salt-palette-opacity-disabled));\n --salt-color-blue-600-fade-fill: rgba(21, 92, 147, var(--salt-palette-opacity-disabled));\n\n --salt-color-white-fade-separatorOpacity-primary: rgba(255, 255, 255, var(--salt-palette-opacity-primary-border));\n --salt-color-white-fade-separatorOpacity-secondary: rgba(255, 255, 255, var(--salt-palette-opacity-secondary-border));\n --salt-color-white-fade-separatorOpacity-tertiary: rgba(255, 255, 255, var(--salt-palette-opacity-tertiary-border));\n --salt-color-black-fade-separatorOpacity-primary: rgba(0, 0, 0, var(--salt-palette-opacity-primary-border));\n --salt-color-black-fade-separatorOpacity-secondary: rgba(0, 0, 0, var(--salt-palette-opacity-secondary-border));\n --salt-color-black-fade-separatorOpacity-tertiary: rgba(0, 0, 0, var(--salt-palette-opacity-tertiary-border));\n}\n", ".vuu-density-touch {\n --salt-icon-size-base: 16px;\n --salt-icon-size-status-adornment: 12px;\n}\n\n.vuu-density-low {\n --salt-icon-size-base: 14px;\n --salt-icon-size-status-adornment: 10px;\n}\n\n.vuu-density-medium {\n --salt-icon-size-base: 12px;\n --salt-icon-size-status-adornment: 8px;\n}\n\n.vuu-density-high {\n --salt-icon-size-base: 10px;\n --salt-icon-size-status-adornment: 6px;\n}\n", ".vuu-theme {\n --salt-opacity-0: 0;\n --salt-opacity-8: 0.08;\n --salt-opacity-15: 0.15;\n --salt-opacity-25: 0.25;\n --salt-opacity-40: 0.4;\n --salt-opacity-70: 0.7;\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-shadow-1-color: rgba(0, 0, 0, 0.1);\n --salt-shadow-2-color: rgba(0, 0, 0, 0.1);\n --salt-shadow-3-color: rgba(0, 0, 0, 0.15);\n --salt-shadow-4-color: rgba(0, 0, 0, 0.2);\n --salt-shadow-5-color: rgba(0, 0, 0, 0.3);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-shadow-1-color: rgba(0, 0, 0, 0.5);\n --salt-shadow-2-color: rgba(0, 0, 0, 0.5);\n --salt-shadow-3-color: rgba(0, 0, 0, 0.55);\n --salt-shadow-4-color: rgba(0, 0, 0, 0.55);\n --salt-shadow-5-color: rgba(0, 0, 0, 0.65);\n}\n\n.vuu-theme {\n --salt-shadow-0: none;\n --salt-shadow-1: 0 1px 3px 0 var(--salt-shadow-1-color);\n --salt-shadow-2: 0 2px 4px 0 var(--salt-shadow-2-color);\n --salt-shadow-3: 0 4px 8px 0 var(--salt-shadow-3-color);\n --salt-shadow-4: 0 6px 10px 0 var(--salt-shadow-4-color);\n --salt-shadow-5: 0 12px 40px 0 var(--salt-shadow-5-color);\n}\n", "/** Size */\n.vuu-density-touch,\n.vuu-density-low,\n.vuu-density-medium,\n.vuu-density-high {\n --salt-size-basis-unit: 4px;\n\n --salt-size-adornmentGap: calc(0.75 * var(--salt-size-unit));\n --salt-size-container-spacing: calc(3 * var(--salt-size-unit));\n --salt-size-separator-strokeWidth: 1px;\n --salt-size-selectable: calc(var(--salt-size-base) - (1.5 * var(--salt-size-unit)) - (0.5 * var(--salt-size-basis-unit)));\n --salt-size-separator-height: calc(var(--salt-size-compact) + 1.5 * var(--salt-size-basis-unit));\n --salt-size-sharktooth-height: 5px;\n --salt-size-sharktooth-width: 10px;\n --salt-size-stackable: calc(var(--salt-size-base) + var(--salt-size-unit));\n}\n\n.vuu-density-high {\n --salt-size-unit: calc(var(--salt-size-basis-unit) * 1);\n --salt-size-compact: calc(var(--salt-size-basis-unit) * 1.5);\n --salt-size-accent: calc(var(--salt-size-basis-unit) * 0.5);\n\n /* New size work */\n --salt-size-adornment: 6px;\n --salt-size-bar: 2px;\n --salt-size-base: 20px;\n --salt-size-border: 1px;\n --salt-size-selectable: 12px;\n --salt-size-icon: 12px;\n}\n\n.vuu-density-medium {\n --salt-size-unit: calc(var(--salt-size-basis-unit) * 2);\n --salt-size-compact: calc(var(--salt-size-basis-unit) * 2);\n --salt-size-accent: calc(var(--salt-size-basis-unit) * 1);\n\n /* New size work */\n --salt-size-adornment: 8px;\n --salt-size-bar: 4px;\n --salt-size-base: 28px;\n --salt-size-border: 1px;\n --salt-size-selectable: 14px;\n --salt-size-icon: 12px;\n}\n\n.vuu-density-low {\n --salt-size-unit: calc(var(--salt-size-basis-unit) * 3);\n --salt-size-compact: calc(var(--salt-size-basis-unit) * 2.5);\n --salt-size-accent: calc(var(--salt-size-basis-unit) * 1.5);\n\n /* New size work */\n --salt-size-adornment: 10px;\n --salt-size-bar: 6px;\n --salt-size-base: 36px;\n --salt-size-border: 1px;\n --salt-size-selectable: 16px;\n --salt-size-icon: 14px;\n}\n\n.vuu-density-touch {\n --salt-size-unit: calc(var(--salt-size-basis-unit) * 4);\n --salt-size-compact: calc(var(--salt-size-basis-unit) * 3);\n --salt-size-accent: calc(var(--salt-size-basis-unit) * 2);\n\n /* New size work */\n --salt-size-adornment: 12px;\n --salt-size-bar: 8px;\n --salt-size-base: 44px;\n --salt-size-border: 1px;\n --salt-size-selectable: 18px;\n --salt-size-icon: 16px;\n}\n", ".vuu-density-touch {\n --salt-spacing-100: 16px;\n}\n\n.vuu-density-low {\n --salt-spacing-100: 12px;\n}\n\n.vuu-density-medium {\n --salt-spacing-100: 8px;\n}\n\n.vuu-density-high {\n --salt-spacing-100: 4px;\n}\n\n.vuu-density-touch,\n.vuu-density-low,\n.vuu-density-medium,\n.vuu-density-high {\n --salt-spacing-25: calc(0.25 * var(--salt-spacing-100));\n --salt-spacing-50: calc(0.5 * var(--salt-spacing-100));\n --salt-spacing-75: calc(0.75 * var(--salt-spacing-100));\n\n --salt-spacing-150: calc(1.5 * var(--salt-spacing-100));\n --salt-spacing-200: calc(2 * var(--salt-spacing-100));\n --salt-spacing-250: calc(2.5 * var(--salt-spacing-100));\n --salt-spacing-300: calc(3 * var(--salt-spacing-100));\n --salt-spacing-350: calc(3.5 * var(--salt-spacing-100));\n --salt-spacing-400: calc(4 * var(--salt-spacing-100));\n}\n", ".vuu-theme {\n --salt-typography-fontFamily: \"Nunito Sans\";\n --salt-typography-fontFamily-code: \"PT Mono\";\n\n --salt-typography-fontWeight-light: 300;\n --salt-typography-fontWeight-regular: 400;\n --salt-typography-fontWeight-medium: 500;\n --salt-typography-fontWeight-semiBold: 600;\n --salt-typography-fontWeight-bold: 700;\n --salt-typography-fontWeight-extraBold: 800;\n}\n", ".vuu-density-touch,\n.vuu-density-low,\n.vuu-density-medium,\n.vuu-density-high {\n --salt-zIndex-default: 1;\n --salt-zIndex-popout: 1000;\n --salt-zIndex-docked: 1050;\n --salt-zIndex-appHeader: 1100;\n --salt-zIndex-drawer: 1200;\n --salt-zIndex-modal: 1300;\n --salt-zIndex-notification: 1400;\n --salt-zIndex-dragObject: 1420;\n --salt-zIndex-contextMenu: 1450;\n --salt-zIndex-flyover: 1500;\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-accent-background: var(--salt-color-blue-500);\n --salt-palette-accent-background-disabled: var(--salt-color-blue-500-fade-background);\n --salt-palette-accent-border: var(--salt-color-blue-500);\n --salt-palette-accent-border-disabled: var(--salt-color-blue-500-fade-border);\n --salt-palette-accent-foreground: var(--salt-color-white);\n --salt-palette-accent-foreground-disabled: var(--salt-color-white-fade-foreground);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-accent-background: var(--salt-color-blue-500);\n --salt-palette-accent-background-disabled: var(--salt-color-blue-500-fade-background);\n --salt-palette-accent-border: var(--salt-color-blue-500);\n --salt-palette-accent-border-disabled: var(--salt-color-blue-500-fade-border);\n --salt-palette-accent-foreground: var(--salt-color-white);\n --salt-palette-accent-foreground-disabled: var(--salt-color-white-fade-foreground);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-error-background: var(--salt-color-red-10);\n --salt-palette-error-background-selected: var(--salt-color-red-20);\n --salt-palette-error-border: var(--salt-color-red-500);\n --salt-palette-error-foreground: var(--salt-color-red-500);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-error-background: var(--salt-color-red-900);\n --salt-palette-error-background-selected: var(--salt-color-red-900);\n --salt-palette-error-border: var(--salt-color-red-500);\n --salt-palette-error-foreground: var(--salt-color-red-500);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-info-background: var(--salt-color-blue-10);\n --salt-palette-info-border: var(--salt-color-blue-500);\n --salt-palette-info-foreground: var(--salt-color-blue-500);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-info-background: var(--salt-color-blue-900);\n --salt-palette-info-border: var(--salt-color-blue-500);\n --salt-palette-info-foreground: var(--salt-color-blue-500);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-interact-background: transparent;\n --salt-palette-interact-background-blurSelected: var(--salt-color-gray-30);\n --salt-palette-interact-background-hover: var(--vuu-color-gray-10);\n --salt-palette-interact-background-active: var(--vuu-color-blue-40);\n --salt-palette-interact-background-disabled: var(--vuu-color-gray-35);\n --salt-palette-interact-background-activeDisabled: var(--salt-color-blue-30-fade-background);\n --salt-palette-interact-border: var(--vuu-color-gray-45);\n --salt-palette-interact-border-active: var(--vuu-color-purple-10);\n --salt-palette-interact-border-activeDisabled: var(--salt-color-blue-600-fade-fill);\n --salt-palette-interact-border-disabled: var(--salt-color-gray-200-fade-border);\n --salt-palette-interact-border-hover: var(--vuu-color-pink-10);\n --salt-palette-interact-border-readonly: var(--salt-color-gray-200-fade-border-readonly);\n --salt-palette-interact-foreground: var(--salt-color-gray-200);\n --salt-palette-interact-foreground-active: var(--vuu-color-purple-10);\n --salt-palette-interact-foreground-activeDisabled: var(--salt-color-blue-600-fade-foreground);\n --salt-palette-interact-foreground-disabled: var(--salt-color-gray-200-fade-foreground);\n --salt-palette-interact-foreground-hover: var(--salt-color-blue-500);\n --salt-palette-interact-outline: var(--salt-color-blue-600);\n\n --salt-palette-interact-cta-background: var(--vuu-color-purple-10);\n --salt-palette-interact-cta-background-active: var(--vuu-color-purple-10);\n --salt-palette-interact-cta-background-activeDisabled: var(--salt-color-blue-700-fade-background);\n --salt-palette-interact-cta-background-disabled: var(--salt-color-blue-600-fade-background);\n --salt-palette-interact-cta-background-hover: var(--vuu-color-pink-10);\n --salt-palette-interact-cta-foreground: var(--salt-color-white);\n --salt-palette-interact-cta-foreground-active: var(--salt-color-white);\n --salt-palette-interact-cta-foreground-activeDisabled: var(--salt-color-white-fade-foreground);\n --salt-palette-interact-cta-foreground-disabled: var(--salt-color-white-fade-foreground);\n --salt-palette-interact-cta-foreground-hover: var(--vuu-color-gray-80);\n --salt-palette-interact-primary-background: var(--vuu-color-white);\n --salt-palette-interact-primary-background-active: var(--vuu-color-gray-50);\n --salt-palette-interact-primary-background-activeDisabled: var(--salt-color-gray-200-fade-background);\n --salt-palette-interact-primary-background-disabled: var(--salt-color-gray-60-fade-background);\n --salt-palette-interact-primary-background-hover: var(--vuu-color-pink-10);\n --salt-palette-interact-primary-foreground: var(--vuu-color-gray-50);\n --salt-palette-interact-primary-foreground-active: var(--salt-color-white);\n --salt-palette-interact-primary-foreground-activeDisabled: var(--salt-color-white-fade-foreground);\n --salt-palette-interact-primary-foreground-disabled: var(--salt-color-gray-900-fade-foreground);\n --salt-palette-interact-primary-foreground-hover: var(--vuu-color-gray-80);\n --salt-palette-interact-secondary-background: transparent;\n --salt-palette-interact-secondary-background-active: var(--vuu-color-purple-10);\n --salt-palette-interact-secondary-background-activeDisabled: var(--salt-color-gray-200-fade-background);\n --salt-palette-interact-secondary-background-disabled: transparent;\n --salt-palette-interact-secondary-background-hover: var(--salt-color-gray-40);\n --salt-palette-interact-secondary-foreground: var(--salt-color-gray-900);\n --salt-palette-interact-secondary-foreground-active: var(--salt-color-white);\n --salt-palette-interact-secondary-foreground-activeDisabled: var(--salt-color-white-fade-foreground);\n --salt-palette-interact-secondary-foreground-disabled: var(--salt-color-gray-900-fade-foreground);\n --salt-palette-interact-secondary-foreground-hover: var(--salt-color-gray-900);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-interact-background: transparent;\n --salt-palette-interact-background-active: var(--salt-color-blue-700);\n --salt-palette-interact-background-blurSelected: var(--salt-color-gray-600);\n --salt-palette-interact-background-hover: var(--salt-color-blue-800);\n --salt-palette-interact-background-disabled: transparent;\n --salt-palette-interact-background-activeDisabled: var(--salt-color-blue-700-fade-background);\n --salt-palette-interact-border: var(--salt-color-gray-90);\n --salt-palette-interact-border-active: var(--salt-color-blue-100);\n --salt-palette-interact-border-activeDisabled: var(--salt-color-blue-100-fade-fill);\n --salt-palette-interact-border-disabled: var(--salt-color-gray-90-fade-border);\n --salt-palette-interact-border-hover: var(--salt-color-blue-500);\n --salt-palette-interact-border-readonly: var(--salt-color-gray-90-fade-border-readonly);\n --salt-palette-interact-foreground: var(--salt-color-gray-90);\n --salt-palette-interact-foreground-active: var(--salt-color-blue-100);\n --salt-palette-interact-foreground-activeDisabled: var(--salt-color-blue-100-fade-foreground);\n --salt-palette-interact-foreground-disabled: var(--salt-color-gray-90-fade-foreground);\n --salt-palette-interact-foreground-hover: var(--salt-color-blue-500);\n --salt-palette-interact-outline: var(--vuu-color-pink-10);\n\n --salt-palette-interact-cta-background: var(--salt-color-blue-600);\n --salt-palette-interact-cta-background-active: var(--salt-color-blue-700);\n --salt-palette-interact-cta-background-activeDisabled: var(--salt-color-blue-700-fade-background);\n --salt-palette-interact-cta-background-disabled: var(--salt-color-blue-600-fade-background);\n --salt-palette-interact-cta-background-hover: var(--salt-color-blue-500);\n --salt-palette-interact-cta-foreground: var(--salt-color-white);\n --salt-palette-interact-cta-foreground-active: var(--salt-color-white);\n --salt-palette-interact-cta-foreground-activeDisabled: var(--salt-color-white-fade-foreground);\n --salt-palette-interact-cta-foreground-disabled: var(--salt-color-white-fade-foreground);\n --salt-palette-interact-cta-foreground-hover: var(--salt-color-white);\n --salt-palette-interact-primary-background: var(--salt-color-gray-300);\n --salt-palette-interact-primary-background-active: var(--salt-color-gray-70);\n --salt-palette-interact-primary-background-activeDisabled: var(--salt-color-gray-70-fade-background);\n --salt-palette-interact-primary-background-disabled: var(--salt-color-gray-300-fade-background);\n --salt-palette-interact-primary-background-hover: var(--salt-color-gray-200);\n --salt-palette-interact-primary-foreground: var(--salt-color-white);\n --salt-palette-interact-primary-foreground-active: var(--salt-color-gray-900);\n --salt-palette-interact-primary-foreground-activeDisabled: var(--salt-color-gray-900-fade-foreground);\n --salt-palette-interact-primary-foreground-disabled: var(--salt-color-white-fade-foreground);\n --salt-palette-interact-primary-foreground-hover: var(--salt-color-white);\n --salt-palette-interact-secondary-background: transparent;\n --salt-palette-interact-secondary-background-active: var(--salt-color-gray-70);\n --salt-palette-interact-secondary-background-activeDisabled: var(--salt-color-gray-70-fade-background);\n --salt-palette-interact-secondary-background-disabled: transparent;\n --salt-palette-interact-secondary-background-hover: var(--salt-color-gray-200);\n --salt-palette-interact-secondary-foreground: var(--salt-color-white);\n --salt-palette-interact-secondary-foreground-active: var(--salt-color-gray-900);\n --salt-palette-interact-secondary-foreground-activeDisabled: var(--salt-color-gray-900-fade-foreground);\n --salt-palette-interact-secondary-foreground-disabled: var(--salt-color-white-fade-foreground);\n --salt-palette-interact-secondary-foreground-hover: var(--salt-color-white);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-navigate-primary-background: transparent;\n --salt-palette-navigate-primary-background-active: transparent;\n --salt-palette-navigate-primary-background-hover: var(--salt-color-gray-20);\n --salt-palette-navigate-secondary-background: transparent;\n --salt-palette-navigate-secondary-background-active: transparent;\n --salt-palette-navigate-secondary-background-hover: var(--salt-color-gray-30);\n --salt-palette-navigate-tertiary-background: transparent;\n --salt-palette-navigate-tertiary-background-active: transparent;\n --salt-palette-navigate-tertiary-background-hover: var(--salt-color-gray-20);\n --salt-palette-navigate-foreground-hover: var(--salt-color-blue-600);\n --salt-palette-navigate-foreground-active: var(--salt-color-blue-700);\n --salt-palette-navigate-foreground-visited: var(--salt-color-purple-800);\n --salt-palette-navigate-indicator-hover: var(--salt-color-gray-90);\n --salt-palette-navigate-indicator-active: var(--vuu-color-purple-10);\n --salt-palette-navigate-indicator-activeDisabled: var(--salt-color-orange-600-fade-border);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-navigate-primary-background: transparent;\n --salt-palette-navigate-primary-background-active: transparent;\n --salt-palette-navigate-primary-background-hover: var(--salt-color-gray-700);\n --salt-palette-navigate-secondary-background: transparent;\n --salt-palette-navigate-secondary-background-active: transparent;\n --salt-palette-navigate-secondary-background-hover: var(--salt-color-gray-600);\n --salt-palette-navigate-tertiary-background: transparent;\n --salt-palette-navigate-tertiary-background-active: transparent;\n --salt-palette-navigate-tertiary-background-hover: var(--salt-color-gray-700);\n --salt-palette-navigate-foreground-hover: var(--salt-color-blue-200);\n --salt-palette-navigate-foreground-active: var(--salt-color-blue-300);\n --salt-palette-navigate-foreground-visited: var(--salt-color-purple-100);\n --salt-palette-navigate-indicator-hover: var(--salt-color-gray-90);\n --salt-palette-navigate-indicator-active: var(--vuu-color-pink-10);\n --salt-palette-navigate-indicator-activeDisabled: var(--salt-color-orange-400-fade-border);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-negative-foreground: var(--salt-color-red-700);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-negative-foreground: var(--salt-color-red-300);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-neutral-primary-background: var(--salt-color-white);\n --salt-palette-neutral-primary-background-disabled: var(--salt-color-white-fade-background);\n --salt-palette-neutral-primary-background-readonly: var(--salt-color-white-fade-background-readonly);\n --salt-palette-neutral-primary-foreground: var(--vuu-color-gray-80);\n --salt-palette-neutral-primary-foreground-disabled: var(--salt-color-gray-900-fade-foreground);\n --salt-palette-neutral-primary-separator: var(--salt-color-black-fade-separatorOpacity-primary);\n --salt-palette-neutral-primary-border: var(--vuu-color-purple-10);\n --salt-palette-neutral-primary-border-disabled: var(--salt-color-gray-60-fade-border);\n --salt-palette-neutral-secondary-background: var(--vuu-color-gray-20);\n --salt-palette-neutral-secondary-background-disabled: var(--salt-color-gray-20-fade-background);\n --salt-palette-neutral-secondary-background-readonly: var(--salt-color-gray-20-fade-background-readonly);\n --salt-palette-neutral-secondary-border: var(--salt-color-gray-90);\n --salt-palette-neutral-secondary-border-disabled: var(--salt-color-gray-90-fade-border);\n --salt-palette-neutral-secondary-foreground: var(--salt-color-gray-200);\n --salt-palette-neutral-secondary-foreground-disabled: var(--salt-color-gray-200-fade-foreground);\n --salt-palette-neutral-backdrop: var(--salt-color-black-fade-backdrop);\n --salt-palette-neutral-secondary-separator: var(--salt-color-black-fade-separatorOpacity-secondary);\n --salt-palette-neutral-tertiary-background: transparent;\n --salt-palette-neutral-tertiary-background-disabled: transparent;\n --salt-palette-neutral-tertiary-border: transparent;\n --salt-palette-neutral-tertiary-border-disabled: transparent;\n --salt-palette-neutral-tertiary-separator: var(--vuu-color-gray-05);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-neutral-primary-background: var(--salt-color-gray-800);\n --salt-palette-neutral-primary-background-disabled: var(--salt-color-gray-800-fade-background);\n --salt-palette-neutral-primary-background-readonly: var(--salt-color-gray-800-fade-background-readonly);\n --salt-palette-neutral-primary-border: var(--salt-color-gray-300);\n --salt-palette-neutral-primary-border-disabled: var(--salt-color-gray-300-fade-border);\n --salt-palette-neutral-primary-foreground: var(--salt-color-white);\n --salt-palette-neutral-primary-foreground-disabled: var(--salt-color-white-fade-foreground);\n --salt-palette-neutral-primary-separator: var(--salt-color-white-fade-separatorOpacity-primary);\n --salt-palette-neutral-secondary-background: var(--salt-color-gray-600);\n --salt-palette-neutral-secondary-background-disabled: var(--salt-color-gray-600-fade-background);\n --salt-palette-neutral-secondary-background-readonly: var(--salt-color-gray-600-fade-background-readonly);\n --salt-palette-neutral-secondary-border: var(--salt-color-gray-90);\n --salt-palette-neutral-secondary-border-disabled: var(--salt-color-gray-90-fade-border);\n --salt-palette-neutral-secondary-foreground: var(--salt-color-gray-70);\n --salt-palette-neutral-secondary-foreground-disabled: var(--salt-color-gray-70-fade-foreground);\n --salt-palette-neutral-backdrop: var(--salt-color-black-fade-backdrop);\n --salt-palette-neutral-secondary-separator: var(--salt-color-white-fade-separatorOpacity-secondary);\n --salt-palette-neutral-tertiary-background: transparent;\n --salt-palette-neutral-tertiary-background-disabled: transparent;\n --salt-palette-neutral-tertiary-border: transparent;\n --salt-palette-neutral-tertiary-border-disabled: transparent;\n --salt-palette-neutral-tertiary-separator: var(--salt-color-white-fade-separatorOpacity-tertiary);\n}\n", ".vuu-theme {\n --salt-palette-opacity-backdrop: var(--salt-opacity-70);\n --salt-palette-opacity-disabled: var(--salt-opacity-40);\n --salt-palette-opacity-background-readonly: var(--salt-opacity-0);\n --salt-palette-opacity-border-readonly: var(--salt-opacity-15);\n --salt-palette-opacity-primary-border: var(--salt-opacity-40);\n --salt-palette-opacity-secondary-border: var(--salt-opacity-25);\n --salt-palette-opacity-tertiary-border: var(--salt-opacity-15);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-positive-foreground: var(--salt-color-green-700);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-positive-foreground: var(--salt-color-green-300);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-success-background: var(--salt-color-green-10);\n --salt-palette-success-background-selected: var(--salt-color-green-20);\n --salt-palette-success-border: var(--salt-color-green-500);\n --salt-palette-success-foreground: var(--salt-color-green-500);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-success-background: var(--salt-color-green-900);\n --salt-palette-success-background-selected: var(--salt-color-green-900);\n --salt-palette-success-border: var(--salt-color-green-400);\n --salt-palette-success-foreground: var(--salt-color-green-400);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-track-background: var(--salt-color-gray-60);\n --salt-palette-track-background-disabled: var(--salt-color-gray-60-fade-background);\n --salt-palette-track-border: var(--salt-color-gray-90);\n --salt-palette-track-border-disabled: var(--salt-color-gray-90-fade-border);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-track-background: var(--salt-color-gray-300);\n --salt-palette-track-background-disabled: var(--salt-color-gray-300-fade-background);\n --salt-palette-track-border: var(--salt-color-gray-90);\n --salt-palette-track-border-disabled: var(--salt-color-gray-90-fade-border);\n}\n", ".vuu-theme[data-mode=\"light\"] {\n --salt-palette-warning-background: var(--salt-color-orange-10);\n --salt-palette-warning-background-selected: var(--salt-color-orange-20);\n --salt-palette-warning-border: var(--salt-color-orange-700);\n --salt-palette-warning-foreground: var(--salt-color-orange-700);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n --salt-palette-warning-background: var(--salt-color-orange-900);\n --salt-palette-warning-background-selected: var(--salt-color-orange-900);\n --salt-palette-warning-border: var(--salt-color-orange-500);\n --salt-palette-warning-foreground: var(--salt-color-orange-500);\n}\n", ".vuu-density-high {\n --salt-accent-fontSize: 8px;\n --salt-accent-lineHeight: 11px;\n}\n.vuu-density-medium {\n --salt-accent-fontSize: 10px;\n --salt-accent-lineHeight: 13px;\n}\n.vuu-density-low {\n --salt-accent-fontSize: 12px;\n --salt-accent-lineHeight: 16px;\n}\n.vuu-density-touch {\n --salt-accent-fontSize: 14px;\n --salt-accent-lineHeight: 18px;\n}\n\n.vuu-theme {\n --salt-accent-background: var(--salt-palette-accent-background);\n --salt-accent-background-disabled: var(--salt-palette-accent-background-disabled);\n --salt-accent-borderColor: var(--salt-palette-accent-border);\n --salt-accent-borderColor-disabled: var(--salt-palette-accent-border-disabled);\n --salt-accent-foreground: var(--salt-palette-accent-foreground);\n --salt-accent-foreground-disabled: var(--salt-palette-accent-foreground-disabled);\n --salt-accent-fontWeight: var(--salt-typography-fontWeight-semiBold);\n}\n", ".vuu-theme {\n --salt-actionable-cursor-hover: pointer;\n --salt-actionable-cursor-active: pointer;\n --salt-actionable-cursor-disabled: not-allowed;\n\n --salt-actionable-letterSpacing: 0.6px;\n --salt-actionable-textAlign: center;\n --salt-actionable-textTransform: uppercase;\n\n /* Primary variant */\n --salt-actionable-primary-foreground: var(--salt-palette-interact-primary-foreground);\n --salt-actionable-primary-foreground-hover: var(--salt-palette-interact-primary-foreground-hover);\n --salt-actionable-primary-foreground-active: var(--salt-palette-interact-primary-foreground-active);\n --salt-actionable-primary-foreground-disabled: var(--salt-palette-interact-primary-foreground-disabled);\n --salt-actionable-primary-background: var(--salt-palette-interact-primary-background);\n --salt-actionable-primary-background-hover: var(--salt-palette-interact-primary-background-hover);\n --salt-actionable-primary-background-active: var(--salt-palette-interact-primary-background-active);\n --salt-actionable-primary-background-disabled: var(--salt-palette-interact-primary-background-disabled);\n --salt-actionable-primary-fontWeight: var(--salt-typography-fontWeight-bold);\n\n /* CTA variant */\n --salt-actionable-cta-foreground: var(--salt-palette-interact-cta-foreground);\n --salt-actionable-cta-foreground-hover: var(--salt-palette-interact-cta-foreground-hover);\n --salt-actionable-cta-foreground-active: var(--salt-palette-interact-cta-foreground-active);\n --salt-actionable-cta-foreground-disabled: var(--salt-palette-interact-cta-foreground-disabled);\n --salt-actionable-cta-background: var(--salt-palette-interact-cta-background);\n --salt-actionable-cta-background-hover: var(--salt-palette-interact-cta-background-hover);\n --salt-actionable-cta-background-active: var(--salt-palette-interact-cta-background-active);\n --salt-actionable-cta-background-disabled: var(--salt-palette-interact-cta-background-disabled);\n --salt-actionable-cta-fontWeight: var(--salt-typography-fontWeight-bold);\n\n /* Secondary variant */\n --salt-actionable-secondary-foreground: var(--salt-palette-interact-secondary-foreground);\n --salt-actionable-secondary-foreground-hover: var(--salt-palette-interact-secondary-foreground-hover);\n --salt-actionable-secondary-foreground-active: var(--salt-palette-interact-secondary-foreground-active);\n --salt-actionable-secondary-foreground-disabled: var(--salt-palette-interact-secondary-foreground-disabled);\n --salt-actionable-secondary-background: var(--salt-palette-interact-secondary-background);\n --salt-actionable-secondary-background-hover: var(--salt-palette-interact-secondary-background-hover);\n --salt-actionable-secondary-background-active: var(--salt-palette-interact-secondary-background-active);\n --salt-actionable-secondary-background-disabled: var(--salt-palette-interact-secondary-background-disabled);\n --salt-actionable-secondary-fontWeight: var(--salt-typography-fontWeight-semiBold);\n}\n", ".vuu-theme {\n --salt-container-borderStyle: solid;\n\n --salt-container-primary-background: var(--salt-palette-neutral-primary-background);\n --salt-container-primary-background-disabled: var(--salt-palette-neutral-primary-background-disabled);\n --salt-container-primary-borderColor: var(--salt-palette-neutral-primary-border);\n --salt-container-primary-borderColor-disabled: var(--salt-palette-neutral-primary-border-disabled);\n\n --salt-container-secondary-background: var(--salt-palette-neutral-secondary-background);\n --salt-container-secondary-background-disabled: var(--salt-palette-neutral-secondary-background-disabled);\n --salt-container-secondary-borderColor: var(--salt-palette-neutral-secondary-border);\n --salt-container-secondary-borderColor-disabled: var(--salt-palette-neutral-secondary-border-disabled);\n\n --salt-container-tertiary-background: var(--salt-palette-neutral-tertiary-background);\n --salt-container-tertiary-background-disabled: var(--salt-palette-neutral-tertiary-background-disabled);\n --salt-container-tertiary-borderColor: var(--salt-palette-neutral-tertiary-border);\n --salt-container-tertiary-borderColor-disabled: var(--salt-palette-neutral-tertiary-border-disabled);\n}\n", ".vuu-theme {\n --salt-draggable-horizontal-cursor-hover: row-resize;\n --salt-draggable-horizontal-cursor-active: row-resize;\n\n --salt-draggable-vertical-cursor-hover: col-resize;\n --salt-draggable-vertical-cursor-active: col-resize;\n\n --salt-draggable-grab-cursor-hover: grab;\n --salt-draggable-grab-cursor-active: grabbing;\n}\n", ".vuu-theme {\n --salt-target-background-hover: var(--salt-palette-interact-background-hover);\n\n --salt-target-borderColor-hover: var(--salt-palette-interact-border-hover);\n --salt-target-borderStyle: dashed;\n --salt-target-borderStyle-hover: solid;\n --salt-target-borderStyle-disabled: dashed;\n\n --salt-target-cursor-disabled: not-allowed;\n}\n", ".vuu-theme {\n --salt-editable-cursor-hover: text;\n --salt-editable-cursor-active: text;\n --salt-editable-cursor-disabled: not-allowed;\n --salt-editable-cursor-readonly: text;\n\n --salt-editable-borderStyle: solid;\n --salt-editable-borderStyle-hover: solid;\n --salt-editable-borderStyle-active: solid;\n --salt-editable-borderStyle-disabled: solid;\n --salt-editable-borderStyle-readonly: solid;\n --salt-editable-borderWidth-active: 2px;\n\n --salt-editable-borderColor: var(--salt-palette-interact-border);\n --salt-editable-borderColor-active: var(--salt-palette-interact-border-active);\n --salt-editable-borderColor-disabled: var(--salt-palette-interact-border-disabled);\n --salt-editable-borderColor-hover: var(--salt-palette-interact-border-hover);\n --salt-editable-borderColor-readonly: var(--salt-palette-interact-border-readonly);\n\n --salt-editable-primary-background: var(--salt-palette-neutral-primary-background);\n --salt-editable-primary-background-active: var(--salt-palette-neutral-primary-background);\n --salt-editable-primary-background-disabled: var(--salt-palette-neutral-primary-background-disabled);\n --salt-editable-primary-background-hover: var(--salt-palette-neutral-primary-background);\n --salt-editable-primary-background-readonly: var(--salt-palette-neutral-primary-background-readonly);\n\n --salt-editable-secondary-background: var(--salt-palette-neutral-secondary-background);\n --salt-editable-secondary-background-active: var(--salt-palette-neutral-secondary-background);\n --salt-editable-secondary-background-disabled: var(--salt-palette-neutral-secondary-background-disabled);\n --salt-editable-secondary-background-hover: var(--salt-palette-neutral-secondary-background);\n --salt-editable-secondary-background-readonly: var(--salt-palette-neutral-secondary-background-readonly);\n\n --salt-editable-help-fontStyle: italic;\n}\n", ".vuu-theme {\n --vuu-editable-borderColor-active: var(--editable-border-active, #6D18BD);\n}\n\n.saltInput-focused {\n border-color: var(--vuu-editable-borderColor-active) !important;\n}", ".vuu-theme {\n --salt-navigable-cursor-active: pointer;\n --salt-navigable-cursor-hover: pointer;\n --salt-navigable-cursor-disabled: not-allowed;\n --salt-navigable-cursor-edit: text;\n\n --salt-navigable-fontWeight: var(--salt-typography-fontWeight-regular);\n --salt-navigable-fontWeight-hover: var(--salt-typography-fontWeight-regular);\n --salt-navigable-fontWeight-active: var(--salt-typography-fontWeight-semiBold);\n --salt-navigable-fontWeight-edit: var(--salt-typography-fontWeight-regular);\n\n --salt-navigable-indicator-hover: var(--salt-palette-navigate-indicator-hover);\n --salt-navigable-indicator-active: var(--salt-palette-navigate-indicator-active);\n --salt-navigable-indicator-activeDisabled: var(--salt-palette-navigate-indicator-activeDisabled);\n\n --salt-navigable-primary-background: var(--salt-palette-navigate-primary-background);\n --salt-navigable-primary-background-hover: var(--salt-palette-navigate-primary-background-hover);\n --salt-navigable-primary-background-active: var(--salt-palette-navigate-primary-background-active);\n\n --salt-navigable-secondary-background: var(--salt-palette-navigate-secondary-background);\n --salt-navigable-secondary-background-hover: var(--salt-palette-navigate-secondary-background-hover);\n --salt-navigable-secondary-background-active: var(--salt-palette-navigate-secondary-background-active);\n\n --salt-navigable-tertiary-background: var(--salt-palette-navigate-tertiary-background);\n --salt-navigable-tertiary-background-hover: var(--salt-palette-navigate-tertiary-background-hover);\n --salt-navigable-tertiary-background-active: var(--salt-palette-navigate-tertiary-background-active);\n}\n", ".vuu-theme {\n --salt-overlayable-shadow-scroll: var(--salt-shadow-1);\n --salt-overlayable-shadow-borderRegion: var(--salt-shadow-2);\n --salt-overlayable-shadow: var(--salt-shadow-2);\n --salt-overlayable-shadow-hover: var(--salt-shadow-3);\n --salt-overlayable-shadow-popout: var(--salt-shadow-4);\n --salt-overlayable-shadow-drag: var(--salt-shadow-4);\n --salt-overlayable-shadow-modal: var(--salt-shadow-5);\n\n --salt-overlayable-background: var(--salt-palette-neutral-backdrop);\n}\n", ".vuu-theme {\n --salt-selectable-cursor-hover: pointer;\n --salt-selectable-cursor-selected: pointer;\n --salt-selectable-cursor-blurSelected: pointer;\n --salt-selectable-cursor-disabled: not-allowed;\n --salt-selectable-cursor-readonly: not-allowed;\n\n --salt-selectable-borderStyle: solid;\n --salt-selectable-borderStyle-hover: solid;\n --salt-selectable-borderStyle-selected: solid;\n --salt-selectable-borderStyle-blurSelected: solid;\n\n --salt-selectable-borderColor: var(--salt-palette-interact-border);\n --salt-selectable-borderColor-hover: var(--salt-palette-interact-border-hover);\n --salt-selectable-borderColor-selected: var(--salt-palette-interact-border-active);\n --salt-selectable-borderColor-selectedDisabled: var(--salt-palette-interact-border-activeDisabled);\n --salt-selectable-borderColor-disabled: var(--salt-palette-interact-border-disabled);\n --salt-selectable-borderColor-readonly: var(--salt-palette-interact-border-readonly);\n\n --salt-selectable-foreground: var(--salt-palette-interact-foreground);\n --salt-selectable-foreground-disabled: var(--salt-palette-interact-foreground-disabled);\n --salt-selectable-foreground-hover: var(--salt-palette-interact-foreground-hover);\n --salt-selectable-foreground-selected: var(--salt-palette-interact-foreground-active);\n --salt-selectable-foreground-selectedDisabled: var(--salt-palette-interact-foreground-activeDisabled);\n --salt-selectable-background: var(--salt-palette-interact-background);\n --salt-selectable-background-hover: var(--salt-palette-interact-background-hover);\n --salt-selectable-background-selected: var(--salt-palette-interact-background-active);\n --salt-selectable-background-blurSelected: var(--salt-palette-interact-background-blurSelected);\n --salt-selectable-background-disabled: var(--salt-palette-interact-background-disabled);\n --salt-selectable-background-selectedDisabled: var(--salt-palette-interact-background-activeDisabled);\n\n --salt-selectable-cta-foreground-hover: var(--salt-palette-interact-cta-foreground-hover);\n --salt-selectable-cta-foreground-selected: var(--salt-palette-interact-cta-foreground-active);\n --salt-selectable-cta-foreground-selectedDisabled: var(--salt-palette-interact-cta-foreground-activeDisabled);\n --salt-selectable-cta-background: var(--salt-palette-interact-background);\n --salt-selectable-cta-background-disabled: var(--salt-palette-interact-background-disabled);\n --salt-selectable-cta-background-hover: var(--salt-palette-interact-cta-background-hover);\n --salt-selectable-cta-background-selected: var(--salt-palette-interact-cta-background-active);\n --salt-selectable-cta-background-selectedDisabled: var(--salt-palette-interact-cta-background-activeDisabled);\n\n --salt-selectable-primary-foreground-hover: var(--salt-palette-interact-primary-foreground-hover);\n --salt-selectable-primary-foreground-selected: var(--salt-palette-interact-primary-foreground-active);\n --salt-selectable-primary-foreground-selectedDisabled: var(--salt-palette-interact-primary-foreground-activeDisabled);\n --salt-selectable-primary-background: var(--salt-palette-interact-background);\n --salt-selectable-primary-background-disabled: var(--salt-palette-interact-background-disabled);\n --salt-selectable-primary-background-hover: var(--salt-palette-interact-primary-background-hover);\n --salt-selectable-primary-background-selected: var(--salt-palette-interact-primary-background-active);\n --salt-selectable-primary-background-selectedDisabled: var(--salt-palette-interact-primary-background-activeDisabled);\n\n --salt-selectable-secondary-foreground-hover: var(--salt-palette-interact-secondary-foreground-hover);\n --salt-selectable-secondary-foreground-selected: var(--salt-palette-interact-secondary-foreground-active);\n --salt-selectable-secondary-foreground-selectedDisabled: var(--salt-palette-interact-secondary-foreground-activeDisabled);\n --salt-selectable-secondary-background: var(--salt-palette-interact-background);\n --salt-selectable-secondary-background-disabled: var(--salt-palette-interact-background-disabled);\n --salt-selectable-secondary-background-hover: var(--salt-palette-interact-secondary-background-hover);\n --salt-selectable-secondary-background-selected: var(--salt-palette-interact-secondary-background-active);\n --salt-selectable-secondary-background-selectedDisabled: var(--salt-palette-interact-secondary-background-activeDisabled);\n}\n", ".vuu-theme {\n --salt-separable-borderStyle: solid;\n\n --salt-separable-primary-borderColor: var(--salt-palette-neutral-primary-separator);\n --salt-separable-secondary-borderColor: var(--salt-palette-neutral-secondary-separator);\n --salt-separable-tertiary-borderColor: var(--salt-palette-neutral-tertiary-separator);\n}\n", ".vuu-theme {\n --salt-status-info-foreground: var(--salt-palette-info-foreground);\n --salt-status-success-foreground: var(--salt-palette-success-foreground);\n --salt-status-warning-foreground: var(--salt-palette-warning-foreground);\n --salt-status-error-foreground: var(--salt-palette-error-foreground);\n --salt-status-static-foreground: var(--salt-palette-neutral-secondary-foreground);\n --salt-status-negative-foreground: var(--salt-palette-negative-foreground);\n --salt-status-positive-foreground: var(--salt-palette-positive-foreground);\n\n --salt-status-info-borderColor: var(--salt-palette-info-border);\n --salt-status-success-borderColor: var(--salt-palette-success-border);\n --salt-status-warning-borderColor: var(--salt-palette-warning-border);\n --salt-status-error-borderColor: var(--salt-palette-error-border);\n\n --salt-status-info-background: var(--salt-palette-info-background);\n --salt-status-success-background: var(--salt-palette-success-background);\n --salt-status-warning-background: var(--salt-palette-warning-background);\n --salt-status-error-background: var(--salt-palette-error-background);\n\n --salt-status-success-background-selected: var(--salt-palette-success-background-selected);\n --salt-status-warning-background-selected: var(--salt-palette-warning-background-selected);\n --salt-status-error-background-selected: var(--salt-palette-error-background-selected);\n}\n", ".vuu-theme {\n --salt-taggable-cursor-hover: pointer;\n --salt-taggable-cursor-active: pointer;\n --salt-taggable-cursor-disabled: not-allowed;\n\n --salt-taggable-background: var(--salt-palette-interact-primary-background);\n --salt-taggable-background-hover: var(--salt-palette-interact-primary-background-hover);\n --salt-taggable-background-active: var(--salt-palette-interact-primary-background-active);\n --salt-taggable-background-disabled: var(--salt-palette-interact-primary-background-disabled);\n\n --salt-taggable-foreground: var(--salt-palette-interact-primary-foreground);\n --salt-taggable-foreground-hover: var(--salt-palette-interact-primary-foreground-hover);\n --salt-taggable-foreground-active: var(--salt-palette-interact-primary-foreground-active);\n --salt-taggable-foreground-disabled: var(--salt-palette-interact-primary-foreground-disabled);\n}\n", ".vuu-theme {\n /* Misc */\n --salt-text-letterSpacing: 0;\n --salt-text-textAlign: left;\n --salt-text-textAlign-embedded: center;\n --salt-text-textDecoration: none;\n --salt-text-textTransform: none;\n\n /* Body text (should be used as default) */\n --salt-text-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-fontWeight: var(--salt-typography-fontWeight-regular);\n --salt-text-fontWeight-small: var(--salt-typography-fontWeight-light);\n --salt-text-fontWeight-strong: var(--salt-typography-fontWeight-semiBold);\n\n /* H1 */\n --salt-text-h1-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-h1-fontWeight: var(--salt-typography-fontWeight-bold);\n --salt-text-h1-fontWeight-small: var(--salt-typography-fontWeight-medium);\n --salt-text-h1-fontWeight-strong: var(--salt-typography-fontWeight-extraBold);\n\n /* H2 */\n --salt-text-h2-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-h2-fontWeight: var(--salt-typography-fontWeight-semiBold);\n --salt-text-h2-fontWeight-small: var(--salt-typography-fontWeight-regular);\n --salt-text-h2-fontWeight-strong: var(--salt-typography-fontWeight-bold);\n\n /* H3 */\n --salt-text-h3-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-h3-fontWeight: var(--salt-typography-fontWeight-semiBold);\n --salt-text-h3-fontWeight-small: var(--salt-typography-fontWeight-regular);\n --salt-text-h3-fontWeight-strong: var(--salt-typography-fontWeight-bold);\n\n /* H4 */\n --salt-text-h4-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-h4-fontWeight: var(--salt-typography-fontWeight-semiBold);\n --salt-text-h4-fontWeight-small: var(--salt-typography-fontWeight-regular);\n --salt-text-h4-fontWeight-strong: var(--salt-typography-fontWeight-bold);\n\n /* Label */\n --salt-text-label-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-label-fontWeight: var(--salt-typography-fontWeight-regular);\n --salt-text-label-fontWeight-small: var(--salt-typography-fontWeight-light);\n --salt-text-label-fontWeight-strong: var(--salt-typography-fontWeight-semiBold);\n\n /* Display text */\n --salt-text-display1-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-display1-fontWeight: var(--salt-typography-fontWeight-semiBold);\n --salt-text-display1-fontWeight-strong: var(--salt-typography-fontWeight-bold);\n --salt-text-display1-fontWeight-small: var(--salt-typography-fontWeight-regular);\n\n --salt-text-display2-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-display2-fontWeight: var(--salt-typography-fontWeight-semiBold);\n --salt-text-display2-fontWeight-strong: var(--salt-typography-fontWeight-bold);\n --salt-text-display2-fontWeight-small: var(--salt-typography-fontWeight-regular);\n\n --salt-text-display3-fontFamily: var(--salt-typography-fontFamily);\n --salt-text-display3-fontWeight: var(--salt-typography-fontWeight-semiBold);\n --salt-text-display3-fontWeight-strong: var(--salt-typography-fontWeight-bold);\n --salt-text-display3-fontWeight-small: var(--salt-typography-fontWeight-regular);\n\n /* Colors */\n --salt-text-background-selected: var(--salt-palette-interact-background-active);\n\n --salt-text-primary-foreground: var(--salt-palette-neutral-primary-foreground);\n --salt-text-primary-foreground-disabled: var(--salt-palette-neutral-primary-foreground-disabled);\n --salt-text-secondary-foreground: var(--salt-palette-neutral-secondary-foreground);\n --salt-text-secondary-foreground-disabled: var(--salt-palette-neutral-secondary-foreground-disabled);\n\n /* Link */\n --salt-text-link-foreground-hover: var(--salt-palette-navigate-foreground-hover);\n --salt-text-link-foreground-active: var(--salt-palette-navigate-foreground-active);\n --salt-text-link-foreground-visited: var(--salt-palette-navigate-foreground-visited);\n --salt-text-link-textDecoration: underline;\n --salt-text-link-textDecoration-hover: none;\n --salt-text-link-textDecoration-selected: underline;\n\n /* Code */\n --salt-text-code-fontFamily: var(--salt-typography-fontFamily-code);\n}\n\n/* Sizes by density */\n.vuu-density-touch {\n --salt-text-h1-fontSize: 42px;\n --salt-text-h1-lineHeight: 54px;\n\n --salt-text-h2-fontSize: 32px;\n --salt-text-h2-lineHeight: 42px;\n\n --salt-text-h3-fontSize: 24px;\n --salt-text-h3-lineHeight: 32px;\n\n --salt-text-h4-fontSize: 16px;\n --salt-text-h4-lineHeight: 20px;\n\n --salt-text-label-fontSize: 14px;\n --salt-text-label-lineHeight: 18px;\n\n --salt-text-fontSize: 16px;\n --salt-text-lineHeight: 20px;\n --salt-text-minHeight: 20px;\n\n --salt-text-display1-fontSize: 84px;\n --salt-text-display1-lineHeight: 109px;\n\n --salt-text-display2-fontSize: 58px;\n --salt-text-display2-lineHeight: 76px;\n\n --salt-text-display3-fontSize: 42px;\n --salt-text-display3-lineHeight: 54px;\n}\n\n.vuu-density-low {\n --salt-text-h1-fontSize: 32px;\n --salt-text-h1-lineHeight: 42px;\n\n --salt-text-h2-fontSize: 24px;\n --salt-text-h2-lineHeight: 32px;\n\n --salt-text-h3-fontSize: 18px;\n --salt-text-h3-lineHeight: 24px;\n\n --salt-text-h4-fontSize: 14px;\n --salt-text-h4-lineHeight: 18px;\n\n --salt-text-label-fontSize: 12px;\n --salt-text-label-lineHeight: 16px;\n\n --salt-text-fontSize: 14px;\n --salt-text-lineHeight: 18px;\n --salt-text-minHeight: 18px;\n\n --salt-text-display1-fontSize: 68px;\n --salt-text-display1-lineHeight: 88px;\n\n --salt-text-display2-fontSize: 46px;\n --salt-text-display2-lineHeight: 60px;\n\n --salt-text-display3-fontSize: 32px;\n --salt-text-display3-lineHeight: 42px;\n}\n\n.vuu-density-medium {\n --salt-text-h1-fontSize: 24px;\n --salt-text-h1-lineHeight: 32px;\n\n --salt-text-h2-fontSize: 18px;\n --salt-text-h2-lineHeight: 24px;\n\n --salt-text-h3-fontSize: 14px;\n --salt-text-h3-lineHeight: 18px;\n\n --salt-text-h4-fontSize: 12px;\n --salt-text-h4-lineHeight: 16px;\n\n --salt-text-label-fontSize: 11px;\n --salt-text-label-lineHeight: 14px;\n\n --salt-text-fontSize: 12px;\n --salt-text-lineHeight: 16px;\n --salt-text-minHeight: 16px;\n\n --salt-text-display1-fontSize: 54px;\n --salt-text-display1-lineHeight: 70px;\n\n --salt-text-display2-fontSize: 36px;\n --salt-text-display2-lineHeight: 47px;\n\n --salt-text-display3-fontSize: 24px;\n --salt-text-display3-lineHeight: 32px;\n}\n\n.vuu-density-high {\n --salt-text-h1-fontSize: 18px;\n --salt-text-h1-lineHeight: 24px;\n\n --salt-text-h2-fontSize: 14px;\n --salt-text-h2-lineHeight: 18px;\n\n --salt-text-h3-fontSize: 12px;\n --salt-text-h3-lineHeight: 16px;\n\n --salt-text-h4-fontSize: 11px;\n --salt-text-h4-lineHeight: 14px;\n\n --salt-text-label-fontSize: 10px;\n --salt-text-label-lineHeight: 13px;\n\n --salt-text-fontSize: 12px;\n --salt-text-lineHeight: 14px;\n --salt-text-minHeight: 14px;\n\n --salt-text-display1-fontSize: 42px;\n --salt-text-display1-lineHeight: 54px;\n\n --salt-text-display2-fontSize: 28px;\n --salt-text-display2-lineHeight: 36px;\n\n --salt-text-display3-fontSize: 18px;\n --salt-text-display3-lineHeight: 24px;\n}\n", ".vuu-theme {\n --salt-track-borderStyle: solid;\n --salt-track-borderStyle-active: solid;\n --salt-track-borderStyle-complete: solid;\n --salt-track-borderStyle-incomplete: dotted;\n\n --salt-track-borderWidth: 2px;\n --salt-track-borderWidth-active: 2px;\n --salt-track-borderWidth-complete: 2px;\n --salt-track-borderWidth-incomplete: 2px;\n\n --salt-track-fontWeight: var(--salt-typography-fontWeight-semiBold);\n --salt-track-textAlign: center;\n\n --salt-track-background: var(--salt-palette-track-background);\n --salt-track-background-disabled: var(--salt-palette-track-background-disabled);\n --salt-track-borderColor: var(--salt-palette-track-border);\n --salt-track-borderColor-disabled: var(--salt-palette-track-border-disabled);\n}\n", ".vuu-theme {\n /* Differential */\n --salt-differential-positive-foreground: var(--salt-palette-positive-foreground);\n --salt-differential-negative-foreground: var(--salt-palette-negative-foreground);\n\n /* Editable */\n --salt-editable-tertiary-background: var(--salt-palette-neutral-tertiary-background);\n --salt-editable-tertiary-background-active: var(--salt-palette-neutral-tertiary-background);\n --salt-editable-tertiary-background-disabled: var(--salt-palette-neutral-tertiary-background-disabled);\n --salt-editable-tertiary-background-hover: var(--salt-palette-neutral-tertiary-background);\n --salt-editable-tertiary-background-readonly: var(--salt-palette-neutral-tertiary-background-readonly);\n\n /* Measured */\n --salt-measured-borderStyle: solid;\n --salt-measured-borderStyle-active: solid;\n --salt-measured-borderStyle-complete: solid;\n --salt-measured-borderStyle-incomplete: dotted;\n --salt-measured-borderWidth: 2px;\n --salt-measured-borderWidth-active: 2px;\n --salt-measured-borderWidth-complete: 2px;\n --salt-measured-borderWidth-incomplete: 2px;\n\n --salt-measured-fontWeight: var(--salt-typography-fontWeight-semiBold);\n --salt-measured-textAlign: center;\n\n --salt-measured-background: var(--salt-palette-measured-background);\n --salt-measured-background-disabled: var(--salt-palette-measured-background-disabled);\n --salt-measured-borderColor: var(--salt-palette-measured-border);\n --salt-measured-borderColor-disabled: var(--salt-palette-measured-border-disabled);\n --salt-measured-fill: var(--salt-palette-measured-fill);\n --salt-measured-fill-disabled: var(--salt-palette-measured-fill-disabled);\n --salt-measured-foreground: var(--salt-palette-measured-foreground);\n --salt-measured-foreground-hover: var(--salt-palette-measured-foreground-active);\n --salt-measured-foreground-active: var(--salt-palette-measured-foreground-active);\n --salt-measured-foreground-undo: var(--salt-palette-measured-foreground-active);\n --salt-measured-foreground-activeDisabled: var(--salt-palette-measured-foreground-activeDisabled);\n --salt-measured-foreground-disabled: var(--salt-palette-measured-foreground-disabled);\n\n /* Overlayable */\n --salt-overlayable-shadow-scroll-color: var(--salt-shadow-1-color);\n\n /* Selectable */\n --salt-selectable-foreground-partial: var(--salt-palette-interact-foreground-partial);\n --salt-selectable-foreground-partialDisabled: var(--salt-palette-interact-foreground-partialDisabled);\n\n --salt-selectable-cta-foreground: var(--salt-palette-interact-foreground);\n --salt-selectable-cta-foreground-disabled: var(--salt-palette-interact-foreground-disabled);\n --salt-selectable-primary-foreground: var(--salt-palette-interact-foreground);\n --salt-selectable-primary-foreground-disabled: var(--salt-palette-interact-foreground-disabled);\n --salt-selectable-secondary-foreground: var(--salt-palette-interact-foreground);\n --salt-selectable-secondary-foreground-disabled: var(--salt-palette-interact-foreground-disabled);\n\n /* Status */\n --salt-status-info-background-emphasize: var(--salt-status-info-background);\n --salt-status-success-background-emphasize: var(--salt-status-success-background);\n --salt-status-warning-background-emphasize: var(--salt-status-warning-background);\n --salt-status-error-background-emphasize: var(--salt-status-error-background);\n\n --salt-status-info-foreground-disabled: var(--salt-palette-info-foreground-disabled);\n --salt-status-success-foreground-disabled: var(--salt-palette-success-foreground-disabled);\n --salt-status-warning-foreground-disabled: var(--salt-palette-warning-foreground-disabled);\n --salt-status-error-foreground-disabled: var(--salt-palette-error-foreground-disabled);\n --salt-status-static-foreground-disabled: var(--salt-palette-neutral-secondary-foreground-disabled);\n --salt-status-negative-foreground-disabled: var(--salt-palette-negative-foreground-disabled);\n --salt-status-positive-foreground-disabled: var(--salt-palette-positive-foreground-disabled);\n\n --salt-status-info-borderColor-disabled: var(--salt-palette-info-border-disabled);\n --salt-status-success-borderColor-disabled: var(--salt-palette-success-border-disabled);\n --salt-status-warning-borderColor-disabled: var(--salt-palette-warning-border-disabled);\n --salt-status-error-borderColor-disabled: var(--salt-palette-error-border-disabled);\n}\n", ".vuu-theme {\n --salt-color-orange-500-fade-foreground: rgba(234, 115, 25, var(--salt-palette-opacity-foreground));\n --salt-color-orange-700-fade-foreground: rgba(214, 85, 19, var(--salt-palette-opacity-foreground));\n --salt-color-orange-400-fade-background: rgba(238, 133, 43, var(--salt-palette-opacity-background));\n --salt-color-orange-600-fade-background: rgba(224, 101, 25, var(--salt-palette-opacity-background));\n --salt-color-blue-300-fade-fill: rgba(51, 141, 205, var(--salt-palette-opacity-fill));\n --salt-color-blue-500-fade-fill: rgba(38, 112, 169, var(--salt-palette-opacity-fill));\n}\n", "/*\n* **Deprecated:** Use duration instead\n*/\n.vuu-theme {\n /* Delay */\n --salt-delay-instant: 100ms;\n --salt-delay-perceptible: 300ms;\n --salt-delay-notable: 1000ms;\n --salt-delay-cutoff: 10000ms;\n\n /* Icon */\n --salt-size-icon-base: var(--salt-icon-size-base);\n\n /* Opacity */\n --salt-opacity-1: 0.15;\n --salt-opacity-2: 0.25;\n --salt-opacity-3: 0.4;\n --salt-opacity-4: 0.7;\n}\n\n.vuu-density-touch,\n.vuu-density-low,\n.vuu-density-medium,\n.vuu-density-high {\n /* Size */\n --salt-size-selection: var(--salt-size-selectable);\n --salt-size-brandBar: 4px; /* Use --salt-size-accent */\n --salt-size-graphic-small: 12px;\n --salt-size-graphic-medium: 24px;\n --salt-size-graphic-large: 48px;\n --salt-size-divider-height: var(--salt-size-separator-height);\n --salt-size-divider-strokeWidth: var(--salt-size-separator-strokeWidth);\n\n --salt-size-detail: var(--salt-size-compact);\n}\n", "/*\n* **Deprecated:**\n* Delete below on breaking change\n*/\n\n.vuu-theme {\n --salt-palette-error-background-emphasize: var(--salt-palette-error-background);\n --salt-palette-warning-background-emphasize: var(--salt-palette-warning-background);\n --salt-palette-success-background-emphasize: var(--salt-palette-success-background);\n --salt-palette-info-background-emphasize: var(--salt-palette-info-background);\n\n --salt-palette-opacity-fill: var(--salt-palette-opacity-disabled);\n --salt-palette-opacity-stroke: var(--salt-palette-opacity-disabled);\n --salt-palette-opacity-background: var(--salt-palette-opacity-disabled);\n --salt-palette-opacity-border: var(--salt-palette-opacity-disabled);\n --salt-palette-opacity-foreground: var(--salt-palette-opacity-disabled);\n}\n\n.vuu-theme[data-mode=\"light\"] {\n /* Interact */\n --salt-palette-interact-foreground-partial: var(--salt-color-blue-600);\n --salt-palette-interact-foreground-partialDisabled: var(--salt-color-blue-600-fade-foreground);\n\n /* Measure */\n --salt-palette-measured-fill: var(--salt-color-blue-500);\n --salt-palette-measured-fill-disabled: var(--salt-color-blue-500-fade-fill);\n --salt-palette-measured-foreground: var(--salt-color-gray-90);\n --salt-palette-measured-foreground-active: var(--salt-color-blue-500);\n --salt-palette-measured-foreground-disabled: var(--salt-color-gray-90-fade-foreground);\n --salt-palette-measured-foreground-activeDisabled: var(--salt-color-blue-500-fade-fill);\n --salt-palette-measured-background: var(--salt-color-gray-60);\n --salt-palette-measured-background-disabled: var(--salt-color-gray-60-fade-background);\n --salt-palette-measured-border: var(--salt-color-gray-90);\n --salt-palette-measured-border-disabled: var(--salt-color-gray-90-fade-border);\n\n /* Neutral */\n --salt-palette-neutral-tertiary-background-readonly: transparent;\n\n /* Status */\n --salt-palette-error-foreground-disabled: var(--salt-color-red-500-fade-foreground);\n --salt-palette-error-border-disabled: var(--salt-color-red-500-fade-border);\n\n --salt-palette-info-border-disabled: var(--salt-color-blue-500-fade-border);\n --salt-palette-info-foreground-disabled: var(--salt-color-blue-500-fade-foreground);\n\n --salt-palette-negative-foreground-disabled: var(--salt-color-red-700-fade-foreground);\n\n --salt-palette-positive-foreground-disabled: var(--salt-color-green-700-fade-foreground);\n\n --salt-palette-success-border-disabled: var(--salt-color-green-500-fade-border);\n --salt-palette-success-foreground-disabled: var(--salt-color-green-500-fade-foreground);\n\n --salt-palette-warning-foreground-disabled: var(--salt-color-orange-700-fade-foreground);\n --salt-palette-warning-border-disabled: var(--salt-color-orange-700-fade-border);\n}\n\n.vuu-theme[data-mode=\"dark\"] {\n /* Interact */\n --salt-palette-interact-foreground-partial: var(--salt-color-blue-100);\n --salt-palette-interact-foreground-partialDisabled: var(--salt-color-blue-100-fade-foreground);\n\n /* Measure */\n --salt-palette-measured-fill: var(--salt-color-blue-300);\n --salt-palette-measured-fill-disabled: var(--salt-color-blue-300-fade-fill);\n --salt-palette-measured-foreground: var(--salt-color-gray-90);\n --salt-palette-measured-foreground-active: var(--salt-color-blue-300);\n --salt-palette-measured-foreground-disabled: var(--salt-color-gray-90-fade-foreground);\n --salt-palette-measured-foreground-activeDisabled: var(--salt-color-blue-300-fade-fill);\n --salt-palette-measured-background: var(--salt-color-gray-300);\n --salt-palette-measured-background-disabled: var(--salt-color-gray-300-fade-background);\n --salt-palette-measured-border: var(--salt-color-gray-90);\n --salt-palette-measured-border-disabled: var(--salt-color-gray-90-fade-border);\n\n /* Neutral */\n --salt-palette-neutral-tertiary-background-readonly: transparent;\n\n /* Status */\n --salt-palette-error-foreground-disabled: var(--salt-color-red-500-fade-foreground);\n --salt-palette-error-border-disabled: var(--salt-color-red-500-fade-border);\n\n --salt-palette-info-border-disabled: var(--salt-color-blue-500-fade-border);\n --salt-palette-info-foreground-disabled: var(--salt-color-blue-500-fade-foreground);\n\n --salt-palette-negative-foreground-disabled: var(--salt-color-red-300-fade-foreground);\n\n --salt-palette-positive-foreground-disabled: var(--salt-color-green-300-fade-foreground);\n\n --salt-palette-success-border-disabled: var(--salt-color-green-400-fade-border);\n --salt-palette-success-foreground-disabled: var(--salt-color-green-400-fade-foreground);\n\n --salt-palette-warning-foreground-disabled: var(--salt-color-orange-500-fade-foreground);\n --salt-palette-warning-border-disabled: var(--salt-color-orange-500-fade-border);\n}\n", ".saltButton {\n white-space: nowrap;;\n}\n\n.saltButton-primary {\n --saltButton-borderColor: var(--salt-actionable-primary-foreground);\n --saltButton-borderWidth: 1px;\n --saltButton-borderRadius: 6px;\n --saltButton-borderStyle: solid;\n --vuu-icon-color: var(--saltIcon-color);\n}\n\n.saltButton-primary:hover {\n --saltButton-borderColor: var(--salt-actionable-primary-background-hover)\n}", ".saltCheckbox {\n --vuu-icon-size: 12px;\n --vuu-icon-left: -1px;\n --vuu-icon-top: -1px;\n}\n\n.saltCheckboxIcon {\n border-radius: 3px;\n height: 12px;\n width: 12px;\n}\n\n.saltCheckboxIcon-checked {\n background-color: var(--vuuCheckboxIcon-background-checked, var(--salt-selectable-background-selected));\n}\n\n\n.saltCheckboxIcon-checked.saltCheckboxIcon-disabled, \n.saltCheckbox:hover .saltCheckboxIcon-checked.saltCheckboxIcon-disabled {\n background-color: var(--salt-selectable-background-disabled);\n border-color: transparent;;\n}\n\n\n.saltCheckboxIcon-checked:after {\n content: \"\";\n background-color: white;\n left: var(--vuu-icon-left, auto);\n height: var(--vuu-icon-height, var(--vuu-icon-size, 12px));\n -webkit-mask: var(--vuu-svg-tick) center center/var(--vuu-icon-size) var(--vuu-icon-size);\n mask: var(--vuu-icon-svg) center center/var(--vuu-icon-size) var(--vuu-icon-size);\n mask-repeat: no-repeat;\n -webkit-mask-repeat: no-repeat;\n position: absolute;\n top: var(--vuu-icon-top, auto);\n width: var(--vuu-icon-width, var(--vuu-icon-size, 12px));\n}", ".saltIcon {\n display: none;\n} ", ".saltInput-activationIndicator {\n display: none;\n}\n\n.saltInput-primary {\n --saltInput-height: 24px;\n border: solid 1px var(--input-borderColor, var(--salt-editable-borderColor));\n border-radius: 6px;\n}\n\n.saltInput-focused:hover, \n.saltInput-focused {\n --input-borderColor: var(--vuu-color-purple-10);\n}", ".vuu-theme .vuuSplitter {\n --splitter-background: var(--vuu-color-gray-05);\n --splitter-size: 9px;\n --splitter-borderColor: white;\n --splitter-borderStyle: none solid none solid;\n --splitter-borderWidth: 4px;\n}\n\n.vuu-theme .vuuSplitter-column {\n --splitter-borderStyle: solid none solid none;\n}", ".saltSwitch {\n --vuu-icon-left: -1px;\n\n}\n\n.saltSwitch-track,\n.saltSwitch-track:hover {\n background-color: var(--vuu-color-gray-45);\n border: none;\n border-radius: 4px;\n height: 14px;\n padding: 0 2px;\n width: 26px;\n}\n\n.saltSwitch-thumb {\n background-color: var(--vuu-color-white);\n border: none;\n border-radius: 3px;\n height: 10px;\n margin:0;\n width: 10px;\n}\n\n.saltSwitch-checked .saltSwitch-track {\n background-color: var(--salt-selectable-background-selected);\n}\n\n.saltSwitch-checked .saltSwitch-thumb,\n.saltSwitch-checked:hover .saltSwitch-thumb {\n background-color: white;\n transform: translateX(calc(100% + 2px));\n}\n\n\n.saltSwitch-checked .saltSwitch-thumb:after {\n background-color: var(--vuu-color-purple-10);\n content: \"\";\n left: var(--vuu-icon-left, auto);\n height: var(--vuu-icon-height, var(--vuu-icon-size, 12px));\n -webkit-mask: var(--vuu-svg-tick) center center/var(--vuu-icon-size) var(--vuu-icon-size);\n mask: var(--vuu-icon-svg) center center/var(--vuu-icon-size) var(--vuu-icon-size);\n mask-repeat: no-repeat;\n -webkit-mask-repeat: no-repeat;\n position: absolute;\n top: var(--vuu-icon-top, auto);\n width: var(--vuu-icon-width, var(--vuu-icon-size, 12px));\n\n}", ".saltToggleButtonGroup {\n border-radius: 6px;\n gap: 0;\n padding: 0;\n}\n\n.saltToggleButtonGroup-horizontal .saltToggleButton {\n height: 24px;\n}\n\n.vuuIconToggleButton {\n --vuu-icon-size: var(--vuuIconToggleButton-iconSize, 48px);\n width: 48px;\n}\n\n.vuuIconToggleButton:first-child {\n border-radius: 4px 0 0 4px;\n}\n.vuuIconToggleButton:last-child {\n border-radius: 0 4px 4px 0;\n}"], + "mappings": "AACA,WACE,wBACA,kBACA,gBACA,kBACA,kBACA,y45BACA,8KAEF,WACE,wBACA,kBACA,gBACA,kBACA,kBACA,y45BACA,8KAEF,WACE,wBACA,kBACA,gBACA,kBACA,kBACA,y45BACA,8KAGF,WACE,wBACA,kBACA,gBACA,kBACA,kBACA,y45BACA,8KAGA,WACE,wBACA,kBACA,gBACA,kBACA,kBACA,y45BACA,8KAGF,WACE,wBACA,kBACA,gBACA,kBACA,kBACA,y45BACA,8KCvDH,WACC,0CACA,8CACA,oCACA,8CACA,wCAGF,YACE,gDACA,+CAGF,4BACE,mBAGF,2BACE,kBAKF,mBAGE,sBAGF,qBACE,kBACA,WACA,UAjCF,sBAoCE,gBACA,mBACA,mBACA,eCvCF,0EAIE,kCACA,gCACA,gCACA,8BACA,uCACA,kCACA,+BACA,0CAGA,iHACA,mHACA,qHACA,uHAEA,wHACA,0HACA,4HACA,8HAGA,iHACA,uHACA,qHACA,2FAIF,2BAEI,4CACA,+DAGA,0CACA,2DAGJ,4BAEI,0CACA,6DAGA,4CACA,6DAGJ,4BAEI,4CACA,yEAGA,0CACA,0DAGJ,6BAEI,0CACA,4DAGA,4CACA,uEAGJ,6BAEI,4CACA,8DAGA,0CACA,0DAGJ,8BAEI,0CACA,4DAGA,4CACA,4DAGJ,8BAEI,4CACA,0EAGA,0CACA,2DAGJ,+BAEI,0CACA,6DAGA,4CACA,wEAIJ,2BAEI,kCACA,4CACA,sDAIA,0CACA,kDAGJ,8BAEI,iCACA,4CACA,sDAIA,0CACA,kDAGJ,6BAEI,+CAIA,2CAIJ,4BAEI,6CAGA,6CCxJJ,WAEE,sBAEA,qCACA,yBACA,yBAGA,wCAGA,uCACA,0EAEA,wCAGA,wCACA,wCACA,wCACA,yCACA,wCACA,wCACA,wCACA,wCACA,wCACA,wCACA,uCACA,qCAEF,wCACA,uCACA,oDAEA,qCAEE,yCAKA,uCACA,iCAEA,wCACA,wCACA,wCACA,wCACA,wCACA,yCACA,wCACA,uCACA,uCACA,uCACA,uCACA,uCACA,sCACA,sCAEA,2CACA,2CACA,2CACA,2CACA,0CACA,2CACA,2CACA,2CACA,2CACA,2CACA,2CACA,0CACA,0CACA,yCAEA,0CACA,0CACA,0CACA,0CACA,0CACA,0CACA,0CACA,yCACA,yCACA,yCACA,yCACA,wCACA,uCACA,wCAEA,yCACA,yCACA,yCACA,yCACA,yCACA,0CACA,yCACA,yCACA,yCACA,wCACA,yCACA,uCACA,sCACA,sCAEA,yCACA,yCACA,yCACA,yCACA,yCACA,0CACA,yCACA,yCACA,yCACA,yCACA,wCACA,uCACA,uCACA,uCAEA,2CACA,2CACA,2CACA,2CACA,2CACA,4CACA,4CACA,4CACA,2CACA,2CACA,2CACA,2CACA,0CACA,yCAEA,yCACA,yCACA,yCACA,yCACA,yCACA,yCACA,yCACA,yCACA,yCACA,0CACA,yCACA,uCACA,uCACA,uCACA,uCACA,uCACA,uCACA,uCCxJF,WACE,6BACA,iCACA,4BACA,4BCJF,WACE,iGACA,gGACA,+FACA,gGACA,gGACA,gGACA,8FACA,gGACA,gGACA,gGACA,+FACA,8FACA,8FACA,8FACA,8FAEA,4FACA,4FACA,4FACA,4FACA,0FACA,4FACA,4FACA,8FACA,8FACA,8FACA,6FACA,0FAEA,4GACA,4GAEA,gGACA,gGACA,+FACA,8FACA,gGACA,gGACA,gGACA,gGACA,8FACA,8FACA,8FACA,8FAEA,kHACA,oHACA,kHACA,kHAEA,yFAEA,2FACA,yFAEA,kHACA,sHACA,oHACA,4GACA,gHACA,8GC7DF,mBACE,4BACA,wCAGF,iBACE,4BACA,wCAGF,oBACE,4BACA,uCAGF,kBACE,4BACA,uCCjBF,WACE,oBACA,sBACA,uBACA,uBACA,sBACA,sBCNF,4BACE,yCACA,yCACA,0CACA,yCACA,yCAGF,2BACE,yCACA,yCACA,0CACA,0CACA,0CAGF,WACE,sBACA,wDACA,wDACA,wDACA,yDACA,0DCrBF,0EAIE,4BAEA,4DACA,+DACA,uCACA,yHACA,iGACA,mCACA,mCACA,2EAGF,kBACE,wDACA,6DACA,2DAGA,2BACA,qBACA,uBACA,wBACA,6BACA,uBAGF,oBACE,wDACA,2DACA,0DAGA,2BACA,qBACA,uBACA,wBACA,6BACA,uBAGF,iBACE,wDACA,6DACA,4DAGA,4BACA,qBACA,uBACA,wBACA,6BACA,uBAGF,mBACE,wDACA,2DACA,0DAGA,4BACA,qBACA,uBACA,wBACA,6BACA,uBCtEF,mBACE,yBAGF,iBACE,yBAGF,oBACE,wBAGF,kBACE,wBAGF,0EAIE,uDACA,sDACA,uDAEA,wDACA,sDACA,wDACA,sDACA,wDACA,sDC7BF,WACE,4CACA,6CAEA,wCACA,0CACA,yCACA,2CACA,uCACA,4CCTF,0EAIE,yBACA,2BACA,2BACA,8BACA,2BACA,0BACA,iCACA,+BACA,gCACA,4BCbF,uDACE,6DACA,sFACA,yDACA,8EACA,0DACA,mFCNF,4BACE,0DACA,mEACA,uDACA,2DAGF,2BACE,2DACA,oEACA,uDACA,2DCXF,4BACE,0DACA,uDACA,2DAGF,2BACE,2DACA,uDACA,2DCTF,4BACE,gDACA,2EACA,mEACA,oEACA,sEACA,6FACA,yDACA,kEACA,oFACA,gFACA,+DACA,yFACA,+DACA,sEACA,8FACA,wFACA,qEACA,4DAEA,mEACA,0EACA,kGACA,4FACA,uEACA,gEACA,uEACA,+FACA,yFACA,uEACA,mEACA,4EACA,sGACA,+FACA,2EACA,qEACA,2EACA,mGACA,gGACA,2EACA,0DACA,gFACA,wGACA,mEACA,8EACA,yEACA,6EACA,qGACA,kGACA,+EAGF,2BACE,gDACA,sEACA,4EACA,qEACA,yDACA,8FACA,0DACA,kEACA,oFACA,+EACA,iEACA,wFACA,8DACA,sEACA,8FACA,uFACA,qEACA,0DAEA,mEACA,0EACA,kGACA,4FACA,yEACA,gEACA,uEACA,+FACA,yFACA,sEACA,uEACA,6EACA,qGACA,gGACA,6EACA,oEACA,8EACA,sGACA,6FACA,0EACA,0DACA,+EACA,uGACA,mEACA,+EACA,sEACA,gFACA,wGACA,+FACA,4ECrGF,4BACE,wDACA,+DACA,4EACA,0DACA,iEACA,8EACA,yDACA,gEACA,6EACA,qEACA,sEACA,yEACA,mEACA,qEACA,2FAGF,2BACE,wDACA,+DACA,6EACA,0DACA,iEACA,+EACA,yDACA,gEACA,8EACA,qEACA,sEACA,yEACA,mEACA,mEACA,2FCjCF,4BACE,8DAGF,2BACE,8DCLF,4BACE,mEACA,4FACA,qGACA,oEACA,+FACA,gGACA,kEACA,sFACA,sEACA,gGACA,yGACA,mEACA,wFACA,wEACA,iGACA,uEACA,oGACA,wDACA,iEACA,oDACA,6DACA,oEAGF,2BACE,sEACA,+FACA,wGACA,kEACA,uFACA,mEACA,4FACA,gGACA,wEACA,iGACA,0GACA,mEACA,wFACA,uEACA,gGACA,uEACA,oGACA,wDACA,iEACA,oDACA,6DACA,kGC/CF,WACE,wDACA,wDACA,kEACA,+DACA,8DACA,gEACA,+DCPF,4BACE,gEAGF,2BACE,gECLF,4BACE,8DACA,uEACA,2DACA,+DAGF,2BACE,+DACA,wEACA,2DACA,+DCXF,4BACE,2DACA,oFACA,uDACA,4EAGF,2BACE,4DACA,qFACA,uDACA,4ECXF,4BACE,+DACA,wEACA,4DACA,gEAGF,2BACE,gEACA,yEACA,4DACA,gECXF,kBACE,4BACA,+BAEF,oBACE,6BACA,+BAEF,iBACE,6BACA,+BAEF,mBACE,6BACA,+BAGF,WACE,gEACA,kFACA,6DACA,+EACA,gEACA,kFACA,qECxBF,WACE,wCACA,yCACA,+CAEA,sCACA,oCACA,2CAGA,sFACA,kGACA,oGACA,wGACA,sFACA,kGACA,oGACA,wGACA,6EAGA,8EACA,0FACA,4FACA,gGACA,8EACA,0FACA,4FACA,gGACA,yEAGA,0FACA,sGACA,wGACA,4GACA,0FACA,sGACA,wGACA,4GACA,mFCxCF,WACE,oCAEA,oFACA,sGACA,iFACA,mGAEA,wFACA,0GACA,qFACA,uGAEA,sFACA,wGACA,mFACA,qGChBF,WACE,qDACA,sDAEA,mDACA,oDAEA,yCACA,8CCRF,WACE,8EAEA,2EACA,kCACA,uCACA,2CAEA,2CCRF,WACE,mCACA,oCACA,6CACA,sCAEA,mCACA,yCACA,0CACA,4CACA,4CACA,wCAEA,iEACA,+EACA,mFACA,6EACA,mFAEA,mFACA,0FACA,qGACA,yFACA,qGAEA,uFACA,8FACA,yGACA,6FACA,yGAEA,uCC/BF,WACE,0EAGF,mBACE,8DCLF,WACE,wCACA,uCACA,8CACA,mCAEA,uEACA,6EACA,+EACA,4EAEA,+EACA,iFACA,iGAEA,qFACA,iGACA,mGAEA,yFACA,qGACA,uGAEA,uFACA,mGACA,qGCzBF,WACE,uDACA,6DACA,gDACA,sDACA,uDACA,qDACA,sDAEA,oECTF,WACE,wCACA,2CACA,+CACA,+CACA,+CAEA,qCACA,2CACA,8CACA,kDAEA,mEACA,+EACA,mFACA,mGACA,qFACA,qFAEA,sEACA,wFACA,kFACA,sFACA,sGACA,sEACA,kFACA,sFACA,gGACA,wFACA,sGAEA,0FACA,8FACA,8GACA,0EACA,4FACA,0FACA,8FACA,8GAEA,kGACA,sGACA,sHACA,8EACA,gGACA,kGACA,sGACA,sHAEA,sGACA,0GACA,0HACA,gFACA,kGACA,sGACA,0GACA,0HCxDF,WACE,oCAEA,oFACA,wFACA,sFCLF,WACE,mEACA,yEACA,yEACA,qEACA,kFACA,2EACA,2EAEA,gEACA,sEACA,sEACA,kEAEA,mEACA,yEACA,yEACA,qEAEA,2FACA,2FACA,uFCrBF,WACE,sCACA,uCACA,6CAEA,4EACA,wFACA,0FACA,8FAEA,4EACA,wFACA,0FACA,8FCbF,WAEE,6BACA,4BACA,uCACA,iCACA,gCAGA,0DACA,kEACA,sEACA,0EAGA,6DACA,kEACA,0EACA,8EAGA,6DACA,sEACA,2EACA,yEAGA,6DACA,sEACA,2EACA,yEAGA,6DACA,sEACA,2EACA,yEAGA,gEACA,wEACA,4EACA,gFAGA,mEACA,4EACA,+EACA,iFAEA,mEACA,4EACA,+EACA,iFAEA,mEACA,4EACA,+EACA,iFAGA,gFAEA,+EACA,iGACA,mFACA,qGAGA,iFACA,mFACA,qFACA,2CACA,4CACA,oDAGA,oEAIF,mBACE,8BACA,gCAEA,8BACA,gCAEA,8BACA,gCAEA,8BACA,gCAEA,iCACA,mCAEA,2BACA,6BACA,4BAEA,oCACA,uCAEA,oCACA,sCAEA,oCACA,sCAGF,iBACE,8BACA,gCAEA,8BACA,gCAEA,8BACA,gCAEA,8BACA,gCAEA,iCACA,mCAEA,2BACA,6BACA,4BAEA,oCACA,sCAEA,oCACA,sCAEA,oCACA,sCAGF,oBACE,8BACA,gCAEA,8BACA,gCAEA,8BACA,gCAEA,8BACA,gCAEA,iCACA,mCAEA,2BACA,6BACA,4BAEA,oCACA,sCAEA,oCACA,sCAEA,oCACA,sCAGF,kBACE,8BACA,gCAEA,8BACA,gCAEA,8BACA,gCAEA,8BACA,gCAEA,iCACA,mCAEA,2BACA,6BACA,4BAEA,oCACA,sCAEA,oCACA,sCAEA,oCACA,sCCtMF,WACE,gCACA,uCACA,yCACA,4CAEA,8BACA,qCACA,uCACA,yCAEA,oEACA,+BAEA,8DACA,gFACA,2DACA,6ECjBF,WAEE,iFACA,iFAGA,qFACA,4FACA,uGACA,2FACA,uGAGA,mCACA,0CACA,4CACA,+CACA,iCACA,wCACA,0CACA,4CAEA,uEACA,kCAEA,oEACA,sFACA,iEACA,mFACA,wDACA,0EACA,oEACA,iFACA,kFACA,gFACA,kGACA,sFAGA,mEAGA,sFACA,sGAEA,0EACA,4FACA,8EACA,gGACA,gFACA,kGAGA,4EACA,kFACA,kFACA,8EAEA,qFACA,2FACA,2FACA,uFACA,oGACA,6FACA,6FAEA,kFACA,wFACA,wFACA,oFCrEF,WACE,oGACA,mGACA,oGACA,oGACA,sFACA,sFCHF,WAEE,0BACA,8BACA,yBACA,yBAGA,kDAGA,sBACA,sBACA,qBACA,qBAGF,0EAKE,mDACA,0BACA,gCACA,iCACA,gCACA,8DACA,wEAEA,6CC5BF,WACE,gFACA,oFACA,oFACA,8EAEA,kEACA,oEACA,wEACA,oEACA,wEAGF,4BAEE,uEACA,+FAGA,yDACA,4EACA,8DACA,sEACA,uFACA,wFACA,8DACA,uFACA,0DACA,+EAGA,iEAGA,oFACA,4EAEA,4EACA,oFAEA,uFAEA,yFAEA,gFACA,wFAEA,yFACA,iFAGF,2BAEE,uEACA,+FAGA,yDACA,4EACA,8DACA,sEACA,uFACA,wFACA,+DACA,wFACA,0DACA,+EAGA,iEAGA,oFACA,4EAEA,4EACA,oFAEA,uFAEA,yFAEA,gFACA,wFAEA,yFACA,iFC3FF,YACI,mBAGJ,oBACI,oEACA,8BACA,+BACA,gCACA,wCAGJ,0BACI,2ECbJ,cACI,sBACA,sBACA,qBAGJ,kBANA,kBAQI,YACA,WAGJ,0BACI,uGAIJ,4HAEI,4DACA,yBAIJ,gCACI,WACA,sBACA,gCACA,0DACA,yFACA,iFACA,sBACA,8BACA,kBACA,8BACA,wDCnCJ,UACI,aCDJ,+BACI,aAGJ,mBACI,yBACA,4EANJ,kBAUA,4CAEI,gDCZJ,wBACI,gDACA,qBACA,8BACA,8CACA,4BAGJ,+BACI,8CCTJ,YACI,sBAIJ,0CAEI,0CACA,YARJ,kBAUI,YAVJ,cAYI,WAGJ,kBACI,wCACA,YAjBJ,kBAmBI,YAnBJ,SAqBI,WAGJ,sCACI,4DAGJ,kFAEI,sBACA,sCAIJ,4CACI,4CACA,WACA,gCACA,0DACA,yFACA,iFACA,sBACA,8BACA,kBACA,8BACA,wDC9CJ,yCAEI,MAFJ,UAMA,oDACI,YAGJ,qBACI,2DACA,WAGJ,iCAfA,0BAkBA,gCAlBA", + "names": [] +} diff --git a/vuu-ui/cypress/support/e2e/constants.ts b/vuu-ui/cypress/support/e2e/constants.ts index 23f0a315c..04abf6b22 100644 --- a/vuu-ui/cypress/support/e2e/constants.ts +++ b/vuu-ui/cypress/support/e2e/constants.ts @@ -1,2 +1,2 @@ export const SHELL_WITH_NEW_THEME_URL = - "/Apps/ShellWithNewTheme?standalone&theme=vuu"; + "/Apps/ShellWithNewThemeAndLayoutManagement?standalone&theme=vuu"; diff --git a/vuu-ui/global.d.ts b/vuu-ui/global.d.ts new file mode 100644 index 000000000..a19d27bfe --- /dev/null +++ b/vuu-ui/global.d.ts @@ -0,0 +1,3 @@ +declare module "@thomaschaplin/isin-generator" { + export const isinGenerator = () => undefined; +} diff --git a/vuu-ui/package-lock.json b/vuu-ui/package-lock.json index 0a4501c7b..a5edb95de 100644 --- a/vuu-ui/package-lock.json +++ b/vuu-ui/package-lock.json @@ -57,9 +57,9 @@ "stylelint": "^15.0.0", "tinycolor2": "1.4.2", "typescript": "4.9.5", - "vite": "4.2.0", + "vite": "4.5.0", "vite-tsconfig-paths": "^4.0.3", - "vitest": "0.33.0" + "vitest": "0.34.6" }, "engines": { "node": ">=16.0.0" @@ -1236,6 +1236,10 @@ "resolved": "packages/vuu-data-react", "link": true }, + "node_modules/@finos/vuu-data-test": { + "resolved": "packages/vuu-data-test", + "link": true + }, "node_modules/@finos/vuu-data-types": { "resolved": "packages/vuu-data-types", "link": true @@ -2717,26 +2721,26 @@ } }, "node_modules/@vitest/expect": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.33.0.tgz", - "integrity": "sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", "dev": true, "dependencies": { - "@vitest/spy": "0.33.0", - "@vitest/utils": "0.33.0", - "chai": "^4.3.7" + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.33.0.tgz", - "integrity": "sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", "dev": true, "dependencies": { - "@vitest/utils": "0.33.0", + "@vitest/utils": "0.34.6", "p-limit": "^4.0.0", "pathe": "^1.1.1" }, @@ -2772,9 +2776,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.33.0.tgz", - "integrity": "sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", "dev": true, "dependencies": { "magic-string": "^0.30.1", @@ -2786,12 +2790,12 @@ } }, "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -2806,9 +2810,9 @@ "dev": true }, "node_modules/@vitest/spy": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.33.0.tgz", - "integrity": "sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", "dev": true, "dependencies": { "tinyspy": "^2.1.1" @@ -2818,9 +2822,9 @@ } }, "node_modules/@vitest/utils": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.33.0.tgz", - "integrity": "sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", "dev": true, "dependencies": { "diff-sequences": "^29.4.3", @@ -2832,21 +2836,21 @@ } }, "node_modules/@vitest/utils/node_modules/diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -3523,18 +3527,18 @@ } }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" }, "engines": { "node": ">=4" @@ -3593,10 +3597,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -5225,7 +5232,7 @@ "pend": "~1.2.0" } }, - "node_modules/feature-vuu-basket-trading": { + "node_modules/feature-basket-trading": { "resolved": "sample-apps/feature-basket-trading", "link": true }, @@ -5480,9 +5487,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -7205,12 +7212,12 @@ } }, "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "dependencies": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "node_modules/lru-cache": { @@ -7232,9 +7239,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.2.tgz", - "integrity": "sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -8223,15 +8230,15 @@ } }, "node_modules/mlly": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", - "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.10.0", "pathe": "^1.1.1", "pkg-types": "^1.0.3", - "ufo": "^1.1.2" + "ufo": "^1.3.0" } }, "node_modules/mri": { @@ -8770,9 +8777,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -10228,18 +10235,18 @@ } }, "node_modules/tinypool": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.6.0.tgz", - "integrity": "sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true, "engines": { "node": ">=14.0.0" @@ -10519,9 +10526,9 @@ } }, "node_modules/ufo": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.2.0.tgz", - "integrity": "sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", "dev": true }, "node_modules/unbox-primitive": { @@ -10817,15 +10824,14 @@ } }, "node_modules/vite": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.0.tgz", - "integrity": "sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", "dev": true, "dependencies": { - "esbuild": "^0.17.5", - "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" @@ -10833,12 +10839,16 @@ "engines": { "node": "^14.18.0 || >=16.0.0" }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", + "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", @@ -10851,6 +10861,9 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -10866,9 +10879,9 @@ } }, "node_modules/vite-node": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.33.0.tgz", - "integrity": "sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -10876,7 +10889,7 @@ "mlly": "^1.4.0", "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -10907,41 +10920,413 @@ } } }, - "node_modules/vite/node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, "bin": { - "resolve": "bin/resolve" + "esbuild": "bin/esbuild" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "node_modules/vitest": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.33.0.tgz", - "integrity": "sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.33.0", - "@vitest/runner": "0.33.0", - "@vitest/snapshot": "0.33.0", - "@vitest/spy": "0.33.0", - "@vitest/utils": "0.33.0", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", - "chai": "^4.3.7", + "chai": "^4.3.10", "debug": "^4.3.4", "local-pkg": "^0.4.3", "magic-string": "^0.30.1", @@ -10950,9 +11335,9 @@ "std-env": "^3.3.3", "strip-literal": "^1.0.1", "tinybench": "^2.5.0", - "tinypool": "^0.6.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.33.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", "why-is-node-running": "^2.2.2" }, "bin": { @@ -11415,6 +11800,15 @@ "react": "^17.0.2" } }, + "packages/vuu-data-test": { + "name": "@finos/vuu-data-test", + "version": "0.0.26", + "license": "Apache-2.0", + "devDependencies": { + "@finos/vuu-data": "0.0.26", + "@finos/vuu-datagrid-types": "0.0.26" + } + }, "packages/vuu-data-types": { "name": "@finos/vuu-data-types", "version": "0.0.26", @@ -11461,9 +11855,12 @@ "dependencies": { "@finos/vuu-data": "0.0.26", "@finos/vuu-datagrid-types": "0.0.26", + "@finos/vuu-filters": "0.0.26", + "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-table": "0.0.26", "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26" }, "peerDependencies": { @@ -11533,6 +11930,9 @@ "license": "Apache-2.0", "dependencies": { "@finos/vuu-popups": "0.0.26", + "@finos/vuu-table": "0.0.26", + "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26", "@salt-ds/core": "1.8.0" }, @@ -11547,7 +11947,11 @@ "version": "0.0.26", "license": "Apache-2.0", "dependencies": { + "@finos/vuu-data": "0.0.26", "@finos/vuu-data-types": "0.0.26", + "@finos/vuu-layout": "0.0.26", + "@finos/vuu-shell": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26", "@salt-ds/core": "1.8.0", "@salt-ds/icons": "1.5.1", @@ -11570,7 +11974,12 @@ "license": "Apache-2.0", "dependencies": { "@finos/vuu-data": "0.0.26", + "@finos/vuu-filters": "0.0.26", + "@finos/vuu-icons": "0.0.26", "@finos/vuu-layout": "0.0.26", + "@finos/vuu-table": "0.0.26", + "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26", "@salt-ds/core": "1.8.0", "@salt-ds/icons": "1.5.1", @@ -11588,8 +11997,12 @@ "license": "Apache-2.0", "dependencies": { "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-react": "0.0.26", "@finos/vuu-data-types": "0.0.26", + "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", + "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26", "@salt-ds/core": "1.8.0", "@salt-ds/icons": "1.5.1", @@ -11723,7 +12136,6 @@ } }, "sample-apps/feature-basket-trading": { - "name": "feature-vuu-basket-trading", "version": "0.0.26", "license": "Apache-2.0", "dependencies": { @@ -11912,6 +12324,7 @@ "dependencies": { "@faker-js/faker": "^8.0.2", "@finos/vuu-data-ag-grid": "0.0.26", + "@finos/vuu-data-test": "0.0.26", "@finos/vuu-datagrid": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -12656,6 +13069,7 @@ "requires": { "@faker-js/faker": "^8.0.2", "@finos/vuu-data-ag-grid": "0.0.26", + "@finos/vuu-data-test": "0.0.26", "@finos/vuu-datagrid": "0.0.26", "@finos/vuu-filters": "0.0.26", "@finos/vuu-layout": "0.0.26", @@ -12721,6 +13135,13 @@ "@finos/vuu-utils": "0.0.26" } }, + "@finos/vuu-data-test": { + "version": "file:packages/vuu-data-test", + "requires": { + "@finos/vuu-data": "0.0.26", + "@finos/vuu-datagrid-types": "0.0.26" + } + }, "@finos/vuu-data-types": { "version": "file:packages/vuu-data-types", "requires": { @@ -12747,9 +13168,12 @@ "requires": { "@finos/vuu-data": "0.0.26", "@finos/vuu-datagrid-types": "0.0.26", + "@finos/vuu-filters": "0.0.26", + "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-table": "0.0.26", "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26" } }, @@ -12796,6 +13220,9 @@ "version": "file:packages/vuu-layout", "requires": { "@finos/vuu-popups": "0.0.26", + "@finos/vuu-table": "0.0.26", + "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26", "@salt-ds/core": "1.8.0" } @@ -12803,7 +13230,11 @@ "@finos/vuu-popups": { "version": "file:packages/vuu-popups", "requires": { + "@finos/vuu-data": "0.0.26", "@finos/vuu-data-types": "0.0.26", + "@finos/vuu-layout": "0.0.26", + "@finos/vuu-shell": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26", "@salt-ds/core": "1.8.0", "@salt-ds/icons": "1.5.1", @@ -12817,7 +13248,12 @@ "version": "file:packages/vuu-shell", "requires": { "@finos/vuu-data": "0.0.26", + "@finos/vuu-filters": "0.0.26", + "@finos/vuu-icons": "0.0.26", "@finos/vuu-layout": "0.0.26", + "@finos/vuu-table": "0.0.26", + "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26", "@salt-ds/core": "1.8.0", "@salt-ds/icons": "1.5.1", @@ -12828,8 +13264,12 @@ "version": "file:packages/vuu-table", "requires": { "@finos/vuu-data": "0.0.26", + "@finos/vuu-data-react": "0.0.26", "@finos/vuu-data-types": "0.0.26", + "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", + "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26", "@salt-ds/core": "1.8.0", "@salt-ds/icons": "1.5.1", @@ -13969,23 +14409,23 @@ } }, "@vitest/expect": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.33.0.tgz", - "integrity": "sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.34.6.tgz", + "integrity": "sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==", "dev": true, "requires": { - "@vitest/spy": "0.33.0", - "@vitest/utils": "0.33.0", - "chai": "^4.3.7" + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", + "chai": "^4.3.10" } }, "@vitest/runner": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.33.0.tgz", - "integrity": "sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.34.6.tgz", + "integrity": "sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==", "dev": true, "requires": { - "@vitest/utils": "0.33.0", + "@vitest/utils": "0.34.6", "p-limit": "^4.0.0", "pathe": "^1.1.1" }, @@ -14008,9 +14448,9 @@ } }, "@vitest/snapshot": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.33.0.tgz", - "integrity": "sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.34.6.tgz", + "integrity": "sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==", "dev": true, "requires": { "magic-string": "^0.30.1", @@ -14019,12 +14459,12 @@ }, "dependencies": { "pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "requires": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } @@ -14038,18 +14478,18 @@ } }, "@vitest/spy": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.33.0.tgz", - "integrity": "sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.34.6.tgz", + "integrity": "sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==", "dev": true, "requires": { "tinyspy": "^2.1.1" } }, "@vitest/utils": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.33.0.tgz", - "integrity": "sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.6.tgz", + "integrity": "sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==", "dev": true, "requires": { "diff-sequences": "^29.4.3", @@ -14058,18 +14498,18 @@ }, "dependencies": { "diff-sequences": { - "version": "29.4.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", - "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true }, "pretty-format": { - "version": "29.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", - "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "requires": { - "@jest/schemas": "^29.6.0", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } @@ -14552,18 +14992,18 @@ "dev": true }, "chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", + "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==", "dev": true, "requires": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.0.8" } }, "chalk": { @@ -14597,10 +15037,13 @@ "dev": true }, "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } }, "check-more-types": { "version": "2.24.0", @@ -15800,7 +16243,7 @@ "pend": "~1.2.0" } }, - "feature-vuu-basket-trading": { + "feature-basket-trading": { "version": "file:sample-apps/feature-basket-trading", "requires": { "@finos/vuu-data": "0.0.26", @@ -16081,9 +16524,9 @@ "dev": true }, "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, "get-intrinsic": { @@ -17314,12 +17757,12 @@ } }, "loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, "requires": { - "get-func-name": "^2.0.0" + "get-func-name": "^2.0.1" } }, "lru-cache": { @@ -17338,9 +17781,9 @@ "dev": true }, "magic-string": { - "version": "0.30.2", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.2.tgz", - "integrity": "sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", "dev": true, "requires": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -17976,15 +18419,15 @@ } }, "mlly": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.0.tgz", - "integrity": "sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", + "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", "dev": true, "requires": { - "acorn": "^8.9.0", + "acorn": "^8.10.0", "pathe": "^1.1.1", "pkg-types": "^1.0.3", - "ufo": "^1.1.2" + "ufo": "^1.3.0" } }, "mri": { @@ -18367,9 +18810,9 @@ } }, "postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", @@ -19428,15 +19871,15 @@ "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" }, "tinypool": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.6.0.tgz", - "integrity": "sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.7.0.tgz", + "integrity": "sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==", "dev": true }, "tinyspy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", - "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", + "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", "dev": true }, "tmp": { @@ -19626,9 +20069,9 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" }, "ufo": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.2.0.tgz", - "integrity": "sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.1.tgz", + "integrity": "sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==", "dev": true }, "unbox-primitive": { @@ -19839,35 +20282,207 @@ } }, "vite": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.0.tgz", - "integrity": "sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", "dev": true, "requires": { - "esbuild": "^0.17.5", + "esbuild": "^0.18.10", "fsevents": "~2.3.2", - "postcss": "^8.4.21", - "resolve": "^1.22.1", - "rollup": "^3.18.0" + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "dependencies": { - "resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "dev": true, + "optional": true + }, + "esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } } } }, "vite-node": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.33.0.tgz", - "integrity": "sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.34.6.tgz", + "integrity": "sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==", "dev": true, "requires": { "cac": "^6.7.14", @@ -19875,7 +20490,7 @@ "mlly": "^1.4.0", "pathe": "^1.1.1", "picocolors": "^1.0.0", - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0-0" } }, "vite-tsconfig-paths": { @@ -19890,23 +20505,23 @@ } }, "vitest": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.33.0.tgz", - "integrity": "sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==", + "version": "0.34.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", + "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, "requires": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", - "@vitest/expect": "0.33.0", - "@vitest/runner": "0.33.0", - "@vitest/snapshot": "0.33.0", - "@vitest/spy": "0.33.0", - "@vitest/utils": "0.33.0", + "@vitest/expect": "0.34.6", + "@vitest/runner": "0.34.6", + "@vitest/snapshot": "0.34.6", + "@vitest/spy": "0.34.6", + "@vitest/utils": "0.34.6", "acorn": "^8.9.0", "acorn-walk": "^8.2.0", "cac": "^6.7.14", - "chai": "^4.3.7", + "chai": "^4.3.10", "debug": "^4.3.4", "local-pkg": "^0.4.3", "magic-string": "^0.30.1", @@ -19915,9 +20530,9 @@ "std-env": "^3.3.3", "strip-literal": "^1.0.1", "tinybench": "^2.5.0", - "tinypool": "^0.6.0", - "vite": "^3.0.0 || ^4.0.0", - "vite-node": "0.33.0", + "tinypool": "^0.7.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0", + "vite-node": "0.34.6", "why-is-node-running": "^2.2.2" } }, diff --git a/vuu-ui/package.json b/vuu-ui/package.json index 952d788e6..c471237d9 100644 --- a/vuu-ui/package.json +++ b/vuu-ui/package.json @@ -35,11 +35,12 @@ "showcase:build": " cd showcase && node scripts/build.mjs", "test:cypress": "cypress run --component --browser chrome --headless", "test:cypress:local": "cypress open --component --browser chrome", + "test:cypress:e2e": "cypress run --browser chrome", "test:vite": "vitest run", "bump": "node ./scripts/version.mjs", "pub": "node ./scripts/publish.mjs", "pub:debug": "node ./scripts/publish.mjs --debug", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit --project tsconfig-typecheck.json" }, "dependencies": { "@types/jest": "^26.0.20", @@ -82,9 +83,9 @@ "stylelint": "^15.0.0", "tinycolor2": "1.4.2", "typescript": "4.9.5", - "vite": "4.2.0", + "vite": "4.5.0", "vite-tsconfig-paths": "^4.0.3", - "vitest": "0.33.0" + "vitest": "0.34.6" }, "engines": { "node": ">=16.0.0" diff --git a/vuu-ui/packages/vuu-codemirror/src/codemirror-basic-setup.ts b/vuu-ui/packages/vuu-codemirror/src/codemirror-basic-setup.ts index 0b8f7413c..5c2352232 100644 --- a/vuu-ui/packages/vuu-codemirror/src/codemirror-basic-setup.ts +++ b/vuu-ui/packages/vuu-codemirror/src/codemirror-basic-setup.ts @@ -1,16 +1,16 @@ -import { KeyBinding } from "@codemirror/view"; +import { closeBrackets } from "@codemirror/autocomplete"; +import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { - closeBrackets, defaultHighlightStyle, - defaultKeymap, + syntaxHighlighting, +} from "@codemirror/language"; +import { Extension } from "@codemirror/state"; +import { drawSelection, - Extension, highlightSpecialChars, - history, - historyKeymap, + KeyBinding, keymap, - syntaxHighlighting, -} from "./index"; +} from "@codemirror/view"; const keyBindings = [ ...defaultKeymap, diff --git a/vuu-ui/packages/vuu-codemirror/tsconfig.json b/vuu-ui/packages/vuu-codemirror/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-codemirror/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-data-ag-grid/tsconfig.json b/vuu-ui/packages/vuu-data-ag-grid/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-data-ag-grid/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts index add3835c9..42cb853f8 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useTypeaheadSuggestions.ts @@ -30,32 +30,20 @@ export const getTypeaheadParams = ( // const containSpace = (text: string) => text.indexOf(" ") !== -1; // const replaceSpace = (text: string) => text.replace(/\s/g, SPECIAL_SPACE); -export const useTypeaheadSuggestions = () => { - const getTypeaheadSuggestions: SuggestionFetcher = useCallback( - async (params: TypeaheadParams) => { - const rpcMessage = - params.length === 2 - ? ({ - method: "getUniqueFieldValues", - params, - ...TYPEAHEAD_MESSAGE_CONSTANTS, - } as ClientToServerGetUniqueValues) - : ({ - method: "getUniqueFieldValuesStartingWith", - params, - ...TYPEAHEAD_MESSAGE_CONSTANTS, - } as ClientToServerGetUniqueValuesStartingWith); - - const suggestions = await makeRpcCall(rpcMessage); - - // TODO replacing space with underscores like this is not being correctly handled elsewhere - return suggestions; - // return suggestions.some(containSpace) - // ? suggestions.map(replaceSpace) - // : suggestions; - }, - [] - ); - - return getTypeaheadSuggestions; -}; +export const useTypeaheadSuggestions = () => + useCallback(async (params: TypeaheadParams) => { + const rpcMessage = + params.length === 2 + ? ({ + method: "getUniqueFieldValues", + params, + ...TYPEAHEAD_MESSAGE_CONSTANTS, + } as ClientToServerGetUniqueValues) + : ({ + method: "getUniqueFieldValuesStartingWith", + params, + ...TYPEAHEAD_MESSAGE_CONSTANTS, + } as ClientToServerGetUniqueValuesStartingWith); + + return makeRpcCall(rpcMessage); + }, []); diff --git a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts index 3d1afb08d..3780574a6 100644 --- a/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts +++ b/vuu-ui/packages/vuu-data-react/src/hooks/useVuuMenuActions.ts @@ -213,7 +213,7 @@ export type VuuServerMenuOptions = { columnMap: ColumnMap; columnName: string; row: DataSourceRow; - selectedRows: DataSourceRow[]; + selectedRowsCount: number; viewport: string; }; @@ -373,6 +373,7 @@ export const useVuuMenuActions = ({ `useViewServer handleMenuAction, can't handle action type ${menuId}` ); } + return false; }, [clientSideMenuActionHandler, dataSource, onRpcResponse] diff --git a/vuu-ui/packages/vuu-data-react/tsconfig.json b/vuu-ui/packages/vuu-data-react/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-data-react/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-data-test/package.json b/vuu-ui/packages/vuu-data-test/package.json new file mode 100644 index 000000000..9204184cf --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/package.json @@ -0,0 +1,15 @@ +{ + "name": "@finos/vuu-data-test", + "version": "0.0.26", + "main": "src/index.ts", + "author": "heswell", + "license": "Apache-2.0", + "scripts": { + "build": "node ../../scripts/run-build.mjs", + "type-defs": "node ../../scripts/build-type-defs.mjs" + }, + "devDependencies": { + "@finos/vuu-data": "0.0.26", + "@finos/vuu-datagrid-types": "0.0.26" + } +} diff --git a/vuu-ui/showcase/src/examples/utils/UpdateGenerator.ts b/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts similarity index 53% rename from vuu-ui/showcase/src/examples/utils/UpdateGenerator.ts rename to vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts index 07db30289..f76e03023 100644 --- a/vuu-ui/showcase/src/examples/utils/UpdateGenerator.ts +++ b/vuu-ui/packages/vuu-data-test/src/UpdateGenerator.ts @@ -1,6 +1,7 @@ -import { VuuRange, VuuRowDataItemType } from "packages/vuu-protocol-types"; +import { ArrayDataSource } from "@finos/vuu-data"; +import { VuuRange } from "@finos/vuu-protocol-types"; +import { random } from "./simul/reference-data"; import { RowUpdates, UpdateGenerator, UpdateHandler } from "./rowUpdates"; -import { random } from "./reference-data"; const getNewValue = (value: number) => { const multiplier = random(0, 100) / 1000; @@ -9,7 +10,7 @@ const getNewValue = (value: number) => { }; export class BaseUpdateGenerator implements UpdateGenerator { - private data: ReadonlyArray = []; + private dataSource: ArrayDataSource | undefined; private range: VuuRange | undefined; private updateHandler: UpdateHandler | undefined; private updating = false; @@ -27,8 +28,8 @@ export class BaseUpdateGenerator implements UpdateGenerator { } } - setData(data: ReadonlyArray) { - this.data = data; + setDataSource(dataSource: ArrayDataSource) { + this.dataSource = dataSource; } setUpdateHandler(updateHandler: UpdateHandler) { @@ -54,29 +55,31 @@ export class BaseUpdateGenerator implements UpdateGenerator { update = () => { if (this.range && this.updateHandler) { const updates: RowUpdates[] = []; - for ( - let rowIndex = this.range.from; - rowIndex < this.range.to; - rowIndex++ - ) { - const shallUpdateRow = random(0, 10) < 2; - if (shallUpdateRow) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const rowUpdates: RowUpdates = [rowIndex]; - const row = this.data[rowIndex]; - for (const colIdx of this.tickingColumns) { - const shallUpdateColumn = random(0, 10) < 5; - if (shallUpdateColumn) { - rowUpdates.push(colIdx, getNewValue(row[colIdx] as number)); + const data = this.dataSource?.currentData; + if (data && data?.length > 0) { + const maxRange = Math.min(this.range.to, data.length); + for (let rowIndex = this.range.from; rowIndex < maxRange; rowIndex++) { + const shallUpdateRow = random(0, 10) < 2; + if (shallUpdateRow) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const rowUpdates: RowUpdates = [rowIndex]; + const row = data[rowIndex]; + for (const colIdx of this.tickingColumns) { + const shallUpdateColumn = random(0, 10) < 5; + if (shallUpdateColumn) { + rowUpdates.push(colIdx, getNewValue(row[colIdx] as number)); + } + } + if (rowUpdates.length > 1) { + updates.push(rowUpdates); } } - if (rowUpdates.length > 1) { - updates.push(rowUpdates); - } + } + if (updates.length > 0) { + this.updateHandler(updates); } } - this.updateHandler(updates); } if (this.updating) { diff --git a/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts b/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts new file mode 100644 index 000000000..aa1edde74 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/basket-schemas.ts @@ -0,0 +1,125 @@ +import { TableSchema } from "@finos/vuu-data"; + +export type BasketsTableName = + | "algoType" + | "basket" + | "basketConstituent" + | "basketTrading" + | "basketTradingConstituent" + | "basketTrdConsPrices" + | "priceStrategyType"; + +export const schemas: Readonly< + Record> +> = { + algoType: { + columns: [ + { name: "algoType", serverDataType: "string" }, + { name: "id", serverDataType: "int" }, + ], + key: "id", + table: { module: "BASKET", table: "algoType" }, + }, + basket: { + columns: [ + { name: "id", serverDataType: "string" }, + { name: "name", serverDataType: "string" }, + { name: "notionalValue", serverDataType: "double" }, + { name: "notionalValueUsd", serverDataType: "double" }, + ], + key: "id", + table: { module: "BASKET", table: "basket" }, + }, + basketConstituent: { + columns: [ + { name: "basketId", serverDataType: "string" }, + { name: "change", serverDataType: "string" }, + { name: "lastTrade", serverDataType: "string" }, + { name: "ric", serverDataType: "string" }, + { name: "ricBasketId", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "volume", serverDataType: "string" }, + { name: "weighting", serverDataType: "double" }, + ], + key: "ricBasketId", + table: { module: "BASKET", table: "basketConstituent" }, + }, + basketTrading: { + columns: [ + { name: "basketId", serverDataType: "string" }, + { name: "basketName", serverDataType: "string" }, + { name: "filledPct", serverDataType: "double" }, + { name: "fxRateToUsd", serverDataType: "double" }, + { name: "instanceId", serverDataType: "string" }, + { name: "status", serverDataType: "string" }, + { name: "totalNotional", serverDataType: "double" }, + { name: "totalNotionalUsd", serverDataType: "double" }, + { name: "units", serverDataType: "int" }, + ], + key: "instanceId", + table: { module: "BASKET", table: "basketTrading" }, + }, + basketTradingConstituent: { + columns: [ + { name: "algo", serverDataType: "string" }, + { name: "algoParams", serverDataType: "string" }, + { name: "basketId", serverDataType: "string" }, + { name: "bid", serverDataType: "double" }, + { name: "description", serverDataType: "string" }, + { name: "instanceId", serverDataType: "string" }, + { name: "instanceIdRic", serverDataType: "string" }, + { name: "limitPrice", serverDataType: "double" }, + { name: "notionalLocal", serverDataType: "double" }, + { name: "notionalUsd", serverDataType: "double" }, + { name: "pctFilled", serverDataType: "double" }, + { name: "priceSpread", serverDataType: "int" }, + { name: "priceStrategyId", serverDataType: "int" }, + { name: "quantity", serverDataType: "long" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "venue", serverDataType: "string" }, + { name: "weighting", serverDataType: "double" }, + ], + key: "instanceIdRic", + table: { module: "BASKET", table: "basketTradingConstituent" }, + }, + basketTrdConsPrices: { + columns: [ + { name: "algo", serverDataType: "string" }, + { name: "algoParams", serverDataType: "string" }, + { name: "ask", serverDataType: "double" }, + { name: "askSize", serverDataType: "double" }, + { name: "basketId", serverDataType: "string" }, + { name: "bid", serverDataType: "double" }, + { name: "bidSize", serverDataType: "double" }, + { name: "close", serverDataType: "double" }, + { name: "description", serverDataType: "string" }, + { name: "instanceId", serverDataType: "string" }, + { name: "instanceIdRic", serverDataType: "string" }, + { name: "last", serverDataType: "double" }, + { name: "limitPrice", serverDataType: "double" }, + { name: "notionalLocal", serverDataType: "double" }, + { name: "notionalUsd", serverDataType: "double" }, + { name: "open", serverDataType: "double" }, + { name: "pctFilled", serverDataType: "double" }, + { name: "phase", serverDataType: "string" }, + { name: "priceSpread", serverDataType: "int" }, + { name: "priceStrategyId", serverDataType: "int" }, + { name: "quantity", serverDataType: "long" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "venue", serverDataType: "string" }, + { name: "weighting", serverDataType: "double" }, + ], + key: "instanceIdRic", + table: { module: "BASKET", table: "basketTradingConstituent" }, + }, + priceStrategyType: { + columns: [ + { name: "priceStrategy", serverDataType: "string" }, + { name: "id", serverDataType: "int" }, + ], + key: "", + table: { module: "BASKET", table: "priceStrategyType" }, + }, +}; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts new file mode 100644 index 000000000..7de4feae6 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basket-generator.ts @@ -0,0 +1,10 @@ +import { BasketColumnMap, BasketReferenceData } from "../reference-data"; +import { getGenerators } from "../../generatorTemplate"; + +const [RowGenerator, ColumnGenerator] = getGenerators( + "basket", + BasketColumnMap, + BasketReferenceData +); + +export { RowGenerator, ColumnGenerator }; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts new file mode 100644 index 000000000..9db313bcf --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketConstituent-generator.ts @@ -0,0 +1,13 @@ +import { + BasketConstituentColumnMap, + BasketConstituentReferenceData, +} from "../reference-data"; +import { getGenerators } from "../../generatorTemplate"; + +const [RowGenerator, ColumnGenerator] = getGenerators( + "basketConstituent", + BasketConstituentColumnMap, + BasketConstituentReferenceData +); + +export { RowGenerator, ColumnGenerator }; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts new file mode 100644 index 000000000..2fe2e2ed8 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTrading-generator.ts @@ -0,0 +1,13 @@ +import { + BasketTradingColumnMap, + BasketTradingReferenceData, +} from "../reference-data"; +import { getGenerators } from "../../generatorTemplate"; + +const [RowGenerator, ColumnGenerator] = getGenerators( + "basketTrading", + BasketTradingColumnMap, + BasketTradingReferenceData +); + +export { RowGenerator, ColumnGenerator }; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts new file mode 100644 index 000000000..90fd547c2 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/basketTradingConstituent-generator.ts @@ -0,0 +1,13 @@ +import { + BasketTradingConstituentColumnMap, + BasketTradingConstituentReferenceData, +} from "../reference-data"; +import { getGenerators } from "../../generatorTemplate"; + +const [RowGenerator, ColumnGenerator] = getGenerators( + "basketTradingConstituent", + BasketTradingConstituentColumnMap, + BasketTradingConstituentReferenceData +); + +export { RowGenerator, ColumnGenerator }; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts new file mode 100644 index 000000000..7c1a90d42 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/data-generators/index.ts @@ -0,0 +1,4 @@ +export * as basket from "./basket-generator"; +export * as basketConstituent from "./basketConstituent-generator"; +export * as basketTrading from "./basketTrading-generator"; +export * as basketTradingConstituent from "./basketTradingConstituent-generator"; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts new file mode 100644 index 000000000..5dbf416cc --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basket.ts @@ -0,0 +1,22 @@ +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { ColumnMap } from "@finos/vuu-utils"; +import { getSchema } from "../../schemas"; + +const schema = getSchema("basket"); + +export const BasketColumnMap = Object.values(schema.columns).reduce( + (map, col, index) => { + map[col.name] = index; + return map; + }, + {} +); + +const data: VuuDataRow[] = [ + [".NASDAQ100", ".NASDAQ100", 0, 0], + [".HSI", ".HSI", 0, 0], + [".FTSE100", ".FTSE100", 0, 0], + [".SP500", ".SP500", 0, 0], +]; + +export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts new file mode 100644 index 000000000..933435dd6 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketConstituent.ts @@ -0,0 +1,43 @@ +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { getSchema } from "../../schemas"; +import { ColumnMap } from "@finos/vuu-utils"; +import ftse from "./ftse100"; + +const schema = getSchema("basketConstituent"); + +export const BasketConstituentColumnMap = Object.values( + schema.columns +).reduce((map, col, index) => { + map[col.name] = index; + return map; +}, {}); + +const data: VuuDataRow[] = []; + +// const start = performance.now(); +// Create 100_000 Instruments + +for (const row of ftse) { + // prettier-ignore + const [ric, name, lastTrade, change, volume] = row; + + const basketId = ".FTSE100"; + const side = "BUY"; + const weighting = 1; + + data.push([ + basketId, + change, + lastTrade, + ric, + `${ric}-${basketId}`, + side, + volume, + weighting, + ]); +} + +// const end = performance.now(); +// console.log(`generating 100,000 instrumentPrices took ${end - start} ms`); + +export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts new file mode 100644 index 000000000..165531d04 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTrading.ts @@ -0,0 +1,45 @@ +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { ColumnMap } from "@finos/vuu-utils"; +import { getSchema } from "../../schemas"; + +import baskets, { BasketColumnMap } from "./basket"; +import basketConstituents from "./basketConstituent"; + +const schema = getSchema("basketTrading"); + +export const BasketTradingColumnMap = Object.values( + schema.columns +).reduce((map, col, index) => { + map[col.name] = index; + return map; +}, {}); + +let instance = 1; + +const data: VuuDataRow[] = []; + +const createBasket = (basketId: string, basketName: string) => { + const key = BasketColumnMap.basketId; + const basketRow = baskets.find((basket) => basket[key] === basketId); + const basketTradingRow = [ + basketId, + basketName, + 0, + 0, + `steve-${instance++}`, + "OFF-MARKET", + 0, + 0, + 0, + ]; + data.push(basketTradingRow); +}; + +createBasket(".FTSE", "Steve FTSE 1"); +createBasket(".FTSE", "Steve FTSE 2"); +createBasket(".FTSE", "Steve FTSE 3"); +createBasket(".FTSE", "Steve FTSE 4"); +createBasket(".FTSE", "Steve FTSE 5"); +createBasket(".FTSE", "Steve FTSE 6"); + +export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts new file mode 100644 index 000000000..01e3471ba --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/basketTradingConstituent.ts @@ -0,0 +1,16 @@ +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { ColumnMap } from "@finos/vuu-utils"; +import { getSchema } from "../../schemas"; + +const schema = getSchema("basketTradingConstituent"); + +export const BasketTradingConstituentColumnMap = Object.values( + schema.columns +).reduce((map, col, index) => { + map[col.name] = index; + return map; +}, {}); + +const data: VuuDataRow[] = []; + +export default data; diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/ftse100.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/ftse100.ts new file mode 100644 index 000000000..3adb247c5 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/ftse100.ts @@ -0,0 +1,102 @@ +// Symbol,Name,Last Trade,Change,Volume +// prettier-ignore +export default [ + ["AAL.L","Anglo American PLC","436.35 13:13","5.35 (1.24%)","5,799,089"], + ["ABF.L","Associated British Foods PLC","3,435.60 13:12","7.40 (0.21%)","86,808"], + ["ADM.L","Admiral Group PLC","1,627.00 13:13",""], + ["ADN.L","Aberdeen Asset Management PLC","334.00 13:13","2.50 (0.75%)","806,880"], + ["AHT.L","Ashtead Group PLC","1,027.00 13:13","6.00 (0.59%)","331,255"], + ["ANTO.L","Antofagasta PLC","484.10 13:13","11.70 (2.48%)","1,753,976"], + ["ARM.L","ARM Holdings PLC","1,058.00 13:13","3.00 (0.28%)","475,927"], + ["AV.L","Aviva PLC","493.97 13:13","2.23 (0.45%)","2,226,835"], + ["AZN.L","AstraZeneca PLC","4,399.50 13:13","2.50 (0.06%)","815,133"], + ["BA.L","BAE Systems PLC","478.10 13:13","4.30 (0.91%)","2,039,934"], + ["BAB.L","Babcock International Group PLC","988.00 13:13","9.50 (0.97%)","209,614"], + ["BARC.L","Barclays PLC","226.30 13:13", "1.15 (0.51%)","6,575,664"], + ["BATS.L","British American Tobacco PLC","3,803.50 13:13", "8.50 (0.22%)","465,110"], + ["BDEV.L","Barratt Developments PLC","576.00 13:13", "0.50 (0.09%)","1,044,365"], + ["BG.L","BG Group PLC","1,013.50 13:13", "5.50 (0.55%)","1,507,332"], + ["BKG.L","Berkeley Group Holdings (The) PLC","3,126.00 13:13", "15.00 (0.48%)","95,071"], + ["BLND.L","British Land Co PLC","828.06 13:12", "10.44 (1.25%)","1,802,548"], + ["BLT.L","BHP Billiton PLC","881.40 13:13", "4.30 (0.49%)","4,947,287"], + ["BNZL.L","Bunzl PLC","1,875.40 13:05", "4.60 (0.24%)","104,541"], + ["BP.L","BP PLC","381.50 13:13", "2.95 (0.78%)","10,493,561"], + ["BRBY.L","Burberry Group PLC","1,269.00 13:13", "7.00 (0.55%)","295,647"], + ["BT-A","L,BT Group PLC","489.20 13:13", "3.70 (0.75%)","3,914,982"], + ["CCL.L","Carnival PLC","3,426.00 13:12", "22.00 (0.64%)","86,257"], + ["CNA.L","Centrica PLC","212.80 13:13", "0.60 (0.28%)","2,144,540"], + ["CPG.L","Compass Group PLC","1,054.00 13:08", "5.00 (0.48%)","1,001,167"], + ["CPI.L","Capita PLC","1,235.00 13:11", "1.00 (0.08%)","244,591"], + ["CRH.L","CRH PLC","1,783.20 13:12", "17.80 (0.99%)","897,325"], + ["DC.L","DIXONS CARPHONE","462.10 13:11",""], + ["DGE.L","Diageo PLC","1,881.50 13:13", "6.50 (0.34%)","756,906"], + ["DLG.L","Direct Line Insurance Group PLC","403.80 13:13", "0.40 (0.10%)","1,095,340"], + ["EXPN.L","Experian PLC","1,191.00 13:12", "2.00 (0.17%)","467,283"], + ["EZJ.L","easyJet PLC","1,682.00 13:12", "28.00 (1.64%)","1,191,230"], + ["FRES.L","Fresnillo PLC","678.50 13:12", "6.50 (0.97%)","381,871"], + ["GFS.L","G4S PLC","232.30 13:03", "2.00 (0.85%)","1,096,551"], + ["GKN.L","GKN PLC","294.80 13:12", "2.50 (0.86%)","792,247"], + ["GLEN.L","Glencore PLC","90.48 13:13", "1.65 (1.86%)","41,631,528"], + ["GSK.L","GlaxoSmithKline PLC","1,345.00 13:13", "0.50 (0.04%)","1,767,356"], + ["HIK.L","Hikma Pharmaceuticals PLC","2,010.00 13:04", "57.00 (2.92%)","261,511"], + ["HL.L","Hargreaves Lansdown PLC","1,488.03 13:12", "9.97 (0.67%)","372,261"], + ["HMSO.L","Hammerson PLC","597.50 13:11", "3.50 (0.58%)","478,301"], + ["HSBA.L","HSBC Holdings PLC","519.70 13:13", "0.50 (0.10%)","7,415,629"], + ["IAG.L","International Consolidated Airlines Group SA","575.40 13:12", "16.10 (2.72%)","4,311,514"], + ["IHG.L","InterContinental Hotels Group PLC","2,481.00 13:12", "19.00 (0.76%)","219,918"], + ["III.L","3i Group PLC","487.30 13:11", "4.50 (0.92%)","189,987"], + ["IMT.L","Imperial Tobacco Group PLC","3,571.00 13:13", "29.00 (0.81%)","926,816"], + ["INTU.L","intu properties PLC","319.90 13:09", "4.60 (1.42%)","514,821"], + ["ISAT.L","Inmarsat PLC","1,054.44 13:13", "3.44 (0.33%)","988,089"], + ["ITRK.L","Intertek Group PLC","2,643.00 13:14", "3.00 (0.11%)","45,868"], + ["ITV.L","ITV PLC","267.30 13:14", "2.60 (0.96%)","3,453,208"], + ["JMAT.L","Johnson Matthey PLC","2,445.00 13:14", "29.00 (1.20%)","276,397"], + ["KGF.L","Kingfisher PLC","346.20 13:14", "4.30 (1.23%)","1,021,408"], + ["LAND.L","Land Securities Group PLC","1,239.00 13:13", "7.00 (0.56%)","384,973"], + ["LGEN.L","Legal & General Group PLC","266.00 13:14", "1.60 (0.60%)","1,998,399"], + ["LLOY.L","Lloyds Banking Group PLC","73.86 13:14", "0.02 (0.03%)","18,907,878"], + ["LSE.L","London Stock Exchange Group PLC","2,544.00 13:11", "6.00 (0.24%)","129,657"], + ["MGGT.L","Meggitt PLC","386.00 13:15", "3.20 (0.84%)","611,044"], + ["MKS.L","Marks & Spencer Group PLC","514.75 13:12", "3.25 (0.63%)","920,128"], + ["MNDI.L","Mondi PLC","1,463.00 13:14", "7.00 (0.48%)","383,546"], + ["MRW.L","Morrison (Wm) Supermarkets PLC","155.20 13:14",""], + ["NG.L","National Grid PLC","926.40 13:14", "1.10 (0.12%)","1,659,592"], + ["NXT.L","Next PLC","7,765.00 13:11", "95.00 (1.21%)","114,062"], + ["OML.L","Old Mutual PLC","198.50 13:14", "0.40 (0.20%)","2,040,849"], + ["PRU.L","Prudential PLC","1,499.50 13:15", "14.00 (0.93%)","580,870"], + ["PSON.L","Pearson PLC","794.00 13:09", "5.00 (0.63%)","1,177,953"], + ["RB.L","Reckitt Benckiser Group PLC","6,293.00 13:14", "34.00 (0.54%)","281,172"], + ["RBS.L","Royal Bank of Scotland Group PLC","313.40 13:14", "2.40 (0.77%)","2,100,058"], + ["RDSA.L","Royal Dutch Shell PLC","1,636.00 13:14", "18.00 (1.11%)","2,467,461"], + ["RDSB.L","Royal Dutch Shell PLC","1,652.00 13:15", "14.50 (0.89%)","1,457,434"], + ["REL.L","Reed Elsevier PLC","1,170.00 13:14","0.00 (0.00%)","908,802"], + ["RIO.L","Rio Tinto PLC","2,235.00 13:15", "21.00 (0.95%)","2,190,722"], + ["RMG.L","Royal Mail PLC","453.50 13:14", "1.20 (0.26%)","995,316"], + ["RR.L","Rolls-Royce Group PLC","546.63 13:14", "8.38 (1.51%)","2,792,915"], + ["RRS.L","Randgold Resources Ltd","3,929.00 13:14","0.00 (0.00%)","135,524"], + ["RSA.L","RSA Insurance Group PLC","437.10 13:14", "0.10 (0.02%)","395,477"], + ["SAB.L","SABMiller PLC","4,011.00 13:15", "1.00 (0.02%)","892,451"], + ["SBRY.L","Sainsbury (J) PLC","255.80 13:14", "7.40 (2.98%)","2,395,670"], + ["SDR.L","Schroders PLC","2,930.00 13:09", "12.00 (0.41%)","44,674"], + ["SGE.L","Sage Group (The) PLC","545.50 13:13", "0.50 (0.09%)","539,717"], + ["SHP.L","Shire PLC","4,685.00 13:14", "22.00 (0.47%)","221,318"], + ["SKY.L","SKY","1,095.00 13:12", "4.00 (0.37%)","925,016"], + ["SL.L","Standard Life PLC","399.90 13:14", "3.20 (0.79%)","861,636"], + ["SMIN.L","Smiths Group PLC","992.50 13:14", "27.50 (2.70%)","640,309"], + ["SN.L","Smith & Nephew PLC","1,110.00 13:14", "9.00 (0.82%)","480,018"], + ["SPD.L","Sports Direct International PLC","694.50 13:11", "1.50 (0.22%)","157,981"], + ["SSE.L","SSE PLC","1,463.00 13:13", "2.00 (0.14%)","562,454"], + ["STAN.L","Standard Chartered PLC","583.00 13:14", "0.60 (0.10%)","2,018,697"], + ["STJ.L","St James's Place PLC","964.00 13:14", "11.00 (1.13%)","418,480"], + ["SVT.L","Severn Trent PLC","2,199.00 13:12", "1.00 (0.05%)","95,342"], + ["TPK.L","Travis Perkins PLC","1,945.00 13:13", "4.00 (0.21%)","92,916"], + ["TSCO.L","Tesco PLC","171.54 13:14", "2.54 (1.50%)","9,831,136"], + ["TUI.L","TUI AG","1,115.00 13:10", "5.00 (0.45%)","458,970"], + ["TW.L","Taylor Wimpey PLC","183.90 13:15", "1.10 (0.59%)","3,180,729"], + ["ULVR.L","Unilever PLC","2,791.00 13:14", "29.00 (1.03%)","824,827"], + ["UU.L","United Utilities Group PLC","959.00 13:10", "2.50 (0.26%)","436,911"], + ["VOD.L","Vodafone Group PLC","224.25 13:15", "1.30 (0.58%)","17,572,036"], + ["WOS.L","Wolseley PLC","3,657.00 13:14", "4.00 (0.11%)","179,536"], + ["WPP.L","WPP PLC","1,502.00 13:15", "12.00 (0.79%)","857,887"], + ["WTB.L","Whitbread PLC","4,484.00 13:16", "60.00 (1.32%)","141,036"] +] diff --git a/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts new file mode 100644 index 000000000..4ece82ad2 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/basket/reference-data/index.ts @@ -0,0 +1,13 @@ +export { default as BasketReferenceData, BasketColumnMap } from "./basket"; +export { + default as BasketConstituentReferenceData, + BasketConstituentColumnMap, +} from "./basketConstituent"; +export { + default as BasketTradingReferenceData, + BasketTradingColumnMap, +} from "./basketTrading"; +export { + default as BasketTradingConstituentReferenceData, + BasketTradingConstituentColumnMap, +} from "./basketTradingConstituent"; diff --git a/vuu-ui/packages/vuu-data-test/src/generatorTemplate.ts b/vuu-ui/packages/vuu-data-test/src/generatorTemplate.ts new file mode 100644 index 000000000..01c76e1a7 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/generatorTemplate.ts @@ -0,0 +1,44 @@ +import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { ColumnMap } from "@finos/vuu-utils"; +import { VuuDataRow } from "@finos/vuu-protocol-types"; +import { ColumnGeneratorFn, RowGeneratorFactory } from "./vuu-row-generator"; +import { getSchema, VuuTableName } from "./schemas"; + +export const getGenerators = ( + tableName: VuuTableName, + columnMap: ColumnMap, + data: VuuDataRow[] +): [RowGeneratorFactory, ColumnGeneratorFn] => [ + (columnNames?: string[]) => (index: number) => { + if (index >= data.length) { + return undefined; + } + if (columnNames) { + return columnNames.map((name) => data[index][columnMap[name]]); + } else { + return data[index].slice(0, 7); + } + }, + + ( + columns = [] + //columnConfig: ExtendedColumnConfig = {} + ) => { + const schema = getSchema(tableName); + const result: ColumnDescriptor[] = schema.columns; + if (typeof columns === "number") { + throw Error(`${tableName}Generator must be passed columns (strings)`); + } else if (columns.length === 0) { + return result; + } else { + return columns.map((name) => { + const column = result.find((col) => col.name === name); + if (column) { + return column; + } else { + throw Error(`${tableName}Generator no column ${name}`); + } + }); + } + }, +]; diff --git a/vuu-ui/packages/vuu-data-test/src/index.ts b/vuu-ui/packages/vuu-data-test/src/index.ts new file mode 100644 index 000000000..da4cd3769 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/index.ts @@ -0,0 +1,2 @@ +export * from "./schemas"; +export * from "./vuu-row-generator"; diff --git a/vuu-ui/showcase/src/examples/utils/rowUpdates.ts b/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts similarity index 90% rename from vuu-ui/showcase/src/examples/utils/rowUpdates.ts rename to vuu-ui/packages/vuu-data-test/src/rowUpdates.ts index e419b865a..d430b0e86 100644 --- a/vuu-ui/showcase/src/examples/utils/rowUpdates.ts +++ b/vuu-ui/packages/vuu-data-test/src/rowUpdates.ts @@ -1,7 +1,8 @@ import { VuuRange, VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { ArrayDataSource } from "@finos/vuu-data"; export interface UpdateGenerator { - setData: (data: ReadonlyArray) => void; + setDataSource: (dataSource: ArrayDataSource) => void; setRange: (range: VuuRange) => void; setUpdateHandler: (updateHandler: UpdateHandler) => void; } diff --git a/vuu-ui/packages/vuu-data-test/src/schemas.ts b/vuu-ui/packages/vuu-data-test/src/schemas.ts new file mode 100644 index 000000000..79adf5250 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/schemas.ts @@ -0,0 +1,29 @@ +import { TableSchema } from "@finos/vuu-data"; +import { + type BasketsTableName, + schemas as basketSchemas, +} from "./basket/basket-schemas"; +import { + type SimulTableName, + schemas as simulSchemas, +} from "./simul/simul-schemas"; + +export type VuuTableName = BasketsTableName | SimulTableName; +export const schemas: Record = { + ...basketSchemas, + ...simulSchemas, +}; + +const allSchemas: Readonly>> = { + ...basketSchemas, + ...simulSchemas, +}; + +export const getAllSchemas = () => schemas; + +export const getSchema = (tableName: VuuTableName) => { + if (allSchemas[tableName]) { + return allSchemas[tableName]; + } + throw Error(`getSchema no schema for table ${tableName}`); +}; diff --git a/vuu-ui/showcase/src/examples/utils/child-order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts similarity index 78% rename from vuu-ui/showcase/src/examples/utils/child-order-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts index 771e375d8..d1af767b5 100644 --- a/vuu-ui/showcase/src/examples/utils/child-order-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/child-order-generator.ts @@ -1,7 +1,9 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { ExtendedColumnConfig } from "./useTableConfig"; -import { ColumnGenerator, RowGenerator } from "./vuu-row-generator"; -import { schemas } from "./useSchemas"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; +import { getSchema } from "../../index"; import { currencies, locations, suffixes } from "./generatedData"; function random(min: number, max: number) { @@ -29,7 +31,7 @@ const algos = ["Algo 1", "Algo 2", "Algo 3", "Algo 4", "Algo 5"]; const maxIndex = 20 * 20 * 20 * 20 * 8; -export const ChildOrderRowGenerator: RowGenerator = () => (index: number) => { +export const RowGenerator: RowGeneratorFactory = () => (index: number) => { if (index > maxIndex) { throw Error("generateRow index val is too high"); } @@ -75,12 +77,9 @@ export const ChildOrderRowGenerator: RowGenerator = () => (index: number) => { ]; }; -export const ChildOrderColumnGenerator: ColumnGenerator = ( - columns = [], - columnConfig: ExtendedColumnConfig = {} -) => { - console.log({ columnConfig }); - const schemaColumns: ColumnDescriptor[] = schemas.childOrders.columns; +export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { + const schema = getSchema("childOrders"); + const schemaColumns: ColumnDescriptor[] = schema.columns; if (typeof columns === "number") { throw Error("ChildOrderColumnGenerator must be passed columns (strings)"); } else if (columns.length === 0) { diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts new file mode 100644 index 000000000..0cf067d08 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generate-data-utils.ts @@ -0,0 +1,60 @@ +import { faker } from "@faker-js/faker"; +import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; + +export function createArray(numofrows: number): VuuRowDataItemType[][] { + const result = []; + + for (let i = 0; i < numofrows; i++) { + const FakerDataGenerator = [ + faker.company.name(), + faker.finance.currencyCode(), + Number(faker.finance.amount({ min: 5, max: 10, dec: 2 })), + faker.finance.amount({ min: 100, max: 2000, dec: 0 }), + faker.finance.transactionType(), + faker.finance.transactionDescription(), + faker.date.anytime().getMilliseconds(), + faker.finance.accountName(), + faker.finance.accountNumber(), + faker.commerce.department(), + faker.commerce.product(), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + faker.finance.amount({ min: 5, max: 10, dec: 2 }), + ]; + result.push([ + i + 1, + FakerDataGenerator[0], + FakerDataGenerator[1], + Number(FakerDataGenerator[2]), + FakerDataGenerator[3] as number, + Number( + Math.floor( + Number(FakerDataGenerator[2]) * Number(FakerDataGenerator[3]) + ) + ), + FakerDataGenerator[4], + FakerDataGenerator[5], + FakerDataGenerator[6], + FakerDataGenerator[7], + FakerDataGenerator[8], + FakerDataGenerator[9], + FakerDataGenerator[10], + FakerDataGenerator[11], + FakerDataGenerator[12], + FakerDataGenerator[13], + FakerDataGenerator[14], + FakerDataGenerator[15], + FakerDataGenerator[16], + FakerDataGenerator[17], + Number(FakerDataGenerator[18]), + ]); + } + + return result; +} diff --git a/vuu-ui/showcase/src/examples/utils/generatedData.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/generatedData.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/generatedData.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/generatedData.ts diff --git a/vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts new file mode 100644 index 000000000..b378012d4 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/index.ts @@ -0,0 +1,6 @@ +export * as childOrders from "./child-order-generator"; +export * as instruments from "./instrument-generator"; +export * as instrumentPrices from "./instrument-prices-generator"; +export * as orders from "./order-generator"; +export * as parentOrders from "./parent-order-generator"; +export * as prices from "./prices-generator"; diff --git a/vuu-ui/showcase/src/examples/utils/instrument-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts similarity index 74% rename from vuu-ui/showcase/src/examples/utils/instrument-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts index d73933d4c..609c7eda5 100644 --- a/vuu-ui/showcase/src/examples/utils/instrument-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-generator.ts @@ -1,12 +1,18 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { ColumnGenerator, RowGenerator } from "./vuu-row-generator"; -import { schemas } from "./useSchemas"; -import { InstrumentReferenceData, InstrumentColumnMap } from "./reference-data"; -import "./reference-data"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; +import { getSchema } from "../../index"; +import { + InstrumentReferenceData, + InstrumentColumnMap, +} from "../reference-data"; import { getCalculatedColumnType, isCalculatedColumn } from "@finos/vuu-utils"; -import { ExtendedColumnConfig } from "./useTableConfig"; -export const InstrumentRowGenerator: RowGenerator = +export type ExtendedColumnConfig = { [key: string]: Partial }; + +export const RowGenerator: RowGeneratorFactory = (columnNames?: string[]) => (index: number) => { if (index >= InstrumentReferenceData.length) { throw Error("generateRow index val is too high"); @@ -22,11 +28,12 @@ export const InstrumentRowGenerator: RowGenerator = } }; -export const InstrumentColumnGenerator: ColumnGenerator = ( +export const ColumnGenerator: ColumnGeneratorFn = ( columns = [], columnConfig: ExtendedColumnConfig = {} ) => { - const instrumentColumns: ColumnDescriptor[] = schemas.instruments.columns; + const schema = getSchema("instruments"); + const instrumentColumns: ColumnDescriptor[] = schema.columns; if (typeof columns === "number") { throw Error("InstrumentColumnGenerator must be passed columns (strings)"); } else if (columns.length === 0) { diff --git a/vuu-ui/showcase/src/examples/utils/instrument-prices-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts similarity index 70% rename from vuu-ui/showcase/src/examples/utils/instrument-prices-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts index f97b151a2..3059b73cc 100644 --- a/vuu-ui/showcase/src/examples/utils/instrument-prices-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/instrument-prices-generator.ts @@ -3,23 +3,26 @@ import { buildColumnMap } from "@finos/vuu-utils/src"; import { InstrumentPricesColumnMap, InstrumentPricesReferenceData, -} from "./reference-data"; -import { BaseUpdateGenerator } from "./UpdateGenerator"; -import { schemas } from "./useSchemas"; -import { ColumnGenerator, RowGenerator } from "./vuu-row-generator"; +} from "../reference-data"; +import { BaseUpdateGenerator } from "../../UpdateGenerator"; +import { getSchema } from "../../index"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; -const { instrumentPrices: instrumentPriceSchema } = schemas; +const instrumentPriceSchema = getSchema("instrumentPrices"); -export const InstrumentPricesRowGenerator: RowGenerator = +export const RowGenerator: RowGeneratorFactory = (columnNames?: string[]) => (index: number) => { if (index >= InstrumentPricesReferenceData.length) { throw Error("generateRow index val is too high"); } if (columnNames) { return columnNames.map( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore (name) => + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore InstrumentPricesReferenceData[index][InstrumentPricesColumnMap[name]] ); } else { @@ -31,15 +34,15 @@ const { bid, bidSize, ask, askSize } = buildColumnMap( instrumentPriceSchema.columns ); -export const createInstrumentPriceUpdateGenerator = () => +export const createUpdateGenerator = () => new BaseUpdateGenerator([bid, bidSize, ask, askSize]); -export const InstrumentPricesColumnGenerator: ColumnGenerator = ( +export const ColumnGenerator: ColumnGeneratorFn = ( columns = [] //columnConfig: ExtendedColumnConfig = {} ) => { const instrumentPriceColumns: ColumnDescriptor[] = - schemas.instrumentPrices.columns; + instrumentPriceSchema.columns; if (typeof columns === "number") { throw Error( "InstrumentPricesColumnGenerator must be passed columns (strings)" diff --git a/vuu-ui/showcase/src/examples/utils/order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts similarity index 83% rename from vuu-ui/showcase/src/examples/utils/order-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts index e407a18b3..df7389829 100644 --- a/vuu-ui/showcase/src/examples/utils/order-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/order-generator.ts @@ -1,7 +1,11 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { ExtendedColumnConfig } from "./useTableConfig"; -import { ColumnGenerator, RowGenerator } from "./vuu-row-generator"; -import { schemas } from "./useSchemas"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; +import { getSchema } from "../../index"; + +export type ExtendedColumnConfig = { [key: string]: Partial }; function random(min: number, max: number) { min = Math.ceil(min); @@ -29,7 +33,7 @@ const traders = ["Arkwright", "Enfield", "Bailey", "Cui", "Kohl"]; const maxIndex = 20 * 20 * 20 * 20 * 8; -export const OrderRowGenerator: RowGenerator = () => (index: number) => { +export const RowGenerator: RowGeneratorFactory = () => (index: number) => { if (index > maxIndex) { throw Error("generateRow index val is too high"); } @@ -70,12 +74,13 @@ export const OrderRowGenerator: RowGenerator = () => (index: number) => { ]; }; -export const OrderColumnGenerator: ColumnGenerator = ( +export const ColumnGenerator: ColumnGeneratorFn = ( columns = [], columnConfig: ExtendedColumnConfig = {} ) => { console.log({ columnConfig }); - const instrumentColumns: ColumnDescriptor[] = schemas.orders.columns; + const schema = getSchema("orders"); + const instrumentColumns: ColumnDescriptor[] = schema.columns; if (typeof columns === "number") { throw Error("OrderColumnGenerator must be passed columns (strings)"); } else if (columns.length === 0) { diff --git a/vuu-ui/showcase/src/examples/utils/parent-order-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts similarity index 77% rename from vuu-ui/showcase/src/examples/utils/parent-order-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts index 39f08829d..78c773c1c 100644 --- a/vuu-ui/showcase/src/examples/utils/parent-order-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/parent-order-generator.ts @@ -1,7 +1,9 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; -import { ExtendedColumnConfig } from "./useTableConfig"; -import { ColumnGenerator, RowGenerator } from "./vuu-row-generator"; -import { schemas } from "./useSchemas"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; +import { getSchema } from "../../index"; import { currencies, locations, suffixes } from "./generatedData"; function random(min: number, max: number) { @@ -21,7 +23,7 @@ const algos = ["Algo 1", "Algo 2", "Algo 3", "Algo 4", "Algo 5"]; const maxIndex = 20 * 20 * 20 * 20 * 8; -export const ParentOrderRowGenerator: RowGenerator = () => (index: number) => { +export const RowGenerator: RowGeneratorFactory = () => (index: number) => { if (index > maxIndex) { throw Error("generateRow index val is too high"); } @@ -67,12 +69,9 @@ export const ParentOrderRowGenerator: RowGenerator = () => (index: number) => { ]; }; -export const ParentOrderColumnGenerator: ColumnGenerator = ( - columns = [], - columnConfig: ExtendedColumnConfig = {} -) => { - console.log({ columnConfig }); - const schemaColumns: ColumnDescriptor[] = schemas.parentOrders.columns; +export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { + const schema = getSchema("parentOrders"); + const schemaColumns: ColumnDescriptor[] = schema.columns; if (typeof columns === "number") { throw Error("ParentOrderColumnGenerator must be passed columns (strings)"); } else if (columns.length === 0) { diff --git a/vuu-ui/showcase/src/examples/utils/prices-generator.ts b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts similarity index 59% rename from vuu-ui/showcase/src/examples/utils/prices-generator.ts rename to vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts index de65fd92b..223cbf01e 100644 --- a/vuu-ui/showcase/src/examples/utils/prices-generator.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/data-generators/prices-generator.ts @@ -1,12 +1,14 @@ import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; import { buildColumnMap } from "@finos/vuu-utils"; -import { PriceReferenceData } from "./reference-data"; -import { schemas } from "./useSchemas"; -import { ExtendedColumnConfig } from "./useTableConfig"; -import { ColumnGenerator, RowGenerator } from "./vuu-row-generator"; -import { BaseUpdateGenerator } from "./UpdateGenerator"; +import { PriceReferenceData } from "../reference-data"; +import { + ColumnGeneratorFn, + RowGeneratorFactory, +} from "../../vuu-row-generator"; +import { BaseUpdateGenerator } from "../../UpdateGenerator"; +import { getAllSchemas } from "../../index"; -export const PricesRowGenerator: RowGenerator = () => (index: number) => { +export const RowGenerator: RowGeneratorFactory = () => (index: number) => { if (index >= PriceReferenceData.length) { throw Error("generateRow index val is too high"); } @@ -14,19 +16,14 @@ export const PricesRowGenerator: RowGenerator = () => (index: number) => { return PriceReferenceData[index]; }; +const schemas = getAllSchemas(); const { prices: pricesSchema } = schemas; - const { bid, bidSize, ask, askSize } = buildColumnMap(pricesSchema.columns); const tickingColumns = [bid, bidSize, ask, askSize]; - -export const createPriceUpdateGenerator = () => +export const createUpdateGenerator = () => new BaseUpdateGenerator(tickingColumns); -export const PricesColumnGenerator: ColumnGenerator = ( - columns = [], - columnConfig: ExtendedColumnConfig = {} -) => { - console.log({ columnConfig }); +export const ColumnGenerator: ColumnGeneratorFn = (columns = []) => { const schemaColumns: ColumnDescriptor[] = pricesSchema.columns; if (typeof columns === "number") { throw Error("PricesColumnGenerator must be passed columns (strings)"); diff --git a/vuu-ui/packages/vuu-data-test/src/simul/index.ts b/vuu-ui/packages/vuu-data-test/src/simul/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/currencies.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/currencies.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/currencies.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/currencies.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/index.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/index.ts similarity index 77% rename from vuu-ui/showcase/src/examples/utils/reference-data/index.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/index.ts index 55e0cfded..35f607a34 100644 --- a/vuu-ui/showcase/src/examples/utils/reference-data/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/index.ts @@ -7,10 +7,6 @@ export { default as InstrumentPricesReferenceData, InstrumentPricesColumnMap, } from "./instrument-prices"; -export { - default as BasketDesignReferenceData, - BasketDesignColumnMap, -} from "./basket-design"; export { default as PriceReferenceData } from "./prices"; export * from "./locations"; export * from "./utils"; diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/instrument-prices.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/instrument-prices.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/instrument-prices.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/instrument-prices.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/instruments.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/instruments.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/instruments.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/instruments.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/locations.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/locations.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/locations.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/locations.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/lotsizes.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/lotsizes.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/lotsizes.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/lotsizes.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/priceStrategies.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/priceStrategies.ts similarity index 71% rename from vuu-ui/showcase/src/examples/utils/reference-data/priceStrategies.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/priceStrategies.ts index ad60c9093..94721d109 100644 --- a/vuu-ui/showcase/src/examples/utils/reference-data/priceStrategies.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/priceStrategies.ts @@ -1,6 +1,6 @@ export const priceStrategies = [ - "Strategy 1", - "Strategy 2", + "Peg to near touch", + "Limit", "Strategy 3", "Strategy 4", "Strategy 5", diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/prices.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/prices.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/reference-data/prices.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/prices.ts diff --git a/vuu-ui/showcase/src/examples/utils/reference-data/utils.ts b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/utils.ts similarity index 53% rename from vuu-ui/showcase/src/examples/utils/reference-data/utils.ts rename to vuu-ui/packages/vuu-data-test/src/simul/reference-data/utils.ts index 4ad30a00b..dd9a3b663 100644 --- a/vuu-ui/showcase/src/examples/utils/reference-data/utils.ts +++ b/vuu-ui/packages/vuu-data-test/src/simul/reference-data/utils.ts @@ -3,3 +3,9 @@ export function random(min: number, max: number) { max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } + +export function randomPercentage(value: number) { + const dec = random(2, 99); + const percentage = dec / 100; + return value * percentage; +} diff --git a/vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts b/vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts new file mode 100644 index 000000000..b15bab971 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/simul/simul-schemas.ts @@ -0,0 +1,137 @@ +import type { TableSchema } from "@finos/vuu-data"; +import type { ColumnDescriptor } from "@finos/vuu-datagrid-types"; + +export type SimulTableName = + | "instruments" + | "instrumentPrices" + | "orders" + | "childOrders" + | "parentOrders" + | "prices"; + +// These Schemas take the form of the schemas that we create +// with TABLE_META returned by Vuu. +export const schemas: Readonly>> = + { + instruments: { + columns: [ + { name: "bbg", serverDataType: "string" }, + { name: "currency", serverDataType: "string" }, + { name: "description", serverDataType: "string" }, + { name: "exchange", serverDataType: "string" }, + { name: "isin", serverDataType: "string" }, + { name: "lotSize", serverDataType: "int" }, + { name: "ric", serverDataType: "string" }, + ], + key: "ric", + table: { module: "SIMUL", table: "instruments" }, + }, + instrumentPrices: { + columns: [ + { name: "ask", serverDataType: "double" }, + { name: "askSize", serverDataType: "double" }, // type: "int" + { name: "bbg", serverDataType: "string" }, + { name: "bid", serverDataType: "double" }, + { name: "bidSize", serverDataType: "double" }, + { name: "close", serverDataType: "double" }, + { name: "currency", serverDataType: "string" }, + { name: "description", serverDataType: "string" }, + { name: "exchange", serverDataType: "string" }, + { name: "isin", serverDataType: "string" }, + { name: "last", serverDataType: "double" }, + { name: "lotSize", serverDataType: "int" }, + { name: "open", serverDataType: "double" }, + { name: "phase", serverDataType: "string" }, + { name: "ric", serverDataType: "string" }, + { name: "scenario", serverDataType: "string" }, + ], + key: "ric", + table: { module: "SIMUL", table: "instrumentPrices" }, + }, + orders: { + columns: [ + { name: "ccy", serverDataType: "string" }, + { name: "created", serverDataType: "long" }, + { name: "filledQuantity", serverDataType: "double" }, + { name: "lastUpdate", serverDataType: "long" }, + { name: "orderId", serverDataType: "string" }, + { name: "quantity", serverDataType: "double" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "trader", serverDataType: "string" }, + ], + key: "orderId", + table: { module: "SIMUL", table: "orders" }, + }, + childOrders: { + columns: [ + { name: "account", serverDataType: "string" }, + { name: "averagePrice", serverDataType: "double" }, + { name: "ccy", serverDataType: "string" }, + { name: "exchange", serverDataType: "string" }, + { name: "filledQty", serverDataType: "double" }, + { name: "id", serverDataType: "string" }, + { name: "idAsInt", serverDataType: "int" }, + { name: "lastUpdate", serverDataType: "long" }, + { name: "openQty", serverDataType: "double" }, + { name: "parentOrderId", serverDataType: "string" }, + { name: "price", serverDataType: "double" }, + { name: "quantity", serverDataType: "double" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "status", serverDataType: "string" }, + { name: "strategy", serverDataType: "string" }, + { name: "volLimit", serverDataType: "int" }, + ], + key: "id", + table: { module: "SIMUL", table: "childOrders" }, + }, + parentOrders: { + columns: [ + { name: "account", serverDataType: "string" }, + { name: "algo", serverDataType: "string" }, + { name: "averagePrice", serverDataType: "double" }, + { name: "ccy", serverDataType: "string" }, + { name: "childCount", serverDataType: "int" }, + { name: "exchange", serverDataType: "string" }, + { name: "filledQty", serverDataType: "double" }, + { name: "id", serverDataType: "string" }, + { name: "idAsInt", serverDataType: "int" }, + { name: "lastUpdate", serverDataType: "long" }, + { name: "openQty", serverDataType: "double" }, + { name: "price", serverDataType: "double" }, + { name: "quantity", serverDataType: "double" }, + { name: "ric", serverDataType: "string" }, + { name: "side", serverDataType: "string" }, + { name: "status", serverDataType: "string" }, + { name: "volLimit", serverDataType: "int" }, + ], + key: "id", + table: { module: "SIMUL", table: "parentOrders" }, + }, + prices: { + columns: [ + { name: "ask", serverDataType: "double" }, + { name: "askSize", serverDataType: "double" }, // type: "int" + { name: "bid", serverDataType: "double" }, + { name: "bidSize", serverDataType: "double" }, + { name: "close", serverDataType: "double" }, + { name: "last", serverDataType: "double" }, + { name: "open", serverDataType: "double" }, + { name: "phase", serverDataType: "string" }, + { name: "ric", serverDataType: "string" }, + { name: "scenario", serverDataType: "string" }, + ], + key: "ric", + table: { module: "SIMUL", table: "prices" }, + }, + }; + +export type ColumnState = { [key: string]: TableSchema }; + +export interface ColumnActionUpdate { + type: "updateColumn"; + column: ColumnDescriptor; +} + +export type ColumnAction = ColumnActionUpdate; diff --git a/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts b/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts new file mode 100644 index 000000000..3468f64c1 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/src/vuu-row-generator.ts @@ -0,0 +1,110 @@ +import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { VuuRowDataItemType, VuuTable } from "@finos/vuu-protocol-types"; +import * as simulDataGenerators from "./simul/data-generators"; +import * as basketDataGenerators from "./basket/data-generators"; +import { UpdateGenerator } from "./rowUpdates"; + +type RowAtIndexFunc = (index: number) => T | undefined; + +export const VuuColumnGenerator = (columnCount: number): string[] => + ["Row No"].concat( + Array(columnCount) + .fill("") + .map((_, i) => `Column ${i + 1}`) + ); + +export type RowGeneratorFactory = ( + columns: string[] +) => RowAtIndexFunc; + +export type ColumnGeneratorFn = ( + columns?: number | string[], + columnConfig?: { [key: string]: Partial } +) => ColumnDescriptor[]; + +export const DefaultRowGenerator: RowGeneratorFactory = + (columns: string[]) => (index) => { + return [`row ${index + 1}`].concat( + Array(columns.length) + .fill(true) + .map((v, j) => `value ${j + 1} @ ${index + 1}`) + ); + }; + +export const DefaultColumnGenerator: ColumnGeneratorFn = ( + columns, + columnConfig = {} +) => { + if (typeof columns === "number") { + return [{ name: "row number", width: 150 }].concat( + Array(columns) + .fill(true) + .map((_, i) => { + const name = `column ${i + 1}`; + return { name, width: 100, ...columnConfig[name] }; + }) + ); + } else { + throw Error("DefaultColumnGenerator must be passed columns (number)"); + } +}; + +const defaultGenerators = { + ColumnGenerator: DefaultColumnGenerator, + RowGenerator: DefaultRowGenerator, +}; + +export const getColumnAndRowGenerator = ( + table?: VuuTable +): + | [ColumnGeneratorFn, RowGeneratorFactory] + | [ColumnGeneratorFn, RowGeneratorFactory, () => UpdateGenerator] => { + const tableName = table?.table ?? ""; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + switch (table?.module) { + case "SIMUL": { + const { ColumnGenerator, RowGenerator, createUpdateGenerator } = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + simulDataGenerators[tableName] ?? defaultGenerators; + return [ColumnGenerator, RowGenerator, createUpdateGenerator]; + } + + case "BASKET": { + const { ColumnGenerator, RowGenerator, createUpdateGenerator } = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + basketDataGenerators[tableName] ?? defaultGenerators; + return [ColumnGenerator, RowGenerator, createUpdateGenerator]; + } + case undefined: { + const { ColumnGenerator, RowGenerator } = defaultGenerators; + return [ColumnGenerator, RowGenerator]; + } + default: + throw Error( + `vuu-row-gererator table ${table?.table} was requested but no generator is registered` + ); + } +}; + +export const populateArray = ( + count: number, + colGen: ColumnGeneratorFn, + rowGen: RowGeneratorFactory, + columns?: number | string[] +) => { + const columnDescriptors = colGen(columns); + const generateRow = rowGen(columnDescriptors.map((col) => col.name)); + const data: Array = []; + for (let i = 0; i < count; i++) { + const row = generateRow(i); + if (row) { + data[i] = row; + } else { + break; + } + } + return data; +}; diff --git a/vuu-ui/packages/vuu-data-test/tsconfig.json b/vuu-ui/packages/vuu-data-test/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-data-test/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-data-types/tsconfig.json b/vuu-ui/packages/vuu-data-types/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-data-types/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts index 430e595dd..82262beec 100644 --- a/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/array-data-source/array-data-source.ts @@ -16,6 +16,8 @@ import { buildColumnMap, ColumnMap, EventEmitter, + getAddedItems, + getMissingItems, getSelectionStatus, KeySet, logger, @@ -57,7 +59,6 @@ export interface ArrayDataSourceConstructorProps data: Array; rangeChangeRowset?: "delta" | "full"; } - const { debug } = logger("ArrayDataSource"); const { RENDER_IDX, SELECTED } = metadataKeys; @@ -132,7 +133,7 @@ export class ArrayDataSource public viewport: string; private keys = new KeySet(this.#range); - private processedData: readonly DataSourceRow[] | undefined = undefined; + protected processedData: readonly DataSourceRow[] | undefined = undefined; constructor({ aggregations, @@ -148,6 +149,10 @@ export class ArrayDataSource }: ArrayDataSourceConstructorProps) { super(); + console.log(`ArrayDataSource`, { + columnDescriptors, + }); + if (!data || !columnDescriptors) { throw Error( "ArrayDataSource constructor called without data or without columnDescriptors" @@ -191,18 +196,20 @@ export class ArrayDataSource }: SubscribeProps, callback: SubscribeCallback ) { - if (this.status !== "initialising") { - throw Error( - "ArrayDataSource subscribe should not be called more than once" - ); - } - this.clientCallback = callback; + this.viewport = viewport; + this.status = "subscribed"; + this.lastRangeServed = { from: 0, to: 0 }; + + let config = this.#config; - if (aggregations || columns || filter || groupBy || sort) { - //TODO use setter so we build the sorted/grouped etc dataset - this.#config = { - ...this.#config, + const hasConfigProps = aggregations || columns || filter || groupBy || sort; + if (hasConfigProps) { + if (range) { + this.#range = range; + } + config = { + ...config, aggregations: aggregations || this.#config.aggregations, columns: columns || this.#config.columns, filter: filter || this.#config.filter, @@ -211,35 +218,35 @@ export class ArrayDataSource }; } - this.viewport = viewport; - - this.status = "subscribed"; - this.clientCallback?.({ - ...this.#config, + ...config, type: "subscribed", clientViewportId: this.viewport, range: this.#range, tableSchema: this.tableSchema, }); - this.clientCallback({ - clientViewportId: this.viewport, - mode: "size-only", - type: "viewport-update", - size: this.#data.length, - }); - - if (range) { - // set range and trigger dispatch of initial rows - this.range = range; - } else if (this.#range !== NULL_RANGE) { - this.sendRowsToClient(); + if (hasConfigProps) { + // invoke setter to action config + this.config = config; + } else { + this.clientCallback({ + clientViewportId: this.viewport, + mode: "size-only", + type: "viewport-update", + size: this.#data.length, + }); + if (range) { + // set range and trigger dispatch of initial rows + this.range = range; + } else if (this.#range !== NULL_RANGE) { + this.sendRowsToClient(); + } } } unsubscribe() { - console.log("noop"); + console.log("unsubscribe noop"); } suspend() { @@ -248,17 +255,17 @@ export class ArrayDataSource } resume() { - console.log("noop"); + console.log("resume noop"); return this; } disable() { - console.log("noop"); + console.log("disable noop"); return this; } enable() { - console.log("noop"); + console.log("enable noop"); return this; } @@ -293,11 +300,20 @@ export class ArrayDataSource return this.#data; } + // Only used by the UpdateGenerator + get currentData() { + return this.processedData ?? this.#data; + } + + get table() { + return this.tableSchema.table; + } + get config() { return this.#config; } - set config(config: DataSourceConfig | undefined) { + set config(config: DataSourceConfig) { if (configChanged(this.#config, config)) { if (config) { const originalConfig = this.#config; @@ -317,7 +333,7 @@ export class ArrayDataSource let processedData: DataSourceRow[] | undefined; if (hasFilter(config)) { - const { filterStruct } = config.filter; + const { filter, filterStruct = parseFilter(filter) } = config.filter; if (filterStruct) { const fn = filterPredicate(this.#columnMap, filterStruct); processedData = this.#data.filter(fn); @@ -434,6 +450,17 @@ export class ArrayDataSource } set columns(columns: string[]) { + const addedColumns = getAddedItems(this.config.columns, columns); + if (addedColumns.length > 0) { + const columnsWithoutDescriptors = getMissingItems( + this.columnDescriptors, + addedColumns, + (col) => col.name + ); + console.log(`columnsWithoutDescriptors`, { + columnsWithoutDescriptors, + }); + } this.config = { ...this.#config, columns, @@ -550,8 +577,8 @@ export class ArrayDataSource console.log({ row, colName, value }); } - applyEdit(rowIndex: number, columnName: string, value: VuuColumnDataType) { - console.log(`ArrayDataSource applyEdit ${rowIndex} ${columnName} ${value}`); + applyEdit(row: DataSourceRow, columnName: string, value: VuuColumnDataType) { + console.log(`ArrayDataSource applyEdit ${row[0]} ${columnName} ${value}`); return true; } diff --git a/vuu-ui/packages/vuu-data/src/connection-manager.ts b/vuu-ui/packages/vuu-data/src/connection-manager.ts index dc81e4dbc..086cc3763 100644 --- a/vuu-ui/packages/vuu-data/src/connection-manager.ts +++ b/vuu-ui/packages/vuu-data/src/connection-manager.ts @@ -75,6 +75,8 @@ const pendingRequests = new Map(); type WorkerOptions = { protocol: WebSocketProtocol; + retryLimitDisconnect?: number; + retryLimitStartup?: number; url: string; token?: string; username: string | undefined; @@ -89,6 +91,8 @@ type WorkerOptions = { const getWorker = async ({ handleConnectionStatusChange, protocol, + retryLimitDisconnect, + retryLimitStartup, token = "", username, url, @@ -120,6 +124,8 @@ const getWorker = async ({ window.clearTimeout(timer); worker.postMessage({ protocol, + retryLimitDisconnect, + retryLimitStartup, token, type: "connect", url, @@ -158,6 +164,7 @@ function handleMessageFromWorker({ } else if (isConnectionStatusMessage(message)) { ConnectionManager.emit("connection-status", message); } else if (isConnectionQualityMetrics(message)) { + console.log({ message }); ConnectionManager.emit("connection-metrics", message); } else { const requestId = (message as VuuUIMessageInRPC).requestId; @@ -274,6 +281,10 @@ export type ConnectOptions = { authToken?: string; username?: string; protocol?: WebSocketProtocol; + /** Max number of reconnect attempts in the event of unsuccessful websocket connection at startup */ + retryLimitStartup?: number; + /** Max number of reconnect attempts in the event of a disconnected websocket connection */ + retryLimitDisconnect?: number; }; class _ConnectionManager extends EventEmitter { @@ -284,6 +295,8 @@ class _ConnectionManager extends EventEmitter { authToken, username, protocol, + retryLimitDisconnect, + retryLimitStartup, }: ConnectOptions): Promise { // By passing handleMessageFromWorker here, we can get connection status //messages while we wait for worker to resolve. @@ -292,6 +305,8 @@ class _ConnectionManager extends EventEmitter { url, token: authToken, username, + retryLimitDisconnect, + retryLimitStartup, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore handleConnectionStatusChange: handleMessageFromWorker, @@ -321,6 +336,8 @@ export const connectToServer = async ({ protocol = undefined, authToken, username, + retryLimitDisconnect, + retryLimitStartup, }: ConnectOptions) => { try { const serverAPI = await ConnectionManager.connect({ @@ -328,6 +345,8 @@ export const connectToServer = async ({ url, authToken, username, + retryLimitDisconnect, + retryLimitStartup, }); resolveServer(serverAPI); } catch (err: unknown) { diff --git a/vuu-ui/packages/vuu-data/src/data-source.ts b/vuu-ui/packages/vuu-data/src/data-source.ts index 4f327b0d2..6e5b083d4 100644 --- a/vuu-ui/packages/vuu-data/src/data-source.ts +++ b/vuu-ui/packages/vuu-data/src/data-source.ts @@ -212,7 +212,7 @@ const equivalentColumns: DataConfigPredicate = ( (cols1 === undefined && cols2?.length === 0) || (cols2 === undefined && cols1?.length === 0); -const columnsChanged: DataConfigPredicate = (config, newConfig) => { +export const columnsChanged: DataConfigPredicate = (config, newConfig) => { const { columns: cols1 } = config; const { columns: cols2 } = newConfig; @@ -475,17 +475,24 @@ export type DataSourceEvents = { }; export type DataSourceEditHandler = ( - rowIndex: number, + row: DataSourceRow, columnName: string, value: VuuColumnDataType ) => boolean; +export type RpcResponse = + | MenuRpcResponse + | VuuUIMessageInRPCEditReject + | VuuUIMessageInRPCEditResponse; + +export type RpcResponseHandler = (response: RpcResponse) => boolean; + export interface DataSource extends EventEmitter { aggregations: VuuAggregation[]; applyEdit: DataSourceEditHandler; closeTreeNode: (key: string, cascade?: boolean) => void; columns: string[]; - config: DataSourceConfig | undefined; + config: DataSourceConfig; suspend?: () => void; resume?: () => void; enable?: () => void; @@ -494,12 +501,7 @@ export interface DataSource extends EventEmitter { groupBy: VuuGroupBy; menuRpcCall: ( rpcRequest: Omit | ClientToServerEditRpc - ) => Promise< - | MenuRpcResponse - | VuuUIMessageInRPCEditReject - | VuuUIMessageInRPCEditResponse - | undefined - >; + ) => Promise; openTreeNode: (key: string) => void; range: VuuRange; select: SelectionChangeHandler; @@ -510,6 +512,7 @@ export interface DataSource extends EventEmitter { props: SubscribeProps, callback: SubscribeCallback ) => Promise; + table?: VuuTable; title?: string; unsubscribe: () => void; viewport?: string; diff --git a/vuu-ui/packages/vuu-data/src/json-data-source.ts b/vuu-ui/packages/vuu-data/src/json-data-source.ts index 9131b38c8..b7879cf58 100644 --- a/vuu-ui/packages/vuu-data/src/json-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/json-data-source.ts @@ -25,7 +25,9 @@ import type { DataSourceEvents, SubscribeCallback, SubscribeProps, + WithFullConfig, } from "./data-source"; +import { vanillaConfig } from "./data-source"; import { MenuRpcResponse, VuuUIMessageInRPCEditReject, @@ -59,7 +61,7 @@ export class JsonDataSource private visibleRows: DataSourceRow[] = []; #aggregations: VuuAggregation[] = []; - #columns: string[] = []; + #config: WithFullConfig = vanillaConfig; #data: DataSourceRow[]; #filter: DataSourceFilter = { filter: "" }; #groupBy: VuuGroupBy = []; @@ -90,6 +92,7 @@ export class JsonDataSource } [this.columnDescriptors, this.#data] = jsonToDataSourceRows(data); + this.visibleRows = this.#data .filter((row) => row[DEPTH] === 0) .map((row, index) => @@ -100,7 +103,10 @@ export class JsonDataSource this.#aggregations = aggregations; } if (this.columnDescriptors) { - this.#columns = this.columnDescriptors.map((c) => c.name); + this.#config = { + ...this.#config, + columns: this.columnDescriptors.map((c) => c.name), + }; } if (filter) { this.#filter = filter; @@ -128,13 +134,14 @@ export class JsonDataSource ) { this.clientCallback = callback; - console.log(`subscribe range ${range?.from} ${range?.to}`); - if (aggregations) { this.#aggregations = aggregations; } if (columns) { - this.#columns = columns; + this.#config = { + ...this.#config, + columns, + }; } if (filter) { this.#filter = filter; @@ -162,7 +169,7 @@ export class JsonDataSource aggregations: this.#aggregations, type: "subscribed", clientViewportId: this.viewport, - columns: this.#columns, + columns: this.#config.columns, filter: this.#filter, groupBy: this.#groupBy, range: this.#range, @@ -240,8 +247,6 @@ export class JsonDataSource size: this.visibleRows.length, type: "viewport-update", }); - - console.log(this.expandedRows); } closeTreeNode(key: string, cascade = false) { @@ -258,7 +263,7 @@ export class JsonDataSource } get config() { - return undefined; + return this.#config; } get selectedRowsCount() { @@ -295,12 +300,15 @@ export class JsonDataSource } get columns() { - return this.#columns; + return this.#config.columns; } set columns(columns: string[]) { - this.#columns = columns; - console.log(`ArrayDataSource setColumns ${columns.join(",")}`); + // TODO use setter + this.#config = { + ...this.#config, + columns, + }; } get aggregations() { @@ -311,6 +319,10 @@ export class JsonDataSource this.#aggregations = aggregations; } + set data(data: JsonData) { + console.log(`set JsonDataSource data`); + } + get sort() { return this.#sort; } @@ -327,7 +339,6 @@ export class JsonDataSource set filter(filter: DataSourceFilter) { // TODO should we wait until server ACK before we assign #sort ? this.#filter = filter; - console.log(`RemoteDataSource ${JSON.stringify(filter)}`); } get groupBy() { @@ -402,9 +413,6 @@ export class JsonDataSource ); } - const sections = rowKey.split("|").slice(1); - console.log({ sections, parentRow }); - return []; } diff --git a/vuu-ui/packages/vuu-data/src/remote-data-source.ts b/vuu-ui/packages/vuu-data/src/remote-data-source.ts index 7d6f95173..1d81b5807 100644 --- a/vuu-ui/packages/vuu-data/src/remote-data-source.ts +++ b/vuu-ui/packages/vuu-data/src/remote-data-source.ts @@ -1,4 +1,4 @@ -import { DataSourceFilter } from "@finos/vuu-data-types"; +import { DataSourceFilter, DataSourceRow } from "@finos/vuu-data-types"; import { Selection } from "@finos/vuu-datagrid-types"; import { ClientToServerEditRpc, @@ -18,6 +18,7 @@ import { EventEmitter, itemsOrOrderChanged, logger, + metadataKeys, throttle, uuid, } from "@finos/vuu-utils"; @@ -43,6 +44,19 @@ type RangeRequest = (range: VuuRange) => void; const { info } = logger("RemoteDataSource"); +const { KEY } = metadataKeys; + +type DataSourceStatus = + | "disabled" + | "disabling" + | "enabled" + | "enabling" + | "initialising" + | "subscribing" + | "subscribed" + | "suspended" + | "unsubscribed"; + /*----------------------------------------------------------------- A RemoteDataSource manages a single subscription via the ServerProxy ----------------------------------------------------------------*/ @@ -52,7 +66,7 @@ export class RemoteDataSource { private bufferSize: number; private server: ServerAPI | null = null; - private status = "initialising"; + private status: DataSourceStatus = "initialising"; private clientCallback: SubscribeCallback | undefined; private configChangePending: DataSourceConfig | undefined; private rangeRequest: RangeRequest; @@ -135,7 +149,7 @@ export class RemoteDataSource this.#range = range; } - if (this.status !== "initialising") { + if (this.status !== "initialising" && this.status !== "unsubscribed") { return; } @@ -203,7 +217,11 @@ export class RemoteDataSource this.server?.unsubscribe(this.viewport); } this.server?.destroy(this.viewport); + this.server = null; this.removeAllListeners(); + this.status = "unsubscribed"; + this.viewport = undefined; + this.range = { from: 0, to: 0 }; } suspend() { @@ -371,7 +389,7 @@ export class RemoteDataSource return this.#config; } - set config(config: DataSourceConfig | undefined) { + set config(config: DataSourceConfig) { if (configChanged(this.#config, config)) { if (config) { const newConfig: DataSourceConfig = @@ -598,8 +616,15 @@ export class RemoteDataSource } } - applyEdit(rowIndex: number, columnName: string, value: VuuColumnDataType) { - console.log(`ArrayDataSource applyEdit ${rowIndex} ${columnName} ${value}`); + applyEdit(row: DataSourceRow, columnName: string, value: VuuColumnDataType) { + this.menuRpcCall({ + rowKey: row[KEY], + field: columnName, + value: parseInt(value), + type: "VP_EDIT_CELL_RPC", + }).then(() => { + console.log("response"); + }); return true; } } diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts b/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts index a5357ecd5..40cebe351 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/array-backed-moving-window.ts @@ -7,6 +7,27 @@ type RangeTuple = [boolean, readonly VuuRow[] /*, readonly VuuRow[]*/]; const log = logger("array-backed-moving-window"); +function dataIsUnchanged(newRow: VuuRow, existingRow?: VuuRow) { + if (!existingRow) { + return false; + } + + if (existingRow.data.length !== newRow.data.length) { + return false; + } + + if (existingRow.sel !== newRow.sel) { + return false; + } + + for (let i = 0; i < existingRow.data.length; i++) { + if (existingRow.data[i] !== newRow.data[i]) { + return false; + } + } + return true; +} + export class ArrayBackedMovingWindow { #range: WindowRange; @@ -79,9 +100,13 @@ export class ArrayBackedMovingWindow { setAtIndex(row: VuuRow) { const { rowIndex: index } = row; + const internalIndex = index - this.#range.from; + //TODO measure the performance impact of this check + if (dataIsUnchanged(row, this.internalData[internalIndex])) { + return false; + } const isWithinClientRange = this.isWithinClientRange(index); if (isWithinClientRange || this.isWithinRange(index)) { - const internalIndex = index - this.#range.from; if (!this.internalData[internalIndex] && isWithinClientRange) { this.rowsWithinRange += 1; } diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts b/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts index 035bb129d..34a8f09f3 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts @@ -506,8 +506,6 @@ export class ServerProxy { | WithRequestId | WithRequestId ) { - debug?.(`handleMessageFromClient: ${message.type}`); - if (isViewportMessage(message)) { if (message.type === "disable") { // Viewport may already have been unsubscribed @@ -720,7 +718,7 @@ export class ServerProxy { case "REMOVE_VP_SUCCESS": { - const viewport = this.viewports.get(body.viewPortId); + const viewport = viewports.get(body.viewPortId); if (viewport) { this.mapClientToServerViewport.delete(viewport.clientViewportId); viewports.delete(body.viewPortId); diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts index 4a51c0a71..3858fcab2 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts @@ -258,7 +258,6 @@ export class Viewport { if (lastMode === mode) { const ts = Date.now(); - console.log(`read data now ${ts}`); this.lastUpdateStatus.count += 1; this.lastUpdateStatus.ts = ts; elapsedTime = lastTS === 0 ? 0 : ts - lastTS; @@ -919,7 +918,6 @@ export class Viewport { // alleviate pressure on UI DataTable. private shouldThrottleMessage = (mode: DataUpdateMode) => { const elapsedTime = this.setLastUpdate(mode); - console.log(`elapsed time = ${elapsedTime}`); return ( mode === "size-only" && elapsedTime > 0 && @@ -930,7 +928,7 @@ export class Viewport { private throttleMessage = (mode: DataUpdateMode) => { if (this.shouldThrottleMessage(mode)) { - console.log("throttling updates setTimeout to 2000"); + info?.("throttling updates setTimeout to 2000"); if (this.updateThrottleTimer === undefined) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts b/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts index 32f6198a2..c416bc5b6 100644 --- a/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts +++ b/vuu-ui/packages/vuu-data/src/vuuUIMessageTypes.ts @@ -167,6 +167,8 @@ export interface VuuUIMessageOutConnect { token: string; url: string; username?: string; + retryLimitDisconnect?: number; + retryLimitStartup?: number; } export interface VuuUIMessageOutSubscribe extends ServerProxySubscribeMessage { diff --git a/vuu-ui/packages/vuu-data/src/websocket-connection.ts b/vuu-ui/packages/vuu-data/src/websocket-connection.ts index 2526a178c..604138cbd 100644 --- a/vuu-ui/packages/vuu-data/src/websocket-connection.ts +++ b/vuu-ui/packages/vuu-data/src/websocket-connection.ts @@ -27,9 +27,21 @@ const WS = "ws"; // to stop semGrep complaining const isWebsocketUrl = (url: string) => url.startsWith(WS + "://") || url.startsWith(WS + "s://"); -const connectionAttempts: { - [key: string]: { attemptsRemaining: number; status: ConnectionStatus }; -} = {}; +type ConnectionTracking = { + [key: string]: { + connect: { + allowed: number; + remaining: number; + }; + reconnect: { + allowed: number; + remaining: number; + }; + status: ConnectionStatus; + }; +}; + +const connectionAttemptStatus: ConnectionTracking = {}; const setWebsocket = Symbol("setWebsocket"); const connectionCallback = Symbol("connectionCallback"); @@ -37,19 +49,34 @@ const connectionCallback = Symbol("connectionCallback"); export async function connect( connectionString: string, protocol: WebSocketProtocol, - callback: ConnectionCallback + callback: ConnectionCallback, + retryLimitDisconnect = 10, + retryLimitStartup = 5 ): Promise { + connectionAttemptStatus[connectionString] = { + status: "connecting", + connect: { + allowed: retryLimitStartup, + remaining: retryLimitStartup, + }, + reconnect: { + allowed: retryLimitDisconnect, + remaining: retryLimitDisconnect, + }, + }; return makeConnection(connectionString, protocol, callback); } async function reconnect(connection: WebsocketConnection) { //TODO it's not enough to reconnect with a new websocket, we have to log back in as well - makeConnection( - connection.url, - connection.protocol, - connection[connectionCallback], - connection - ); + // Temp don't try to reconnect at all until better interop with a proxy is implemented + // makeConnection( + // connection.url, + // connection.protocol, + // connection[connectionCallback], + // connection + // ); + throw Error("connection broken"); } async function makeConnection( @@ -58,12 +85,14 @@ async function makeConnection( callback: ConnectionCallback, connection?: WebsocketConnection ): Promise { - const connectionStatus = - connectionAttempts[url] || - (connectionAttempts[url] = { - attemptsRemaining: 5, - status: "disconnected", - }); + const { + status: currentStatus, + connect: connectStatus, + reconnect: reconnectStatus, + } = connectionAttemptStatus[url]; + + const trackedStatus = + currentStatus === "connecting" ? connectStatus : reconnectStatus; try { callback({ type: "connection-status", status: "connecting" }); @@ -89,10 +118,12 @@ async function makeConnection( callback({ type: "connection-status", status }); websocketConnection.status = status; + // reset the retry attempts for subsequent disconnections + trackedStatus.remaining = trackedStatus.allowed; + return websocketConnection as Connection; - } catch (evt) { - console.log({ evt }); - const retry = --connectionStatus.attemptsRemaining > 0; + } catch (err) { + const retry = --trackedStatus.remaining > 0; callback({ type: "connection-status", status: "disconnected", @@ -100,7 +131,7 @@ async function makeConnection( retry, }); if (retry) { - return makeConnectionIn(url, protocol, callback, connection, 10000); + return makeConnectionIn(url, protocol, callback, connection, 2000); } else { throw Error("Failed to establish connection"); } @@ -204,7 +235,7 @@ export class WebsocketConnection implements Connection { messagesLength: this.messagesCount, }); this.messagesCount = 0; - }, 1000); + }, 2000); ws.onerror = () => { error(`⚡ connection error`); diff --git a/vuu-ui/packages/vuu-data/src/worker.ts b/vuu-ui/packages/vuu-data/src/worker.ts index 3481d0d8a..881746c68 100644 --- a/vuu-ui/packages/vuu-data/src/worker.ts +++ b/vuu-ui/packages/vuu-data/src/worker.ts @@ -25,7 +25,9 @@ async function connectToServer( protocol: WebSocketProtocol, token: string, username: string | undefined, - onConnectionStatusChange: (msg: ConnectionStatusMessage) => void + onConnectionStatusChange: (msg: ConnectionStatusMessage) => void, + retryLimitDisconnect?: number, + retryLimitStartup?: number ) { const connection = await connectWebsocket( url, @@ -34,9 +36,10 @@ async function connectToServer( // never be called until subscriptions have been made, so this is safe. //TODO do we need to listen in to the connection messages here so we can lock back in, in the event of a reconnenct ? (msg) => { - if (isConnectionQualityMetrics(msg)) + if (isConnectionQualityMetrics(msg)) { + console.log("post connection metrics"); postMessage({ type: "connection-metrics", messages: msg }); - else if (isConnectionStatusMessage(msg)) { + } else if (isConnectionStatusMessage(msg)) { onConnectionStatusChange(msg); if (msg.status === "reconnected") { server.reconnect(); @@ -44,7 +47,9 @@ async function connectToServer( } else { server.handleMessageFromServer(msg); } - } + }, + retryLimitDisconnect, + retryLimitStartup ); server = new ServerProxy(connection, (msg) => sendMessageToClient(msg)); @@ -72,7 +77,9 @@ const handleMessageFromClient = async ({ message.protocol, message.token, message.username, - postMessage + postMessage, + message.retryLimitDisconnect, + message.retryLimitStartup ); postMessage({ type: "connected" }); break; diff --git a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts index fc78984e7..0824c8771 100644 --- a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts +++ b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts @@ -2574,9 +2574,9 @@ describe("ServerProxy", () => { body: { ...COMMON_TABLE_ROW_ATTRS, rows: [ - ...createTableRows("server-vp-1", 0, 1, 100, 1, 1), + ...createTableRows("server-vp-1", 0, 1, 100, 1, 1, 2000), sizeRow("server-vp-2", 20), - ...createTableRows("server-vp-2", 0, 10), + ...createTableRows("server-vp-2", 0, 10, 100, 2, 0, 2000), ], }, }); @@ -2587,7 +2587,7 @@ describe("ServerProxy", () => { { mode: "update", rows: [ - [0,0,true,false,0,0,'key-00', 0,'key-00', 'name 00',1000,true], + [0,0,true,false,0,0,'key-00', 0,'key-00', 'name 00',2000,true], ], type: 'viewport-update', clientViewportId: 'client-vp-1' @@ -2598,16 +2598,16 @@ describe("ServerProxy", () => { { mode: "batch", rows: [ - [0,0,true,false,0,0,'key-00', 0,'key-00', 'name 00',1000,true], - [1,1,true,false,0,0,"key-01",0,"key-01","name 01",1001,true], - [2,2,true,false,0,0,"key-02",0,"key-02","name 02",1002,true], - [3,3,true,false,0,0,"key-03",0,"key-03","name 03",1003,true], - [4,4,true,false,0,0,"key-04",0,"key-04","name 04",1004,true], - [5,5,true,false,0,0,"key-05",0,"key-05","name 05",1005,true], - [6,6,true,false,0,0,"key-06",0,"key-06","name 06",1006,true], - [7,7,true,false,0,0,"key-07",0,"key-07","name 07",1007,true], - [8,8,true,false,0,0,"key-08",0,"key-08","name 08",1008,true], - [9,9,true,false,0,0,"key-09",0,"key-09","name 09",1009,true] + [0,0,true,false,0,0,'key-00', 0,'key-00', 'name 00',2000,true], + [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2001,true], + [2,2,true,false,0,0,"key-02",0,"key-02","name 02",2002,true], + [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2003,true], + [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2004,true], + [5,5,true,false,0,0,"key-05",0,"key-05","name 05",2005,true], + [6,6,true,false,0,0,"key-06",0,"key-06","name 06",2006,true], + [7,7,true,false,0,0,"key-07",0,"key-07","name 07",2007,true], + [8,8,true,false,0,0,"key-08",0,"key-08","name 08",2008,true], + [9,9,true,false,0,0,"key-09",0,"key-09","name 09",2009,true] ], size: 100, type: 'viewport-update', diff --git a/vuu-ui/packages/vuu-data/test/test-utils.ts b/vuu-ui/packages/vuu-data/test/test-utils.ts index 44e2570fc..43a4d2e0c 100644 --- a/vuu-ui/packages/vuu-data/test/test-utils.ts +++ b/vuu-ui/packages/vuu-data/test/test-utils.ts @@ -46,14 +46,15 @@ export const createTableRows = ( to, vpSize = 100, ts = 1, - sel: 0 | 1 = 0 + sel: 0 | 1 = 0, + numericValue = 1000 ): VuuRow[] => { const results: VuuRow[] = []; for (let rowIndex = from; rowIndex < to; rowIndex++) { const key = ("0" + rowIndex).slice(-2); const rowKey = `key-${key}`; results.push({ - data: [rowKey, `name ${key}`, 1000 + rowIndex, true], + data: [rowKey, `name ${key}`, numericValue + rowIndex, true], rowIndex, rowKey, updateType: "U", diff --git a/vuu-ui/packages/vuu-data/test/websocket-connection.test.ts b/vuu-ui/packages/vuu-data/test/websocket-connection.test.ts new file mode 100644 index 000000000..eae0185f7 --- /dev/null +++ b/vuu-ui/packages/vuu-data/test/websocket-connection.test.ts @@ -0,0 +1,109 @@ +import "./global-mocks"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { + connect as connectWebsocket, + ConnectionMessage, +} from "../src/websocket-connection"; + +describe("websocket-connection", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + }); + + it("tries to connect by default a maximum of 5 times before throwing Exception", async () => { + const statusMessages: ConnectionMessage[] = []; + const callback = async (message: ConnectionMessage) => { + statusMessages.push(message); + await vi.advanceTimersByTimeAsync(2000); + }; + + try { + await connectWebsocket("tst/url", "", callback); + } catch (e) { + expect(e.message).toEqual("Failed to establish connection"); + } + + expect(statusMessages.length).toEqual(10); + expect(statusMessages).toEqual([ + { type: "connection-status", status: "connecting" }, + { + type: "connection-status", + status: "disconnected", + reason: "failed to connect", + retry: true, + }, + { type: "connection-status", status: "connecting" }, + { + type: "connection-status", + status: "disconnected", + reason: "failed to connect", + retry: true, + }, + { type: "connection-status", status: "connecting" }, + { + type: "connection-status", + status: "disconnected", + reason: "failed to connect", + retry: true, + }, + { type: "connection-status", status: "connecting" }, + { + type: "connection-status", + status: "disconnected", + reason: "failed to connect", + retry: true, + }, + { type: "connection-status", status: "connecting" }, + { + type: "connection-status", + status: "disconnected", + reason: "failed to connect", + retry: false, + }, + ]); + }); + + it("fires connection-status messages when connecting/connected", async () => { + class MockWebSocket { + private openHandler: any; + private errorHandler: any; + constructor() { + setTimeout(() => { + this?.openHandler(); + }, 0); + } + set onopen(callback) { + this.openHandler = callback; + } + set onerror(callback) { + this.errorHandler = callback; + } + } + vi.stubGlobal("WebSocket", MockWebSocket); + + const statusMessages: ConnectionMessage[] = []; + const callback = async (message: ConnectionMessage) => { + statusMessages.push(message); + await vi.advanceTimersByTimeAsync(10); + }; + + try { + await connectWebsocket("tst/url", "", callback); + } catch (e) { + expect(e.message).toEqual("Failed to establish connection"); + } + + expect(statusMessages.length).toEqual(2); + expect(statusMessages).toEqual([ + { type: "connection-status", status: "connecting" }, + { + type: "connection-status", + status: "connection-open-awaiting-session", + }, + ]); + }); +}); diff --git a/vuu-ui/packages/vuu-data/tsconfig.json b/vuu-ui/packages/vuu-data/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-data/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-datagrid-types/index.d.ts b/vuu-ui/packages/vuu-datagrid-types/index.d.ts index 7e4b75b21..92b7b7ea7 100644 --- a/vuu-ui/packages/vuu-datagrid-types/index.d.ts +++ b/vuu-ui/packages/vuu-datagrid-types/index.d.ts @@ -7,6 +7,7 @@ import type { } from "@finos/vuu-protocol-types"; import type { FunctionComponent, MouseEvent } from "react"; import type { ClientSideValidationChecker } from "@finos/vuu-ui-controls"; +import type { ColumnMap } from "@finos/vuu-utils"; export type TableSelectionModel = "none" | "single" | "checkbox" | "extended"; @@ -19,7 +20,7 @@ export type TableHeading = { label: string; width: number }; export type TableHeadings = TableHeading[][]; export type DataCellEditHandler = ( - rowIndex: number, + row: DataSourceRow, columnName: string, value: VuuColumnDataType ) => boolean; @@ -42,6 +43,7 @@ export interface TableAttributes { columnDefaultWidth?: number; columnFormatHeader?: "capitalize" | "uppercase"; columnSeparators?: boolean; + showHighlightedRow?: boolean; rowSeparators?: boolean; zebraStripes?: boolean; } @@ -127,7 +129,6 @@ export interface ColumnDescriptor { align?: ColumnAlignment; className?: string; editable?: boolean; - expression?: string; flex?: number; /** Optional additional level(s) of heading to display above label. diff --git a/vuu-ui/packages/vuu-datagrid-types/tsconfig.json b/vuu-ui/packages/vuu-datagrid-types/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-datagrid-types/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-datagrid/src/ColumnBearer.tsx b/vuu-ui/packages/vuu-datagrid/src/ColumnBearer.tsx index 30aa5c68a..a41f5a104 100644 --- a/vuu-ui/packages/vuu-datagrid/src/ColumnBearer.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/ColumnBearer.tsx @@ -17,8 +17,8 @@ import "./column-bearer.css"; import { GridModelType } from "./grid-model"; import { ColumnDragState, dragPhase } from "./gridTypes"; import { buildColumnMap } from "@finos/vuu-utils"; -import { KeyedColumnDescriptor } from "packages/vuu-datagrid-types"; -import { DataSourceRow } from "packages/vuu-data-types"; +import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { DataSourceRow } from "@finos/vuu-data-types"; const LEFT = "left"; const RIGHT = "right"; diff --git a/vuu-ui/packages/vuu-datagrid/src/Viewport.tsx b/vuu-ui/packages/vuu-datagrid/src/Viewport.tsx index b3d9db874..79398ef93 100644 --- a/vuu-ui/packages/vuu-datagrid/src/Viewport.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/Viewport.tsx @@ -30,7 +30,7 @@ export interface ViewportScrollApi { // Temp, until we manage selection properly const getSelectedRows = (data: DataSourceRow[]) => { - return data.filter((d) => d[metadataKeys.SELECTED] === 1); + return data.filter((d) => d[metadataKeys.SELECTED] !== 0); }; export const Viewport = forwardRef(function Viewport( diff --git a/vuu-ui/packages/vuu-datagrid/src/cell-renderers/progress-cell.tsx b/vuu-ui/packages/vuu-datagrid/src/cell-renderers/progress-cell.tsx index cab062d9c..863b9cd12 100644 --- a/vuu-ui/packages/vuu-datagrid/src/cell-renderers/progress-cell.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/cell-renderers/progress-cell.tsx @@ -5,7 +5,7 @@ import React from "react"; import { isTypeDescriptor } from "@finos/vuu-utils"; import { GridCellProps } from "../grid-cells"; import "./progress-cell.css"; -import { ColumnTypeRenderer } from "packages/vuu-datagrid-types"; +import { ColumnTypeRenderer } from "@finos/vuu-datagrid-types"; const ProgressCell = React.memo(function ProgressCell({ column, diff --git a/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts b/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts index 26a15ed81..b581ae1de 100644 --- a/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts +++ b/vuu-ui/packages/vuu-datagrid/src/context-menu/build-context-menu-descriptors.ts @@ -4,7 +4,7 @@ import { ContextMenuGroupItemDescriptor, ContextMenuItemDescriptor, MenuBuilder, -} from "packages/vuu-data-types"; +} from "@finos/vuu-data-types"; import { VuuAggregation, VuuGroupBy, diff --git a/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts b/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts index af6b3bcb6..ae21ba02a 100644 --- a/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts +++ b/vuu-ui/packages/vuu-datagrid/src/context-menu/useContextMenu.ts @@ -2,8 +2,8 @@ import { DataSource } from "@finos/vuu-data"; import { DataSourceFilter, MenuActionHandler } from "@finos/vuu-data-types"; import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { MenuActionClosePopup } from "@finos/vuu-popups"; import { removeColumnFromFilter, setAggregations } from "@finos/vuu-utils"; -import { MenuActionClosePopup } from "packages/vuu-popups/src"; import { AggregationType } from "../constants"; import { GridModelDispatch } from "../grid-context"; import { GridModelType } from "../grid-model/gridModelTypes"; diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/HeaderCell.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-cells/HeaderCell.tsx index 75971e900..956020dc8 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/HeaderCell.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/HeaderCell.tsx @@ -14,7 +14,7 @@ import { SortIndicator, sortStatus } from "./sort-indicator"; import { useCellResize } from "./useCellResize"; import "./HeaderCell.css"; -import { DataSourceFilter } from "packages/vuu-data-types"; +import { DataSourceFilter } from "@finos/vuu-data-types"; const classBase = "hwHeaderCell"; const NO_AGGREGATION = { aggType: -1 }; diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-cells/filter-indicator.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-cells/filter-indicator.tsx index 1bfbc2a1f..886ef56a6 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-cells/filter-indicator.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-cells/filter-indicator.tsx @@ -5,7 +5,7 @@ import cx from "classnames"; import { HTMLAttributes, useCallback, useMemo } from "react"; import "./filter-indicator.css"; -import { KeyedColumnDescriptor } from "packages/vuu-datagrid-types"; +import { KeyedColumnDescriptor } from "@finos/vuu-datagrid-types"; export const Direction = { ASC: "asc", diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-context.tsx b/vuu-ui/packages/vuu-datagrid/src/grid-context.tsx index 4ba6f7c18..87e908643 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-context.tsx +++ b/vuu-ui/packages/vuu-datagrid/src/grid-context.tsx @@ -14,7 +14,7 @@ import { GridAction, KeyedColumnDescriptor, } from "@finos/vuu-datagrid-types"; -import { DataSourceFilter } from "packages/vuu-data-types"; +import { DataSourceFilter } from "@finos/vuu-data-types"; export interface GridActionGroup { type: "group"; diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-hooks/use-data-source.ts b/vuu-ui/packages/vuu-datagrid/src/grid-hooks/use-data-source.ts index 01515f741..4f5bfd144 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-hooks/use-data-source.ts +++ b/vuu-ui/packages/vuu-datagrid/src/grid-hooks/use-data-source.ts @@ -7,7 +7,7 @@ import { toColumnDescriptor, WindowRange, } from "@finos/vuu-utils"; -import { DataSourceRow } from "packages/vuu-data-types"; +import { DataSourceRow } from "@finos/vuu-data-types"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useGridContext } from "../grid-context"; import { GridModelType } from "../grid-model/gridModelTypes"; diff --git a/vuu-ui/packages/vuu-datagrid/src/grid-hooks/useGridActionDispatcher.ts b/vuu-ui/packages/vuu-datagrid/src/grid-hooks/useGridActionDispatcher.ts index f23b7641c..945c29065 100644 --- a/vuu-ui/packages/vuu-datagrid/src/grid-hooks/useGridActionDispatcher.ts +++ b/vuu-ui/packages/vuu-datagrid/src/grid-hooks/useGridActionDispatcher.ts @@ -3,7 +3,7 @@ import { DataSourceAction, GridAction, ScrollAction, -} from "packages/vuu-datagrid-types"; +} from "@finos/vuu-datagrid-types"; import { useCallback } from "react"; import { GridActionSelection, GridModelDispatch } from "../grid-context"; diff --git a/vuu-ui/packages/vuu-datagrid/tsconfig.json b/vuu-ui/packages/vuu-datagrid/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-datagrid/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-datatable/package.json b/vuu-ui/packages/vuu-datatable/package.json index fe06ce318..736175a7e 100644 --- a/vuu-ui/packages/vuu-datatable/package.json +++ b/vuu-ui/packages/vuu-datatable/package.json @@ -11,9 +11,12 @@ "dependencies": { "@finos/vuu-data": "0.0.26", "@finos/vuu-datagrid-types": "0.0.26", + "@finos/vuu-filters": "0.0.26", + "@finos/vuu-layout": "0.0.26", "@finos/vuu-popups": "0.0.26", "@finos/vuu-table": "0.0.26", "@finos/vuu-table-extras": "0.0.26", + "@finos/vuu-ui-controls": "0.0.26", "@finos/vuu-utils": "0.0.26" }, "peerDependencies": { diff --git a/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx b/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx index 0afc6f2ea..1b60895f8 100644 --- a/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx +++ b/vuu-ui/packages/vuu-datatable/src/configurable-table/ConfigurableTable.tsx @@ -1,5 +1,5 @@ import { GridConfig } from "@finos/vuu-datagrid-types"; -import { Table, TableProps } from "@finos/vuu-table"; +import { Table, TablePropsDeprecated as TableProps } from "@finos/vuu-table"; import { ReactElement, useCallback, useState } from "react"; import { Dialog } from "@finos/vuu-popups"; diff --git a/vuu-ui/packages/vuu-datatable/src/filter-table/FilterTable.css b/vuu-ui/packages/vuu-datatable/src/filter-table/FilterTable.css index 9ba374908..55ae31733 100644 --- a/vuu-ui/packages/vuu-datatable/src/filter-table/FilterTable.css +++ b/vuu-ui/packages/vuu-datatable/src/filter-table/FilterTable.css @@ -1,4 +1,5 @@ .vuuFilterTable { + --vuuFilterBar-flex: 0 0 33px; display: flex; flex-direction: column; } \ No newline at end of file diff --git a/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx b/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx index 20428e9b8..5c128e1d2 100644 --- a/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx +++ b/vuu-ui/packages/vuu-datatable/src/json-table/JsonTable.tsx @@ -1,29 +1,54 @@ import { TableProps } from "@finos/vuu-table"; import { JsonData } from "@finos/vuu-utils"; -import { Table } from "@finos/vuu-table"; +import { TableNext } from "@finos/vuu-table"; import { JsonDataSource } from "@finos/vuu-data"; -import { useMemo } from "react"; -import { ColumnDescriptor } from "@finos/vuu-datagrid-types"; +import { useEffect, useMemo, useRef } from "react"; +import { TableConfig } from "@finos/vuu-datagrid-types"; export interface JsonTableProps extends Omit { + config?: Pick< + TableConfig, + "columnSeparators" | "rowSeparators" | "zebraStripes" + >; source: JsonData | undefined; } export const JsonTable = ({ - source = { "": "" }, + config, + source: sourceProp = { "": "" }, ...tableProps }: JsonTableProps) => { - const [dataSource, tableConfig] = useMemo((): [ - JsonDataSource, - { columns: ColumnDescriptor[] } - ] => { - const ds = new JsonDataSource({ - data: source, + const sourceRef = useRef(sourceProp); + const dataSourceRef = useRef(); + useMemo(() => { + dataSourceRef.current = new JsonDataSource({ + data: sourceRef.current, }); + }, []); - return [ds, { columns: ds.columnDescriptors }]; - }, [source]); + const tableConfig = useMemo(() => { + return { + ...config, + columns: dataSourceRef.current?.columnDescriptors ?? [], + }; + }, [config]); - return ; + useEffect(() => { + if (dataSourceRef.current) { + dataSourceRef.current.data = sourceProp; + } + }, [sourceProp]); + + if (dataSourceRef.current === undefined) { + return null; + } + + return ( + + ); }; diff --git a/vuu-ui/packages/vuu-datatable/tsconfig.json b/vuu-ui/packages/vuu-datatable/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-datatable/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-filter-parser/tsconfig.json b/vuu-ui/packages/vuu-filter-parser/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-filter-parser/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-filter-types/tsconfig.json b/vuu-ui/packages/vuu-filter-types/tsconfig.json new file mode 100644 index 000000000..db9582a16 --- /dev/null +++ b/vuu-ui/packages/vuu-filter-types/tsconfig.json @@ -0,0 +1,6 @@ +{ +"extends": "../../tsconfig.json", +"compilerOptions":{ + "composite": true +}, +} diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.css b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.css index 7b17b18fe..eda2b904a 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.css +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.css @@ -1,9 +1,9 @@ .vuuFilterBar { --vuu-svg-tune: url('data:image/svg+xml;utf8,'); - --vuuToolbar-height: 26px; + --vuuToolbar-height: 28px; --salt-container-primary-borderColor: var(--vuu-color-purple-10); - + --vuuOverflowContainer-minWidth: 0; --saltButton-height: 26px; --saltButton-width: 26px; @@ -11,9 +11,10 @@ background-color: var(--salt-container-secondary-background); border-bottom: solid 1px #D6D7DA; display: flex; + flex: var(--vuuFilterBar-flex); gap: 4px; height: 33px; - padding: 3px 8px; + padding: 0px 8px; } .vuuFilterbar-icon { @@ -32,5 +33,5 @@ } .vuuFilterBar .vuuToolbar { - flex: 1 1 auto; + flex: 0 1 auto; } diff --git a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx index 4c059f280..7186d9d31 100644 --- a/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx +++ b/vuu-ui/packages/vuu-filters/src/filter-bar/FilterBar.tsx @@ -1,7 +1,7 @@ import { TableSchema } from "@finos/vuu-data"; import { DataSourceFilter } from "@finos/vuu-data-types"; import { Filter } from "@finos/vuu-filter-types"; -import { Toolbar } from "@finos/vuu-layout"; +import { ActiveItemChangeHandler, Toolbar } from "@finos/vuu-layout"; import { Prompt } from "@finos/vuu-popups"; import { Button } from "@salt-ds/core"; import cx from "classnames"; @@ -18,8 +18,8 @@ export interface FilterBarProps extends HTMLAttributes { FilterClauseEditorProps?: Partial; activeFilterIndex?: number[]; filters: Filter[]; - onActiveChange?: (itemIndex: number[]) => void; onApplyFilter: (filter: DataSourceFilter) => void; + onChangeActiveFilterIndex: ActiveItemChangeHandler; onFiltersChanged?: (filters: Filter[]) => void; showMenu?: boolean; tableSchema: TableSchema; @@ -28,12 +28,12 @@ export interface FilterBarProps extends HTMLAttributes { const classBase = "vuuFilterBar"; export const FilterBar = ({ - activeFilterIndex: activeFilterIndexProp, + activeFilterIndex: activeFilterIndexProp = [], FilterClauseEditorProps, className: classNameProp, filters: filtersProp, - onActiveChange, onApplyFilter, + onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFiltersChanged, showMenu: showMenuProp = false, tableSchema, @@ -42,12 +42,14 @@ export const FilterBar = ({ const rootRef = useRef(null); const { activeFilterIndex, + addButtonProps, editFilter, filters, onClickAddFilter, onClickRemoveFilter, onChangeFilterClause, - onFilterActivation, + onChangeActiveFilterIndex, + onNavigateOutOfBounds, onKeyDown, onMenuAction, pillProps, @@ -57,8 +59,8 @@ export const FilterBar = ({ activeFilterIndex: activeFilterIndexProp, containerRef: rootRef, filters: filtersProp, - onActiveChange, onApplyFilter, + onChangeActiveFilterIndex: onChangeActiveFilterIndexProp, onFiltersChanged, showMenu: showMenuProp, }); @@ -78,16 +80,6 @@ export const FilterBar = ({ ); }); - items.push( -