diff --git a/content/modules/ROOT/nav.adoc b/content/modules/ROOT/nav.adoc index 9cb5fa58..1c10ff8d 100644 --- a/content/modules/ROOT/nav.adoc +++ b/content/modules/ROOT/nav.adoc @@ -38,6 +38,7 @@ include::bpm:partial$nav.adoc[] * xref:ROOT:add-ons.adoc[] + -- +include::ai-tools:partial$nav.adoc[] include::appsettings:partial$nav.adoc[] include::audit:partial$nav.adoc[] include::authorization-server:partial$nav.adoc[] diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/.gitignore b/content/modules/ai-tools/examples/ai-tools-ex1/.gitignore new file mode 100644 index 00000000..57ec2854 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/.gitignore @@ -0,0 +1,31 @@ +.jmix + +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +# Remove this line if you want to follow recommendations in src/main/bundles/README.md +src/main/bundles/ + +### IntelliJ IDEA ### +.idea/* +!.idea/encodings.xml +*.iws +*.iml +*.ipr +out/ +/target/ + +### VS Code ### +.vscode/ + +# The following files are generated/updated by vaadin-gradle-plugin +node_modules/ +src/main/frontend/generated/ +pnpmfile.js +vite.generated.ts +webpack.generated.js + +.DS_Store diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/.idea/encodings.xml b/content/modules/ai-tools/examples/ai-tools-ex1/.idea/encodings.xml new file mode 100644 index 00000000..da0415a0 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/build.gradle b/content/modules/ai-tools/examples/ai-tools-ex1/build.gradle new file mode 100644 index 00000000..7451693b --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/build.gradle @@ -0,0 +1,62 @@ +plugins { + id 'io.jmix' version '3.0.0-M5' + id 'java' +} + +apply plugin: 'org.springframework.boot' +apply plugin: 'com.vaadin' + +jmix { + bomVersion = '3.0.999-SNAPSHOT' +} + +vaadin { + optimizeBundle = false +} + +group = 'com.company' +version = '0.0.1-SNAPSHOT' + +repositories { + mavenCentral() + maven { + url = 'https://nexus.jmix.io/repository/public' + } +} + +dependencies { + implementation 'io.jmix.core:jmix-core-starter' + implementation 'io.jmix.data:jmix-eclipselink-starter' + implementation 'io.jmix.security:jmix-security-starter' + implementation 'io.jmix.security:jmix-security-flowui-starter' + implementation 'io.jmix.security:jmix-security-data-starter' + implementation 'io.jmix.localfs:jmix-localfs-starter' + implementation 'io.jmix.flowui:jmix-flowui-starter' + implementation 'com.vaadin:vaadin-dev' + implementation 'io.jmix.flowui:jmix-flowui-data-starter' + implementation 'io.jmix.flowui:jmix-flowui-themes' + implementation 'io.jmix.datatools:jmix-datatools-starter' + implementation 'io.jmix.datatools:jmix-datatools-flowui-starter' + + implementation 'org.springframework.boot:spring-boot-starter-web' + + implementation 'org.hsqldb:hsqldb' + + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + testImplementation 'io.jmix.flowui:jmix-flowui-test-assist' + + // tag::dependencies[] + implementation 'io.jmix.aitools:jmix-aitools-starter' + implementation 'io.jmix.aitools:jmix-aitools-flowui-starter' + implementation 'io.jmix.aitools:jmix-aitools-flowui-data-starter' + // end::dependencies[] + + // Spring AI model API, required to declare custom @Tool methods in the application. + implementation 'org.springframework.ai:spring-ai-model' +} + +test { + useJUnitPlatform() +} diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/gradle.properties b/content/modules/ai-tools/examples/ai-tools-ex1/gradle.properties new file mode 100644 index 00000000..8b4371be --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/gradle.properties @@ -0,0 +1,2 @@ +hilla.active=false +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/gradle/wrapper/gradle-wrapper.jar b/content/modules/ai-tools/examples/ai-tools-ex1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..1b33c55b Binary files /dev/null and b/content/modules/ai-tools/examples/ai-tools-ex1/gradle/wrapper/gradle-wrapper.jar differ diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/gradle/wrapper/gradle-wrapper.properties b/content/modules/ai-tools/examples/ai-tools-ex1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..5dd3c012 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/gradlew b/content/modules/ai-tools/examples/ai-tools-ex1/gradlew new file mode 100644 index 00000000..23d15a93 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +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 + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/gradlew.bat b/content/modules/ai-tools/examples/ai-tools-ex1/gradlew.bat new file mode 100644 index 00000000..db3a6ac2 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem 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, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/settings.gradle b/content/modules/ai-tools/examples/ai-tools-ex1/settings.gradle new file mode 100644 index 00000000..54b1f660 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'aitools-onboarding' diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/frontend/index.html b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/frontend/index.html new file mode 100644 index 00000000..0701a4b0 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/frontend/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + +
+ + diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/OnboardingApplication.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/OnboardingApplication.java new file mode 100644 index 00000000..8807bb5e --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/OnboardingApplication.java @@ -0,0 +1,63 @@ +package com.company.onboarding; + +import com.google.common.base.Strings; +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.component.dependency.StyleSheet; +import com.vaadin.flow.component.page.Push; +import com.vaadin.flow.server.PWA; +import com.vaadin.flow.theme.Theme; +import com.vaadin.flow.theme.aura.Aura; +import com.vaadin.flow.theme.lumo.Lumo; +import io.jmix.flowui.theme.aura.JmixAura; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.autoconfigure.DataSourceProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; + +import javax.sql.DataSource; + +@Push +@StyleSheet(Aura.STYLESHEET) +@StyleSheet(JmixAura.STYLESHEET) +@StyleSheet("themes/onboarding-aura/styles.css") +@PWA(name = "Onboarding", shortName = "Onboarding") +@SpringBootApplication +@StyleSheet(Lumo.UTILITY_STYLESHEET) +public class OnboardingApplication implements AppShellConfigurator { + + @Autowired + private Environment environment; + + public static void main(String[] args) { + SpringApplication.run(OnboardingApplication.class, args); + } + + @Bean + @Primary + @ConfigurationProperties("main.datasource") + DataSourceProperties dataSourceProperties() { + return new DataSourceProperties(); + } + + @Bean + @Primary + @ConfigurationProperties("main.datasource.hikari") + DataSource dataSource(final DataSourceProperties dataSourceProperties) { + return dataSourceProperties.initializeDataSourceBuilder().build(); + } + + @EventListener + public void printApplicationUrl(final ApplicationStartedEvent event) { + LoggerFactory.getLogger(OnboardingApplication.class).info("Application started at " + + "http://localhost:" + + environment.getProperty("local.server.port") + + Strings.nullToEmpty(environment.getProperty("server.servlet.context-path"))); + } +} diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/OnboardingTools.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/OnboardingTools.java new file mode 100644 index 00000000..8353372e --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/OnboardingTools.java @@ -0,0 +1,41 @@ +package com.company.onboarding.ai; + +import com.company.onboarding.entity.Step; +import io.jmix.aitools.tool.AiToolStatusPublisher; +import io.jmix.aitools.tool.JmixAiTool; +import io.jmix.core.DataManager; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +// tag::custom-tool[] +@Component +public class OnboardingTools implements JmixAiTool { // <1> + + @Autowired + private DataManager dataManager; + @Autowired + private AiToolStatusPublisher statusPublisher; + + @Tool(name = "getStepCatalog", // <2> + description = "Returns the catalog of onboarding steps with their duration in days.") + public String getStepCatalog(ToolContext toolContext) { // <3> + String message = "Loading the onboarding step catalog"; + statusPublisher.update(message, toolContext); // <4> + + List steps = dataManager.load(Step.class).all().list(); // <5> + + statusPublisher.complete(message, steps.size() + " steps", toolContext); + + return steps.stream() + .sorted(Comparator.comparing(Step::getSortValue)) + .map(step -> step.getName() + " — " + step.getDuration() + " day(s)") + .collect(Collectors.joining("\n")); + } +} +// end::custom-tool[] diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/SortedEntitiesTool.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/SortedEntitiesTool.java new file mode 100644 index 00000000..444ebaa5 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/SortedEntitiesTool.java @@ -0,0 +1,35 @@ +package com.company.onboarding.ai; + +import io.jmix.aitools.dataload.introspection.AvailableEntityService; +import io.jmix.aitools.dataload.introspection.model.EntitySummary; +import io.jmix.aitools.dataload.tool.DataLoadAiTool; +import io.jmix.aitools.tool.ToolOverride; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Comparator; +import java.util.List; + +// tag::tool-override[] +@Component +public class SortedEntitiesTool implements DataLoadAiTool { // <1> + + @Autowired + private AvailableEntityService availableEntityService; + + @Tool(description = "Returns entities available to the user, ordered by localized name.") + @ToolOverride("aitls_getAvailableEntities") // <2> + public List getAvailableEntities() { + return availableEntityService.getEntitySummaries().stream() // <3> + .sorted(Comparator.comparing(this::firstLocalizedName)) + .toList(); + } + + private String firstLocalizedName(EntitySummary summary) { + return summary.getLocalizedNames().isEmpty() + ? summary.getEntityName() + : summary.getLocalizedNames().get(0); + } +} +// end::tool-override[] diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/SupportAssistant.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/SupportAssistant.java new file mode 100644 index 00000000..eab959de --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/ai/SupportAssistant.java @@ -0,0 +1,20 @@ +package com.company.onboarding.ai; + +import io.jmix.aitools.service.AiAssistantService; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +// tag::assistant-service[] +@Component +public class SupportAssistant { + + @Autowired + private AiAssistantService aiAssistantService; + + @Nullable + public String ask(String question) { + return aiAssistantService.send(question); // <1> + } +} +// end::assistant-service[] diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/Department.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/Department.java new file mode 100644 index 00000000..a26e616a --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/Department.java @@ -0,0 +1,68 @@ +package com.company.onboarding.entity; + +import io.jmix.core.entity.annotation.JmixGeneratedValue; +import io.jmix.core.metamodel.annotation.InstanceName; +import io.jmix.core.metamodel.annotation.JmixEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +@JmixEntity +@Table(name = "DEPARTMENT", indexes = { + @Index(name = "IDX_DEPARTMENT_HR_MANAGER", columnList = "HR_MANAGER_ID") +}, uniqueConstraints = { + @UniqueConstraint(name = "IDX_DEPARTMENT_UNQ_NAME", columnNames = {"NAME"}) +}) +@Entity +public class Department { + @JmixGeneratedValue + @Column(name = "ID", nullable = false) + @Id + private UUID id; + + @Column(name = "VERSION", nullable = false) + @Version + private Integer version; + + @InstanceName + @Column(name = "NAME", nullable = false) + @NotNull + private String name; + + @JoinColumn(name = "HR_MANAGER_ID") + @ManyToOne(fetch = FetchType.LAZY) + private User hrManager; + + public User getHrManager() { + return hrManager; + } + + public void setHrManager(User hrManager) { + this.hrManager = hrManager; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/OnboardingStatus.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/OnboardingStatus.java new file mode 100644 index 00000000..64b89a24 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/OnboardingStatus.java @@ -0,0 +1,33 @@ +package com.company.onboarding.entity; + +import io.jmix.core.metamodel.datatype.EnumClass; + +import org.springframework.lang.Nullable; + + +public enum OnboardingStatus implements EnumClass { + + NOT_STARTED(10), + IN_PROGRESS(20), + COMPLETED(30); + + private final Integer id; + + OnboardingStatus(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + @Nullable + public static OnboardingStatus fromId(Integer id) { + for (OnboardingStatus at : OnboardingStatus.values()) { + if (at.getId().equals(id)) { + return at; + } + } + return null; + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/Step.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/Step.java new file mode 100644 index 00000000..89395958 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/Step.java @@ -0,0 +1,76 @@ +package com.company.onboarding.entity; + +import io.jmix.core.entity.annotation.JmixGeneratedValue; +import io.jmix.core.metamodel.annotation.InstanceName; +import io.jmix.core.metamodel.annotation.JmixEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +@JmixEntity +@Table(name = "STEP") +@Entity +public class Step { + @JmixGeneratedValue + @Column(name = "ID", nullable = false) + @Id + private UUID id; + + @Column(name = "VERSION", nullable = false) + @Version + private Integer version; + + @InstanceName + @Column(name = "NAME", nullable = false) + @NotNull + private String name; + + @NotNull + @Column(name = "DURATION", nullable = false) + private Integer duration; + + @Column(name = "SORT_VALUE", nullable = false) + @NotNull + private Integer sortValue; + + public Integer getSortValue() { + return sortValue; + } + + public void setSortValue(Integer sortValue) { + this.sortValue = sortValue; + } + + public Integer getDuration() { + return duration; + } + + public void setDuration(Integer duration) { + this.duration = duration; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/User.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/User.java new file mode 100644 index 00000000..4943f09a --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/User.java @@ -0,0 +1,235 @@ +package com.company.onboarding.entity; + +import io.jmix.core.FileRef; +import io.jmix.core.HasTimeZone; +import io.jmix.core.annotation.Secret; +import io.jmix.core.entity.annotation.JmixGeneratedValue; +import io.jmix.core.entity.annotation.SystemLevel; +import io.jmix.core.metamodel.annotation.Composition; +import io.jmix.core.metamodel.annotation.DependsOnProperties; +import io.jmix.core.metamodel.annotation.InstanceName; +import io.jmix.core.metamodel.annotation.JmixEntity; +import io.jmix.security.authentication.JmixUserDetails; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import org.springframework.security.core.GrantedAuthority; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@JmixEntity +@Entity +@Table(name = "USER_", indexes = { + @Index(name = "IDX_USER__ON_USERNAME", columnList = "USERNAME", unique = true), + @Index(name = "IDX_USER__DEPARTMENT", columnList = "DEPARTMENT_ID") +}) +public class User implements JmixUserDetails, HasTimeZone { + + @Id + @Column(name = "ID", nullable = false) + @JmixGeneratedValue + private UUID id; + + @Version + @Column(name = "VERSION", nullable = false) + private Integer version; + + @Column(name = "USERNAME", nullable = false) + protected String username; + + @Secret + @SystemLevel + @Column(name = "PASSWORD") + protected String password; + + @Column(name = "FIRST_NAME") + protected String firstName; + + @Column(name = "LAST_NAME") + protected String lastName; + + @Email + @Column(name = "EMAIL") + protected String email; + + @Column(name = "ACTIVE") + protected Boolean active = true; + + @Column(name = "TIME_ZONE_ID") + protected String timeZoneId; + + @Column(name = "ONBOARDING_STATUS") + private Integer onboardingStatus; + + @JoinColumn(name = "DEPARTMENT_ID") + @ManyToOne(fetch = FetchType.LAZY) + private Department department; + + @OrderBy("sortValue") + @Composition + @OneToMany(mappedBy = "user") + private List steps; + + @Column(name = "JOINING_DATE") + private LocalDate joiningDate; + + @Column(name = "PICTURE", length = 1024) + private FileRef picture; + + @Transient + protected Collection authorities; + + public FileRef getPicture() { + return picture; + } + + public void setPicture(FileRef picture) { + this.picture = picture; + } + + public LocalDate getJoiningDate() { + return joiningDate; + } + + public void setJoiningDate(LocalDate joiningDate) { + this.joiningDate = joiningDate; + } + + public List getSteps() { + return steps; + } + + public void setSteps(List steps) { + this.steps = steps; + } + + public Department getDepartment() { + return department; + } + + public void setDepartment(Department department) { + this.department = department; + } + + public OnboardingStatus getOnboardingStatus() { + return onboardingStatus == null ? null : OnboardingStatus.fromId(onboardingStatus); + } + + public void setOnboardingStatus(OnboardingStatus onboardingStatus) { + this.onboardingStatus = onboardingStatus == null ? null : onboardingStatus.getId(); + } + + public UUID getId() { + return id; + } + + public void setId(final UUID id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(final Integer version) { + this.version = version; + } + + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + public void setUsername(final String username) { + this.username = username; + } + + public Boolean getActive() { + return active; + } + + public void setActive(final Boolean active) { + this.active = active; + } + + public void setPassword(final String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(final String email) { + this.email = email; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(final String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(final String lastName) { + this.lastName = lastName; + } + + @Override + public Collection getAuthorities() { + return authorities != null ? authorities : Collections.emptyList(); + } + + @Override + public void setAuthorities(final Collection authorities) { + this.authorities = authorities; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return Boolean.TRUE.equals(active); + } + + @InstanceName + @DependsOnProperties({"firstName", "lastName", "username"}) + public String getDisplayName() { + return String.format("%s %s [%s]", (firstName != null ? firstName : ""), + (lastName != null ? lastName : ""), username).trim(); + } + + @Override + public String getTimeZoneId() { + return timeZoneId; + } + + public void setTimeZoneId(final String timeZoneId) { + this.timeZoneId = timeZoneId; + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/UserStep.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/UserStep.java new file mode 100644 index 00000000..5669fe38 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/entity/UserStep.java @@ -0,0 +1,103 @@ +package com.company.onboarding.entity; + +import io.jmix.core.entity.annotation.JmixGeneratedValue; +import io.jmix.core.metamodel.annotation.JmixEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDate; +import java.util.UUID; + +@JmixEntity +@Table(name = "USER_STEP", indexes = { + @Index(name = "IDX_USER_STEP_USER", columnList = "USER_ID"), + @Index(name = "IDX_USER_STEP_STEP", columnList = "STEP_ID") +}) +@Entity +public class UserStep { + @JmixGeneratedValue + @Column(name = "ID", nullable = false) + @Id + private UUID id; + + @Column(name = "VERSION", nullable = false) + @Version + private Integer version; + + @JoinColumn(name = "USER_ID", nullable = false) + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private User user; + + @JoinColumn(name = "STEP_ID", nullable = false) + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private Step step; + + @Column(name = "DUE_DATE", nullable = false) + @NotNull + private LocalDate dueDate; + + @Column(name = "COMPLETED_DATE") + private LocalDate completedDate; + + @Column(name = "SORT_VALUE", nullable = false) + @NotNull + private Integer sortValue; + + public Integer getSortValue() { + return sortValue; + } + + public void setSortValue(Integer sortValue) { + this.sortValue = sortValue; + } + + public LocalDate getCompletedDate() { + return completedDate; + } + + public void setCompletedDate(LocalDate completedDate) { + this.completedDate = completedDate; + } + + public LocalDate getDueDate() { + return dueDate; + } + + public void setDueDate(LocalDate dueDate) { + this.dueDate = dueDate; + } + + public Step getStep() { + return step; + } + + public void setStep(Step step) { + this.step = step; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/listener/DemoDataInitializer.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/listener/DemoDataInitializer.java new file mode 100644 index 00000000..f003ee32 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/listener/DemoDataInitializer.java @@ -0,0 +1,300 @@ +package com.company.onboarding.listener; + +import com.company.onboarding.entity.Department; +import com.company.onboarding.entity.Step; +import com.company.onboarding.entity.User; +import com.company.onboarding.entity.UserStep; +import io.jmix.core.DataManager; +import io.jmix.core.FileRef; +import io.jmix.core.FileStorage; +import io.jmix.core.SaveContext; +import io.jmix.core.security.Authenticated; +import io.jmix.security.role.assignment.RoleAssignmentRoleType; +import io.jmix.securitydata.entity.RoleAssignmentEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.io.ClassPathResource; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Component +public class DemoDataInitializer { + + @Autowired + private DataManager dataManager; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private FileStorage fileStorage; + + @EventListener + @Authenticated + public void onApplicationStarted(ApplicationStartedEvent event) { + if (dataManager.load(Step.class).all().maxResults(1).list().size() > 0) { + return; + } + List steps = initSteps(); + List departments = initDepartments(); + List users = initUsers(steps, departments); + assignRoles(users); + } + + private List initSteps() { + Step step; + ArrayList list = new ArrayList<>(); + + step = dataManager.create(Step.class); + step.setName("Safety briefing"); + step.setDuration(1); + step.setSortValue(10); + list.add(dataManager.save(step)); + + step = dataManager.create(Step.class); + step.setName("Fill in profile"); + step.setDuration(1); + step.setSortValue(20); + list.add(dataManager.save(step)); + + step = dataManager.create(Step.class); + step.setName("Check all functions"); + step.setDuration(2); + step.setSortValue(30); + list.add(dataManager.save(step)); + + step = dataManager.create(Step.class); + step.setName("Information security training"); + step.setDuration(3); + step.setSortValue(40); + list.add(dataManager.save(step)); + + step = dataManager.create(Step.class); + step.setName("Internal procedures studying"); + step.setDuration(5); + step.setSortValue(50); + list.add(dataManager.save(step)); + + return list; + } + + private List initDepartments() { + Department department; + List list = new ArrayList<>(); + + department = dataManager.create(Department.class); + department.setName("Human Resources"); + list.add(dataManager.save(department)); + + department = dataManager.create(Department.class); + department.setName("Marketing"); + list.add(dataManager.save(department)); + + department = dataManager.create(Department.class); + department.setName("Operations"); + list.add(dataManager.save(department)); + + department = dataManager.create(Department.class); + department.setName("Finance"); + list.add(dataManager.save(department)); + + return list; + } + + private List initUsers(List steps, List departments) { + User user; + SaveContext saveContext; + List list = new ArrayList<>(); + + saveContext = new SaveContext(); + user = dataManager.create(User.class); + user.setUsername("alice"); + user.setPassword(createPassword()); + user.setFirstName("Alice"); + user.setLastName("Brown"); + user.setDepartment(departments.get(0)); + user.setJoiningDate(LocalDate.now().minusYears(2).minusWeeks(3)); + user.setPicture(uploadPicture("com/company/onboarding/demo/" , "alice.png")); + saveContext.saving(user); + list.add(user); + for (Step step : steps) { + UserStep userStep = dataManager.create(UserStep.class); + userStep.setUser(user); + userStep.setStep(step); + userStep.setDueDate(user.getJoiningDate().plusDays(step.getDuration())); + userStep.setCompletedDate(user.getJoiningDate().plusDays(step.getDuration() - 1)); + userStep.setSortValue(step.getSortValue()); + saveContext.saving(userStep); + } + dataManager.save(saveContext); + + Department marketingDept = departments.get(1); + marketingDept.setHrManager(user); + dataManager.save(marketingDept); + + saveContext = new SaveContext(); + user = dataManager.create(User.class); + user.setUsername("james"); + user.setPassword(createPassword()); + user.setFirstName("James"); + user.setLastName("Wilson"); + user.setDepartment(departments.get(0)); + user.setJoiningDate(LocalDate.now().minusYears(1).minusWeeks(5)); + user.setPicture(uploadPicture("com/company/onboarding/demo/" , "james.png")); + saveContext.saving(user); + list.add(user); + for (Step step : steps) { + UserStep userStep = dataManager.create(UserStep.class); + userStep.setUser(user); + userStep.setStep(step); + userStep.setDueDate(user.getJoiningDate().plusDays(step.getDuration())); + userStep.setCompletedDate(user.getJoiningDate().plusDays(step.getDuration() - 1)); + userStep.setSortValue(step.getSortValue()); + saveContext.saving(userStep); + } + dataManager.save(saveContext); + + Department operationsDept = departments.get(2); + operationsDept.setHrManager(user); + dataManager.save(operationsDept); + + saveContext = new SaveContext(); + user = dataManager.create(User.class); + user.setUsername("mary"); + user.setPassword(createPassword()); + user.setFirstName("Mary"); + user.setLastName("Jones"); + user.setDepartment(departments.get(1)); + user.setJoiningDate(LocalDate.now().minusDays(3)); + user.setPicture(uploadPicture("com/company/onboarding/demo/", "mary.png")); + saveContext.saving(user); + list.add(user); + for (Step step : steps) { + UserStep userStep = dataManager.create(UserStep.class); + userStep.setUser(user); + userStep.setStep(step); + userStep.setDueDate(user.getJoiningDate().plusDays(step.getDuration())); + userStep.setCompletedDate(null); + userStep.setSortValue(step.getSortValue()); + saveContext.saving(userStep); + } + dataManager.save(saveContext); + + saveContext = new SaveContext(); + user = dataManager.create(User.class); + user.setUsername("linda"); + user.setPassword(createPassword()); + user.setFirstName("Linda"); + user.setLastName("Evans"); + user.setDepartment(departments.get(2)); + user.setJoiningDate(LocalDate.now().minusDays(2)); + user.setPicture(uploadPicture("com/company/onboarding/demo/", "linda.png")); + saveContext.saving(user); + list.add(user); + for (Step step : steps) { + UserStep userStep = dataManager.create(UserStep.class); + userStep.setUser(user); + userStep.setStep(step); + userStep.setDueDate(user.getJoiningDate().plusDays(step.getDuration())); + userStep.setCompletedDate(null); + userStep.setSortValue(step.getSortValue()); + saveContext.saving(userStep); + } + dataManager.save(saveContext); + + saveContext = new SaveContext(); + user = dataManager.create(User.class); + user.setUsername("susan"); + user.setPassword(createPassword()); + user.setFirstName("Susan"); + user.setLastName("Baker"); + user.setDepartment(departments.get(2)); + user.setJoiningDate(LocalDate.now()); + user.setPicture(uploadPicture("com/company/onboarding/demo/", "susan.png")); + saveContext.saving(user); + list.add(user); + for (Step step : steps) { + UserStep userStep = dataManager.create(UserStep.class); + userStep.setUser(user); + userStep.setStep(step); + userStep.setDueDate(user.getJoiningDate().plusDays(step.getDuration())); + userStep.setCompletedDate(null); + userStep.setSortValue(step.getSortValue()); + saveContext.saving(userStep); + } + dataManager.save(saveContext); + + saveContext = new SaveContext(); + user = dataManager.create(User.class); + user.setUsername("bob"); + user.setPassword(createPassword()); + user.setFirstName("Robert"); + user.setLastName("Taylor"); + user.setDepartment(departments.get(2)); + user.setJoiningDate(LocalDate.now().minusDays(1)); + user.setPicture(uploadPicture("com/company/onboarding/demo/", "bob.png")); + saveContext.saving(user); + list.add(user); + for (Step step : steps) { + UserStep userStep = dataManager.create(UserStep.class); + userStep.setUser(user); + userStep.setStep(step); + userStep.setDueDate(user.getJoiningDate().plusDays(step.getDuration())); + userStep.setCompletedDate(userStep.getDueDate().isBefore(LocalDate.now().plusDays(1)) ? userStep.getDueDate() : null); + userStep.setSortValue(step.getSortValue()); + saveContext.saving(userStep); + } + dataManager.save(saveContext); + + return list; + } + + private FileRef uploadPicture(String path, String fileName) { + ClassPathResource resource = new ClassPathResource(path + fileName); + try (InputStream stream = resource.getInputStream()) { + return fileStorage.saveStream(fileName, stream); + } catch (IOException e) { + throw new RuntimeException("Cannot read resource: " + path + fileName, e); + } + } + + private String createPassword() { + return passwordEncoder.encode("1"); + } + + private void assignRoles(List users) { + for (User user : users) { + boolean isHrManager = Arrays.asList("alice", "james").contains(user.getUsername()); + + RoleAssignmentEntity roleAssignment; + + roleAssignment = dataManager.create(RoleAssignmentEntity.class); + roleAssignment.setUsername(user.getUsername()); + roleAssignment.setRoleCode("ui-minimal"); + roleAssignment.setRoleType(RoleAssignmentRoleType.RESOURCE); + dataManager.save(roleAssignment); + + roleAssignment = dataManager.create(RoleAssignmentEntity.class); + roleAssignment.setUsername(user.getUsername()); + roleAssignment.setRoleCode(isHrManager ? "hr-manager" : "employee"); + roleAssignment.setRoleType(RoleAssignmentRoleType.RESOURCE); + dataManager.save(roleAssignment); + + if (isHrManager) { + roleAssignment = dataManager.create(RoleAssignmentEntity.class); + roleAssignment.setUsername(user.getUsername()); + roleAssignment.setRoleCode("hr-manager-rl"); + roleAssignment.setRoleType(RoleAssignmentRoleType.ROW_LEVEL); + dataManager.save(roleAssignment); + } + } + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/listener/UserStepEventListener.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/listener/UserStepEventListener.java new file mode 100644 index 00000000..9f6e8c20 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/listener/UserStepEventListener.java @@ -0,0 +1,47 @@ +package com.company.onboarding.listener; + +import com.company.onboarding.entity.OnboardingStatus; +import com.company.onboarding.entity.User; +import com.company.onboarding.entity.UserStep; +import io.jmix.core.DataManager; +import io.jmix.core.Id; +import io.jmix.core.event.EntityChangedEvent; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class UserStepEventListener { + + @Autowired + private DataManager dataManager; + + @EventListener + public void onUserStepChangedBeforeCommit(final EntityChangedEvent event) { + User user; + if (event.getType() != EntityChangedEvent.Type.DELETED) { + Id userStepId = event.getEntityId(); + UserStep userStep = dataManager.load(userStepId).one(); + user = userStep.getUser(); + } else { + Id userId = event.getChanges().getOldReferenceId("user"); + if (userId == null) { + throw new IllegalStateException("Cannot get User from deleted UserStep"); + } + user = dataManager.load(userId).one(); + } + + long completedCount = user.getSteps().stream() + .filter(us -> us.getCompletedDate() != null) + .count(); + if (completedCount == 0) { + user.setOnboardingStatus(OnboardingStatus.NOT_STARTED); + } else if (completedCount == user.getSteps().size()) { + user.setOnboardingStatus(OnboardingStatus.COMPLETED); + } else { + user.setOnboardingStatus(OnboardingStatus.IN_PROGRESS); + } + + dataManager.save(user); + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/DatabaseUserRepository.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/DatabaseUserRepository.java new file mode 100644 index 00000000..fc6581b0 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/DatabaseUserRepository.java @@ -0,0 +1,31 @@ +package com.company.onboarding.security; + +import com.company.onboarding.entity.User; +import io.jmix.securitydata.user.AbstractDatabaseUserRepository; +import org.springframework.context.annotation.Primary; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +@Primary +@Component("UserRepository") +public class DatabaseUserRepository extends AbstractDatabaseUserRepository { + + @Override + protected Class getUserClass() { + return User.class; + } + + @Override + protected void initSystemUser(final User systemUser) { + final Collection authorities = getGrantedAuthoritiesBuilder() + .addResourceRole(FullAccessRole.CODE) + .build(); + systemUser.setAuthorities(authorities); + } + + @Override + protected void initAnonymousUser(final User anonymousUser) { + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/EmployeeRole.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/EmployeeRole.java new file mode 100644 index 00000000..f5a89592 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/EmployeeRole.java @@ -0,0 +1,40 @@ +package com.company.onboarding.security; + +import com.company.onboarding.entity.Step; +import com.company.onboarding.entity.User; +import com.company.onboarding.entity.UserStep; +import io.jmix.security.model.EntityAttributePolicyAction; +import io.jmix.security.model.EntityPolicyAction; +import io.jmix.security.role.annotation.EntityAttributePolicy; +import io.jmix.security.role.annotation.EntityPolicy; +import io.jmix.security.role.annotation.ResourceRole; +import io.jmix.securityflowui.role.annotation.MenuPolicy; +import io.jmix.securityflowui.role.annotation.ViewPolicy; + +@ResourceRole(name = "Employee", code = "employee", scope = "UI") +public interface EmployeeRole { + @MenuPolicy(menuIds = "MyOnboardingView") + @ViewPolicy(viewIds = "MyOnboardingView") + void screens(); + + @EntityAttributePolicy(entityClass = User.class, + attributes = "*", + action = EntityAttributePolicyAction.VIEW) + @EntityPolicy(entityClass = User.class, + actions = {EntityPolicyAction.READ, EntityPolicyAction.UPDATE}) + void user(); + + @EntityAttributePolicy(entityClass = UserStep.class, + attributes = "*", + action = EntityAttributePolicyAction.VIEW) + @EntityPolicy(entityClass = UserStep.class, + actions = {EntityPolicyAction.READ, EntityPolicyAction.UPDATE}) + void userStep(); + + @EntityAttributePolicy(entityClass = Step.class, + attributes = "*", + action = EntityAttributePolicyAction.VIEW) + @EntityPolicy(entityClass = Step.class, + actions = EntityPolicyAction.READ) + void step(); +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/FullAccessRole.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/FullAccessRole.java new file mode 100644 index 00000000..21ab0382 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/FullAccessRole.java @@ -0,0 +1,23 @@ +package com.company.onboarding.security; + +import io.jmix.security.model.EntityAttributePolicyAction; +import io.jmix.security.model.EntityPolicyAction; +import io.jmix.security.role.annotation.EntityAttributePolicy; +import io.jmix.security.role.annotation.EntityPolicy; +import io.jmix.security.role.annotation.ResourceRole; +import io.jmix.security.role.annotation.SpecificPolicy; +import io.jmix.securityflowui.role.annotation.MenuPolicy; +import io.jmix.securityflowui.role.annotation.ViewPolicy; + +@ResourceRole(name = "Full Access", code = FullAccessRole.CODE) +public interface FullAccessRole { + + String CODE = "system-full-access"; + + @EntityPolicy(entityName = "*", actions = {EntityPolicyAction.ALL}) + @EntityAttributePolicy(entityName = "*", attributes = "*", action = EntityAttributePolicyAction.MODIFY) + @ViewPolicy(viewIds = "*") + @MenuPolicy(menuIds = "*") + @SpecificPolicy(resources = "*") + void fullAccess(); +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/HRManagerRole.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/HRManagerRole.java new file mode 100644 index 00000000..676ee08a --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/HRManagerRole.java @@ -0,0 +1,48 @@ +package com.company.onboarding.security; + +import com.company.onboarding.entity.Department; +import com.company.onboarding.entity.Step; +import com.company.onboarding.entity.User; +import com.company.onboarding.entity.UserStep; +import io.jmix.security.model.EntityAttributePolicyAction; +import io.jmix.security.model.EntityPolicyAction; +import io.jmix.security.role.annotation.EntityAttributePolicy; +import io.jmix.security.role.annotation.EntityPolicy; +import io.jmix.security.role.annotation.ResourceRole; +import io.jmix.securityflowui.role.annotation.MenuPolicy; +import io.jmix.securityflowui.role.annotation.ViewPolicy; + +@ResourceRole(name = "HR Manager", code = "hr-manager", scope = "UI") +public interface HRManagerRole { + @MenuPolicy(menuIds = "User.list") + @ViewPolicy(viewIds = {"User.detail", "User.list"}) + void screens(); + + @EntityAttributePolicy(entityClass = Department.class, + attributes = "*", + action = EntityAttributePolicyAction.VIEW) + @EntityPolicy(entityClass = Department.class, + actions = EntityPolicyAction.READ) + void department(); + + @EntityAttributePolicy(entityClass = Step.class, + attributes = "*", + action = EntityAttributePolicyAction.VIEW) + @EntityPolicy(entityClass = Step.class, + actions = EntityPolicyAction.READ) + void step(); + + @EntityAttributePolicy(entityClass = User.class, + attributes = "*", + action = EntityAttributePolicyAction.MODIFY) + @EntityPolicy(entityClass = User.class, + actions = EntityPolicyAction.ALL) + void user(); + + @EntityAttributePolicy(entityClass = UserStep.class, + attributes = "*", + action = EntityAttributePolicyAction.MODIFY) + @EntityPolicy(entityClass = UserStep.class, + actions = EntityPolicyAction.ALL) + void userStep(); +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/HrManagerRlRole.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/HrManagerRlRole.java new file mode 100644 index 00000000..cab3dbc1 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/HrManagerRlRole.java @@ -0,0 +1,21 @@ +package com.company.onboarding.security; + +import com.company.onboarding.entity.Department; +import com.company.onboarding.entity.User; +import io.jmix.security.role.annotation.JpqlRowLevelPolicy; +import io.jmix.security.role.annotation.RowLevelRole; + +@RowLevelRole(name = "HR manager's departments and users", + code = "hr-manager-rl") +public interface HrManagerRlRole { + + @JpqlRowLevelPolicy( + entityClass = Department.class, + where = "{E}.hrManager.id = :current_user_id") + void department1(); + + @JpqlRowLevelPolicy( + entityClass = User.class, + where = "{E}.department.hrManager.id = :current_user_id") + void department(); +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/OnboardingSecurityConfiguration.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/OnboardingSecurityConfiguration.java new file mode 100644 index 00000000..c62d0201 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/OnboardingSecurityConfiguration.java @@ -0,0 +1,49 @@ +package com.company.onboarding.security; + +import io.jmix.core.JmixSecurityFilterChainOrder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +/** + * This configuration complements standard security configurations that come from Jmix modules (security-flowui, oidc, + * authserver). + *

