├── .dockerignore ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .idea └── icon.png ├── .mvn └── wrapper │ ├── .gitignore │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── lombok.config ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── docker │ ├── Dockerfile.jvm │ ├── Dockerfile.native │ └── Dockerfile.native-micro ├── java │ └── com │ │ └── melloware │ │ └── quarkus │ │ ├── panache │ │ ├── Car.java │ │ └── CarResource.java │ │ ├── socket │ │ ├── PushWebSocket.java │ │ ├── SocketMessage.java │ │ ├── SocketMessageType.java │ │ └── SocketResource.java │ │ └── support │ │ ├── QueryRequest.java │ │ └── QueryResponse.java ├── resources │ ├── application.properties │ └── db │ │ ├── changeLog.xml │ │ └── changes │ │ ├── 00100_create_schema.xml │ │ └── 00101_load_data.xml └── webui │ ├── .editorconfig │ ├── .env.development │ ├── .env.production │ ├── .eslintrc.json │ ├── .prettierrc.json │ ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json │ ├── farm.config.ts │ ├── index.html │ ├── openapi.json │ ├── openapi.yaml │ ├── orval.config.ts │ ├── package-lock.json │ ├── package.json │ ├── public │ └── static │ │ └── images │ │ ├── favicon.ico │ │ ├── plus-sign.svg │ │ ├── primereact-dark.svg │ │ ├── primereact.svg │ │ ├── quarkus.svg │ │ └── react.svg │ ├── src │ ├── App.css │ ├── App.tsx │ ├── AppMenu.tsx │ ├── AppMenuItem.ts │ ├── CrudPage.tsx │ ├── assets │ │ └── layout │ │ │ ├── _overrides.scss │ │ │ ├── _variables.scss │ │ │ ├── layout.scss │ │ │ └── sass │ │ │ ├── _config.scss │ │ │ ├── _content.scss │ │ │ ├── _footer.scss │ │ │ ├── _layout.scss │ │ │ ├── _main.scss │ │ │ ├── _menu.scss │ │ │ ├── _mixins.scss │ │ │ ├── _responsive.scss │ │ │ ├── _splash.scss │ │ │ ├── _topbar.scss │ │ │ ├── _typography.scss │ │ │ └── _utils.scss │ ├── index.tsx │ └── service │ │ ├── AxiosMutator.ts │ │ ├── CarService.ts │ │ └── CarService.zod.ts │ ├── tsconfig.json │ └── tsconfig.node.json └── test └── resources ├── dev-flow.excalidraw ├── dev-flow.png └── quarkus-primereact-screen.png /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !target/*-runner 3 | !target/*-runner.jar 4 | !target/lib/* 5 | !target/quarkus-app/ 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # New SPACE Developers Using Windows: 2 | 3 | 4 | # This file exists to prevent Windows users from having to manually convert the end of line sequence (EOL) for each text file after initially cloning this repo. This is NOT applicable to Linux users, only to Windows users. 5 | # After cloning the repo and ensuring the working tree is clean, run the following commands in order to auto-update the EOL on text files: 6 | # git rm --cached -r . 7 | # git reset --hard 8 | 9 | # Text files to be modified from EOL = Carriage Return Line Feed (CRLF) to EOL = Line Feed (LF): 10 | 11 | *.js eol=lf 12 | *.jsx eol=lf 13 | *.json eol=lf 14 | *.ts eol=lf 15 | *.tsx eol=lf 16 | 17 | # Binary files NOT to be modified: 18 | 19 | *.ico -text diff 20 | *.png -text diff 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [melloware] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "npm" 8 | directory: "/src/main/webui" 9 | schedule: 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '17' 23 | distribution: 'temurin' 24 | cache: maven 25 | - name: Build with Maven 26 | run: mvn -B package --file pom.xml -Dnative 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .project 3 | .classpath 4 | .settings/ 5 | bin/ 6 | dist/ 7 | 8 | # IntelliJ 9 | .idea 10 | *.ipr 11 | *.iml 12 | *.iws 13 | 14 | # NetBeans 15 | nb-configuration.xml 16 | 17 | # VS Code 18 | .vscode 19 | 20 | # OSX 21 | .DS_Store 22 | 23 | # SASS 24 | .sass-cache 25 | sassdoc 26 | 27 | # NODE 28 | node_modules/ 29 | .quinoa 30 | 31 | # Vim 32 | *.swp 33 | *.swo 34 | 35 | # patch 36 | *.orig 37 | *.rej 38 | 39 | # Maven 40 | target/ 41 | pom.xml.tag 42 | pom.xml.releaseBackup 43 | pom.xml.versionsBackup 44 | release.properties -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melloware/quarkus-primereact/HEAD/.idea/icon.png -------------------------------------------------------------------------------- /.mvn/wrapper/.gitignore: -------------------------------------------------------------------------------- 1 | maven-wrapper.jar 2 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.net.Authenticator; 23 | import java.net.PasswordAuthentication; 24 | import java.net.URI; 25 | import java.net.URL; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | import java.nio.file.Paths; 29 | import java.nio.file.StandardCopyOption; 30 | import java.util.concurrent.ThreadLocalRandom; 31 | 32 | public final class MavenWrapperDownloader { 33 | private static final String WRAPPER_VERSION = "3.3.2"; 34 | 35 | private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE")); 36 | 37 | public static void main(String[] args) { 38 | log("Apache Maven Wrapper Downloader " + WRAPPER_VERSION); 39 | 40 | if (args.length != 2) { 41 | System.err.println(" - ERROR wrapperUrl or wrapperJarPath parameter missing"); 42 | System.exit(1); 43 | } 44 | 45 | try { 46 | log(" - Downloader started"); 47 | final URL wrapperUrl = URI.create(args[0]).toURL(); 48 | final String jarPath = args[1].replace("..", ""); // Sanitize path 49 | final Path wrapperJarPath = Paths.get(jarPath).toAbsolutePath().normalize(); 50 | downloadFileFromURL(wrapperUrl, wrapperJarPath); 51 | log("Done"); 52 | } catch (IOException e) { 53 | System.err.println("- Error downloading: " + e.getMessage()); 54 | if (VERBOSE) { 55 | e.printStackTrace(); 56 | } 57 | System.exit(1); 58 | } 59 | } 60 | 61 | private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath) 62 | throws IOException { 63 | log(" - Downloading to: " + wrapperJarPath); 64 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 65 | final String username = System.getenv("MVNW_USERNAME"); 66 | final char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 67 | Authenticator.setDefault(new Authenticator() { 68 | @Override 69 | protected PasswordAuthentication getPasswordAuthentication() { 70 | return new PasswordAuthentication(username, password); 71 | } 72 | }); 73 | } 74 | Path temp = wrapperJarPath 75 | .getParent() 76 | .resolve(wrapperJarPath.getFileName() + "." 77 | + Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp"); 78 | try (InputStream inStream = wrapperUrl.openStream()) { 79 | Files.copy(inStream, temp, StandardCopyOption.REPLACE_EXISTING); 80 | Files.move(temp, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING); 81 | } finally { 82 | Files.deleteIfExists(temp); 83 | } 84 | log(" - Downloader complete"); 85 | } 86 | 87 | private static void log(String msg) { 88 | if (VERBOSE) { 89 | System.out.println(msg); 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melloware/quarkus-primereact/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=source 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | mellowaredev@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Melloware 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | Quarkus logo 4 | Plus sign 5 | mDNS logo 6 |
7 | 8 |

Quarkus PrimeReact

9 |
10 |
11 | 12 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://opensource.org/licenses/MIT) 13 | [![Actions CI](https://img.shields.io/github/actions/workflow/status/melloware/quarkus-primereact/build.yml?branch=main&logo=GitHub&style=for-the-badge)](https://github.com/melloware/quarkus-primereact/actions/workflows/build.yml) 14 | [![Quarkus](https://img.shields.io/badge/quarkus-power-blue?logo=quarkus&style=for-the-badge)](https://github.com/quarkusio/quarkus) 15 | ![React.js](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 16 | ![Typescript](https://img.shields.io/badge/typescript-%23323330.svg?style=for-the-badge&logo=typescript&logoColor=%23F7DF1E) 17 | 18 | **If you like this project, please consider supporting me ❤️** 19 | 20 | [![GitHub Sponsor](https://img.shields.io/badge/GitHub-FFDD00?style=for-the-badge&logo=github&logoColor=black)](https://github.com/sponsors/melloware) 21 | [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.me/mellowareinc) 22 | 23 | This [monorepo](https://en.wikipedia.org/wiki/Monorepo) is a minimal CRUD service exposing a couple of endpoints over REST, 24 | with a front-end based on React so you can play with it from your browser. 25 | 26 | While the code is surprisingly simple, under the hood this is using: 27 | 28 | - [Quarkus REST](https://quarkus.io/guides/rest) for REST API endpoints with OpenAPI documentation 29 | - [Quarkus REST Problem](https://github.com/quarkiverse/quarkus-resteasy-problem) for consistent REST API error handling 30 | - [Quarkus WebSockets Next](https://quarkus.io/guides/websockets-next-tutorial) for real-time WebSocket communication 31 | - [Quarkus Quinoa](https://github.com/quarkiverse/quarkus-quinoa) to handle allowing this monorepo to serve React and Java code 32 | - [Hibernate ORM with Panache](https://quarkus.io/guides/hibernate-orm-panache) to perform the CRUD operations on the database 33 | - [PostgreSQL](https://www.postgresql.org/) database; automatically starts an embedded DB 34 | - [Liquibase](https://www.liquibase.com/) to automatically update database 35 | - [React + PrimeReact](https://primereact.org/) for a top notch user interface including lazy datatable 36 | - [React Websocket](https://github.com/robtaussig/react-use-websocket) to handle websocket connections 37 | - [TanStack Form](https://tanstack.com/form/latest) to validate user input data 38 | - [TanStack Query](https://tanstack.com/query/latest) for powerful asynchronous state management for TypeScript 39 | - [Orval](https://orval.dev/) to generate TanStack Query client Typescript from the OpenAPI definition 40 | - [Zod](https://zod.dev/) for TypeScript-first schema validation 41 | 42 | ## Requirements 43 | 44 | To compile and run this demo you will need: 45 | 46 | - JDK 17+ 47 | - Apache Maven 48 | 49 | ## Code Generation 50 | 51 | This project uses [Orval](https://orval.dev/) to generate the [TanStack Query](https://tanstack.com/query/latest) client Typescript from the OpenAPI definition. 52 | 53 | [![Code Generation](https://github.com/melloware/quarkus-primereact/blob/main/src/test/resources/dev-flow.png)](https://github.com/melloware/quarkus-primereact) 54 | 55 | 56 | ## Developing 57 | 58 | ### Live coding with Quarkus 59 | 60 | The Maven Quarkus plugin provides a development mode that supports 61 | live coding. To try this out: 62 | 63 | ```bash 64 | $ ./mvnw quarkus:dev 65 | ``` 66 | 67 | Watch as it starts up a temporary PostreSQL database just for this session. In this mode you can make changes to the code and have the changes immediately applied, by just refreshing your browser. 68 | 69 | > :bulb: 70 | Hot reload works add a new REST endpoint and see it update in realtime. Try it! 71 | 72 | Now open your web browser to http://localhost:8080/ to see it in action. 73 | 74 | [![Quarkus Monorepo](https://github.com/melloware/quarkus-primereact/blob/main/src/test/resources/quarkus-primereact-screen.png)](https://github.com/melloware/quarkus-primereact) 75 | 76 | ## Building 77 | 78 | ### Run Quarkus PrimeReact in JVM mode 79 | 80 | When you're done iterating in developer mode, you can run the application as a 81 | conventional jar file. 82 | 83 | First compile it: 84 | 85 | ```bash 86 | $ ./mvnw clean package 87 | ``` 88 | 89 | Then run it with: 90 | 91 | ```bash 92 | $ java -jar ./target/quarkus-app/quarkus-run.jar 93 | ``` 94 | 95 | Or build it as a single executable JAR file (known as an uber-jar): 96 | 97 | ```bash 98 | $ ./mvnw clean package -Dquarkus.package.type=uber-jar 99 | ``` 100 | 101 | Then run it with: 102 | 103 | ```bash 104 | $ java -jar ./target/quarkus-primereact-{version}-runner.jar 105 | ``` 106 | 107 | Navigate to: 108 | 109 | 110 | 111 | ### Run Quarkus PrimeReact in Docker 112 | 113 | You can easily build a Docker image of this application with the following command: 114 | 115 | ```bash 116 | $ ./mvnw -Pdocker 117 | ``` 118 | 119 | You will be able to run this binary directly where ${version} is the current project version: 120 | 121 | ```bash 122 | $ docker run -i --rm -p 8000:8000 melloware/quarkus-primereact:latest 123 | ``` 124 | 125 | > :bulb: 126 | Now observe the time it took to boot, and remember: that time was mostly spent to generate the tables in your database and import the initial data. 127 | 128 | ## See it in your browser 129 | 130 | Navigate to: 131 | 132 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation=true 2 | lombok.log.fieldName=LOG 3 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ]; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ]; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ]; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ]; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false 54 | darwin=false 55 | mingw=false 56 | case "$(uname)" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true ;; 59 | Darwin*) 60 | darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | JAVA_HOME="$(/usr/libexec/java_home)" 66 | export JAVA_HOME 67 | else 68 | JAVA_HOME="/Library/Java/Home" 69 | export JAVA_HOME 70 | fi 71 | fi 72 | ;; 73 | esac 74 | 75 | if [ -z "$JAVA_HOME" ]; then 76 | if [ -r /etc/gentoo-release ]; then 77 | JAVA_HOME=$(java-config --jre-home) 78 | fi 79 | fi 80 | 81 | # For Cygwin, ensure paths are in UNIX format before anything is touched 82 | if $cygwin; then 83 | [ -n "$JAVA_HOME" ] \ 84 | && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") 85 | [ -n "$CLASSPATH" ] \ 86 | && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") 87 | fi 88 | 89 | # For Mingw, ensure paths are in UNIX format before anything is touched 90 | if $mingw; then 91 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \ 92 | && JAVA_HOME="$( 93 | cd "$JAVA_HOME" || ( 94 | echo "cannot cd into $JAVA_HOME." >&2 95 | exit 1 96 | ) 97 | pwd 98 | )" 99 | fi 100 | 101 | if [ -z "$JAVA_HOME" ]; then 102 | javaExecutable="$(which javac)" 103 | if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then 104 | # readlink(1) is not available as standard on Solaris 10. 105 | readLink=$(which readlink) 106 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then 107 | if $darwin; then 108 | javaHome="$(dirname "$javaExecutable")" 109 | javaExecutable="$(cd "$javaHome" && pwd -P)/javac" 110 | else 111 | javaExecutable="$(readlink -f "$javaExecutable")" 112 | fi 113 | javaHome="$(dirname "$javaExecutable")" 114 | javaHome=$(expr "$javaHome" : '\(.*\)/bin') 115 | JAVA_HOME="$javaHome" 116 | export JAVA_HOME 117 | fi 118 | fi 119 | fi 120 | 121 | if [ -z "$JAVACMD" ]; then 122 | if [ -n "$JAVA_HOME" ]; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD="$JAVA_HOME/jre/sh/java" 126 | else 127 | JAVACMD="$JAVA_HOME/bin/java" 128 | fi 129 | else 130 | JAVACMD="$( 131 | \unset -f command 2>/dev/null 132 | \command -v java 133 | )" 134 | fi 135 | fi 136 | 137 | if [ ! -x "$JAVACMD" ]; then 138 | echo "Error: JAVA_HOME is not defined correctly." >&2 139 | echo " We cannot execute $JAVACMD" >&2 140 | exit 1 141 | fi 142 | 143 | if [ -z "$JAVA_HOME" ]; then 144 | echo "Warning: JAVA_HOME environment variable is not set." >&2 145 | fi 146 | 147 | # traverses directory structure from process work directory to filesystem root 148 | # first directory with .mvn subdirectory is considered project base directory 149 | find_maven_basedir() { 150 | if [ -z "$1" ]; then 151 | echo "Path not specified to find_maven_basedir" >&2 152 | return 1 153 | fi 154 | 155 | basedir="$1" 156 | wdir="$1" 157 | while [ "$wdir" != '/' ]; do 158 | if [ -d "$wdir"/.mvn ]; then 159 | basedir=$wdir 160 | break 161 | fi 162 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 163 | if [ -d "${wdir}" ]; then 164 | wdir=$( 165 | cd "$wdir/.." || exit 1 166 | pwd 167 | ) 168 | fi 169 | # end of workaround 170 | done 171 | printf '%s' "$( 172 | cd "$basedir" || exit 1 173 | pwd 174 | )" 175 | } 176 | 177 | # concatenates all lines of a file 178 | concat_lines() { 179 | if [ -f "$1" ]; then 180 | # Remove \r in case we run on Windows within Git Bash 181 | # and check out the repository with auto CRLF management 182 | # enabled. Otherwise, we may read lines that are delimited with 183 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word 184 | # splitting rules. 185 | tr -s '\r\n' ' ' <"$1" 186 | fi 187 | } 188 | 189 | log() { 190 | if [ "$MVNW_VERBOSE" = true ]; then 191 | printf '%s\n' "$1" 192 | fi 193 | } 194 | 195 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")") 196 | if [ -z "$BASE_DIR" ]; then 197 | exit 1 198 | fi 199 | 200 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 201 | export MAVEN_PROJECTBASEDIR 202 | log "$MAVEN_PROJECTBASEDIR" 203 | 204 | ########################################################################################## 205 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 206 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 207 | ########################################################################################## 208 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" 209 | if [ -r "$wrapperJarPath" ]; then 210 | log "Found $wrapperJarPath" 211 | else 212 | log "Couldn't find $wrapperJarPath, downloading it ..." 213 | 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" 216 | else 217 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" 218 | fi 219 | while IFS="=" read -r key value; do 220 | # 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 '=' ) 221 | safeValue=$(echo "$value" | tr -d '\r') 222 | case "$key" in wrapperUrl) 223 | wrapperUrl="$safeValue" 224 | break 225 | ;; 226 | esac 227 | done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 228 | log "Downloading from: $wrapperUrl" 229 | 230 | if $cygwin; then 231 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") 232 | fi 233 | 234 | if command -v wget >/dev/null; then 235 | log "Found wget ... using wget" 236 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" 237 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 238 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 239 | else 240 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | fi 242 | elif command -v curl >/dev/null; then 243 | log "Found curl ... using curl" 244 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" 245 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 246 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 247 | else 248 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" 249 | fi 250 | else 251 | log "Falling back to using Java to download" 252 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" 253 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" 254 | # For Cygwin, switch paths to Windows format before running javac 255 | if $cygwin; then 256 | javaSource=$(cygpath --path --windows "$javaSource") 257 | javaClass=$(cygpath --path --windows "$javaClass") 258 | fi 259 | if [ -e "$javaSource" ]; then 260 | if [ ! -e "$javaClass" ]; then 261 | log " - Compiling MavenWrapperDownloader.java ..." 262 | ("$JAVA_HOME/bin/javac" "$javaSource") 263 | fi 264 | if [ -e "$javaClass" ]; then 265 | log " - Running MavenWrapperDownloader.java ..." 266 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" 267 | fi 268 | fi 269 | fi 270 | fi 271 | ########################################################################################## 272 | # End of extension 273 | ########################################################################################## 274 | 275 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file 276 | wrapperSha256Sum="" 277 | while IFS="=" read -r key value; do 278 | case "$key" in wrapperSha256Sum) 279 | wrapperSha256Sum=$value 280 | break 281 | ;; 282 | esac 283 | done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" 284 | if [ -n "$wrapperSha256Sum" ]; then 285 | wrapperSha256Result=false 286 | if command -v sha256sum >/dev/null; then 287 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then 288 | wrapperSha256Result=true 289 | fi 290 | elif command -v shasum >/dev/null; then 291 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then 292 | wrapperSha256Result=true 293 | fi 294 | else 295 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 296 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 297 | exit 1 298 | fi 299 | if [ $wrapperSha256Result = false ]; then 300 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 301 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 302 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 303 | exit 1 304 | fi 305 | fi 306 | 307 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 308 | 309 | # For Cygwin, switch paths to Windows format before running java 310 | if $cygwin; then 311 | [ -n "$JAVA_HOME" ] \ 312 | && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") 313 | [ -n "$CLASSPATH" ] \ 314 | && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") 315 | [ -n "$MAVEN_PROJECTBASEDIR" ] \ 316 | && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") 317 | fi 318 | 319 | # Provide a "standardized" way to retrieve the CLI args that will 320 | # work with both Windows and non-Windows executions. 321 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" 322 | export MAVEN_CMD_LINE_ARGS 323 | 324 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 325 | 326 | # shellcheck disable=SC2086 # safe args 327 | exec "$JAVACMD" \ 328 | $MAVEN_OPTS \ 329 | $MAVEN_DEBUG_OPTS \ 330 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 331 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 332 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 333 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. >&2 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. >&2 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. >&2 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. >&2 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file 157 | SET WRAPPER_SHA_256_SUM="" 158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B 160 | ) 161 | IF NOT %WRAPPER_SHA_256_SUM%=="" ( 162 | powershell -Command "&{"^ 163 | "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ 164 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ 165 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ 166 | " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ 167 | " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ 168 | " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ 169 | " exit 1;"^ 170 | "}"^ 171 | "}" 172 | if ERRORLEVEL 1 goto error 173 | ) 174 | 175 | @REM Provide a "standardized" way to retrieve the CLI args that will 176 | @REM work with both Windows and non-Windows executions. 177 | set MAVEN_CMD_LINE_ARGS=%* 178 | 179 | %MAVEN_JAVA_EXE% ^ 180 | %JVM_CONFIG_MAVEN_PROPS% ^ 181 | %MAVEN_OPTS% ^ 182 | %MAVEN_DEBUG_OPTS% ^ 183 | -classpath %WRAPPER_JAR% ^ 184 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 185 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 186 | if ERRORLEVEL 1 goto error 187 | goto end 188 | 189 | :error 190 | set ERROR_CODE=1 191 | 192 | :end 193 | @endlocal & set ERROR_CODE=%ERROR_CODE% 194 | 195 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 196 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 197 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 198 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 199 | :skipRcPost 200 | 201 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 202 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 203 | 204 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 205 | 206 | cmd /C exit /B %ERROR_CODE% 207 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.melloware 6 | quarkus-primereact 7 | 10.9.7 8 | Quarkus PrimeReact 9 | Quarkus monorepo demonstrating Panache REST server with PrimeReact UI client 10 | https://github.com/melloware/quarkus-monorepo 11 | 12 | 3.30.2 13 | 3.4.1 14 | 1.8.0 15 | 0.7.2 16 | 3.21.0 17 | 2.7.0 18 | 1.18.42 19 | 20 | 3.5.4 21 | 17 22 | UTF-8 23 | UTF-8 24 | 25 | 26 | 27 | 28 | io.quarkus 29 | quarkus-bom 30 | ${quarkus.platform.version} 31 | import 32 | pom 33 | 34 | 35 | 36 | 37 | 38 | 39 | io.quarkus 40 | quarkus-rest-jackson 41 | 42 | 43 | io.quarkus 44 | quarkus-websockets-next 45 | 46 | 47 | io.quarkus 48 | quarkus-smallrye-openapi 49 | 50 | 51 | io.quarkus 52 | quarkus-smallrye-health 53 | 54 | 55 | io.quarkus 56 | quarkus-info 57 | 58 | 59 | io.quarkus 60 | quarkus-cache 61 | 62 | 63 | io.quarkus 64 | quarkus-hibernate-orm-panache 65 | 66 | 67 | io.quarkus 68 | quarkus-hibernate-validator 69 | 70 | 71 | io.quarkus 72 | quarkus-liquibase 73 | 74 | 75 | io.quarkus 76 | quarkus-jdbc-postgresql 77 | 78 | 79 | io.quarkus 80 | quarkus-jdbc-postgresql-deployment 81 | provided 82 | 83 | 84 | 85 | io.quarkiverse.quinoa 86 | quarkus-quinoa 87 | ${quarkus.quinoa.version} 88 | 89 | 90 | io.quarkiverse.embedded.postgresql 91 | quarkus-embedded-postgresql 92 | ${quarkus.postgresql.version} 93 | 94 | 95 | io.quarkiverse.ngrok 96 | quarkus-ngrok 97 | ${quarkus.ngrok.version} 98 | 99 | 100 | io.quarkiverse.loggingmanager 101 | quarkus-logging-manager 102 | ${quarkus.logmanager.version} 103 | runtime 104 | 105 | 106 | io.quarkiverse.resteasy-problem 107 | quarkus-resteasy-problem 108 | ${quarkus.rest-problem.version} 109 | 110 | 111 | 112 | com.aayushatharva.brotli4j 113 | native-linux-x86_64 114 | 115 | 116 | 117 | org.projectlombok 118 | lombok 119 | ${lombok.version} 120 | provided 121 | 122 | 123 | 124 | io.quarkus 125 | quarkus-junit5 126 | test 127 | 128 | 129 | 130 | 131 | 132 | 133 | org.codehaus.mojo 134 | exec-maven-plugin 135 | 3.6.2 136 | 137 | docker 138 | ${project.basedir} 139 | 140 | build 141 | -f 142 | src/main/docker/Dockerfile.jvm 143 | -t 144 | melloware/${project.artifactId}:latest 145 | . 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | io.quarkus 154 | quarkus-maven-plugin 155 | ${quarkus.platform.version} 156 | true 157 | 158 | 159 | 160 | build 161 | generate-code 162 | generate-code-tests 163 | native-image-agent 164 | 165 | 166 | 167 | 168 | 169 | org.apache.maven.plugins 170 | maven-compiler-plugin 171 | 3.14.1 172 | 173 | ${project.build.sourceEncoding} 174 | 175 | 176 | org.projectlombok 177 | lombok 178 | ${lombok.version} 179 | 180 | 181 | io.quarkus 182 | quarkus-extension-processor 183 | ${quarkus.platform.version} 184 | 185 | 186 | 187 | 188 | 189 | maven-surefire-plugin 190 | ${surefire-plugin.version} 191 | 192 | 193 | org.jboss.logmanager.LogManager 194 | ${maven.home} 195 | 196 | 197 | 198 | 199 | maven-failsafe-plugin 200 | ${surefire-plugin.version} 201 | 202 | 203 | 204 | integration-test 205 | verify 206 | 207 | 208 | 209 | ${project.build.directory}/${project.build.finalName}-runner 210 | org.jboss.logmanager.LogManager 211 | ${maven.home} 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | docker 223 | 224 | 225 | docker 226 | 227 | 228 | 229 | clean package 230 | 231 | 232 | org.codehaus.mojo 233 | exec-maven-plugin 234 | 235 | 236 | docker-build 237 | package 238 | 239 | exec 240 | 241 | 242 | 243 | build 244 | -f 245 | src/main/docker/Dockerfile.jvm 246 | -t 247 | melloware/${project.artifactId}:latest 248 | . 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | native 259 | 260 | 261 | native 262 | 263 | 264 | 265 | true 266 | true 267 | 268 | 269 | clean package 270 | 271 | 272 | org.codehaus.mojo 273 | exec-maven-plugin 274 | 275 | 276 | docker-build 277 | package 278 | 279 | exec 280 | 281 | 282 | 283 | build 284 | -f 285 | src/main/docker/Dockerfile.native-micro 286 | -t 287 | melloware/${project.artifactId}:${project.version} 288 | . 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /src/main/docker/Dockerfile.jvm: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.jvm -t melloware/quarkus-primereact . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 melloware/quarkus-primereact 15 | # 16 | # If you want to include the debug port into your docker image 17 | # you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. 18 | # Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 19 | # when running the container 20 | # 21 | # Then run the container using : 22 | # 23 | # docker run -i --rm -p 8080:8080 melloware/quarkus-primereact 24 | # 25 | # This image uses the `run-java.sh` script to run the application. 26 | # This scripts computes the command line to execute your Java application, and 27 | # includes memory/GC tuning. 28 | # You can configure the behavior using the following environment properties: 29 | # - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") 30 | # - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options 31 | # in JAVA_OPTS (example: "-Dsome.property=foo") 32 | # - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is 33 | # used to calculate a default maximal heap memory based on a containers restriction. 34 | # If used in a container without any memory constraints for the container then this 35 | # option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio 36 | # of the container available memory as set here. The default is `50` which means 50% 37 | # of the available memory is used as an upper boundary. You can skip this mechanism by 38 | # setting this value to `0` in which case no `-Xmx` option is added. 39 | # - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This 40 | # is used to calculate a default initial heap memory based on the maximum heap memory. 41 | # If used in a container without any memory constraints for the container then this 42 | # option has no effect. If there is a memory constraint then `-Xms` is set to a ratio 43 | # of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` 44 | # is used as the initial heap size. You can skip this mechanism by setting this value 45 | # to `0` in which case no `-Xms` option is added (example: "25") 46 | # - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. 47 | # This is used to calculate the maximum value of the initial heap memory. If used in 48 | # a container without any memory constraints for the container then this option has 49 | # no effect. If there is a memory constraint then `-Xms` is limited to the value set 50 | # here. The default is 4096MB which means the calculated value of `-Xms` never will 51 | # be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") 52 | # - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output 53 | # when things are happening. This option, if set to true, will set 54 | # `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). 55 | # - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: 56 | # true"). 57 | # - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). 58 | # - CONTAINER_CORE_LIMIT: A calculated core limit as described in 59 | # https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") 60 | # - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). 61 | # - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. 62 | # (example: "20") 63 | # - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. 64 | # (example: "40") 65 | # - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. 66 | # (example: "4") 67 | # - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus 68 | # previous GC times. (example: "90") 69 | # - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") 70 | # - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") 71 | # - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should 72 | # contain the necessary JRE command-line options to specify the required GC, which 73 | # will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). 74 | # - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") 75 | # - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") 76 | # - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be 77 | # accessed directly. (example: "foo.example.com,bar.example.com") 78 | # 79 | ### 80 | FROM registry.access.redhat.com/ubi8/openjdk-21:1.20 81 | 82 | ENV LANGUAGE='en_US:en' 83 | 84 | # We make four distinct layers so if there are application changes the library layers can be re-used 85 | COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ 86 | COPY --chown=185 target/quarkus-app/*.jar /deployments/ 87 | COPY --chown=185 target/quarkus-app/app/ /deployments/app/ 88 | COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ 89 | 90 | EXPOSE 8080 91 | USER 185 92 | ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" 93 | ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" 94 | 95 | ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] 96 | 97 | -------------------------------------------------------------------------------- /src/main/docker/Dockerfile.native: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. 3 | # 4 | # Before building the container image run: 5 | # 6 | # ./mvnw package -Dnative 7 | # 8 | # Then, build the image with: 9 | # 10 | # docker build -f src/main/docker/Dockerfile.native -t melloware/quarkus-primereact . 11 | # 12 | # Then run the container using: 13 | # 14 | # docker run -i --rm -p 8080:8080 melloware/quarkus-primereact 15 | # 16 | ### 17 | FROM registry.access.redhat.com/ubi8/ubi-minimal:8.10 18 | WORKDIR /work/ 19 | RUN chown 1001 /work \ 20 | && chmod "g+rwX" /work \ 21 | && chown 1001:root /work 22 | COPY --chown=1001:root target/*-runner /work/application 23 | 24 | EXPOSE 8080 25 | USER 1001 26 | 27 | ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] 28 | -------------------------------------------------------------------------------- /src/main/docker/Dockerfile.native-micro: -------------------------------------------------------------------------------- 1 | #### 2 | # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. 3 | # It uses a micro base image, tuned for Quarkus native executables. 4 | # It reduces the size of the resulting container image. 5 | # Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. 6 | # 7 | # Before building the container image run: 8 | # 9 | # ./mvnw package -Dnative 10 | # 11 | # Then, build the image with: 12 | # 13 | # docker build -f src/main/docker/Dockerfile.native-micro -t melloware/quarkus-primereact . 14 | # 15 | # Then run the container using: 16 | # 17 | # docker run -i --rm -p 8080:8080 melloware/quarkus-primereact 18 | # 19 | ### 20 | FROM quay.io/quarkus/quarkus-micro-image:2.0 21 | WORKDIR /work/ 22 | RUN chown 1001 /work \ 23 | && chmod "g+rwX" /work \ 24 | && chown 1001:root /work 25 | COPY --chown=1001:root target/*-runner /work/application 26 | 27 | EXPOSE 8080 28 | USER 1001 29 | 30 | ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] 31 | -------------------------------------------------------------------------------- /src/main/java/com/melloware/quarkus/panache/Car.java: -------------------------------------------------------------------------------- 1 | package com.melloware.quarkus.panache; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.Instant; 5 | 6 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 7 | 8 | import io.quarkus.hibernate.orm.panache.PanacheEntity; 9 | import jakarta.persistence.Cacheable; 10 | import jakarta.persistence.Column; 11 | import jakarta.persistence.Entity; 12 | import jakarta.persistence.Table; 13 | import jakarta.persistence.Version; 14 | import jakarta.validation.constraints.DecimalMax; 15 | import jakarta.validation.constraints.DecimalMin; 16 | import jakarta.validation.constraints.Max; 17 | import jakarta.validation.constraints.Min; 18 | import jakarta.validation.constraints.NotBlank; 19 | import jakarta.validation.constraints.Size; 20 | 21 | import lombok.AllArgsConstructor; 22 | import lombok.NoArgsConstructor; 23 | 24 | @Entity 25 | @Table(name = "CAR") 26 | @Cacheable 27 | @AllArgsConstructor 28 | @NoArgsConstructor 29 | @Schema(name = "Car", description = "Entity that represents a car.") 30 | public class Car extends PanacheEntity { 31 | 32 | @Column(unique = true) 33 | @NotBlank(message = "VIN may not be blank") 34 | @Size(max=17, message = "VIN may not be more than 17 characters") 35 | @Schema(required = true, examples = {"WVGEF9BP4DD085048"}, description = "VIN number") 36 | public String vin; 37 | @NotBlank(message = "Make may not be blank") 38 | @Size(max=255, message = "Make may not be more than 255 characters") 39 | @Schema(required = true, examples = {"BMW"}, description = "Manufacturer") 40 | public String make; 41 | @NotBlank(message = "Model may not be blank") 42 | @Size(max=255, message = "Model may not be more than 255 characters") 43 | @Schema(required = true, examples = {"330ix"}, description = "Model Number") 44 | public String model; 45 | @Min(value = 1960) 46 | @Max(value = 2050) 47 | @Schema(required = true, examples = {"1974"}, description = "Year of manufacture") 48 | public int year; 49 | @NotBlank(message = "Color may not be blank") 50 | @Size(max=20, message = "Color may not be more than 20 characters") 51 | @Schema(required = true, examples = {"891d4c"}, description = "HTML color of the car") 52 | public String color; 53 | @DecimalMin(value = "0.00") 54 | @DecimalMax(value = "250000.00") 55 | @Schema(required = true, examples = {"9999.99"}, description = "Price") 56 | public BigDecimal price; 57 | 58 | @Version 59 | @Column(name = "modified_time") 60 | @Schema(description = "Modified time of the record") 61 | public Instant modifiedTime; 62 | 63 | } -------------------------------------------------------------------------------- /src/main/java/com/melloware/quarkus/panache/CarResource.java: -------------------------------------------------------------------------------- 1 | package com.melloware.quarkus.panache; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import org.eclipse.microprofile.openapi.annotations.Operation; 7 | import org.eclipse.microprofile.openapi.annotations.media.Content; 8 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 9 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 10 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 11 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 12 | 13 | import com.fasterxml.jackson.core.JsonProcessingException; 14 | import com.fasterxml.jackson.databind.ObjectMapper; 15 | import com.melloware.quarkus.support.QueryRequest; 16 | import com.melloware.quarkus.support.QueryResponse; 17 | 18 | import io.quarkus.hibernate.orm.panache.PanacheQuery; 19 | import io.quarkus.panache.common.Sort; 20 | import io.quarkus.runtime.annotations.RegisterForReflection; 21 | import jakarta.enterprise.context.ApplicationScoped; 22 | import jakarta.inject.Inject; 23 | import jakarta.transaction.Transactional; 24 | import jakarta.validation.Valid; 25 | import jakarta.validation.constraints.Min; 26 | import jakarta.ws.rs.Consumes; 27 | import jakarta.ws.rs.DELETE; 28 | import jakarta.ws.rs.GET; 29 | import jakarta.ws.rs.NotFoundException; 30 | import jakarta.ws.rs.POST; 31 | import jakarta.ws.rs.PUT; 32 | import jakarta.ws.rs.Path; 33 | import jakarta.ws.rs.PathParam; 34 | import jakarta.ws.rs.Produces; 35 | import jakarta.ws.rs.QueryParam; 36 | import jakarta.ws.rs.WebApplicationException; 37 | import jakarta.ws.rs.core.MediaType; 38 | import jakarta.ws.rs.core.Response; 39 | import jakarta.ws.rs.core.Response.Status; 40 | import lombok.extern.jbosslog.JBossLog; 41 | 42 | @Path("entity/cars") 43 | @ApplicationScoped 44 | @Produces(MediaType.APPLICATION_JSON) 45 | @Consumes(MediaType.APPLICATION_JSON) 46 | @JBossLog 47 | @RegisterForReflection 48 | @Tag(name = "Car Resource", description = "CRUD operations for the Car entity.") 49 | public class CarResource { 50 | 51 | @Inject 52 | ObjectMapper objectMapper; 53 | 54 | @GET 55 | @Path("{id}") 56 | @Operation(summary = "Get a car by ID", description = "Returns a car based on the provided ID") 57 | @APIResponses({ 58 | @APIResponse(responseCode = "200", description = "Success"), 59 | @APIResponse(responseCode = "404", description = "Car not found") 60 | }) 61 | public Car getSingle(@PathParam("id") @Min(value = 0) Long id) { 62 | LOG.infof("Get Car: %s", id); 63 | Car entity = Car.findById(id); 64 | if (entity == null) { 65 | throw new NotFoundException("Car with id of " + id + " does not exist."); 66 | } 67 | return entity; 68 | } 69 | 70 | @GET 71 | @Path("/manufacturers") 72 | @Operation(summary = "Get all manufacturers", description = "Returns a list of distinct car manufacturers") 73 | @APIResponse(responseCode = "200", description = "Success") 74 | public List getManufacturers() { 75 | LOG.infof("Get Unique Manufacturers..."); 76 | return Car.find("select distinct make from Car order by make").project(String.class).list(); 77 | } 78 | 79 | 80 | @POST 81 | @Transactional 82 | @Operation(summary = "Create a new car", description = "Creates a new car entry") 83 | @APIResponses({ 84 | @APIResponse(responseCode = "201", description = "Car created successfully" ,content = @Content(mediaType = "application/json", schema = @Schema(implementation = Car.class))), 85 | @APIResponse(responseCode = "400", description = "Invalid request format"), 86 | @APIResponse(responseCode = "422", description = "Invalid car data provided") 87 | }) 88 | public Response create(@Valid Car car) { 89 | LOG.infof("Create Car: %s", car); 90 | if (car.id != null) { 91 | // 422 Unprocessable Entity 92 | throw new WebApplicationException("Id was invalidly set on request.", 422); 93 | } 94 | car.persist(); 95 | return Response.ok(car).status(Status.CREATED).build(); 96 | } 97 | 98 | @PUT 99 | @Path("{id}") 100 | @Transactional 101 | @Operation(summary = "Update a car", description = "Updates an existing car based on ID") 102 | @APIResponses({ 103 | @APIResponse(responseCode = "200", description = "Car updated successfully"), 104 | @APIResponse(responseCode = "400", description = "Invalid request format"), 105 | @APIResponse(responseCode = "404", description = "Car not found"), 106 | @APIResponse(responseCode = "422", description = "Invalid car data provided") 107 | }) 108 | public Car update(@PathParam("id") @Min(value = 0) Long id, @Valid Car car) { 109 | LOG.infof("Update Car: %s", id); 110 | Car entity = Car.findById(id); 111 | if (entity == null) { 112 | throw new NotFoundException("Car with id of " + id + " does not exist."); 113 | 114 | } 115 | 116 | // would normally use ModelMapper here: https://modelmapper.org/ 117 | entity.make = car.make; 118 | entity.model = car.model; 119 | entity.year = car.year; 120 | entity.vin = car.vin; 121 | entity.color = car.color; 122 | entity.price = car.price; 123 | 124 | return entity; 125 | } 126 | 127 | @DELETE 128 | @Path("{id}") 129 | @Transactional 130 | @Operation(summary = "Delete a car", description = "Deletes a car based on ID") 131 | @APIResponses({ 132 | @APIResponse(responseCode = "204", description = "Car successfully deleted"), 133 | @APIResponse(responseCode = "404", description = "Car not found") 134 | }) 135 | public Response delete(@PathParam("id") @Min(value = 0) Long id) { 136 | LOG.infof("Delete Car: %s", id); 137 | Car entity = Car.findById(id); 138 | if (entity == null) { 139 | throw new NotFoundException("Car with id of " + id + " does not exist."); 140 | } 141 | entity.delete(); 142 | return Response.noContent().build(); 143 | } 144 | 145 | @GET 146 | @Operation(summary = "List cars", description = "Returns a paginated list of cars with optional filtering and sorting") 147 | @APIResponses({ 148 | @APIResponse(responseCode = "200", description = "Success"), 149 | @APIResponse(responseCode = "400", description = "Invalid request format") 150 | }) 151 | public QueryResponse list(@QueryParam("request") String lazyRequest) throws JsonProcessingException { 152 | LOG.debugf("List Cars: %s", lazyRequest); 153 | try { 154 | // add a delay to simulate a slow response 155 | Thread.sleep(250); 156 | } catch (InterruptedException e) { 157 | // do nothing 158 | } 159 | final QueryResponse response = new QueryResponse<>(); 160 | if (lazyRequest == null || lazyRequest.isEmpty()) { 161 | List results = Car.listAll(Sort.by("make")); 162 | response.setTotalRecords(results.size()); 163 | response.setRecords(results); 164 | return response; 165 | } 166 | 167 | final QueryRequest request = objectMapper.readValue(lazyRequest, QueryRequest.class); 168 | LOG.debug(request); 169 | 170 | // sorts 171 | final Sort sort = request.calculateSort(); 172 | 173 | // filters 174 | final QueryRequest.FilterCriteria filterMeta = request.calculateFilters(QueryRequest.FilterOperator.AND); 175 | final String filterQuery = filterMeta.getQuery(); 176 | final Map filters = filterMeta.getParameters(); 177 | 178 | PanacheQuery query = Car.findAll(sort); 179 | if (!filters.isEmpty()) { 180 | Map map = request.calculateFilterParameters(); 181 | query = Car.find(filterQuery, sort, map); 182 | } 183 | 184 | // range 185 | query.range(request.getFirst(), request.getFirst() + request.getRows()); 186 | 187 | // response 188 | response.setTotalRecords(query.count()); 189 | response.setRecords(query.list()); 190 | return response; 191 | } 192 | } -------------------------------------------------------------------------------- /src/main/java/com/melloware/quarkus/socket/PushWebSocket.java: -------------------------------------------------------------------------------- 1 | package com.melloware.quarkus.socket; 2 | 3 | import org.apache.commons.lang3.exception.ExceptionUtils; 4 | 5 | import io.quarkus.websockets.next.OnClose; 6 | import io.quarkus.websockets.next.OnError; 7 | import io.quarkus.websockets.next.OnOpen; 8 | import io.quarkus.websockets.next.OnPingMessage; 9 | import io.quarkus.websockets.next.OnPongMessage; 10 | import io.quarkus.websockets.next.OnTextMessage; 11 | import io.quarkus.websockets.next.WebSocket; 12 | import io.quarkus.websockets.next.WebSocketConnection; 13 | import io.smallrye.common.annotation.RunOnVirtualThread; 14 | import io.vertx.core.buffer.Buffer; 15 | import jakarta.inject.Inject; 16 | import lombok.extern.jbosslog.JBossLog; 17 | 18 | /** 19 | * WebSocket endpoint for push notifications. 20 | * Handles user connections, disconnections and message broadcasting. 21 | */ 22 | @WebSocket(path = "/push") 23 | @JBossLog 24 | public class PushWebSocket { 25 | 26 | /** 27 | * The WebSocket connection instance. 28 | */ 29 | @Inject 30 | WebSocketConnection connection; 31 | 32 | /** 33 | * Handles new WebSocket connections. 34 | */ 35 | @OnOpen 36 | @RunOnVirtualThread 37 | public void onOpen(WebSocketConnection connection) { 38 | LOG.infof("Websocket connection opened: %s", connection.id()); 39 | } 40 | 41 | /** 42 | * Handles WebSocket disconnections. 43 | */ 44 | @OnClose 45 | @RunOnVirtualThread 46 | public void onClose(WebSocketConnection connection) { 47 | LOG.infof("Websocket connection closed: %s", connection.id()); 48 | } 49 | 50 | /** 51 | * Handles incoming text messages from WebSocket clients. 52 | * Processes JS "ping" messages and returns appropriate "pong" responses. 53 | * 54 | * @param message The text message received from the client 55 | * @return A response message to be sent back to the client 56 | */ 57 | @OnTextMessage(broadcast = false) 58 | @RunOnVirtualThread 59 | public String onMessage(String message) { 60 | if ("ping".equals(message)) { 61 | LOG.debugf("Websocket Ping message received: %s", message); 62 | return "pong"; 63 | } 64 | LOG.infof("Websocket message received: %s", message); 65 | return message; 66 | } 67 | 68 | /** 69 | * Handles incoming ping messages from WebSocket clients. 70 | * Automatically responds with a pong message. 71 | * 72 | * @param data The ping message data received from the client 73 | * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 74 | */ 75 | @OnPingMessage 76 | @RunOnVirtualThread 77 | void ping(Buffer data) { 78 | // an incoming ping data frame that will automatically receive a pong 79 | LOG.debugf("Websocket Ping received: %s", data); 80 | } 81 | 82 | /** 83 | * Handles incoming pong data frame messages from WebSocket clients. 84 | * These are responses to previously sent ping messages. 85 | * 86 | * @param data The pong message data received from the client 87 | * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 88 | */ 89 | @OnPongMessage 90 | @RunOnVirtualThread 91 | void pong(Buffer data) { 92 | // an incoming pong data frame in response to the last ping sent 93 | LOG.debugf("Websocket Pong received: %s", data); 94 | } 95 | 96 | /** 97 | * Handles WebSocket errors and exceptions. 98 | * Logs the root cause of the exception and returns an error message. 99 | * 100 | * @param e The exception that occurred 101 | * @return An error message to be sent back to the client 102 | */ 103 | @OnError 104 | @RunOnVirtualThread 105 | public String onException(Exception e) { 106 | // Handles Exception and all subclasses except for IOException. 107 | LOG.errorf("Websocket Exception: %s", ExceptionUtils.getRootCauseMessage(e)); 108 | return "Error"; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/melloware/quarkus/socket/SocketMessage.java: -------------------------------------------------------------------------------- 1 | package com.melloware.quarkus.socket; 2 | 3 | import java.time.ZoneOffset; 4 | import java.time.ZonedDateTime; 5 | import java.time.temporal.ChronoUnit; 6 | import java.util.Map; 7 | 8 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 9 | 10 | import io.quarkus.runtime.annotations.RegisterForReflection; 11 | import lombok.Builder; 12 | import lombok.Data; 13 | 14 | 15 | @Data 16 | @Builder 17 | @RegisterForReflection 18 | @Schema(description = "WebSocket message for real-time updates") 19 | public class SocketMessage { 20 | 21 | @Schema(description = "Unique identifier for the message", examples = "03001000c0020000") 22 | private String id; 23 | 24 | @Builder.Default 25 | @Schema(description = "UTC timestamp of when the message was created", examples = "2024-03-20T10:30:00Z") 26 | private ZonedDateTime timestamp = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS); 27 | 28 | @Schema(required = true, description = "Type of socket message", examples = {"USER_JOINED", "USER_LEFT", "REFRESH_DATA", "NOTIFICATION"}) 29 | SocketMessageType type; 30 | 31 | @Schema(description = "Optional message payload", examples = {"User connected", "Please refresh your data"}) 32 | String message; 33 | 34 | @Schema(description = "Additional context information for the message", examples = "{\"source\": \"system\", \"priority\": \"high\"}") 35 | private Map context; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/melloware/quarkus/socket/SocketMessageType.java: -------------------------------------------------------------------------------- 1 | package com.melloware.quarkus.socket; 2 | 3 | /** 4 | * Enum representing different types of WebSocket messages that can be sent. 5 | */ 6 | public enum SocketMessageType { 7 | /** 8 | * Indicates that connected clients should refresh their data 9 | */ 10 | REFRESH_DATA, 11 | 12 | /** 13 | * Indicates a notification message should be shown to the user 14 | */ 15 | NOTIFICATION; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/melloware/quarkus/socket/SocketResource.java: -------------------------------------------------------------------------------- 1 | package com.melloware.quarkus.socket; 2 | 3 | import java.util.UUID; 4 | 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.eclipse.microprofile.openapi.annotations.Operation; 7 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; 8 | import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; 9 | import org.eclipse.microprofile.openapi.annotations.tags.Tag; 10 | import org.slf4j.MDC; 11 | 12 | import io.quarkus.runtime.annotations.RegisterForReflection; 13 | import io.quarkus.websockets.next.OpenConnections; 14 | import jakarta.enterprise.context.ApplicationScoped; 15 | import jakarta.inject.Inject; 16 | import jakarta.ws.rs.Consumes; 17 | import jakarta.ws.rs.POST; 18 | import jakarta.ws.rs.Path; 19 | import jakarta.ws.rs.Produces; 20 | import jakarta.ws.rs.QueryParam; 21 | import jakarta.ws.rs.WebApplicationException; 22 | import jakarta.ws.rs.core.MediaType; 23 | import jakarta.ws.rs.core.Response; 24 | import jakarta.ws.rs.core.Response.Status; 25 | import lombok.extern.jbosslog.JBossLog; 26 | 27 | /** 28 | * REST resource for WebSocket operations. 29 | * Provides endpoints for sending notifications and refresh signals to connected 30 | * clients. 31 | */ 32 | @Path("socket") 33 | @ApplicationScoped 34 | @Produces(MediaType.APPLICATION_JSON) 35 | @Consumes(MediaType.APPLICATION_JSON) 36 | @JBossLog 37 | @RegisterForReflection 38 | @Tag(name = "WebSocket Resource", description = "WebSocket operations.") 39 | public class SocketResource { 40 | 41 | /** Connection manager for WebSocket clients */ 42 | @Inject 43 | OpenConnections connections; 44 | 45 | /** 46 | * Pushes a notification message to all connected WebSocket clients. 47 | * 48 | * @param message The notification message to send 49 | * @return Response with status 201 if successful 50 | * @throws WebApplicationException with status 422 if message is blank 51 | */ 52 | @Path("notify") 53 | @POST 54 | @Operation(summary = "Push notification message", description = "Pushes a notification message to all connected clients") 55 | @APIResponses({ 56 | @APIResponse(responseCode = "201", description = "Notification message sent successfully"), 57 | @APIResponse(responseCode = "422", description = "Message cannot be null or blank") 58 | }) 59 | public Response notify(@QueryParam("message") String message) { 60 | if (StringUtils.isBlank(message)) { 61 | // 422 Unprocessable Entity 62 | throw new WebApplicationException("Id was invalidly set on request.", 422); 63 | } 64 | SocketMessage pushMessage = SocketMessage.builder().id(UUID.randomUUID().toString()) 65 | .type(SocketMessageType.NOTIFICATION).message(message).context(MDC.getCopyOfContextMap()).build(); 66 | sendMessage(pushMessage); 67 | return Response.ok(pushMessage).status(Status.CREATED).build(); 68 | } 69 | 70 | /** 71 | * Pushes a refresh signal to all connected WebSocket clients. 72 | * This will trigger clients to refresh their UI data. 73 | * 74 | * @return Response with status 201 if successful 75 | */ 76 | @Path("refresh") 77 | @POST 78 | @Operation(summary = "Push a UI refresh signal", description = "Pushes a UI refresh signal to all connected clients") 79 | @APIResponses({ 80 | @APIResponse(responseCode = "201", description = "Refresh UI message sent successfully"), 81 | }) 82 | public Response refresh() { 83 | SocketMessage pushMessage = SocketMessage.builder().id(UUID.randomUUID().toString()) 84 | .type(SocketMessageType.REFRESH_DATA).context(MDC.getCopyOfContextMap()).build(); 85 | sendMessage(pushMessage); 86 | return Response.ok(pushMessage).status(Status.CREATED).build(); 87 | } 88 | 89 | /** 90 | * Helper method to send a message to all connected WebSocket clients. 91 | * Includes a simulated processing delay of 2 seconds. 92 | * 93 | * @param message The SocketMessage to send to all clients 94 | */ 95 | private void sendMessage(SocketMessage message) { 96 | LOG.infof("WebSocket: %s", message); 97 | try { 98 | // sleep to simulate time processing an event before publishing 99 | Thread.sleep(2000); 100 | } catch (InterruptedException e) { 101 | LOG.error("Error sending websocketmessage", e); 102 | } 103 | connections.forEach(connection -> connection.sendTextAndAwait(message)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/melloware/quarkus/support/QueryRequest.java: -------------------------------------------------------------------------------- 1 | package com.melloware.quarkus.support; 2 | 3 | import java.time.Instant; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | import java.util.stream.Collectors; 11 | 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 14 | 15 | import com.fasterxml.jackson.annotation.JsonIgnore; 16 | 17 | import io.quarkus.panache.common.Sort; 18 | import io.quarkus.runtime.annotations.RegisterForReflection; 19 | import lombok.AllArgsConstructor; 20 | import lombok.Data; 21 | import lombok.EqualsAndHashCode; 22 | import lombok.NoArgsConstructor; 23 | 24 | /** 25 | * Represents a PrimeReact query request from the UI for a complex datatable with multiple sorts, multiple filters, and 26 | * pagination. 27 | *

28 | * This class handles: 29 | *

    30 | *
  • Pagination - first record, rows per page, page number
  • 31 | *
  • Sorting - both single field and multi-field sorting
  • 32 | *
  • Filtering - complex filtering with multiple constraints and operators
  • 33 | *
34 | *

35 | * The class provides methods to: 36 | *

    37 | *
  • Calculate sort criteria for Panache queries
  • 38 | *
  • Build filter criteria with AND/OR operators
  • 39 | *
  • Generate SQL parameters for filters
  • 40 | *
41 | */ 42 | @Data 43 | @NoArgsConstructor 44 | @AllArgsConstructor 45 | @RegisterForReflection 46 | @Schema(description = "Represents a PrimeReact query request from the UI for a complex datatable with multiple sorts, multiple filters, and pagination.") 47 | public class QueryRequest { 48 | 49 | /** 50 | * The index of the first record to return 51 | */ 52 | @Schema(examples = {"1"}, description = "First record") 53 | private int first; 54 | 55 | /** 56 | * Number of rows to return per page 57 | */ 58 | @Schema(examples = {"10"}, description = "Number of rows") 59 | private int rows; 60 | 61 | /** 62 | * Current page number 63 | */ 64 | @Schema(examples = {"1"}, description = "Page number") 65 | private int page; 66 | 67 | /** 68 | * Field name to sort by when using single field sorting 69 | */ 70 | @Schema(examples = {"firstName"}, description = "Sort field if single field sorting") 71 | private String sortField; 72 | 73 | /** 74 | * Sort direction for single field sorting (-1 desc, 0 none, 1 asc) 75 | */ 76 | @Schema(examples = {"1"}, description = "Sort order if single field sorting either -1 desc, 0 none, 1 asc") 77 | private int sortOrder; 78 | 79 | /** 80 | * List of sort criteria for multiple field sorting 81 | */ 82 | @Schema(description = "Multiple sorting list of columns to sort and in which order") 83 | private List multiSortMeta = new ArrayList<>(); 84 | 85 | /** 86 | * Map of column filters and their criteria 87 | */ 88 | @Schema(description = "Map of columns being filtered and their filter criteria") 89 | private Map filters = new HashMap<>(); 90 | 91 | /** 92 | * Map of column name overrides to map UI column names to database column names 93 | * e.g. "codeListName" to "codelist.name" 94 | */ 95 | @JsonIgnore 96 | private Map overrides = new HashMap<>(); 97 | 98 | /** 99 | * Determines if the request uses single field sorting 100 | * 101 | * @return true if using single field sort, false otherwise 102 | */ 103 | @JsonIgnore 104 | public boolean isSingleSort() { 105 | return sortField != null && !sortField.isEmpty(); 106 | } 107 | 108 | /** 109 | * Determines if the request uses multiple field sorting 110 | * 111 | * @return true if using multiple field sort, false otherwise 112 | */ 113 | @JsonIgnore 114 | public boolean isMultipleSort() { 115 | return !getMultiSortMeta().isEmpty(); 116 | } 117 | 118 | /** 119 | * Determines if the table is using filterDisplay="menu" vs filterDisplay="row" 120 | * 121 | * @return true if filtering by menu, false if filtering by row 122 | */ 123 | @JsonIgnore 124 | public boolean isFilterMenu() { 125 | return getFilters().entrySet().stream().anyMatch(e -> StringUtils.isNotBlank(e.getValue().getOperator())); 126 | } 127 | 128 | /** 129 | * Calculates the Panache Sort criteria based on single or multiple field sorting 130 | * 131 | * @return Sort object configured with the requested sort criteria 132 | */ 133 | @JsonIgnore 134 | public Sort calculateSort() { 135 | Sort sort = null; 136 | if (isSingleSort()) { 137 | sort = Sort.by(checkOverride(getSortField()), 138 | getSortOrder() == 1 ? Sort.Direction.Ascending : Sort.Direction.Descending); 139 | } else if (isMultipleSort()) { 140 | for (final QueryRequest.MultiSortMeta multiSortMeta : getMultiSortMeta()) { 141 | if (sort == null) { 142 | sort = Sort.by(checkOverride(multiSortMeta.getField()), multiSortMeta.getSqlOrder()); 143 | } else { 144 | sort.and(checkOverride(multiSortMeta.getField()), multiSortMeta.getSqlOrder()); 145 | } 146 | } 147 | } 148 | return sort; 149 | } 150 | 151 | /** 152 | * Checks if a column name has an override mapping and returns the mapped name if it exists 153 | * 154 | * @param column The original column name 155 | * @return The mapped column name if an override exists, otherwise the original name 156 | */ 157 | private String checkOverride(final String column) { 158 | return overrides.getOrDefault(column, column); 159 | } 160 | 161 | /** 162 | * Calculates the filter criteria and builds the filter query string 163 | * 164 | * @param operator The operator (AND/OR) to use when joining multiple filters 165 | * @return FilterCriteria containing the query string and filter parameters 166 | */ 167 | public FilterCriteria calculateFilters(final FilterOperator operator) { 168 | final Map filters = getFilters(); 169 | 170 | final StringBuilder filterQuery = new StringBuilder(1024); 171 | if (this.isFilterMenu()) { 172 | for (final Map.Entry entry : filters.entrySet()) { 173 | final String column = checkOverride(entry.getKey()); 174 | final MultiFilterMeta filterConstraints = entry.getValue(); 175 | final List constraints = filterConstraints.getConstraints(); 176 | 177 | constraints.removeIf(e -> StringUtils.isBlank(Objects.toString(e.getValue(), null))); 178 | if (constraints.isEmpty()) { 179 | continue; 180 | } 181 | 182 | final AtomicInteger i = new AtomicInteger(); 183 | final String result = filterConstraints.getConstraints().stream() 184 | .map(constraint -> column + constraint.getSqlClause() + " :" + 185 | createColumnVariable(column, i.getAndIncrement())) 186 | .collect(Collectors.joining(" " + filterConstraints.operator + " ")); 187 | if (StringUtils.isNotEmpty(result)) { 188 | if (!filterQuery.isEmpty()) { 189 | filterQuery.append(StringUtils.SPACE).append(operator.name()).append(" ("); 190 | } else { 191 | filterQuery.append("("); 192 | } 193 | filterQuery.append(result).append(")"); 194 | } 195 | } 196 | } else { 197 | filters.entrySet() 198 | .removeIf(e -> StringUtils.isBlank(Objects.toString(e.getValue().getValue(), null))); 199 | final String result = filters.entrySet().stream() 200 | .map(entry -> checkOverride(entry.getKey()) + entry.getValue().getSqlClause() + " :" + 201 | entry.getKey()) 202 | .collect(Collectors.joining(" " + operator.name() + " ")); 203 | filterQuery.append(result); 204 | } 205 | 206 | return new FilterCriteria(filterQuery.toString(), filters); 207 | } 208 | 209 | /** 210 | * Builds a map of filter parameters and their values for SQL query execution 211 | * 212 | * @return Map of parameter names to their values 213 | */ 214 | public Map calculateFilterParameters() { 215 | Map params = new HashMap<>(); 216 | if (this.isFilterMenu()) { 217 | for (final Map.Entry entry : filters.entrySet()) { 218 | final String column = checkOverride(entry.getKey()); 219 | 220 | int i = 0; 221 | for (final QueryRequest.FilterConstraint constraint : entry.getValue().getConstraints()) { 222 | params.put(createColumnVariable(column, i++), constraint.getSqlValue()); 223 | } 224 | } 225 | } else { 226 | params = filters.entrySet().stream() 227 | .collect(Collectors.toMap(e -> checkOverride(e.getKey()), e -> e.getValue().getSqlValue())); 228 | } 229 | return params; 230 | } 231 | 232 | /** 233 | * Creates a unique parameter name for a column and counter 234 | * 235 | * @param column The column name 236 | * @param counter The counter value 237 | * @return A unique parameter name 238 | */ 239 | private String createColumnVariable(final String column, final int counter) { 240 | return StringUtils.remove(column, '.') + counter; 241 | } 242 | 243 | /** 244 | * Enum defining filter operators for joining multiple filters 245 | */ 246 | @RegisterForReflection 247 | public enum FilterOperator { 248 | AND, 249 | OR 250 | } 251 | 252 | /** 253 | * Class representing multiple sort criteria for a single field 254 | */ 255 | @Data 256 | @NoArgsConstructor 257 | @RegisterForReflection 258 | public static class MultiSortMeta { 259 | @Schema(examples = {"lastName"}, description = "Sort field for this multiple sort") 260 | private String field; 261 | @Schema(examples = {"1"}, description = "Sort order for this field either -1 desc, 0 none, 1 asc") 262 | private int order; 263 | 264 | /** 265 | * Converts the numeric order to a Panache Sort.Direction 266 | * 267 | * @return Sort.Direction.Ascending for 1, Sort.Direction.Descending otherwise 268 | */ 269 | @JsonIgnore 270 | public Sort.Direction getSqlOrder() { 271 | return order == 1 ? Sort.Direction.Ascending : Sort.Direction.Descending; 272 | } 273 | } 274 | 275 | /** 276 | * Class representing a single filter constraint 277 | */ 278 | @Data 279 | @NoArgsConstructor 280 | @RegisterForReflection 281 | public static class FilterConstraint { 282 | @Schema(description = "Value to filter this column by") 283 | private Object value; 284 | @Schema(examples = {"equals"}, description = "Filter match mode e.g. equals, notEquals, contains, notContains, gt, gte, lt, lte") 285 | private String matchMode; 286 | 287 | /** 288 | * Gets the SQL operator for the filter match mode 289 | * 290 | * @return The SQL operator string 291 | */ 292 | @JsonIgnore 293 | public String getSqlClause() { 294 | return switch (matchMode) { 295 | case "equals", "dateIs" -> "="; 296 | case "notEquals", "dateIsNot" -> "!="; 297 | case "notContains" -> " not like "; 298 | case "gt", "dateAfter" -> ">"; 299 | case "gte" -> ">="; 300 | case "lt", "dateBefore" -> "<"; 301 | case "lte" -> "<="; 302 | default -> " like "; 303 | }; 304 | } 305 | 306 | /** 307 | * Converts the filter value to the appropriate SQL value based on match mode 308 | * 309 | * @return The converted value for SQL 310 | */ 311 | @JsonIgnore 312 | public Object getSqlValue() { 313 | final Object value = getValue(); 314 | return switch (matchMode) { 315 | case "contains", "notContains" -> "%" + value + "%"; 316 | case "startsWith" -> value + "%"; 317 | case "endsWith" -> "%" + value; 318 | case "gt", "gte", "lt", "lte" -> 319 | Integer.parseInt((String) value); 320 | case "dateAfter", "dateBefore", "dateIs", "dateIsNot" -> Instant.parse((String) value); 321 | default -> value; 322 | }; 323 | } 324 | } 325 | 326 | /** 327 | * Class representing multiple filter constraints for a single field 328 | */ 329 | @Data 330 | @NoArgsConstructor 331 | @EqualsAndHashCode(callSuper = true) 332 | @RegisterForReflection 333 | public static class MultiFilterMeta extends FilterConstraint { 334 | 335 | @Schema(description = "Filter operator either 'and' or 'or'") 336 | private String operator; 337 | 338 | @Schema(description = "List of filter constraints for this filter") 339 | private List constraints = new ArrayList<>(); 340 | 341 | } 342 | 343 | /** 344 | * Class representing the complete filter criteria including query and parameters 345 | */ 346 | @Data 347 | @NoArgsConstructor 348 | @AllArgsConstructor 349 | @RegisterForReflection 350 | public static class FilterCriteria { 351 | private String query; 352 | private Map parameters; 353 | } 354 | } -------------------------------------------------------------------------------- /src/main/java/com/melloware/quarkus/support/QueryResponse.java: -------------------------------------------------------------------------------- 1 | package com.melloware.quarkus.support; 2 | 3 | import java.util.List; 4 | 5 | import org.eclipse.microprofile.openapi.annotations.media.Schema; 6 | 7 | import io.quarkus.runtime.annotations.RegisterForReflection; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | /** 12 | * Represents a PrimeReact query response to the UI for a complex datatable with multiple sorts, multiple filters, and 13 | * pagination. 14 | *

15 | * This class handles the response format expected by PrimeReact datatables including: 16 | *

    17 | *
  • Total record count for pagination
  • 18 | *
  • List of records for current page
  • 19 | *
20 | *

21 | * The generic type T represents the type of records being returned. 22 | * 23 | * @param The type of records in the response 24 | */ 25 | @Data 26 | @NoArgsConstructor 27 | @RegisterForReflection 28 | @Schema(description = "Represents a PrimeReact query response to the UI for a complex datatable with multiple sorts, multiple filters, and pagination.") 29 | public class QueryResponse { 30 | 31 | /** 32 | * The total number of records available that match the query criteria. 33 | * Used for pagination calculations. 34 | */ 35 | @Schema(examples = {"4128"}, description = "Total records available by this query criteria") 36 | private long totalRecords; 37 | 38 | /** 39 | * The list of records for the current page, after applying 40 | * pagination, sorting and filtering criteria. 41 | */ 42 | @Schema(description = "Records for this set of pagination, sorting, filtering.") 43 | private List records; 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ################# 2 | ### LOGGING ### 3 | ################# 4 | quarkus.log.level=INFO 5 | quarkus.log.console.async.enable=true 6 | quarkus.http.access-log.enabled=true 7 | quarkus.http.access-log.exclude-pattern=/q/.* 8 | 9 | ################# 10 | ### DATABASE ### 11 | ################# 12 | quarkus.liquibase.migrate-at-start=true 13 | %dev.quarkus.hibernate-orm.log.sql=true 14 | quarkus.datasource.db-kind=postgresql 15 | quarkus.datasource.jdbc.max-size=20 16 | quarkus.datasource.jdbc.min-size=2 17 | quarkus.datasource.jdbc.initial-size=2 18 | 19 | ################# 20 | ### HTTP ### 21 | ################# 22 | quarkus.http.port=8080 23 | quarkus.http.enable-compression=true 24 | quarkus.http.compressors=deflate,gzip,br 25 | quarkus.http.cors.enabled=true 26 | quarkus.http.filter.others.header.Cache-Control=no-cache 27 | quarkus.http.filter.others.matches=/.* 28 | quarkus.http.filter.others.methods=GET,POST,PUT,DELETE 29 | quarkus.http.filter.others.order=0 30 | quarkus.http.filter.static.header."Cache-Control"=public, immutable, max-age=31536000 31 | quarkus.http.filter.static.matches=/static/.+ 32 | quarkus.http.filter.static.methods=GET,HEAD 33 | quarkus.http.filter.static.order=1 34 | quarkus.websockets-next.server.auto-ping-interval=30 35 | 36 | ################# 37 | ### QUINOA ### 38 | ################# 39 | quarkus.quinoa.package-manager-install=true 40 | quarkus.quinoa.package-manager-install.node-version=24.10.0 41 | quarkus.quinoa.enable-spa-routing=true 42 | 43 | ################# 44 | ### CACHING ### 45 | ################# 46 | #quarkus.cache.caffeine."cars".expire-after-write=10s 47 | #quarkus.cache.caffeine."cars".metrics-enabled=true 48 | #quarkus.cache.caffeine."cars".initial-capacity=10 49 | #quarkus.cache.caffeine."cars".maximum-size=20 50 | 51 | ################# 52 | ### OPEN API ### 53 | ################# 54 | quarkus.resteasy.problem.include-mdc-properties=uuid,application,version 55 | quarkus.resteasy.problem.constraint-violation.status=422 56 | quarkus.resteasy.problem.constraint-violation.title=Unprocessable Content 57 | quarkus.resteasy.problem.constraint-violation.description=Unprocessable Content: server understood the content type of the request content, and the syntax of the request content was correct, but it was unable to process the contained instructions. 58 | quarkus.logging-manager.openapi.included=true 59 | quarkus.smallrye-health.openapi.included=true 60 | quarkus.smallrye-openapi.store-schema-directory=src/main/webui/ 61 | quarkus.swagger-ui.title=Quarkus PrimeReact 62 | quarkus.swagger-ui.always-include=true 63 | quarkus.swagger-ui.deep-linking=true 64 | quarkus.swagger-ui.path=swagger-ui 65 | quarkus.swagger-ui.tags-sorter=alpha 66 | quarkus.swagger-ui.operations-sorter=alpha 67 | quarkus.swagger-ui.theme=flattop 68 | mp.openapi.extensions.smallrye.info.title=Quarkus PrimeReact Monorepo 69 | mp.openapi.extensions.smallrye.info.description=Quarkus monorepo demonstrating Panache REST server with PrimeReact UI client 70 | mp.openapi.extensions.smallrye.info.version=1.0.0 71 | mp.openapi.extensions.smallrye.info.contact.name=Melloware 72 | mp.openapi.extensions.smallrye.info.contact.email=mellowaredev@gmail.com 73 | mp.openapi.extensions.smallrye.info.contact.url=https://melloware.com 74 | 75 | ################# 76 | ### DEV ### 77 | ################# 78 | quarkus.devservices.enabled=false 79 | %dev.quarkus.ngrok.enabled=false 80 | %dev.quarkus.datasource.dev-ui.allow-sql=true 81 | %dev.quarkus.hibernate-orm.dev-ui.allow-hql=true 82 | -------------------------------------------------------------------------------- /src/main/resources/db/changeLog.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/db/changes/00100_create_schema.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | New table CAR. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/webui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | # Overrides Prettier config 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | tab_width = 4 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | max_line_length = 160 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/main/webui/.env.development: -------------------------------------------------------------------------------- 1 | VITE_REACT_APP_API_SERVER = / 2 | VITE_REACT_APP_VERSION=$npm_package_version -------------------------------------------------------------------------------- /src/main/webui/.env.production: -------------------------------------------------------------------------------- 1 | VITE_REACT_APP_API_SERVER = / 2 | VITE_REACT_APP_VERSION=$npm_package_version -------------------------------------------------------------------------------- /src/main/webui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:react/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "ignorePatterns": [ 12 | "node_modules/", 13 | "config/", 14 | "scripts/", 15 | "**/*.d.ts", 16 | "src/__tests__/**" 17 | ], 18 | "plugins": [ 19 | "@typescript-eslint", 20 | "react", 21 | "react-hooks", 22 | "prettier" 23 | ], 24 | "rules": { 25 | "no-console": 2, 26 | "@typescript-eslint/no-non-null-assertion": "off", 27 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off", 28 | "react/react-in-jsx-scope": "off", 29 | "react-hooks/exhaustive-deps": "off", 30 | "@typescript-eslint/no-unused-vars": [ 31 | "error", 32 | { 33 | "args": "all", 34 | "argsIgnorePattern": "^_" 35 | } 36 | ] 37 | }, 38 | "settings": { 39 | "react": { 40 | "version": "detect" 41 | } 42 | }, 43 | "overrides": [ 44 | { 45 | "files": [ 46 | "src/**/service/**" 47 | ], 48 | "rules": { 49 | "@typescript-eslint/no-explicit-any": "off", 50 | "@typescript-eslint/ban-ts-comment": "off" 51 | } 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /src/main/webui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 4, 4 | "trailingComma": "none", 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxSingleQuote": false, 8 | "printWidth": 160, 9 | "bracketSameLine": false 10 | } -------------------------------------------------------------------------------- /src/main/webui/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } -------------------------------------------------------------------------------- /src/main/webui/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "msedge", 6 | "request": "launch", 7 | "name": "Launch Edge against localhost", 8 | "url": "http://localhost:3000", 9 | "webRoot": "${workspaceFolder}" 10 | }, 11 | { 12 | "command": "npm start", 13 | "name": "NPM Start", 14 | "request": "launch", 15 | "type": "node-terminal" 16 | }, 17 | { 18 | "command": "npm test", 19 | "name": "NPM Test", 20 | "request": "launch", 21 | "type": "node-terminal" 22 | }, 23 | { 24 | "command": "npm run sass", 25 | "name": "SASS Watch", 26 | "request": "launch", 27 | "type": "node-terminal" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/main/webui/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "source.organizeImports": "explicit" 5 | }, 6 | "[javascript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.formatOnSave": true 9 | }, 10 | "[typescript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.formatOnSave": true 13 | }, 14 | "[typescriptreact]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode", 16 | "editor.formatOnSave": true 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/webui/farm.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@farmfe/core'; 2 | 3 | export default defineConfig(({ mode }) => ({ 4 | plugins: ['@farmfe/plugin-react', '@farmfe/plugin-sass'] 5 | })); 6 | -------------------------------------------------------------------------------- /src/main/webui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quarkus Monorepo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/webui/orval.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cars: { 3 | output: { 4 | target: 'src/service/CarService.ts', 5 | client: 'react-query', 6 | httpClient: 'axios', 7 | mock: false, 8 | prettier: false, 9 | override: { 10 | useDates: true, 11 | mutator: { 12 | path: 'src/service/AxiosMutator.ts', 13 | name: 'useAxiosMutator' 14 | }, 15 | query: { 16 | useQuery: true 17 | } 18 | } 19 | }, 20 | input: { 21 | target: './openapi.yaml' 22 | } 23 | }, 24 | carsZod: { 25 | output: { 26 | client: 'zod', 27 | target: 'src/service/CarService.zod.ts', 28 | override: { 29 | useDates: true, 30 | zod: { 31 | coerce: { 32 | response: true, 33 | query: true, 34 | param: true, 35 | header: true, 36 | body: true 37 | }, 38 | } 39 | } 40 | }, 41 | input: { 42 | target: './openapi.yaml' 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/main/webui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quarkus-primereact", 3 | "version": "10.9.7", 4 | "description": "Quarkus monorepo demonstrating Quarkus REST server with PrimeReact UI client.", 5 | "homepage": ".", 6 | "private": false, 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/melloware/quarkus-primereact.git" 10 | }, 11 | "license": "MIT", 12 | "dependencies": { 13 | "@tanstack/react-form": "1.27.1", 14 | "@tanstack/react-query": "5.90.12", 15 | "axios": "1.13.2", 16 | "primeflex": "4.0.0", 17 | "primeicons": "7.0.0", 18 | "primelocale": "2.2.3", 19 | "primereact": "10.9.7", 20 | "react": "19.2.1", 21 | "react-dom": "19.2.1", 22 | "react-router-dom": "7.10.1", 23 | "react-transition-group": "4.4.5", 24 | "react-use-websocket": "4.13.0", 25 | "zod": "4.1.13" 26 | }, 27 | "devDependencies": { 28 | "@farmfe/cli": "1.0.5", 29 | "@farmfe/core": "1.7.11", 30 | "@farmfe/plugin-react": "1.2.6", 31 | "@farmfe/plugin-sass": "1.1.0", 32 | "@tanstack/react-query-devtools": "5.91.1", 33 | "@types/node": "24.10.1", 34 | "@types/react": "19.2.7", 35 | "@types/react-dom": "19.2.3", 36 | "@types/react-router-dom": "5.3.3", 37 | "@types/react-transition-group": "4.4.12", 38 | "core-js": "3.47.0", 39 | "cross-env": "10.1.0", 40 | "eslint-config-prettier": "10.1.8", 41 | "eslint-plugin-prettier": "5.5.4", 42 | "orval": "8.0.0-rc.3", 43 | "prettier": "3.7.4", 44 | "react-refresh": "0.18.0", 45 | "typescript": "5.9.3" 46 | }, 47 | "scripts": { 48 | "format": "prettier --write \"{src,__tests__}/**/*.{ts,tsx}\"", 49 | "codegen": "orval && npm run format", 50 | "start": "npm run dev", 51 | "dev": "npm run codegen && farm start", 52 | "build": "npm run codegen && farm build", 53 | "preview": "farm preview", 54 | "clean": "farm clean" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/webui/public/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melloware/quarkus-primereact/HEAD/src/main/webui/public/static/images/favicon.ico -------------------------------------------------------------------------------- /src/main/webui/public/static/images/plus-sign.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 21 | 28 | 29 | 46 | 48 | 49 | 51 | image/svg+xml 52 | 54 | 55 | 56 | 57 | 61 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/main/webui/public/static/images/primereact-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/webui/public/static/images/primereact.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/webui/public/static/images/quarkus.svg: -------------------------------------------------------------------------------- 1 | quarkus_icon_rgb_1024px_reverse -------------------------------------------------------------------------------- /src/main/webui/public/static/images/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main/webui/src/App.css: -------------------------------------------------------------------------------- 1 | body .color-swatch > p { 2 | -webkit-filter: invert(100%); 3 | filter: invert(100%); 4 | } 5 | 6 | body .p-colorpicker-preview { 7 | padding: 1.3rem; 8 | } 9 | 10 | .react-logo { 11 | height: 6em; 12 | padding: 1.5em; 13 | will-change: filter; 14 | animation: react-logo-spin infinite 20s linear; 15 | } 16 | 17 | .react-logo:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes react-logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/webui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { all as locales } from 'primelocale'; 2 | import { useMountEffect } from 'primereact/hooks'; 3 | import { classNames, DomHandler } from 'primereact/utils'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { Link, Route, Routes, useLocation } from 'react-router-dom'; 6 | import { CSSTransition } from 'react-transition-group'; 7 | import { AppMenu } from './AppMenu'; 8 | import { AppMenuItem, AppMenuItemClickParams } from './AppMenuItem'; 9 | import CrudPage from './CrudPage'; 10 | 11 | import 'primeflex/primeflex.css'; 12 | import 'primeicons/primeicons.css'; 13 | import { addLocale, locale, LocaleOptions } from 'primereact/api'; 14 | import 'primereact/resources/primereact.min.css'; 15 | import 'primereact/resources/themes/lara-dark-blue/theme.css'; 16 | import './App.css'; 17 | import './assets/layout/layout.scss'; 18 | 19 | const App = () => { 20 | const [layoutMode] = useState('static'); 21 | const [layoutColorMode] = useState('dark'); 22 | const [inputStyle] = useState('outlined'); 23 | const [ripple] = useState(true); 24 | const [staticMenuInactive, setStaticMenuInactive] = useState(true); 25 | const [overlayMenuActive, setOverlayMenuActive] = useState(false); 26 | const [mobileMenuActive, setMobileMenuActive] = useState(false); 27 | const [mobileTopbarMenuActive, setMobileTopbarMenuActive] = useState(false); 28 | const location = useLocation(); 29 | 30 | let menuClick = false; 31 | let mobileTopbarMenuClick = false; 32 | 33 | useEffect(() => { 34 | if (mobileMenuActive) { 35 | DomHandler.addClass(document.body, 'body-overflow-hidden'); 36 | } else { 37 | DomHandler.removeClass(document.body, 'body-overflow-hidden'); 38 | } 39 | }, [mobileMenuActive]); 40 | 41 | useEffect(() => { 42 | window.scrollTo(0, 0); 43 | }, [location]); 44 | 45 | useMountEffect(() => { 46 | document.documentElement.style.fontSize = '14px'; 47 | 48 | // Determine the browser's locale 49 | const browserLocale = navigator.languages?.[0] || navigator.language; 50 | 51 | // Find the appropriate locale file based on the browser's locale 52 | const selectedLocale = getClosestLocale(browserLocale); 53 | addLocale(browserLocale, selectedLocale); 54 | locale(browserLocale); 55 | }); 56 | 57 | const getClosestLocale = (browserLocale: string): LocaleOptions => { 58 | // First, try to find an exact match 59 | const normalizedLocale = browserLocale.toLowerCase(); 60 | if (normalizedLocale in locales) { 61 | return locales[normalizedLocale as keyof typeof locales]; 62 | } 63 | 64 | // If no exact match, try to find a match for the language part 65 | const languagePart = browserLocale.split('-')[0]; 66 | if (languagePart in locales) { 67 | return locales[languagePart as keyof typeof locales]; 68 | } 69 | 70 | // If still no match, return English as default 71 | return locales.en; 72 | }; 73 | 74 | const onWrapperClick = (_event: React.MouseEvent) => { 75 | if (!menuClick) { 76 | setOverlayMenuActive(false); 77 | setMobileMenuActive(false); 78 | } 79 | 80 | if (!mobileTopbarMenuClick) { 81 | setMobileTopbarMenuActive(false); 82 | } 83 | 84 | mobileTopbarMenuClick = false; 85 | menuClick = false; 86 | }; 87 | 88 | const onToggleMenuClick = (event: React.MouseEvent) => { 89 | menuClick = true; 90 | 91 | if (isDesktop()) { 92 | if (layoutMode === 'overlay') { 93 | if (mobileMenuActive === true) { 94 | setOverlayMenuActive(true); 95 | } 96 | 97 | setOverlayMenuActive((prevState) => !prevState); 98 | setMobileMenuActive(false); 99 | } else if (layoutMode === 'static') { 100 | setStaticMenuInactive((prevState) => !prevState); 101 | } 102 | } else { 103 | setMobileMenuActive((prevState) => !prevState); 104 | } 105 | 106 | event.preventDefault(); 107 | }; 108 | 109 | const onSidebarClick = () => { 110 | menuClick = true; 111 | }; 112 | 113 | const onMobileTopbarMenuClick = (event: React.MouseEvent) => { 114 | mobileTopbarMenuClick = true; 115 | 116 | setMobileTopbarMenuActive((prevState) => !prevState); 117 | event.preventDefault(); 118 | }; 119 | 120 | const onMenuItemClick = (event?: AppMenuItemClickParams) => { 121 | if (!event?.item) { 122 | setOverlayMenuActive(false); 123 | setMobileMenuActive(false); 124 | } 125 | }; 126 | const isDesktop = () => { 127 | return window.innerWidth >= 992; 128 | }; 129 | 130 | const menu = [ 131 | { 132 | label: 'Application', 133 | items: [{ label: 'CRUD', icon: 'pi pi-fw pi-home', to: '/' }] 134 | }, 135 | { 136 | label: 'Server Docs', 137 | items: [ 138 | { label: 'Quarkus', icon: 'pi pi-fw pi-wifi', url: 'https://quarkus.io/', target: '_blank' }, 139 | { label: 'Quinoa', icon: 'pi pi-fw pi-wifi', url: 'https://quarkiverse.github.io/quarkiverse-docs/quarkus-quinoa', target: '_blank' }, 140 | { label: 'REST Problem', icon: 'pi pi-fw pi-wifi', url: 'https://github.com/quarkiverse/quarkus-resteasy-problem', target: '_blank' }, 141 | { label: 'Hibernate/Panache', icon: 'pi pi-fw pi-database', url: 'https://quarkus.io/guides/hibernate-orm-panache', target: '_blank' }, 142 | { label: 'PostgreSQL', icon: 'pi pi-fw pi-database', url: 'https://www.postgresql.org/', target: '_blank' }, 143 | { label: 'Liquibase', icon: 'pi pi-fw pi-database', url: 'https://www.liquibase.com/', target: '_blank' }, 144 | { label: 'OpenAPI', icon: 'pi pi-fw pi-tag', url: 'https://www.openapis.org/', target: '_blank' }, 145 | { label: 'WebSockets Next', icon: 'pi pi-fw pi-wave-pulse', url: 'https://quarkus.io/guides/websockets-next-tutorial', target: '_blank' } 146 | ] 147 | }, 148 | { 149 | label: 'Client Docs', 150 | items: [ 151 | { label: 'React', icon: 'pi pi-fw pi-globe', url: 'https://reactjs.org/', target: '_blank' }, 152 | { label: 'React WebSocket', icon: 'pi pi-fw pi-wave-pulse', url: 'https://github.com/robtaussig/react-use-websocket', target: '_blank' }, 153 | { label: 'PrimeReact', icon: 'pi pi-fw pi-prime', url: 'https://primefaces.org/primereact/', target: '_blank' }, 154 | { label: 'TanStack Form', icon: 'pi pi-fw pi-verified', url: 'https://tanstack.com/form/latest', target: '_blank' }, 155 | { label: 'TanStack Query', icon: 'pi pi-fw pi-tag', url: 'https://tanstack.com/query/latest', target: '_blank' }, 156 | { label: 'Orval', icon: 'pi pi-fw pi-tag', url: 'https://orval.dev/', target: '_blank' }, 157 | { label: 'Zod', icon: 'pi pi-fw pi-verified', url: 'https://zod.dev/', target: '_blank' } 158 | ] 159 | } 160 | ] as AppMenuItem[]; 161 | 162 | const wrapperClass = classNames('layout-wrapper', { 163 | 'layout-overlay': layoutMode === 'overlay', 164 | 'layout-static': layoutMode === 'static', 165 | 'layout-static-sidebar-inactive': staticMenuInactive && layoutMode === 'static', 166 | 'layout-overlay-sidebar-active': overlayMenuActive && layoutMode === 'overlay', 167 | 'layout-mobile-sidebar-active': mobileMenuActive, 168 | 'p-input-filled': inputStyle === 'filled', 169 | 'p-ripple-disabled': ripple === false, 170 | 'layout-theme-light': layoutColorMode === 'light' 171 | }); 172 | 173 | return ( 174 |
175 |
176 | 177 | Quarkus 178 | Quarkus 179 | 180 | 181 | 184 | 185 | 188 | 195 |
196 | 197 |
198 | 199 |
200 | 201 |
202 |
203 | 204 | } /> 205 | 206 |
207 | 208 |
209 | Powered by 210 | 211 | PrimeReact 212 | 213 |
214 |
215 | 216 | 217 |
218 |
219 |
220 | ); 221 | }; 222 | 223 | export default App; 224 | -------------------------------------------------------------------------------- /src/main/webui/src/AppMenu.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Badge } from 'primereact/badge'; 3 | import { Ripple } from 'primereact/ripple'; 4 | import { classNames } from 'primereact/utils'; 5 | import React, { KeyboardEvent, useState } from 'react'; 6 | import { NavLink } from 'react-router-dom'; 7 | import { CSSTransition } from 'react-transition-group'; 8 | import { AppMenuItem, AppMenuItemClickParams } from './AppMenuItem'; 9 | 10 | const AppSubmenu = (props: { 11 | root?: boolean; 12 | parentMenuItemActive?: boolean; 13 | onRootMenuitemClick?: (event: React.MouseEvent, item: AppMenuItem, index: number) => void; 14 | onMenuItemClick?: (event?: AppMenuItemClickParams) => void; 15 | menuMode?: string; 16 | mobileMenuActive?: boolean; 17 | items?: AppMenuItem[] | undefined; 18 | className?: string; 19 | role?: string; 20 | }) => { 21 | const [activeIndex, setActiveIndex] = useState(null); 22 | 23 | const onMenuItemClick = (event: AppMenuItemClickParams) => { 24 | //avoid processing disabled items 25 | if (event.item.disabled) { 26 | event.originalEvent.preventDefault(); 27 | return true; 28 | } 29 | 30 | //execute command 31 | if (event.item.command) { 32 | event.item.command({ originalEvent: event.originalEvent, item: event.item }); 33 | } 34 | 35 | if (event.index === activeIndex) setActiveIndex(null); 36 | else setActiveIndex(event.index); 37 | 38 | if (props.onMenuItemClick) { 39 | props.onMenuItemClick({ 40 | originalEvent: event.originalEvent, 41 | item: event.item, 42 | index: event.index 43 | }); 44 | } 45 | }; 46 | 47 | const onKeyDown = (event: KeyboardEvent) => { 48 | if (event.code === 'Enter' || event.code === 'Space') { 49 | event.preventDefault(); 50 | event.currentTarget.click(); 51 | } 52 | }; 53 | 54 | const renderLinkContent = (item: AppMenuItem) => { 55 | const submenuIcon = item.items && ; 56 | const badge = item.badge && ; 57 | 58 | return ( 59 | 60 | 61 | {item.label} 62 | {submenuIcon} 63 | {badge} 64 | 65 | 66 | ); 67 | }; 68 | 69 | const renderLink = (item: AppMenuItem, i: number) => { 70 | const content = renderLinkContent(item); 71 | 72 | if (item.to) { 73 | return ( 74 | { 76 | const linkClasses = ['p-ripple']; 77 | if (isActive) linkClasses.push('router-link-active router-link-exact-active'); 78 | return linkClasses.join(' '); // returns "registerButton" or "registerButton active 79 | }} 80 | to={item.to} 81 | onClick={(e) => onMenuItemClick({ originalEvent: e, item, index: i })} 82 | target={item.target} 83 | > 84 | {content} 85 | 86 | ); 87 | } else { 88 | return ( 89 | onMenuItemClick({ originalEvent: e, item, index: i })} 97 | target={item.target} 98 | > 99 | {content} 100 | 101 | ); 102 | } 103 | }; 104 | 105 | const items = 106 | props.items && 107 | props.items.map((item, i) => { 108 | const active = activeIndex === i; 109 | const styleClass = classNames(item.badgeStyleClass, { 'layout-menuitem-category': props.root, 'active-menuitem': active && !item.to }); 110 | 111 | if (props.root) { 112 | return ( 113 |
  • 114 | {props.root === true && ( 115 | 116 |
    117 | {item.label} 118 |
    119 | 120 |
    121 | )} 122 |
  • 123 | ); 124 | } else { 125 | return ( 126 |
  • 127 | {renderLink(item, i)} 128 | 129 | 130 | 131 |
  • 132 | ); 133 | } 134 | }); 135 | 136 | return items ? ( 137 |
      138 | {items} 139 |
    140 | ) : null; 141 | }; 142 | 143 | export const AppMenu = (props: { model: AppMenuItem[] | undefined; onMenuItemClick: ((event?: AppMenuItemClickParams) => void) | undefined }) => { 144 | return ( 145 |
    146 | 147 |
    148 | ); 149 | }; 150 | -------------------------------------------------------------------------------- /src/main/webui/src/AppMenuItem.ts: -------------------------------------------------------------------------------- 1 | import type { MenuItem } from 'primereact/menuitem'; 2 | import { To } from 'react-router-dom'; 3 | 4 | export interface AppMenuItemClickParams { 5 | originalEvent: React.MouseEvent; 6 | item: AppMenuItem; 7 | index: number; 8 | } 9 | 10 | export interface AppMenuItem extends MenuItem { 11 | to?: To; 12 | badge?: string; 13 | badgeStyleClass?: string | undefined; 14 | items?: AppMenuItem[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/webui/src/CrudPage.tsx: -------------------------------------------------------------------------------- 1 | import { AnyFieldApi, useForm } from '@tanstack/react-form'; 2 | import { useQueryClient } from '@tanstack/react-query'; 3 | import { FilterMatchMode, FilterOperator, SortOrder } from 'primereact/api'; 4 | import { Button } from 'primereact/button'; 5 | import { Calendar } from 'primereact/calendar'; 6 | import { ColorPicker } from 'primereact/colorpicker'; 7 | import { Column, ColumnFilterElementTemplateOptions } from 'primereact/column'; 8 | import { DataTable, DataTableStateEvent } from 'primereact/datatable'; 9 | import { Dialog } from 'primereact/dialog'; 10 | import { Dropdown } from 'primereact/dropdown'; 11 | import { InputNumber } from 'primereact/inputnumber'; 12 | import { InputSwitch } from 'primereact/inputswitch'; 13 | import { InputText } from 'primereact/inputtext'; 14 | import { Toast } from 'primereact/toast'; 15 | import { Toolbar } from 'primereact/toolbar'; 16 | import { Tooltip } from 'primereact/tooltip'; 17 | import { classNames } from 'primereact/utils'; 18 | import React, { useEffect, useRef, useState } from 'react'; 19 | import useWebSocket from 'react-use-websocket'; 20 | import { z } from 'zod'; 21 | import { ErrorType } from './service/AxiosMutator'; 22 | import { 23 | Car, 24 | HttpProblem, 25 | SocketMessage, 26 | SocketMessageType, 27 | useDeleteEntityCarsId, 28 | useGetEntityCars, 29 | useGetEntityCarsManufacturers, 30 | usePostEntityCars, 31 | usePutEntityCarsId 32 | } from './service/CarService'; 33 | import { postEntityCarsBody } from './service/CarService.zod'; 34 | 35 | type CarInput = z.infer; 36 | 37 | /** 38 | * CRUD page demonstrating multiple TanStack Query and PrimeReact concepts such as lazy querying datable, 39 | * CRUD operations, React Hook Forms for validation etc. 40 | * 41 | * @returns the CrudPage 42 | */ 43 | const CrudPage = () => { 44 | // form 45 | let defaultValues = { 46 | id: undefined, 47 | vin: '', 48 | make: '', 49 | model: '', 50 | color: '', 51 | year: 2022, 52 | price: 0, 53 | modifiedTime: undefined 54 | } as CarInput; 55 | const form = useForm({ 56 | defaultValues: defaultValues, 57 | validators: { 58 | onChange: postEntityCarsBody 59 | }, 60 | onSubmit: async ({ value }) => { 61 | onSubmit(value as Car); 62 | } 63 | }); 64 | 65 | // refs 66 | const toastRef = useRef(null); 67 | const datatable = useRef>(null); 68 | 69 | // state 70 | const [car, setCar] = useState(defaultValues as Car); 71 | const [deleteCarDialog, setDeleteCarDialog] = useState(false); 72 | const [editCarDialog, setEditCarDialog] = useState(false); 73 | const [isMenuFilter, setMenuFilter] = useState(true); 74 | const [isMultipleSort, setMultipleSort] = useState(true); 75 | 76 | // socket 77 | const { lastJsonMessage } = useWebSocket(`${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/push/`, { 78 | shouldReconnect: (event: WebSocketEventMap['close']) => { 79 | console.log('WebSocket closed. Reconnecting...', event); 80 | return true; 81 | }, 82 | reconnectAttempts: 99, 83 | reconnectInterval: (lastAttemptNumber: number) => { 84 | console.log('WebSocket reconnecting...', lastAttemptNumber); 85 | const baseDelay = 1000; // 1 second 86 | const maxDelay = 30000; // 30 seconds 87 | 88 | // Exponential backoff with jitter 89 | const expDelay = Math.min(baseDelay * Math.pow(2, lastAttemptNumber), maxDelay); 90 | const jitter = Math.random() * 1000; // add up to 1s of random jitter 91 | return expDelay + jitter; 92 | }, 93 | heartbeat: { 94 | message: 'ping', 95 | returnMessage: 'pong', 96 | timeout: 10000, 97 | interval: 5000 98 | } 99 | }); 100 | 101 | const menuFilters = { 102 | vin: { operator: FilterOperator.OR, constraints: [{ value: '', matchMode: FilterMatchMode.CONTAINS }] }, 103 | make: { operator: FilterOperator.OR, constraints: [{ value: '', matchMode: FilterMatchMode.CONTAINS }] }, 104 | model: { operator: FilterOperator.AND, constraints: [{ value: '', matchMode: FilterMatchMode.CONTAINS }] }, 105 | color: { operator: FilterOperator.OR, constraints: [{ value: '', matchMode: FilterMatchMode.CONTAINS }] }, 106 | year: { operator: FilterOperator.OR, constraints: [{ value: '', matchMode: FilterMatchMode.GREATER_THAN_OR_EQUAL_TO }] }, 107 | modifiedTime: { operator: FilterOperator.OR, constraints: [{ value: '', matchMode: FilterMatchMode.DATE_AFTER }] } 108 | }; 109 | 110 | const rowFilters = { 111 | vin: { value: '', matchMode: FilterMatchMode.CONTAINS }, 112 | make: { value: '', matchMode: FilterMatchMode.CONTAINS }, 113 | model: { value: '', matchMode: FilterMatchMode.CONTAINS }, 114 | color: { value: '', matchMode: FilterMatchMode.CONTAINS }, 115 | year: { value: '', matchMode: FilterMatchMode.GREATER_THAN_OR_EQUAL_TO }, 116 | modifiedTime: { value: '', matchMode: FilterMatchMode.DATE_AFTER } 117 | }; 118 | 119 | const initialParams: DataTableStateEvent = { 120 | first: 0, 121 | rows: 5, 122 | page: 1, 123 | sortField: '', // single sort only 124 | sortOrder: SortOrder.UNSORTED, // single sort only 125 | multiSortMeta: [ 126 | { field: 'make', order: SortOrder.ASC }, 127 | { field: 'model', order: SortOrder.ASC } 128 | ], 129 | filters: {} 130 | }; 131 | 132 | const [tableParams, setTableParams] = useState(initialParams); 133 | 134 | // queries 135 | const queryClient = useQueryClient(); 136 | const deleteCarMutation = useDeleteEntityCarsId(); 137 | const createCarMutation = usePostEntityCars(); 138 | const updateCarMutation = usePutEntityCarsId(); 139 | const queryCars = useGetEntityCars( 140 | { request: JSON.stringify(tableParams) }, 141 | { 142 | query: { 143 | queryKey: ['list-cars', tableParams], 144 | refetchOnWindowFocus: false, 145 | retry: false, 146 | gcTime: 0, 147 | staleTime: 0 148 | } 149 | } 150 | ); 151 | const queryManufacturers = useGetEntityCarsManufacturers({ 152 | query: { 153 | queryKey: ['unique-manufacturers'], 154 | refetchOnWindowFocus: false, 155 | retry: false, 156 | gcTime: Infinity, 157 | staleTime: Infinity 158 | } 159 | }); 160 | 161 | // hooks 162 | useEffect(() => { 163 | const newParams = { ...initialParams }; 164 | newParams.filters = isMenuFilter ? { ...menuFilters } : { ...rowFilters }; 165 | if (!isMultipleSort) { 166 | newParams.sortField = 'make'; 167 | newParams.sortOrder = SortOrder.ASC; 168 | newParams.multiSortMeta = []; 169 | } 170 | setTableParams(newParams); 171 | }, [isMenuFilter, isMultipleSort]); 172 | 173 | useEffect(() => { 174 | if (lastJsonMessage !== null) { 175 | console.log(lastJsonMessage); 176 | const socketMessage = lastJsonMessage as SocketMessage; 177 | switch (socketMessage.type) { 178 | case SocketMessageType.REFRESH_DATA: 179 | queryClient.invalidateQueries({ queryKey: ['list-cars'] }); 180 | break; 181 | case SocketMessageType.NOTIFICATION: 182 | toast('warn', 'Notification', socketMessage.message); 183 | break; 184 | } 185 | } 186 | }, [lastJsonMessage]); 187 | 188 | const onPage = (event: DataTableStateEvent) => { 189 | setTableParams(event); 190 | }; 191 | 192 | const onSort = (event: DataTableStateEvent) => { 193 | setTableParams(event); 194 | }; 195 | 196 | const onFilter = (event: DataTableStateEvent) => { 197 | event['first'] = 0; 198 | setTableParams(event); 199 | }; 200 | 201 | const exportCSV = () => { 202 | datatable.current?.exportCSV(); 203 | }; 204 | 205 | const toast = (severity?: 'success' | 'info' | 'warn' | 'error' | undefined, summary?: React.ReactNode, detail?: React.ReactNode) => { 206 | toastRef.current?.show({ severity: severity, summary: summary, detail: detail, life: 4000 }); 207 | }; 208 | 209 | const confirmDeleteCar = (item: Car) => { 210 | setCar(item); 211 | setDeleteCarDialog(true); 212 | }; 213 | 214 | const hideDeleteCarDialog = () => { 215 | setDeleteCarDialog(false); 216 | onReset(defaultValues); 217 | }; 218 | 219 | const hideEditDialog = () => { 220 | setEditCarDialog(false); 221 | onReset(defaultValues); 222 | }; 223 | 224 | const onSubmit = (car: Car) => { 225 | if (car.id) { 226 | updateCarMutation.mutate( 227 | { id: car.id!, data: car }, 228 | { 229 | onSuccess: () => { 230 | hideEditDialog(); 231 | toast('success', 'Successful', `${car.year} ${car.make} ${car.model} Updated`); 232 | queryClient.invalidateQueries({ queryKey: ['list-cars'] }); 233 | }, 234 | onError: (error: ErrorType) => { 235 | toast('error', 'Error', error.response?.data?.detail || error.response?.data?.title || 'An unknown error occurred'); 236 | } 237 | } 238 | ); 239 | } else { 240 | createCarMutation.mutate( 241 | { data: car }, 242 | { 243 | onSuccess: () => { 244 | hideEditDialog(); 245 | toast('success', 'Successful', `${car.year} ${car.make} ${car.model} Created`); 246 | queryClient.invalidateQueries({ queryKey: ['list-cars'] }); 247 | }, 248 | onError: (error: ErrorType) => { 249 | toast('error', 'Error', error.response?.data?.detail || error.response?.data?.title || 'An unknown error occurred'); 250 | } 251 | } 252 | ); 253 | } 254 | }; 255 | 256 | const onReset = (data: CarInput) => { 257 | setCar(data as Car); 258 | form.reset(data, { 259 | keepDefaultValues: true 260 | }); 261 | }; 262 | 263 | const editCar = (car: Car) => { 264 | setEditCarDialog(true); 265 | onReset({ ...car } as CarInput); 266 | }; 267 | 268 | const createCar = () => { 269 | setEditCarDialog(true); 270 | onReset(defaultValues); 271 | }; 272 | 273 | const deleteCar = () => { 274 | deleteCarMutation.mutate( 275 | { id: car.id! }, 276 | { 277 | onSuccess: () => { 278 | hideDeleteCarDialog(); 279 | toast('success', 'Successful', `${car.year} ${car.make} ${car.model} Deleted`); 280 | queryClient.invalidateQueries({ queryKey: ['list-cars'] }); 281 | }, 282 | onError: (error: ErrorType) => { 283 | toast('error', 'Error', error.response?.data?.detail || error.response?.data?.title || 'An unknown error occurred'); 284 | } 285 | } 286 | ); 287 | }; 288 | 289 | const colorBodyTemplate = (item: Car) => { 290 | return ( 291 |
    292 | {item.color} 293 |
    294 | ); 295 | }; 296 | 297 | const priceBodyTemplate = (item: Car) => { 298 | return item.price?.toLocaleString('en-US', { style: 'currency', currency: 'USD' }); 299 | }; 300 | 301 | const timeBodyTemplate = (item: Car) => { 302 | return new Date(item.modifiedTime!).toISOString().replace(/T/, ' ').replace(/\..+/, ''); 303 | }; 304 | 305 | const dateFilterTemplate = (options: ColumnFilterElementTemplateOptions) => { 306 | return ( 307 | options.filterApplyCallback(e.value, options.index)} 310 | dateFormat="dd-M-yy" 311 | placeholder="dd-MMM-yy" 312 | monthNavigator 313 | yearNavigator 314 | yearRange="1960:2050" 315 | /> 316 | ); 317 | }; 318 | 319 | const actionBodyTemplate = (item: Car) => { 320 | const className = classNames('p-button-rounded action mr-2'); 321 | const editClassName = classNames(className, 'p-button-success'); 322 | const deleteClassName = classNames(className, 'p-button-danger'); 323 | return ( 324 |
    325 |
    340 | ); 341 | }; 342 | 343 | const deleteCarDialogFooter = ( 344 |
    345 |
    348 | ); 349 | 350 | const leftToolbarTemplate = ( 351 |
    352 |
    354 | ); 355 | const rightToolbarTemplate = ( 356 |
    357 | 360 | { 368 | setTableParams({ ...initialParams }); 369 | setMultipleSort(e.value!); 370 | }} 371 | /> 372 | 375 | { 383 | setTableParams({ ...initialParams }); 384 | setMenuFilter(e.value!); 385 | }} 386 | /> 387 |
    389 | ); 390 | 391 | return ( 392 |
    393 |
    394 | 395 | 396 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 437 | 438 | 439 |
    440 | 441 | 442 |
    { 444 | e.preventDefault(); 445 | e.stopPropagation(); 446 | form.handleSubmit(); 447 | }} 448 | className="p-fluid" 449 | > 450 |
    451 | 452 | {(field) => ( 453 | <> 454 | 457 | field.handleChange(e.target.value)} 461 | onBlur={field.handleBlur} 462 | className={classNames({ 'p-invalid': field.state.meta.errors.length > 0 })} 463 | autoComplete="off" 464 | /> 465 | 466 | 467 | )} 468 | 469 |
    470 |
    471 |
    472 | 473 | {(field) => ( 474 | <> 475 | 478 | field.handleChange(e.value)} 483 | onBlur={field.handleBlur} 484 | className={classNames({ 'p-invalid': field.state.meta.errors.length > 0 })} 485 | /> 486 | 487 | 488 | )} 489 | 490 |
    491 |
    492 | 493 | {(field) => ( 494 | <> 495 | 498 | field.handleChange(e.target.value)} 502 | onBlur={field.handleBlur} 503 | className={classNames({ 'p-invalid': field.state.meta.errors.length > 0 })} 504 | /> 505 | 506 | 507 | )} 508 | 509 |
    510 |
    511 |
    512 |
    513 | 514 | {(field) => ( 515 | <> 516 | 519 | 0 })} 522 | inputId={field.name} 523 | onBlur={field.handleBlur} 524 | onChange={(e) => field.handleChange(e.value?.getFullYear() ?? 0)} 525 | value={new Date(field.state.value, 1, 1)} 526 | view="year" 527 | /> 528 | 529 | 530 | )} 531 | 532 |
    533 |
    534 | 535 | {(field) => ( 536 | <> 537 | 540 | field.handleChange(e.value as string)} 544 | onBlur={field.handleBlur} 545 | className={classNames({ 'p-invalid': field.state.meta.errors.length > 0 })} 546 | defaultColor="ffffff" 547 | /> 548 | 549 | 550 | )} 551 | 552 |
    553 |
    554 | 555 |
    556 | 557 | {(field) => ( 558 | <> 559 | 562 | field.handleChange(e.value as number)} 567 | mode="currency" 568 | currency="USD" 569 | locale="en-US" 570 | inputClassName={classNames({ 'p-invalid': field.state.meta.errors.length > 0 })} 571 | /> 572 | 573 | 574 | )} 575 | 576 |
    577 | 578 |
    579 |
    594 |
    595 |
    596 | 597 | 605 |
    606 | 607 | {car && ( 608 | 609 | Are you sure you want to delete{' '} 610 | 611 | {car.year} {car.make} {car.model}{' '} 612 | 613 | ? 614 | 615 | )} 616 |
    617 |
    618 | 619 | 620 | 621 |
    622 | ); 623 | }; 624 | 625 | function FieldInfo({ field }: { field: AnyFieldApi }) { 626 | if (!field || !field.state || !field.state.meta || !field.state.meta.errors.length) return null; 627 | const error = field.state.meta.errors[0]; 628 | // Map error types to user-friendly messages 629 | let message = ''; 630 | switch (error.code) { 631 | case 'invalid_string': 632 | message = `${error.path[0].charAt(0).toUpperCase() + error.path[0].slice(1)} is required`; 633 | break; 634 | default: 635 | message = error.message; 636 | break; 637 | } 638 | return ( 639 | <> 640 | {field.state.meta.isTouched && field.state.meta.errors.length ? {message} : null} 641 | {field.state.meta.isValidating ? 'Validating...' : null} 642 | 643 | ); 644 | } 645 | 646 | export default React.memo(CrudPage); 647 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/_overrides.scss: -------------------------------------------------------------------------------- 1 | //Suggested location to add your overrides so that migration would be easy by just updating the SASS folder in the future 2 | body { 3 | font-family: var(--font-family); 4 | color: var(--text-color); 5 | background-color: var(--surface-ground); 6 | background-image: unset; 7 | margin: 0; 8 | padding: 0; 9 | min-height: 100%; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/_variables.scss: -------------------------------------------------------------------------------- 1 | /* General */ 2 | $fontSize:14px; 3 | $borderRadius:12px; 4 | $transitionDuration:.2s; 5 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/layout.scss: -------------------------------------------------------------------------------- 1 | @layer primereact { 2 | @import "./_variables"; 3 | @import "./sass/_layout"; 4 | } 5 | @import "./_overrides"; 6 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_config.scss: -------------------------------------------------------------------------------- 1 | .layout-config { 2 | position: fixed; 3 | top: 0; 4 | padding: 0; 5 | right: 0; 6 | width: 20rem; 7 | z-index: 999; 8 | height: 100vh; 9 | transform: translateX(100%); 10 | transition: transform $transitionDuration; 11 | backface-visibility: hidden; 12 | box-shadow: 0px 3px 5px rgba(0,0,0,.02), 0px 0px 2px rgba(0,0,0,.05), 0px 1px 4px rgba(0,0,0,.08) !important; 13 | color: var(--text-color); 14 | background-color: var(--surface-overlay); 15 | border-top-left-radius: 12px; 16 | border-bottom-left-radius: 12px; 17 | 18 | &.layout-config-active { 19 | transform: translateX(0); 20 | } 21 | 22 | .layout-config-button { 23 | display: block; 24 | position: absolute; 25 | width: 52px; 26 | height: 52px; 27 | line-height: 52px; 28 | background-color: var(--primary-color); 29 | color: var(--primary-color-text); 30 | text-align: center; 31 | top: 230px; 32 | left: -52px; 33 | z-index: -1; 34 | overflow: hidden; 35 | cursor: pointer; 36 | border-top-left-radius: $borderRadius; 37 | border-bottom-left-radius: $borderRadius; 38 | border-bottom-right-radius: 0; 39 | border-top-right-radius: 0; 40 | transition: background-color $transitionDuration; 41 | 42 | i { 43 | font-size: 32px; 44 | line-height: inherit; 45 | cursor: pointer; 46 | transform: rotate(0deg); 47 | transition: color $transitionDuration, transform 1s; 48 | } 49 | } 50 | 51 | .layout-config-close { 52 | position: absolute; 53 | right: 1rem; 54 | top: 1rem; 55 | z-index: 1; 56 | } 57 | 58 | .layout-config-content { 59 | position: relative; 60 | overflow: auto; 61 | height: 100vh; 62 | padding: 2rem; 63 | } 64 | 65 | .config-scale { 66 | display: flex; 67 | align-items: center; 68 | margin: 1rem 0 2rem 0; 69 | 70 | .p-button { 71 | margin-right: .5rem; 72 | } 73 | 74 | i { 75 | margin-right: .5rem; 76 | font-size: .75rem; 77 | color: var(--text-color-secondary); 78 | 79 | &.scale-active { 80 | font-size: 1.25rem; 81 | color: var(--primary-color); 82 | } 83 | } 84 | } 85 | 86 | .free-themes { 87 | img { 88 | width: 2rem; 89 | border-radius: 4px; 90 | transition: transform .2s; 91 | display: block; 92 | 93 | &:hover { 94 | transform: scale(1.1); 95 | } 96 | } 97 | 98 | span { 99 | font-size: .75rem; 100 | margin-top: .25rem; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_content.scss: -------------------------------------------------------------------------------- 1 | .layout-main-container { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | justify-content: space-between; 6 | padding: 6rem 2rem 2rem 4rem; 7 | transition: margin-left $transitionDuration; 8 | } 9 | 10 | .layout-main { 11 | flex: 1 1 auto; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_footer.scss: -------------------------------------------------------------------------------- 1 | .layout-footer { 2 | transition: margin-left $transitionDuration; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | padding-top: 1rem; 7 | border-top: 1px solid var(--surface-border); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_layout.scss: -------------------------------------------------------------------------------- 1 | @import "./_mixins"; 2 | @import "./_splash"; 3 | @import "./_main"; 4 | @import "./_topbar"; 5 | @import "./_menu"; 6 | @import "./_config"; 7 | @import "./_content"; 8 | @import "./_footer"; 9 | @import "./_responsive"; 10 | @import "./_utils"; 11 | @import "./_typography"; 12 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_main.scss: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html { 6 | height: 100%; 7 | font-size: $fontSize; 8 | } 9 | 10 | body { 11 | font-family: var(--font-family); 12 | color: var(--text-color); 13 | background-color: var(--surface-ground); 14 | margin: 0; 15 | padding: 0; 16 | min-height: 100%; 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | } 20 | 21 | a, button { 22 | text-decoration: none; 23 | color: var(--primary-color); 24 | } 25 | 26 | .layout-theme-light { 27 | background-color: #edf1f5; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_menu.scss: -------------------------------------------------------------------------------- 1 | .layout-sidebar { 2 | position: fixed; 3 | width: 300px; 4 | height: calc(100vh - 9rem); 5 | z-index: 999; 6 | overflow-y: auto; 7 | user-select: none; 8 | top: 7rem; 9 | left: 2rem; 10 | transition: transform $transitionDuration, left $transitionDuration; 11 | background-color: var(--surface-overlay); 12 | border-radius: 12px; 13 | padding: 1.5rem; 14 | box-shadow: 0px 3px 5px rgba(0,0,0,.02), 0px 0px 2px rgba(0,0,0,.05), 0px 1px 4px rgba(0,0,0,.08) 15 | } 16 | 17 | .layout-menu { 18 | list-style-type: none; 19 | margin: 0; 20 | padding: 0; 21 | 22 | li { 23 | &.layout-menuitem-category { 24 | margin-top: .75rem; 25 | 26 | &:first-child { 27 | margin-top: 0; 28 | } 29 | } 30 | 31 | .layout-menuitem-root-text { 32 | text-transform: uppercase; 33 | color:var(--surface-900); 34 | font-weight: 600; 35 | margin-bottom: .5rem; 36 | font-size: .875rem; 37 | } 38 | 39 | a { 40 | cursor: pointer; 41 | text-decoration: none; 42 | display: flex; 43 | align-items: center; 44 | color:var(--text-color); 45 | transition: color $transitionDuration; 46 | border-radius: $borderRadius; 47 | padding: .75rem 1rem; 48 | transition: background-color .15s; 49 | 50 | span { 51 | margin-left: 0.5rem; 52 | } 53 | 54 | .menuitem-toggle-icon { 55 | margin-left: auto; 56 | } 57 | 58 | &:focus { 59 | @include focused-inset(); 60 | } 61 | 62 | &:hover { 63 | background-color: var(--surface-hover); 64 | } 65 | 66 | &.router-link-exact-active { 67 | font-weight: 700; 68 | color: var(--primary-color); 69 | } 70 | 71 | .p-badge { 72 | margin-left: auto; 73 | } 74 | } 75 | 76 | &.active-menuitem { 77 | > a { 78 | .menuitem-toggle-icon { 79 | &:before { 80 | content: '\e933'; 81 | } 82 | } 83 | } 84 | } 85 | 86 | ul { 87 | list-style-type: none; 88 | margin: 0; 89 | padding: 0; 90 | 91 | &.layout-submenu-wrapper-enter { 92 | max-height: 0; 93 | } 94 | 95 | &.layout-submenu-wrapper-enter-active { 96 | overflow: hidden; 97 | max-height: 1000px; 98 | transition: max-height 1s ease-in-out; 99 | } 100 | 101 | &.layout-submenu-wrapper-enter-done { 102 | transform: none; 103 | } 104 | 105 | &.layout-submenu-wrapper-exit { 106 | max-height: 1000px; 107 | } 108 | 109 | &.layout-submenu-wrapper-exit-active { 110 | overflow: hidden; 111 | max-height: 0; 112 | transition: max-height 0.45s cubic-bezier(0.86, 0, 0.07, 1); 113 | } 114 | 115 | ul { 116 | padding-left: 1rem; 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin focused() { 2 | outline: 0 none; 3 | outline-offset: 0; 4 | transition: box-shadow .2s; 5 | box-shadow: var(--focus-ring); 6 | } 7 | 8 | @mixin focused-inset() { 9 | outline: 0 none; 10 | outline-offset: 0; 11 | transition: box-shadow .2s; 12 | box-shadow: inset var(--focus-ring); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_responsive.scss: -------------------------------------------------------------------------------- 1 | @media (min-width: 992px) { 2 | .layout-wrapper { 3 | &.layout-overlay { 4 | .layout-main-container { 5 | margin-left: 0; 6 | padding-left: 2rem; 7 | } 8 | 9 | .layout-sidebar { 10 | transform: translateX(-100%); 11 | left: 0; 12 | top: 0; 13 | height: 100vh; 14 | border-top-left-radius: 0; 15 | border-bottom-left-radius: 0; 16 | } 17 | 18 | &.layout-overlay-sidebar-active { 19 | .layout-sidebar { 20 | transform: translateX(0); 21 | } 22 | } 23 | } 24 | 25 | &.layout-static { 26 | .layout-main-container { 27 | margin-left: 300px; 28 | } 29 | 30 | &.layout-static-sidebar-inactive { 31 | .layout-sidebar { 32 | transform: translateX(-100%); 33 | left: 0; 34 | } 35 | 36 | .layout-main-container { 37 | margin-left: 0; 38 | padding-left: 2rem; 39 | } 40 | } 41 | } 42 | 43 | .layout-mask { 44 | display: none; 45 | } 46 | } 47 | } 48 | 49 | @media (max-width: 991px) { 50 | .layout-wrapper { 51 | .layout-main-container { 52 | margin-left: 0; 53 | padding-left: 2rem; 54 | } 55 | 56 | .layout-sidebar { 57 | transform: translateX(-100%); 58 | left: 0; 59 | top: 0; 60 | height: 100vh; 61 | border-top-left-radius: 0; 62 | border-bottom-left-radius: 0; 63 | } 64 | 65 | .layout-mask { 66 | z-index: 998; 67 | background-color: var(--mask-bg); 68 | 69 | &.layout-mask-enter-from, 70 | &.layout-mask-leave-to { 71 | background-color: transparent; 72 | } 73 | } 74 | 75 | &.layout-mobile-sidebar-active { 76 | .layout-sidebar { 77 | transform: translateX(0); 78 | } 79 | 80 | .layout-mask { 81 | display: block; 82 | } 83 | } 84 | } 85 | 86 | .body-overflow-hidden { 87 | overflow: hidden; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_splash.scss: -------------------------------------------------------------------------------- 1 | .preloader { 2 | position: fixed; 3 | z-index: 999999; 4 | background: #edf1f5; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | .preloader-content { 9 | border: 0 solid transparent; 10 | border-radius: 50%; 11 | width: 150px; 12 | height: 150px; 13 | position: absolute; 14 | top: calc(50vh - 75px); 15 | left: calc(50vw - 75px); 16 | } 17 | 18 | .preloader-content:before, .preloader-content:after{ 19 | content: ''; 20 | border: 1em solid var(--primary-color); 21 | border-radius: 50%; 22 | width: inherit; 23 | height: inherit; 24 | position: absolute; 25 | top: 0; 26 | left: 0; 27 | animation: loader 2s linear infinite; 28 | opacity: 0; 29 | } 30 | 31 | .preloader-content:before{ 32 | animation-delay: 0.5s; 33 | } 34 | 35 | @keyframes loader{ 36 | 0%{ 37 | transform: scale(0); 38 | opacity: 0; 39 | } 40 | 50%{ 41 | opacity: 1; 42 | } 43 | 100%{ 44 | transform: scale(1); 45 | opacity: 0; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_topbar.scss: -------------------------------------------------------------------------------- 1 | .layout-topbar { 2 | position: fixed; 3 | height: 5rem; 4 | z-index: 997; 5 | left: 0; 6 | top: 0; 7 | width: 100%; 8 | padding: 0 2rem; 9 | background-color: var(--surface-card); 10 | transition: left $transitionDuration; 11 | display: flex; 12 | align-items: center; 13 | box-shadow: 0px 3px 5px rgba(0,0,0,.02), 0px 0px 2px rgba(0,0,0,.05), 0px 1px 4px rgba(0,0,0,.08); 14 | 15 | .layout-topbar-logo { 16 | display: flex; 17 | align-items: center; 18 | color: var(--surface-900); 19 | font-size: 1.5rem; 20 | font-weight: 500; 21 | width: auto; 22 | border-radius: 12px; 23 | 24 | img { 25 | height: 2.5rem; 26 | margin-right: .5rem; 27 | } 28 | 29 | &:focus { 30 | @include focused(); 31 | } 32 | } 33 | 34 | .layout-topbar-button { 35 | display: inline-flex; 36 | justify-content: center; 37 | align-items: center; 38 | position: relative; 39 | color: var(--text-color-secondary); 40 | border-radius: 50%; 41 | width: 3rem; 42 | height: 3rem; 43 | cursor: pointer; 44 | transition: background-color $transitionDuration; 45 | 46 | &:hover { 47 | color: var(--text-color); 48 | background-color: var(--surface-hover); 49 | } 50 | 51 | &:focus { 52 | @include focused(); 53 | } 54 | 55 | i { 56 | font-size: 1.5rem; 57 | } 58 | 59 | span { 60 | font-size: 1rem; 61 | display: none; 62 | } 63 | } 64 | 65 | .layout-menu-button { 66 | margin-left: 2rem; 67 | } 68 | 69 | .layout-topbar-menu-button { 70 | display: none; 71 | 72 | i { 73 | font-size: 1.25rem; 74 | } 75 | } 76 | 77 | .layout-topbar-menu { 78 | margin: 0 0 0 auto; 79 | padding: 0; 80 | list-style: none; 81 | display: flex; 82 | 83 | .layout-topbar-button { 84 | margin-left: 1rem; 85 | } 86 | } 87 | } 88 | 89 | @media (max-width: 991px) { 90 | .layout-topbar { 91 | justify-content: space-between; 92 | 93 | .layout-topbar-logo { 94 | width: auto; 95 | order: 2; 96 | } 97 | 98 | .layout-menu-button { 99 | margin-left: 0; 100 | order: 1; 101 | } 102 | 103 | .layout-topbar-menu-button { 104 | display: inline-flex; 105 | margin-left: 0; 106 | order: 3; 107 | } 108 | 109 | .layout-topbar-menu { 110 | margin-left: 0; 111 | position: absolute; 112 | flex-direction: column; 113 | background-color: var(--surface-overlay); 114 | box-shadow: 0px 3px 5px rgba(0,0,0,.02), 0px 0px 2px rgba(0,0,0,.05), 0px 1px 4px rgba(0,0,0,.08); 115 | border-radius: 12px; 116 | padding: 1rem; 117 | right: 2rem; 118 | top: 5rem; 119 | min-width: 15rem; 120 | display: none; 121 | -webkit-animation: scalein 0.15s linear; 122 | animation: scalein 0.15s linear; 123 | 124 | &.layout-topbar-menu-mobile-active { 125 | display: block 126 | } 127 | 128 | .layout-topbar-button { 129 | margin-left: 0; 130 | display: flex; 131 | width: 100%; 132 | height: auto; 133 | justify-content: flex-start; 134 | border-radius: 12px; 135 | padding: 1rem; 136 | 137 | i { 138 | font-size: 1rem; 139 | margin-right: .5rem; 140 | } 141 | 142 | span { 143 | font-weight: medium; 144 | display: block; 145 | } 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_typography.scss: -------------------------------------------------------------------------------- 1 | h1, h2, h3, h4, h5, h6 { 2 | margin: 1.5rem 0 1rem 0; 3 | font-family: inherit; 4 | font-weight: 500; 5 | line-height: 1.2; 6 | color: inherit; 7 | 8 | &:first-child { 9 | margin-top: 0; 10 | } 11 | } 12 | 13 | h1 { 14 | font-size: 2.5rem; 15 | } 16 | 17 | h2 { 18 | font-size: 2rem; 19 | } 20 | 21 | h3 { 22 | font-size: 1.75rem; 23 | } 24 | 25 | h4 { 26 | font-size: 1.5rem; 27 | } 28 | 29 | h5 { 30 | font-size: 1.25rem; 31 | } 32 | 33 | h6 { 34 | font-size: 1rem; 35 | } 36 | 37 | mark { 38 | background: #FFF8E1; 39 | padding: .25rem .4rem; 40 | border-radius: $borderRadius; 41 | font-family: monospace; 42 | } 43 | 44 | blockquote { 45 | margin: 1rem 0; 46 | padding: 0 2rem; 47 | border-left: 4px solid #90A4AE; 48 | } 49 | 50 | hr { 51 | border-top: solid var(--surface-border); 52 | border-width: 1px 0 0 0; 53 | margin: 1rem 0; 54 | } 55 | 56 | p { 57 | margin: 0 0 1rem 0; 58 | line-height: 1.5; 59 | 60 | &:last-child { 61 | margin-bottom: 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/webui/src/assets/layout/sass/_utils.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: var(--surface-card); 3 | padding: 1.5rem; 4 | margin-bottom: 1rem; 5 | border-radius: $borderRadius; 6 | box-shadow: 0px 3px 5px rgba(0,0,0,.02), 0px 0px 2px rgba(0,0,0,.05), 0px 1px 4px rgba(0,0,0,.08) !important; 7 | 8 | &.card-w-title { 9 | padding-bottom: 2rem; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/webui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 2 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 3 | import { PrimeReactProvider } from 'primereact/api'; 4 | import React from 'react'; 5 | import { createRoot } from 'react-dom/client'; 6 | import { HashRouter } from 'react-router-dom'; 7 | import App from './App'; 8 | 9 | /** 10 | * Initialize the Query client 11 | */ 12 | const queryClient = new QueryClient(); 13 | 14 | const root = createRoot(document.getElementById('root') as HTMLDivElement); 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /src/main/webui/src/service/AxiosMutator.ts: -------------------------------------------------------------------------------- 1 | import Axios, { AxiosError, RawAxiosRequestConfig } from 'axios'; 2 | 3 | export const AXIOS_INSTANCE = Axios.create({ baseURL: process.env.VITE_REACT_APP_API_SERVER! }); 4 | 5 | export const useAxiosMutator = (): ((config: RawAxiosRequestConfig) => Promise) => { 6 | return (config: RawAxiosRequestConfig) => { 7 | const promise = AXIOS_INSTANCE({ ...config }).then(({ data }) => data); 8 | 9 | return promise; 10 | }; 11 | }; 12 | 13 | export default useAxiosMutator; 14 | 15 | // In some case with react-query and swr you want to be able to override the return error type so you can also do it here like this 16 | export type ErrorType = AxiosError; 17 | -------------------------------------------------------------------------------- /src/main/webui/src/service/CarService.zod.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by orval v8.0.0-rc.3 🍺 3 | * Do not edit manually. 4 | * Quarkus PrimeReact Monorepo 5 | * Quarkus monorepo demonstrating Panache REST server with PrimeReact UI client 6 | * OpenAPI spec version: 1.0.0 7 | */ 8 | import * as zod from 'zod'; 9 | 10 | /** 11 | * Returns a paginated list of cars with optional filtering and sorting 12 | * @summary List cars 13 | */ 14 | export const getEntityCarsQueryParams = zod.object({ 15 | request: zod.coerce.string().optional() 16 | }); 17 | 18 | export const getEntityCarsResponseRecordsItemVinMax = 17; 19 | 20 | export const getEntityCarsResponseRecordsItemVinRegExp = new RegExp('\\S'); 21 | export const getEntityCarsResponseRecordsItemMakeMax = 255; 22 | 23 | export const getEntityCarsResponseRecordsItemMakeRegExp = new RegExp('\\S'); 24 | export const getEntityCarsResponseRecordsItemModelMax = 255; 25 | 26 | export const getEntityCarsResponseRecordsItemModelRegExp = new RegExp('\\S'); 27 | export const getEntityCarsResponseRecordsItemYearMin = 1960; 28 | export const getEntityCarsResponseRecordsItemYearMax = 2050; 29 | 30 | export const getEntityCarsResponseRecordsItemColorMax = 20; 31 | 32 | export const getEntityCarsResponseRecordsItemColorRegExp = new RegExp('\\S'); 33 | export const getEntityCarsResponseRecordsItemPriceMin = 0; 34 | export const getEntityCarsResponseRecordsItemPriceMax = 250000; 35 | 36 | export const getEntityCarsResponse = zod 37 | .object({ 38 | totalRecords: zod.coerce.number().optional().describe('Total records available by this query criteria'), 39 | records: zod 40 | .array( 41 | zod 42 | .object({ 43 | id: zod.coerce.number().optional(), 44 | vin: zod.coerce 45 | .string() 46 | .max(getEntityCarsResponseRecordsItemVinMax) 47 | .regex(getEntityCarsResponseRecordsItemVinRegExp) 48 | .describe('VIN number'), 49 | make: zod.coerce 50 | .string() 51 | .max(getEntityCarsResponseRecordsItemMakeMax) 52 | .regex(getEntityCarsResponseRecordsItemMakeRegExp) 53 | .describe('Manufacturer'), 54 | model: zod.coerce 55 | .string() 56 | .max(getEntityCarsResponseRecordsItemModelMax) 57 | .regex(getEntityCarsResponseRecordsItemModelRegExp) 58 | .describe('Model Number'), 59 | year: zod.coerce 60 | .number() 61 | .min(getEntityCarsResponseRecordsItemYearMin) 62 | .max(getEntityCarsResponseRecordsItemYearMax) 63 | .describe('Year of manufacture'), 64 | color: zod.coerce 65 | .string() 66 | .max(getEntityCarsResponseRecordsItemColorMax) 67 | .regex(getEntityCarsResponseRecordsItemColorRegExp) 68 | .describe('HTML color of the car'), 69 | price: zod.coerce 70 | .number() 71 | .min(getEntityCarsResponseRecordsItemPriceMin) 72 | .max(getEntityCarsResponseRecordsItemPriceMax) 73 | .describe('Price'), 74 | modifiedTime: zod.coerce.date().optional() 75 | }) 76 | .describe('Entity that represents a car.') 77 | ) 78 | .optional() 79 | .describe('Records for this set of pagination, sorting, filtering.') 80 | }) 81 | .describe('Represents a PrimeReact query response to the UI for a complex datatable with multiple sorts, multiple filters, and pagination.'); 82 | 83 | /** 84 | * Creates a new car entry 85 | * @summary Create a new car 86 | */ 87 | export const postEntityCarsBodyVinMax = 17; 88 | 89 | export const postEntityCarsBodyVinRegExp = new RegExp('\\S'); 90 | export const postEntityCarsBodyMakeMax = 255; 91 | 92 | export const postEntityCarsBodyMakeRegExp = new RegExp('\\S'); 93 | export const postEntityCarsBodyModelMax = 255; 94 | 95 | export const postEntityCarsBodyModelRegExp = new RegExp('\\S'); 96 | export const postEntityCarsBodyYearMin = 1960; 97 | export const postEntityCarsBodyYearMax = 2050; 98 | 99 | export const postEntityCarsBodyColorMax = 20; 100 | 101 | export const postEntityCarsBodyColorRegExp = new RegExp('\\S'); 102 | export const postEntityCarsBodyPriceMin = 0; 103 | export const postEntityCarsBodyPriceMax = 250000; 104 | 105 | export const postEntityCarsBody = zod 106 | .object({ 107 | id: zod.coerce.number().optional(), 108 | vin: zod.coerce.string().max(postEntityCarsBodyVinMax).regex(postEntityCarsBodyVinRegExp).describe('VIN number'), 109 | make: zod.coerce.string().max(postEntityCarsBodyMakeMax).regex(postEntityCarsBodyMakeRegExp).describe('Manufacturer'), 110 | model: zod.coerce.string().max(postEntityCarsBodyModelMax).regex(postEntityCarsBodyModelRegExp).describe('Model Number'), 111 | year: zod.coerce.number().min(postEntityCarsBodyYearMin).max(postEntityCarsBodyYearMax).describe('Year of manufacture'), 112 | color: zod.coerce.string().max(postEntityCarsBodyColorMax).regex(postEntityCarsBodyColorRegExp).describe('HTML color of the car'), 113 | price: zod.coerce.number().min(postEntityCarsBodyPriceMin).max(postEntityCarsBodyPriceMax).describe('Price'), 114 | modifiedTime: zod.coerce.date().optional() 115 | }) 116 | .describe('Entity that represents a car.'); 117 | 118 | /** 119 | * Returns a list of distinct car manufacturers 120 | * @summary Get all manufacturers 121 | */ 122 | export const getEntityCarsManufacturersResponseItem = zod.coerce.string(); 123 | export const getEntityCarsManufacturersResponse = zod.array(getEntityCarsManufacturersResponseItem); 124 | 125 | /** 126 | * Updates an existing car based on ID 127 | * @summary Update a car 128 | */ 129 | export const putEntityCarsIdPathIdMin = 0; 130 | 131 | export const putEntityCarsIdParams = zod.object({ 132 | id: zod.coerce.number().min(putEntityCarsIdPathIdMin) 133 | }); 134 | 135 | export const putEntityCarsIdBodyVinMax = 17; 136 | 137 | export const putEntityCarsIdBodyVinRegExp = new RegExp('\\S'); 138 | export const putEntityCarsIdBodyMakeMax = 255; 139 | 140 | export const putEntityCarsIdBodyMakeRegExp = new RegExp('\\S'); 141 | export const putEntityCarsIdBodyModelMax = 255; 142 | 143 | export const putEntityCarsIdBodyModelRegExp = new RegExp('\\S'); 144 | export const putEntityCarsIdBodyYearMin = 1960; 145 | export const putEntityCarsIdBodyYearMax = 2050; 146 | 147 | export const putEntityCarsIdBodyColorMax = 20; 148 | 149 | export const putEntityCarsIdBodyColorRegExp = new RegExp('\\S'); 150 | export const putEntityCarsIdBodyPriceMin = 0; 151 | export const putEntityCarsIdBodyPriceMax = 250000; 152 | 153 | export const putEntityCarsIdBody = zod 154 | .object({ 155 | id: zod.coerce.number().optional(), 156 | vin: zod.coerce.string().max(putEntityCarsIdBodyVinMax).regex(putEntityCarsIdBodyVinRegExp).describe('VIN number'), 157 | make: zod.coerce.string().max(putEntityCarsIdBodyMakeMax).regex(putEntityCarsIdBodyMakeRegExp).describe('Manufacturer'), 158 | model: zod.coerce.string().max(putEntityCarsIdBodyModelMax).regex(putEntityCarsIdBodyModelRegExp).describe('Model Number'), 159 | year: zod.coerce.number().min(putEntityCarsIdBodyYearMin).max(putEntityCarsIdBodyYearMax).describe('Year of manufacture'), 160 | color: zod.coerce.string().max(putEntityCarsIdBodyColorMax).regex(putEntityCarsIdBodyColorRegExp).describe('HTML color of the car'), 161 | price: zod.coerce.number().min(putEntityCarsIdBodyPriceMin).max(putEntityCarsIdBodyPriceMax).describe('Price'), 162 | modifiedTime: zod.coerce.date().optional() 163 | }) 164 | .describe('Entity that represents a car.'); 165 | 166 | export const putEntityCarsIdResponseVinMax = 17; 167 | 168 | export const putEntityCarsIdResponseVinRegExp = new RegExp('\\S'); 169 | export const putEntityCarsIdResponseMakeMax = 255; 170 | 171 | export const putEntityCarsIdResponseMakeRegExp = new RegExp('\\S'); 172 | export const putEntityCarsIdResponseModelMax = 255; 173 | 174 | export const putEntityCarsIdResponseModelRegExp = new RegExp('\\S'); 175 | export const putEntityCarsIdResponseYearMin = 1960; 176 | export const putEntityCarsIdResponseYearMax = 2050; 177 | 178 | export const putEntityCarsIdResponseColorMax = 20; 179 | 180 | export const putEntityCarsIdResponseColorRegExp = new RegExp('\\S'); 181 | export const putEntityCarsIdResponsePriceMin = 0; 182 | export const putEntityCarsIdResponsePriceMax = 250000; 183 | 184 | export const putEntityCarsIdResponse = zod 185 | .object({ 186 | id: zod.coerce.number().optional(), 187 | vin: zod.coerce.string().max(putEntityCarsIdResponseVinMax).regex(putEntityCarsIdResponseVinRegExp).describe('VIN number'), 188 | make: zod.coerce.string().max(putEntityCarsIdResponseMakeMax).regex(putEntityCarsIdResponseMakeRegExp).describe('Manufacturer'), 189 | model: zod.coerce.string().max(putEntityCarsIdResponseModelMax).regex(putEntityCarsIdResponseModelRegExp).describe('Model Number'), 190 | year: zod.coerce.number().min(putEntityCarsIdResponseYearMin).max(putEntityCarsIdResponseYearMax).describe('Year of manufacture'), 191 | color: zod.coerce.string().max(putEntityCarsIdResponseColorMax).regex(putEntityCarsIdResponseColorRegExp).describe('HTML color of the car'), 192 | price: zod.coerce.number().min(putEntityCarsIdResponsePriceMin).max(putEntityCarsIdResponsePriceMax).describe('Price'), 193 | modifiedTime: zod.coerce.date().optional() 194 | }) 195 | .describe('Entity that represents a car.'); 196 | 197 | /** 198 | * Returns a car based on the provided ID 199 | * @summary Get a car by ID 200 | */ 201 | export const getEntityCarsIdPathIdMin = 0; 202 | 203 | export const getEntityCarsIdParams = zod.object({ 204 | id: zod.coerce.number().min(getEntityCarsIdPathIdMin) 205 | }); 206 | 207 | export const getEntityCarsIdResponseVinMax = 17; 208 | 209 | export const getEntityCarsIdResponseVinRegExp = new RegExp('\\S'); 210 | export const getEntityCarsIdResponseMakeMax = 255; 211 | 212 | export const getEntityCarsIdResponseMakeRegExp = new RegExp('\\S'); 213 | export const getEntityCarsIdResponseModelMax = 255; 214 | 215 | export const getEntityCarsIdResponseModelRegExp = new RegExp('\\S'); 216 | export const getEntityCarsIdResponseYearMin = 1960; 217 | export const getEntityCarsIdResponseYearMax = 2050; 218 | 219 | export const getEntityCarsIdResponseColorMax = 20; 220 | 221 | export const getEntityCarsIdResponseColorRegExp = new RegExp('\\S'); 222 | export const getEntityCarsIdResponsePriceMin = 0; 223 | export const getEntityCarsIdResponsePriceMax = 250000; 224 | 225 | export const getEntityCarsIdResponse = zod 226 | .object({ 227 | id: zod.coerce.number().optional(), 228 | vin: zod.coerce.string().max(getEntityCarsIdResponseVinMax).regex(getEntityCarsIdResponseVinRegExp).describe('VIN number'), 229 | make: zod.coerce.string().max(getEntityCarsIdResponseMakeMax).regex(getEntityCarsIdResponseMakeRegExp).describe('Manufacturer'), 230 | model: zod.coerce.string().max(getEntityCarsIdResponseModelMax).regex(getEntityCarsIdResponseModelRegExp).describe('Model Number'), 231 | year: zod.coerce.number().min(getEntityCarsIdResponseYearMin).max(getEntityCarsIdResponseYearMax).describe('Year of manufacture'), 232 | color: zod.coerce.string().max(getEntityCarsIdResponseColorMax).regex(getEntityCarsIdResponseColorRegExp).describe('HTML color of the car'), 233 | price: zod.coerce.number().min(getEntityCarsIdResponsePriceMin).max(getEntityCarsIdResponsePriceMax).describe('Price'), 234 | modifiedTime: zod.coerce.date().optional() 235 | }) 236 | .describe('Entity that represents a car.'); 237 | 238 | /** 239 | * Deletes a car based on ID 240 | * @summary Delete a car 241 | */ 242 | export const deleteEntityCarsIdPathIdMin = 0; 243 | 244 | export const deleteEntityCarsIdParams = zod.object({ 245 | id: zod.coerce.number().min(deleteEntityCarsIdPathIdMin) 246 | }); 247 | 248 | /** 249 | * Pushes a notification message to all connected clients 250 | * @summary Push notification message 251 | */ 252 | export const postSocketNotifyQueryParams = zod.object({ 253 | message: zod.coerce.string().optional() 254 | }); 255 | 256 | /** 257 | * Get information on all loggers or a specific logger. 258 | * @summary Information on Logger(s) 259 | */ 260 | export const loggingManagerGetAllQueryParams = zod.object({ 261 | loggerName: zod.coerce.string().optional() 262 | }); 263 | 264 | export const loggingManagerGetAllResponseItem = zod.object({ 265 | configuredLevel: zod 266 | .enum(['OFF', 'SEVERE', 'ERROR', 'FATAL', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'CONFIG', 'FINE', 'FINER', 'FINEST', 'ALL']) 267 | .optional(), 268 | effectiveLevel: zod 269 | .enum(['OFF', 'SEVERE', 'ERROR', 'FATAL', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'TRACE', 'CONFIG', 'FINE', 'FINER', 'FINEST', 'ALL']) 270 | .optional(), 271 | name: zod.coerce.string().optional() 272 | }); 273 | export const loggingManagerGetAllResponse = zod.array(loggingManagerGetAllResponseItem); 274 | 275 | /** 276 | * Update a log level for a certain logger. Use query param `temporary=true` for temporary level. 277 | * @summary Update log level 278 | */ 279 | export const loggingManagerUpdateQueryParams = zod.object({ 280 | temporary: zod.coerce.boolean().optional().describe('Used to set the log level temporarily') 281 | }); 282 | 283 | /** 284 | * This returns all possible log levels 285 | * @summary Get all available levels 286 | */ 287 | export const loggingManagerLevelsResponseItem = zod.enum([ 288 | 'OFF', 289 | 'SEVERE', 290 | 'ERROR', 291 | 'FATAL', 292 | 'WARNING', 293 | 'WARN', 294 | 'INFO', 295 | 'DEBUG', 296 | 'TRACE', 297 | 'CONFIG', 298 | 'FINE', 299 | 'FINER', 300 | 'FINEST', 301 | 'ALL' 302 | ]); 303 | export const loggingManagerLevelsResponse = zod.array(loggingManagerLevelsResponseItem); 304 | 305 | /** 306 | * Check the health of the application 307 | * @summary An aggregated view of the Liveness, Readiness and Startup of this application 308 | */ 309 | export const microprofileHealthRootResponse = zod.object({ 310 | checks: zod 311 | .array( 312 | zod.object({ 313 | data: zod.object({}).nullish(), 314 | name: zod.coerce.string().optional(), 315 | status: zod.enum(['UP', 'DOWN']).optional() 316 | }) 317 | ) 318 | .optional(), 319 | status: zod.enum(['UP', 'DOWN']).optional() 320 | }); 321 | 322 | /** 323 | * Check the liveness of the application 324 | * @summary The Liveness check of this application 325 | */ 326 | export const microprofileHealthLivenessResponse = zod.object({ 327 | checks: zod 328 | .array( 329 | zod.object({ 330 | data: zod.object({}).nullish(), 331 | name: zod.coerce.string().optional(), 332 | status: zod.enum(['UP', 'DOWN']).optional() 333 | }) 334 | ) 335 | .optional(), 336 | status: zod.enum(['UP', 'DOWN']).optional() 337 | }); 338 | 339 | /** 340 | * Check the readiness of the application 341 | * @summary The Readiness check of this application 342 | */ 343 | export const microprofileHealthReadinessResponse = zod.object({ 344 | checks: zod 345 | .array( 346 | zod.object({ 347 | data: zod.object({}).nullish(), 348 | name: zod.coerce.string().optional(), 349 | status: zod.enum(['UP', 'DOWN']).optional() 350 | }) 351 | ) 352 | .optional(), 353 | status: zod.enum(['UP', 'DOWN']).optional() 354 | }); 355 | 356 | /** 357 | * Check the startup of the application 358 | * @summary The Startup check of this application 359 | */ 360 | export const microprofileHealthStartupResponse = zod.object({ 361 | checks: zod 362 | .array( 363 | zod.object({ 364 | data: zod.object({}).nullish(), 365 | name: zod.coerce.string().optional(), 366 | status: zod.enum(['UP', 'DOWN']).optional() 367 | }) 368 | ) 369 | .optional(), 370 | status: zod.enum(['UP', 'DOWN']).optional() 371 | }); 372 | -------------------------------------------------------------------------------- /src/main/webui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } -------------------------------------------------------------------------------- /src/main/webui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["farm.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/dev-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melloware/quarkus-primereact/HEAD/src/test/resources/dev-flow.png -------------------------------------------------------------------------------- /src/test/resources/quarkus-primereact-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/melloware/quarkus-primereact/HEAD/src/test/resources/quarkus-primereact-screen.png --------------------------------------------------------------------------------