+ * You can configure custom API endpoints security by defining {@link SecurityFilterChain} beans in this class. + * In most cases, custom SecurityFilterChain must be applied first, so the proper + * {@link Order} should be defined for the bean. The order value from the + * {@link JmixSecurityFilterChainOrder#CUSTOM} is guaranteed to be smaller than any other filter chain + * order from Jmix. + *

+ * Example: + * + *

+ * @Bean
+ * @Order(JmixSecurityFilterChainOrder.CUSTOM)
+ * SecurityFilterChain publicFilterChain(HttpSecurity http) throws Exception {
+ *     http.securityMatcher("/public/**")
+ *             .authorizeHttpRequests(authorize ->
+ *                     authorize.anyRequest().permitAll()
+ *             );
+ *     return http.build();
+ * }
+ * 
+ * + * @see io.jmix.securityflowui.security.FlowuiVaadinWebSecurity + */ +@Configuration +public class OnboardingSecurityConfiguration { + + @Bean + @Order(JmixSecurityFilterChainOrder.CUSTOM) + SecurityFilterChain publicFilterChain(HttpSecurity http) throws Exception { + http.securityMatcher("/public/**") + .authorizeHttpRequests(authorize -> + authorize.anyRequest().permitAll() + ); + + return http.build(); + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/UiMinimalRole.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/UiMinimalRole.java new file mode 100644 index 00000000..7f59786b --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/security/UiMinimalRole.java @@ -0,0 +1,28 @@ +package com.company.onboarding.security; + +import io.jmix.core.entity.KeyValueEntity; +import io.jmix.security.model.EntityAttributePolicyAction; +import io.jmix.security.model.EntityPolicyAction; +import io.jmix.security.model.SecurityScope; +import io.jmix.security.role.annotation.EntityAttributePolicy; +import io.jmix.security.role.annotation.EntityPolicy; +import io.jmix.security.role.annotation.ResourceRole; +import io.jmix.security.role.annotation.SpecificPolicy; +import io.jmix.securityflowui.role.annotation.ViewPolicy; + +@ResourceRole(name = "UI: minimal access", code = UiMinimalRole.CODE, scope = SecurityScope.UI) +public interface UiMinimalRole { + + String CODE = "ui-minimal"; + + @ViewPolicy(viewIds = "MainView") + void main(); + + @ViewPolicy(viewIds = "LoginView") + @SpecificPolicy(resources = "ui.loginToUi") + void login(); + + @EntityPolicy(entityClass = KeyValueEntity.class, actions = EntityPolicyAction.READ) + @EntityAttributePolicy(entityClass = KeyValueEntity.class, attributes = "*", action = EntityAttributePolicyAction.VIEW) + void keyValueEntity(); +} diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/assistant/AssistantView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/assistant/AssistantView.java new file mode 100644 index 00000000..c883275f --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/assistant/AssistantView.java @@ -0,0 +1,13 @@ +package com.company.onboarding.view.assistant; + +import com.company.onboarding.view.main.MainView; +import com.vaadin.flow.router.Route; +import io.jmix.flowui.view.StandardView; +import io.jmix.flowui.view.ViewController; +import io.jmix.flowui.view.ViewDescriptor; + +@Route(value = "assistant", layout = MainView.class) +@ViewController("AssistantView") +@ViewDescriptor("assistant-view.xml") +public class AssistantView extends StandardView { +} diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/department/DepartmentDetailView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/department/DepartmentDetailView.java new file mode 100644 index 00000000..207c6b9f --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/department/DepartmentDetailView.java @@ -0,0 +1,15 @@ +package com.company.onboarding.view.department; + +import com.company.onboarding.entity.Department; + +import com.company.onboarding.view.main.MainView; + +import com.vaadin.flow.router.Route; +import io.jmix.flowui.view.*; + +@Route(value = "departments/:id", layout = MainView.class) +@ViewController("Department.detail") +@ViewDescriptor("department-detail-view.xml") +@EditedEntityContainer("departmentDc") +public class DepartmentDetailView extends StandardDetailView { +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/department/DepartmentListView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/department/DepartmentListView.java new file mode 100644 index 00000000..d5fae7f3 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/department/DepartmentListView.java @@ -0,0 +1,16 @@ +package com.company.onboarding.view.department; + +import com.company.onboarding.entity.Department; + +import com.company.onboarding.view.main.MainView; + +import com.vaadin.flow.router.Route; +import io.jmix.flowui.view.*; + +@Route(value = "departments", layout = MainView.class) +@ViewController("Department.list") +@ViewDescriptor("department-list-view.xml") +@LookupComponent("departmentsDataGrid") +@DialogMode(width = "50em", height = "37.5em") +public class DepartmentListView extends StandardListView { +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/login/LoginView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/login/LoginView.java new file mode 100644 index 00000000..fa454c54 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/login/LoginView.java @@ -0,0 +1,112 @@ +package com.company.onboarding.view.login; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.login.AbstractLogin.LoginEvent; +import com.vaadin.flow.component.login.LoginI18n; +import com.vaadin.flow.i18n.LocaleChangeEvent; +import com.vaadin.flow.i18n.LocaleChangeObserver; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.VaadinSession; +import io.jmix.core.MessageTools; +import io.jmix.core.security.AccessDeniedException; +import io.jmix.flowui.component.loginform.JmixLoginForm; +import io.jmix.flowui.kit.component.ComponentUtils; +import io.jmix.flowui.kit.component.loginform.JmixLoginI18n; +import io.jmix.flowui.view.*; +import io.jmix.securityflowui.authentication.AuthDetails; +import io.jmix.securityflowui.authentication.LoginViewSupport; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.LockedException; + +@Route(value = "login") +@ViewController("LoginView") +@ViewDescriptor("login-view.xml") +public class LoginView extends StandardView implements LocaleChangeObserver { + + private static final Logger log = LoggerFactory.getLogger(LoginView.class); + + @Autowired + private LoginViewSupport loginViewSupport; + + @ViewComponent + private MessageBundle messageBundle; + + @Autowired + private MessageTools messageTools; + + @ViewComponent + private JmixLoginForm login; + + @Value("${ui.login.defaultUsername:}") + private String defaultUsername; + + @Value("${ui.login.defaultPassword:}") + private String defaultPassword; + + @Subscribe + public void onInit(final InitEvent event) { + initLocales(); + initDefaultCredentials(); + } + + protected void initLocales() { + ComponentUtils.setItemsMap(login, + MapUtils.invertMap(messageTools.getAvailableLocalesMap())); + + login.setSelectedLocale(VaadinSession.getCurrent().getLocale()); + } + + protected void initDefaultCredentials() { + if (StringUtils.isNotBlank(defaultUsername)) { + login.setUsername(defaultUsername); + } + + if (StringUtils.isNotBlank(defaultPassword)) { + login.setPassword(defaultPassword); + } + } + + @Subscribe("login") + public void onLogin(final LoginEvent event) { + try { + loginViewSupport.authenticate( + AuthDetails.of(event.getUsername(), event.getPassword()) + .withLocale(login.getSelectedLocale()) + .withRememberMe(login.isRememberMe()) + ); + } catch (final BadCredentialsException | DisabledException | LockedException | AccessDeniedException e) { + log.warn("Login failed for user '{}': {}", event.getUsername(), e.toString()); + event.getSource().setError(true); + } + } + + @Override + public void localeChange(final LocaleChangeEvent event) { + UI.getCurrent().getPage().setTitle(messageBundle.getMessage("LoginView.title")); + + final JmixLoginI18n loginI18n = JmixLoginI18n.createDefault(); + + final JmixLoginI18n.JmixForm form = new JmixLoginI18n.JmixForm(); + form.setTitle(messageBundle.getMessage("loginForm.headerTitle")); + form.setUsername(messageBundle.getMessage("loginForm.username")); + form.setPassword(messageBundle.getMessage("loginForm.password")); + form.setSubmit(messageBundle.getMessage("loginForm.submit")); + form.setForgotPassword(messageBundle.getMessage("loginForm.forgotPassword")); + form.setRememberMe(messageBundle.getMessage("loginForm.rememberMe")); + loginI18n.setForm(form); + + final LoginI18n.ErrorMessage errorMessage = new LoginI18n.ErrorMessage(); + errorMessage.setTitle(messageBundle.getMessage("loginForm.errorTitle")); + errorMessage.setMessage(messageBundle.getMessage("loginForm.badCredentials")); + loginI18n.setErrorMessage(errorMessage); + + login.setI18n(loginI18n); + } +} diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/main/MainView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/main/MainView.java new file mode 100644 index 00000000..3a5809ed --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/main/MainView.java @@ -0,0 +1,118 @@ +package com.company.onboarding.view.main; + +import com.company.onboarding.entity.User; +import com.google.common.base.Strings; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.avatar.Avatar; +import com.vaadin.flow.component.avatar.AvatarVariant; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; +import io.jmix.core.Messages; +import io.jmix.core.usersubstitution.CurrentUserSubstitution; +import io.jmix.flowui.UiComponents; +import io.jmix.flowui.app.main.StandardMainView; +import io.jmix.flowui.view.Install; +import io.jmix.flowui.view.ViewController; +import io.jmix.flowui.view.ViewDescriptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; + +@Route("") +@ViewController("MainView") +@ViewDescriptor("main-view.xml") +public class MainView extends StandardMainView { + + @Autowired + private Messages messages; + @Autowired + private UiComponents uiComponents; + @Autowired + private CurrentUserSubstitution currentUserSubstitution; + + @Install(to = "userMenu", subject = "buttonRenderer") + private Component userMenuButtonRenderer(final UserDetails userDetails) { + if (!(userDetails instanceof User user)) { + return null; + } + + String userName = generateUserName(user); + + Div content = uiComponents.create(Div.class); + content.setClassName("user-menu-button-content"); + + Avatar avatar = createAvatar(userName); + + Span name = uiComponents.create(Span.class); + name.setText(userName); + name.setClassName("user-menu-text"); + + content.add(avatar, name); + + if (isSubstituted(user)) { + Span subtext = uiComponents.create(Span.class); + subtext.setText(messages.getMessage("userMenu.substituted")); + subtext.setClassName("user-menu-subtext"); + + content.add(subtext); + } + + return content; + } + + @Install(to = "userMenu", subject = "headerRenderer") + private Component userMenuHeaderRenderer(final UserDetails userDetails) { + if (!(userDetails instanceof User user)) { + return null; + } + + Div content = uiComponents.create(Div.class); + content.setClassName("user-menu-header-content"); + + String name = generateUserName(user); + + Avatar avatar = createAvatar(name); + avatar.addThemeVariants(AvatarVariant.LUMO_LARGE); + + Span text = uiComponents.create(Span.class); + text.setText(name); + text.setClassName("user-menu-text"); + + content.add(avatar, text); + + if (name.equals(user.getUsername())) { + text.addClassName("user-menu-text-subtext"); + } else { + Span subtext = uiComponents.create(Span.class); + subtext.setText(user.getUsername()); + subtext.setClassName("user-menu-subtext"); + + content.add(subtext); + } + + return content; + } + + private Avatar createAvatar(String fullName) { + Avatar avatar = uiComponents.create(Avatar.class); + avatar.setName(fullName); + avatar.getElement().setAttribute("tabindex", "-1"); + avatar.setClassName("user-menu-avatar"); + + return avatar; + } + + private String generateUserName(User user) { + String userName = String.format("%s %s", + Strings.nullToEmpty(user.getFirstName()), + Strings.nullToEmpty(user.getLastName())) + .trim(); + + return userName.isEmpty() ? user.getUsername() : userName; + } + + private boolean isSubstituted(User user) { + UserDetails authenticatedUser = currentUserSubstitution.getAuthenticatedUser(); + return user != null && !authenticatedUser.getUsername().equals(user.getUsername()); + } +} diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/myonboarding/MyOnboardingView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/myonboarding/MyOnboardingView.java new file mode 100644 index 00000000..c7ba3093 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/myonboarding/MyOnboardingView.java @@ -0,0 +1,123 @@ +package com.company.onboarding.view.myonboarding; + + +import com.company.onboarding.entity.User; +import com.company.onboarding.entity.UserStep; +import com.company.onboarding.view.main.MainView; +import com.vaadin.flow.component.ClickEvent; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.router.Route; +import io.jmix.core.security.CurrentAuthentication; +import io.jmix.flowui.UiComponents; +import io.jmix.flowui.kit.component.button.JmixButton; +import io.jmix.flowui.model.CollectionContainer; +import io.jmix.flowui.model.CollectionLoader; +import io.jmix.flowui.model.DataContext; +import io.jmix.flowui.model.InstanceContainer; +import io.jmix.flowui.view.*; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + +@Route(value = "MyOnboardingView", layout = MainView.class) +@ViewController("MyOnboardingView") +@ViewDescriptor("my-onboarding-view.xml") +public class MyOnboardingView extends StandardView { + + @Autowired + private CurrentAuthentication currentAuthentication; + + @ViewComponent + private CollectionLoader userStepsDl; + + @Autowired + private UiComponents uiComponents; + + @ViewComponent + private CollectionContainer userStepsDc; + + @ViewComponent + private Span completedStepsLabel; + + @ViewComponent + private Span overdueStepsLabel; + + @ViewComponent + private Span totalStepsLabel; + + @ViewComponent + private DataContext dataContext; + + @Supply(to = "userStepsDataGrid.completed", subject = "renderer") + private Renderer userStepsDataGridCompletedRenderer() { + return new ComponentRenderer<>(userStep -> { + Checkbox checkbox = uiComponents.create(Checkbox.class); + checkbox.setValue(userStep.getCompletedDate() != null); + checkbox.addValueChangeListener(e -> { + if (userStep.getCompletedDate() == null) { + userStep.setCompletedDate(LocalDate.now()); + } else { + userStep.setCompletedDate(null); + } + }); + return checkbox; + }); + } + + private void updateLabels() { + totalStepsLabel.setText("Total steps: " + userStepsDc.getItems().size()); + + long completedCount = userStepsDc.getItems().stream() + .filter(us -> us.getCompletedDate() != null) + .count(); + completedStepsLabel.setText("Completed steps: " + completedCount); + + long overdueCount = userStepsDc.getItems().stream() + .filter(us -> isOverdue(us)) + .count(); + overdueStepsLabel.setText("Overdue steps: " + overdueCount); + } + + private boolean isOverdue(UserStep us) { + return us.getCompletedDate() == null + && us.getDueDate() != null + && us.getDueDate().isBefore(LocalDate.now()); + } + + @Subscribe + public void onBeforeShow(final BeforeShowEvent event) { + final User user = (User) currentAuthentication.getUser(); + userStepsDl.setParameter("user", user); + userStepsDl.load(); + + updateLabels(); + } + + @Subscribe(id = "userStepsDc", target = Target.DATA_CONTAINER) + public void onUserStepsDcItemPropertyChange(final InstanceContainer.ItemPropertyChangeEvent event) { + updateLabels(); + } + + @Subscribe("saveButton") + public void onSaveButtonClick(final ClickEvent event) { + dataContext.save(); + close(StandardOutcome.SAVE); + } + + @Subscribe("discardButton") + public void onDiscardButtonClick(final ClickEvent event) { + close(StandardOutcome.DISCARD); + } + + @Install(to = "userStepsDataGrid.dueDate", subject = "partNameGenerator") + private String userStepsDataGridDueDatePartNameGenerator(final UserStep userStep) { + return isOverdue(userStep) ? "overdue-step" : null; + } +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/step/StepDetailView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/step/StepDetailView.java new file mode 100644 index 00000000..febdfb7e --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/step/StepDetailView.java @@ -0,0 +1,15 @@ +package com.company.onboarding.view.step; + +import com.company.onboarding.entity.Step; + +import com.company.onboarding.view.main.MainView; + +import com.vaadin.flow.router.Route; +import io.jmix.flowui.view.*; + +@Route(value = "steps/:id", layout = MainView.class) +@ViewController("Step.detail") +@ViewDescriptor("step-detail-view.xml") +@EditedEntityContainer("stepDc") +public class StepDetailView extends StandardDetailView { +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/step/StepListView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/step/StepListView.java new file mode 100644 index 00000000..5b8ad952 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/step/StepListView.java @@ -0,0 +1,16 @@ +package com.company.onboarding.view.step; + +import com.company.onboarding.entity.Step; + +import com.company.onboarding.view.main.MainView; + +import com.vaadin.flow.router.Route; +import io.jmix.flowui.view.*; + +@Route(value = "steps", layout = MainView.class) +@ViewController("Step.list") +@ViewDescriptor("step-list-view.xml") +@LookupComponent("stepsDataGrid") +@DialogMode(width = "50em", height = "37.5em") +public class StepListView extends StandardListView { +} \ No newline at end of file diff --git a/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/user/UserDetailView.java b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/user/UserDetailView.java new file mode 100644 index 00000000..cc3c6537 --- /dev/null +++ b/content/modules/ai-tools/examples/ai-tools-ex1/src/main/java/com/company/onboarding/view/user/UserDetailView.java @@ -0,0 +1,174 @@ +package com.company.onboarding.view.user; + +import com.company.onboarding.entity.OnboardingStatus; +import com.company.onboarding.entity.Step; +import com.company.onboarding.entity.User; +import com.company.onboarding.entity.UserStep; +import com.company.onboarding.view.main.MainView; +import com.vaadin.flow.component.ClickEvent; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.data.renderer.ComponentRenderer; +import com.vaadin.flow.data.renderer.Renderer; +import com.vaadin.flow.router.Route; +import io.jmix.core.DataManager; +import io.jmix.core.EntityStates; +import io.jmix.flowui.Notifications; +import io.jmix.flowui.UiComponents; +import io.jmix.flowui.component.textfield.TypedTextField; +import io.jmix.flowui.model.CollectionContainer; +import io.jmix.flowui.model.CollectionPropertyContainer; +import io.jmix.flowui.model.DataContext; +import io.jmix.flowui.model.InstanceContainer; +import io.jmix.flowui.view.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; +import java.util.TimeZone; + +@Route(value = "users/:id", layout = MainView.class) +@ViewController("User.detail") +@ViewDescriptor("user-detail-view.xml") +@EditedEntityContainer("userDc") +public class UserDetailView extends StandardDetailView { + @Autowired + private DataManager dataManager; + + @Autowired + private Notifications notifications; + + @ViewComponent + private DataContext dataContext; + + @ViewComponent + private CollectionPropertyContainer stepsDc; + + @ViewComponent + private TypedTextField usernameField; + @ViewComponent + private PasswordField passwordField; + @ViewComponent + private PasswordField confirmPasswordField; + @ViewComponent + private ComboBox timeZoneField; + + @Autowired + private EntityStates entityStates; + @ViewComponent + private MessageBundle messageBundle; + @Autowired + private PasswordEncoder passwordEncoder; + @Autowired + private UiComponents uiComponents; + + @Subscribe + public void onInit(final InitEvent event) { + timeZoneField.setItems(List.of(TimeZone.getAvailableIDs())); + } + + @Subscribe + public void onInitEntity(final InitEntityEvent event) { + usernameField.setReadOnly(false); + passwordField.setVisible(true); + confirmPasswordField.setVisible(true); + + User user = event.getEntity(); + user.setOnboardingStatus(OnboardingStatus.NOT_STARTED); + } + + @Subscribe + public void onReady(final ReadyEvent event) { + if (entityStates.isNew(getEditedEntity())) { + usernameField.focus(); + } + } + + @Subscribe + public void onValidation(final ValidationEvent event) { + if (entityStates.isNew(getEditedEntity()) + && !Objects.equals(passwordField.getValue(), confirmPasswordField.getValue())) { + event.getErrors().add(messageBundle.getMessage("passwordsDoNotMatch")); + } + } + + @Subscribe + protected void onBeforeSave(final BeforeSaveEvent event) { + if (entityStates.isNew(getEditedEntity())) { + getEditedEntity().setPassword(passwordEncoder.encode(passwordField.getValue())); + } + } + + @Subscribe("generateButton") + public void onGenerateButtonClick(final ClickEvent