mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-06-09 05:48:20 +00:00
0.1.2
This commit is contained in:
parent
ef99c3e0ed
commit
1b4f91a168
91
README.md
91
README.md
@ -6,35 +6,40 @@ Spring Boot Database Admin scans your `@Entity` classes to generate a simple but
|
||||
|
||||
[](https://i.imgur.com/Nz19f8e.png)
|
||||
|
||||
Features:
|
||||
**Features:**
|
||||
* List objects with pagination and sorting
|
||||
* Show detailed object page which also includes `@OneToMany`, `@ManyToMany`, etc... fields
|
||||
* Show object detail page, which also includes `@OneToMany`, `@ManyToMany`, etc... fields
|
||||
* Create/Edit objects
|
||||
* Action logs (i.e. see a history of all write operations done through the web UI)
|
||||
* Action logs: history of all write operations executed through the web UI
|
||||
* Search
|
||||
* Customization
|
||||
|
||||
The code is in a very early version and I'm trying to collect as much feedback as possible in order to fix the
|
||||
most common issues that will inevitably arise. If you are so kind to try the project and you find something
|
||||
broken, please report it as an issue and I will try to take a look at it.
|
||||
**Supported JPA annotations**
|
||||
* Core: @Entity, @Table, @Column, @Lob, @Id
|
||||
* Relationships: @OneToMany, @ManyToOne, @ManyToMany, @OneToOne
|
||||
|
||||
The behaviour you specify with these annotations should be applied automatically by Spring Boot Database Admin as well. Keep in mind that using non-supported annotations will not necessarily result in an error, as they are simply ignored. Depending on what the annotation actually is, this could be just fine or result in an error if it interferes with something that Spring Boot Database Admin relies on.
|
||||
|
||||
The code is still in a very early stage and it might not be robust if you use not-yet-supported JPA annotations.
|
||||
If find something broken with your configuration, please report it as an issue and I will try to take a look at it.
|
||||
|
||||
## Installation
|
||||
|
||||
1. The code is not yet distributed on Maven, so for now you need to install manually. Clone the Github repo and `mvn install` the project, then include the dependency in your `pom.xml`:
|
||||
1. The code is not yet distributed on Maven, so for now you need to install manually. Clone the Github repo and execute `mvn install -DskipTests` in the project's directory. Then, include the dependency in your `pom.xml`:
|
||||
|
||||
```
|
||||
<dependency>
|
||||
<groupId>tech.ailef</groupId>
|
||||
<artifactId>spring-boot-db-admin</artifactId>
|
||||
<version>0.0.4</version>
|
||||
<version>0.0.4</version> <!-- Make sure to put the correct version here -->
|
||||
</dependency>
|
||||
```
|
||||
|
||||
2. A few simple configuration steps are required on your end in order to integrate the library into your project. If you don't want
|
||||
to test on your own code, you can clone the [test project](https://github.com/aileftech/spring-boot-database-admin-test) which provides
|
||||
2. A few simple configuration steps are required on your end in order to integrate the library into your project.
|
||||
If you don't want to test on your own code, you can clone the [test project](https://github.com/aileftech/spring-boot-database-admin-test) which provides
|
||||
a sample database and already configured code.
|
||||
|
||||
If you wish to integrate it into your project instead, the first step is adding these to your `application.properties` file:
|
||||
Otherwise, go ahead and add these to your `application.properties` file:
|
||||
|
||||
```
|
||||
# Optional, default true
|
||||
@ -44,7 +49,7 @@ dbadmin.enabled=true
|
||||
dbadmin.baseUrl=admin
|
||||
|
||||
# The package that contains your @Entity classes
|
||||
dbadmin.modelsPackage=tech.ailef.dbadmin.test.models
|
||||
dbadmin.modelsPackage=put.your.models.package.here
|
||||
```
|
||||
|
||||
The last step is to annotate your `@SpringBootApplication` class containing the `main` method with the following:
|
||||
@ -53,67 +58,21 @@ The last step is to annotate your `@SpringBootApplication` class containing the
|
||||
@ImportAutoConfiguration(DbAdminAutoConfiguration.class)
|
||||
```
|
||||
|
||||
This will autoconfigure the various DbAdmin components when your application starts.
|
||||
This will autoconfigure Spring Boot Database Admin when your application starts. You are good to go!
|
||||
|
||||
3. At this point, when you run your application, you should be able to visit `http://localhost:$PORT/${baseUrl}` and access the web interface.
|
||||
3. At this point, when you run your application, you should be able to visit `http://localhost:${port}/${dbadmin.baseUrl}` and see the web interface.
|
||||
|
||||
## Documentation
|
||||
|
||||
For more detailed documentation, visit https://aileftech.github.io/spring-boot-database-admin/.
|
||||
Available at: https://aileftech.github.io/spring-boot-database-admin/.
|
||||
|
||||
Once you are correctly running Spring Boot Database Admin, you will be able to access the web interface. Most of the features are already available with the basic configuration. However, some customization to the interface might be applied by using appropriate annotations on your classes fields or methods.
|
||||
The following annotations are supported.
|
||||
## Issues
|
||||
|
||||
### @DisplayName
|
||||
```
|
||||
@DisplayName
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
```
|
||||
|
||||
When displaying a reference to an item, by default we show its primary key. If a class has a `@DisplayName`, this method will be used in addition to the primary key whenever possible, giving the user a more readable option.
|
||||
|
||||
### @DisplayFormat
|
||||
```
|
||||
@DisplayFormat(format = "$%.2f")
|
||||
private Double price;
|
||||
```
|
||||
|
||||
Specify a format string to apply when displaying the field.
|
||||
|
||||
### @ComputedColumn
|
||||
```
|
||||
@ComputedColumn
|
||||
public double totalSpent() {
|
||||
double total = 0;
|
||||
for (Order o : orders) {
|
||||
total += o.total();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
```
|
||||
|
||||
This annotation can be used to add values computed at runtime that are shown like additional columns.
|
||||
|
||||
### @Filterable
|
||||
|
||||
```
|
||||
@Filterable
|
||||
private LocalDate createdAt;
|
||||
```
|
||||
|
||||
Place on one or more fields in a class to activate the faceted search feature. This will allow you to easily combine all these filters when operating on the table. Can only be placed on fields that correspond to physical columns on the table (e.g. no `@ManyToMany`/`@OneToMany`) and that are not binary (`byte[]`).
|
||||
|
||||
### @DisplayImage
|
||||
|
||||
```
|
||||
@DisplayImage
|
||||
private byte[] image;
|
||||
```
|
||||
|
||||
This annotation can be placed on binary fields to declare they are storing an image and that we want it displayed when possible. The image will be shown as a small thumbnail.
|
||||
If you find a problem or a bug, please report it as issue. When doing so, include as much information as possible, and in particular:
|
||||
|
||||
* provide the code for the involved `@Entity` classes, if possible
|
||||
* provide the full stack trace of the error
|
||||
* specify if you are using any particular configuration either in your `application.properties` or through annotations
|
||||
|
||||
## Changelog
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/properties.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('h2, h3, h4, h5, h6').forEach(heading => {
|
||||
document.querySelectorAll('h2, h3, h4, h5').forEach(heading => {
|
||||
let tag = heading.tagName.replace('H', '');
|
||||
|
||||
document.getElementById('toc').innerHTML +=
|
||||
@ -63,7 +63,7 @@
|
||||
|
||||
<h2 id="introduction">1. Introduction</h2>
|
||||
|
||||
<p>The following documentation outlines how to install, configure and customize Spring Boot Database Admin panel. Refer to this document for troubleshooting and if you still encounter problems, please report it as an issue on Github.</p>
|
||||
<p>The following documentation outlines how to install, configure and customize Spring Boot Database Admin panel. Refer to this document for troubleshooting and if you still encounter problems, please <a href="https://github.com/aileftech/spring-boot-database-admin/issues" target="_blank">report it as an issue on Github</a>.</p>
|
||||
<div class="separator"></div>
|
||||
<h2 id="getting-started">2. Getting started</h2>
|
||||
<p>Getting started with Spring Boot Database Admin requires including it as a dependency and then performing a simple configuration step.</p>
|
||||
@ -145,6 +145,22 @@ private Double price;
|
||||
|
||||
|
||||
<h4 id="computed-column">3.1.3 @ComputedColumn</h4>
|
||||
<h6>Supported parameters</h6>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">name</td>
|
||||
<td>String</td>
|
||||
<td>false</td>
|
||||
<td>The name of this column in the web interface. The method's name is used if this value is not specified.</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h6>Code example</h6>
|
||||
<pre>
|
||||
<code class="language-java">@ComputedColumn
|
||||
public double totalSpent() {
|
||||
@ -160,14 +176,35 @@ public double totalSpent() {
|
||||
<p>This annotation can be used to add values computed at runtime that are shown like additional columns.</p>
|
||||
|
||||
<h4 id="filterable">3.1.4 @Filterable</h4>
|
||||
|
||||
<h6>Supported parameters</h6>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Required</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">type</td>
|
||||
<td>false</td>
|
||||
<td>Enum (<code>DEFAULT</code>, <code>CATEGORICAL</code>)</td>
|
||||
<td>If <code>CATEGORICAL</code>, this changes the filter in the UI to shows all the possible values directly instead of providing a autocomplete form.</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h6>Code example</h6>
|
||||
<pre>
|
||||
<code class="language-java">@Filterable
|
||||
private LocalDate createdAt;
|
||||
|
||||
@Filterable(type=FilterableType.CATEGORICAL)
|
||||
@ManyToOne
|
||||
private User user;
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
<p>Place on one or more fields in a class to activate the faceted search feature. This will allow you to easily combine all these filters when operating on the table. Can only be placed on fields that correspond to physical columns on the table (e.g. no `@ManyToMany`/`@OneToMany`) and that are not binary (`byte[]`).</p>
|
||||
|
||||
|
||||
<p>Place on one or more fields in a class to activate the faceted search feature. This will allow you to easily combine all these filters when operating on the table. Can only be placed on fields that correspond to physical columns on the table (e.g. no <code>@ManyToMany</code>/<code>@OneToMany</code>) and that are not binary (<code>byte[]</code>).</p>
|
||||
|
||||
<h4 id="display-image">3.1.5 @DisplayImage</h4>
|
||||
|
||||
@ -190,6 +227,10 @@ private byte[] image;
|
||||
<h2>4. Security</h2>
|
||||
<p>Spring Boot Database Admin does not implement authentication and/or authorization mechanisms. However, you can use a standard Spring security configuration in order to limit access to the web UI or specific parts of it.</p>
|
||||
<p>All Spring Boot Database Admin routes start with the value of <code>dbadmin.baseUrl</code> property, and all write operations (edit, create, delete) are implemented as <code>POST</code> calls.</p>
|
||||
<div class="separator"></div>
|
||||
<h2>5. Troubleshooting</h2>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
2
pom.xml
2
pom.xml
@ -11,7 +11,7 @@
|
||||
</parent>
|
||||
<groupId>tech.ailef</groupId>
|
||||
<artifactId>spring-boot-db-admin</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<version>0.1.2</version>
|
||||
<name>spring-boot-db-admin</name>
|
||||
<description>Srping Boot DB Admin Dashboard</description>
|
||||
<properties>
|
||||
|
@ -7,8 +7,9 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
@ -20,12 +21,13 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import tech.ailef.dbadmin.external.annotations.DisplayFormat;
|
||||
import tech.ailef.dbadmin.external.dbmapping.AdvancedJpaRepository;
|
||||
import tech.ailef.dbadmin.external.dbmapping.CustomJpaRepository;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbField;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbFieldType;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
|
||||
@ -43,9 +45,8 @@ import tech.ailef.dbadmin.external.misc.Utils;
|
||||
*/
|
||||
@Component
|
||||
public class DbAdmin {
|
||||
private static final Logger logger = Logger.getLogger(DbAdmin.class.getName());
|
||||
private static final Logger logger = LoggerFactory.getLogger(DbAdmin.class.getName());
|
||||
|
||||
// @PersistenceContext
|
||||
private EntityManager entityManager;
|
||||
|
||||
private List<DbObjectSchema> schemas = new ArrayList<>();
|
||||
@ -125,25 +126,23 @@ public class DbAdmin {
|
||||
try {
|
||||
Class<?> klass = Class.forName(fullClassName);
|
||||
DbObjectSchema schema = new DbObjectSchema(klass, this);
|
||||
AdvancedJpaRepository simpleJpaRepository = new AdvancedJpaRepository(schema, entityManager);
|
||||
CustomJpaRepository simpleJpaRepository = new CustomJpaRepository(schema, entityManager);
|
||||
schema.setJpaRepository(simpleJpaRepository);
|
||||
|
||||
System.out.println("\n\n******************************************************");
|
||||
System.out.println("* Class: " + klass + " - Table: " + schema.getTableName());
|
||||
System.out.println("******************************************************");
|
||||
|
||||
logger.debug("Processing class: " + klass + " - Table: " + schema.getTableName());
|
||||
|
||||
Field[] fields = klass.getDeclaredFields();
|
||||
for (Field f : fields) {
|
||||
System.out.println(" - Mapping field " + f);
|
||||
DbField field = mapField(f, schema);
|
||||
if (field == null) {
|
||||
throw new DbAdminException("Impossible to map field: " + f);
|
||||
}
|
||||
field.setSchema(schema);
|
||||
|
||||
schema.addField(field);
|
||||
}
|
||||
|
||||
logger.debug("Processed " + klass + ", extracted " + schema.getSortedFields().size() + " fields");
|
||||
|
||||
return schema;
|
||||
} catch (ClassNotFoundException |
|
||||
IllegalArgumentException | SecurityException e) {
|
||||
@ -201,6 +200,7 @@ public class DbAdmin {
|
||||
ManyToMany manyToMany = f.getAnnotation(ManyToMany.class);
|
||||
ManyToOne manyToOne = f.getAnnotation(ManyToOne.class);
|
||||
OneToOne oneToOne = f.getAnnotation(OneToOne.class);
|
||||
Lob lob = f.getAnnotation(Lob.class);
|
||||
|
||||
String fieldName = determineFieldName(f);
|
||||
|
||||
@ -212,6 +212,10 @@ public class DbAdmin {
|
||||
DbFieldType fieldType = null;
|
||||
try {
|
||||
fieldType = DbFieldType.fromClass(f.getType());
|
||||
|
||||
if (fieldType != null && lob != null && fieldType == DbFieldType.STRING) {
|
||||
fieldType = DbFieldType.TEXT;
|
||||
}
|
||||
} catch (DbAdminException e) {
|
||||
// If failure, we try to map a relationship on this field
|
||||
}
|
||||
|
@ -16,4 +16,5 @@ import java.lang.annotation.Target;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
public @interface Filterable {
|
||||
public FilterableType type() default FilterableType.DEFAULT;
|
||||
}
|
5
src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java
vendored
Normal file
5
src/main/java/tech/ailef/dbadmin/external/annotations/FilterableType.java
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
package tech.ailef.dbadmin.external.annotations;
|
||||
|
||||
public enum FilterableType {
|
||||
DEFAULT, CATEGORICAL;
|
||||
}
|
@ -19,7 +19,6 @@ import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@ -89,7 +88,7 @@ public class DefaultDbAdminController {
|
||||
model.addAttribute("schemas", schemas);
|
||||
model.addAttribute("query", query);
|
||||
model.addAttribute("counts", counts);
|
||||
model.addAttribute("activePage", "home");
|
||||
model.addAttribute("activePage", "entities");
|
||||
model.addAttribute("title", "Entities | Index");
|
||||
|
||||
return "home";
|
||||
@ -124,17 +123,20 @@ public class DefaultDbAdminController {
|
||||
if (page == null) page = 1;
|
||||
if (pageSize == null) pageSize = 50;
|
||||
|
||||
Set<QueryFilter> queryFilters = Utils.computeFilters(otherParams);
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
Set<QueryFilter> queryFilters = Utils.computeFilters(schema, otherParams);
|
||||
if (otherParams.containsKey("remove_field")) {
|
||||
List<String> fields = otherParams.get("remove_field");
|
||||
|
||||
for (int i = 0; i < fields.size(); i++) {
|
||||
QueryFilter toRemove =
|
||||
new QueryFilter(
|
||||
fields.get(i),
|
||||
schema.getFieldByJavaName(fields.get(i)),
|
||||
CompareOperator.valueOf(otherParams.get("remove_op").get(i).toUpperCase()),
|
||||
otherParams.get("remove_value").get(i)
|
||||
);
|
||||
|
||||
queryFilters.removeIf(f -> f.equals(toRemove));
|
||||
}
|
||||
|
||||
@ -160,8 +162,6 @@ public class DefaultDbAdminController {
|
||||
return "redirect:" + redirectUrl.trim();
|
||||
}
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
try {
|
||||
PaginatedResult<DbObject> result = null;
|
||||
if (query != null || !otherParams.isEmpty()) {
|
||||
|
@ -1,106 +0,0 @@
|
||||
package tech.ailef.dbadmin.external.controller.rest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import tech.ailef.dbadmin.external.DbAdmin;
|
||||
import tech.ailef.dbadmin.external.DbAdminProperties;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
|
||||
import tech.ailef.dbadmin.external.dto.PaginatedResult;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = {"/${dbadmin.baseUrl}/api", "/${dbadmin.baseUrl}/api/"})
|
||||
public class DefaultDbAdminRestController {
|
||||
@Autowired
|
||||
public DbAdmin dbAdmin;
|
||||
|
||||
@Autowired
|
||||
private DbAdminProperties properties;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
// @Autowired
|
||||
// @Qualifier("internalJdbc")
|
||||
// private JdbcTemplate internalJdbc;
|
||||
|
||||
// @GetMapping("/configuration")
|
||||
// public ResponseEntity<?> conf() {
|
||||
// return ResponseEntity.ok(properties.toMap());
|
||||
// }
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> index(@RequestParam(required = false) String query) {
|
||||
checkInit();
|
||||
|
||||
List<DbObjectSchema> schemas = dbAdmin.getSchemas();
|
||||
if (query != null && !query.isBlank()) {
|
||||
schemas = schemas.stream().filter(s -> {
|
||||
return s.getClassName().toLowerCase().contains(query.toLowerCase())
|
||||
|| s.getTableName().toLowerCase().contains(query.toLowerCase());
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(schemas);
|
||||
}
|
||||
|
||||
@GetMapping("/model/{className}")
|
||||
public ResponseEntity<?> list(@PathVariable String className,
|
||||
@RequestParam(required=false) Integer page, @RequestParam(required=false) Integer pageSize,
|
||||
@RequestParam(required=false) String sortKey, @RequestParam(required=false) String sortOrder) {
|
||||
checkInit();
|
||||
DbAdminRepository repository = new DbAdminRepository(jdbcTemplate);
|
||||
|
||||
if (page == null) page = 1;
|
||||
if (pageSize == null) pageSize = 50;
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
PaginatedResult result = repository.findAll(schema, page, pageSize, sortKey, sortOrder);
|
||||
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/model/{className}/schema")
|
||||
public ResponseEntity<?> schema(@PathVariable String className) {
|
||||
checkInit();
|
||||
|
||||
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
|
||||
return ResponseEntity.ok(schema);
|
||||
}
|
||||
|
||||
// @GetMapping("/model/{className}/show/{id}")
|
||||
// public ResponseEntity<?> show(@PathVariable String className, @PathVariable String id,
|
||||
// @RequestParam(required = false) Boolean expand) {
|
||||
// checkInit();
|
||||
// DbAdminRepository repository = new DbAdminRepository(jdbcTemplate);
|
||||
// if (expand == null) expand = true;
|
||||
//
|
||||
// DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
|
||||
//
|
||||
// DbObject object = repository.findById(schema, id).orElseThrow(() -> {
|
||||
// return new ResponseStatusException(
|
||||
// HttpStatus.NOT_FOUND, "Object " + className + " with id " + id + " not found"
|
||||
// );
|
||||
// });
|
||||
//
|
||||
// return ResponseEntity.ok(new DbObjectDTO(object, expand));
|
||||
// }
|
||||
|
||||
private void checkInit() {
|
||||
if (dbAdmin == null)
|
||||
throw new DbAdminException("Not initialized correctly: DB_ADMIN object is null.");
|
||||
}
|
||||
}
|
@ -26,14 +26,14 @@ import tech.ailef.dbadmin.external.dto.QueryFilter;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class AdvancedJpaRepository extends SimpleJpaRepository {
|
||||
public class CustomJpaRepository extends SimpleJpaRepository {
|
||||
|
||||
private EntityManager entityManager;
|
||||
|
||||
private DbObjectSchema schema;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public AdvancedJpaRepository(DbObjectSchema schema, EntityManager em) {
|
||||
public CustomJpaRepository(DbObjectSchema schema, EntityManager em) {
|
||||
super(schema.getJavaClass(), em);
|
||||
this.entityManager = em;
|
||||
this.schema = schema;
|
||||
@ -90,7 +90,7 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<Predicate> queryPredicates = new ArrayList<>();
|
||||
if (q != null) {
|
||||
if (q != null && !q.isBlank()) {
|
||||
for (DbField f : stringFields) {
|
||||
Path path = root.get(f.getJavaName());
|
||||
queryPredicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%"));
|
||||
@ -104,48 +104,51 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
|
||||
if (queryFilters == null) queryFilters = new HashSet<>();
|
||||
for (QueryFilter filter : queryFilters) {
|
||||
CompareOperator op = filter.getOp();
|
||||
String field = filter.getField();
|
||||
DbField dbField = filter.getField();
|
||||
String fieldName = dbField.getJavaName();
|
||||
String v = filter.getValue();
|
||||
|
||||
DbField dbField = schema.getFieldByJavaName(field);
|
||||
Object value = dbField.getType().parseValue(v);
|
||||
|
||||
if (op == CompareOperator.STRING_EQ) {
|
||||
finalPredicates.add(cb.equal(cb.lower(cb.toString(root.get(field))), value.toString().toLowerCase()));
|
||||
if (value == null)
|
||||
finalPredicates.add(cb.isNull(root.get(fieldName)));
|
||||
else
|
||||
finalPredicates.add(cb.equal(cb.lower(cb.toString(root.get(fieldName))), value.toString().toLowerCase()));
|
||||
} else if (op == CompareOperator.CONTAINS) {
|
||||
finalPredicates.add(
|
||||
cb.like(cb.lower(cb.toString(root.get(field))), "%" + value.toString().toLowerCase() + "%")
|
||||
cb.like(cb.lower(cb.toString(root.get(fieldName))), "%" + value.toString().toLowerCase() + "%")
|
||||
);
|
||||
} else if (op == CompareOperator.EQ) {
|
||||
finalPredicates.add(
|
||||
cb.equal(root.get(field), value)
|
||||
cb.equal(root.get(fieldName), value)
|
||||
);
|
||||
} else if (op == CompareOperator.GT) {
|
||||
finalPredicates.add(
|
||||
cb.greaterThan(root.get(field), value.toString())
|
||||
cb.greaterThan(root.get(fieldName), value.toString())
|
||||
);
|
||||
} else if (op == CompareOperator.LT) {
|
||||
finalPredicates.add(
|
||||
cb.lessThan(root.get(field), value.toString())
|
||||
cb.lessThan(root.get(fieldName), value.toString())
|
||||
);
|
||||
} else if (op == CompareOperator.AFTER) {
|
||||
if (value instanceof LocalDate)
|
||||
finalPredicates.add(
|
||||
cb.greaterThan(root.get(field), (LocalDate)value)
|
||||
cb.greaterThan(root.get(fieldName), (LocalDate)value)
|
||||
);
|
||||
else if (value instanceof LocalDateTime)
|
||||
finalPredicates.add(
|
||||
cb.greaterThan(root.get(field), (LocalDateTime)value)
|
||||
cb.greaterThan(root.get(fieldName), (LocalDateTime)value)
|
||||
);
|
||||
|
||||
} else if (op == CompareOperator.BEFORE) {
|
||||
if (value instanceof LocalDate)
|
||||
finalPredicates.add(
|
||||
cb.lessThan(root.get(field), (LocalDate)value)
|
||||
cb.lessThan(root.get(fieldName), (LocalDate)value)
|
||||
);
|
||||
else if (value instanceof LocalDateTime)
|
||||
finalPredicates.add(
|
||||
cb.lessThan(root.get(field), (LocalDateTime)value)
|
||||
cb.lessThan(root.get(fieldName), (LocalDateTime)value)
|
||||
);
|
||||
|
||||
}
|
@ -81,7 +81,7 @@ public class DbAdminRepository {
|
||||
* @return
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public PaginatedResult findAll(DbObjectSchema schema, int page, int pageSize, String sortKey, String sortOrder) {
|
||||
public PaginatedResult<DbObject> findAll(DbObjectSchema schema, int page, int pageSize, String sortKey, String sortOrder) {
|
||||
SimpleJpaRepository repository = schema.getJpaRepository();
|
||||
|
||||
long maxElement = count(schema);
|
||||
@ -193,7 +193,10 @@ public class DbAdminRepository {
|
||||
|
||||
files.keySet().forEach(f -> {
|
||||
try {
|
||||
allValues.put(f, files.get(f).getBytes());
|
||||
// The file parameter gets sent even if empty, so it's needed
|
||||
// to check if the file has actual content, to avoid storing an empty file
|
||||
if (files.get(f).getSize() > 0)
|
||||
allValues.put(f, files.get(f).getBytes());
|
||||
} catch (IOException e) {
|
||||
throw new DbAdminException(e);
|
||||
}
|
||||
@ -217,7 +220,7 @@ public class DbAdminRepository {
|
||||
*/
|
||||
public PaginatedResult<DbObject> search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey,
|
||||
String sortOrder, Set<QueryFilter> queryFilters) {
|
||||
AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
|
||||
CustomJpaRepository jpaRepository = schema.getJpaRepository();
|
||||
|
||||
long maxElement = count(schema, query, queryFilters);
|
||||
int maxPage = (int)(Math.ceil ((double)maxElement / pageSize));
|
||||
@ -242,7 +245,7 @@ public class DbAdminRepository {
|
||||
* @return
|
||||
*/
|
||||
public List<DbObject> search(DbObjectSchema schema, String query) {
|
||||
AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
|
||||
CustomJpaRepository jpaRepository = schema.getJpaRepository();
|
||||
|
||||
return jpaRepository.search(query, 1, 50, null, null, null).stream()
|
||||
.map(o -> new DbObject(o, schema))
|
||||
|
@ -1,10 +1,16 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import tech.ailef.dbadmin.external.annotations.DisplayImage;
|
||||
import tech.ailef.dbadmin.external.annotations.Filterable;
|
||||
import tech.ailef.dbadmin.external.annotations.FilterableType;
|
||||
|
||||
public class DbField {
|
||||
protected String dbName;
|
||||
@ -124,6 +130,26 @@ public class DbField {
|
||||
return format;
|
||||
}
|
||||
|
||||
public boolean isText() {
|
||||
return type == DbFieldType.TEXT;
|
||||
}
|
||||
|
||||
public boolean isFilterable() {
|
||||
return getPrimitiveField().getAnnotation(Filterable.class) != null;
|
||||
}
|
||||
|
||||
public boolean isFilterableCategorical() {
|
||||
Filterable filterable = getPrimitiveField().getAnnotation(Filterable.class);
|
||||
return filterable != null && filterable.type() == FilterableType.CATEGORICAL;
|
||||
}
|
||||
|
||||
public Set<DbFieldValue> getAllValues() {
|
||||
List<?> findAll = schema.getJpaRepository().findAll();
|
||||
return findAll.stream()
|
||||
.map(o -> new DbObject(o, schema).get(this))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DbField [name=" + dbName + ", javaName=" + javaName + ", type=" + type + ", field=" + field
|
||||
@ -131,6 +157,23 @@ public class DbField {
|
||||
+ ", schema=" + schema.getClassName() + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(dbName, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DbField other = (DbField) obj;
|
||||
return Objects.equals(dbName, other.dbName) && type == other.type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ public enum DbFieldType {
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
if (value == null) return null;
|
||||
return LocalDate.parse(value.toString());
|
||||
}
|
||||
|
||||
@ -129,6 +130,7 @@ public enum DbFieldType {
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
if (value == null || value.toString().isBlank()) return null;
|
||||
return LocalDateTime.parse(value.toString());
|
||||
}
|
||||
|
||||
@ -163,6 +165,28 @@ public enum DbFieldType {
|
||||
return List.of(CompareOperator.CONTAINS, CompareOperator.STRING_EQ);
|
||||
}
|
||||
},
|
||||
TEXT {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
return "textarea";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object parseValue(Object value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getJavaClass() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CompareOperator> getCompareOperators() {
|
||||
return List.of(CompareOperator.CONTAINS, CompareOperator.STRING_EQ);
|
||||
}
|
||||
|
||||
},
|
||||
BOOLEAN {
|
||||
@Override
|
||||
public String getHTMLName() {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package tech.ailef.dbadmin.external.dbmapping;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
public class DbFieldValue {
|
||||
@ -39,6 +41,22 @@ public class DbFieldValue {
|
||||
public String toString() {
|
||||
return "DbFieldValue [value=" + value + ", field=" + field + "]";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(field, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DbFieldValue other = (DbFieldValue) obj;
|
||||
return Objects.equals(field, other.field) && Objects.equals(value, other.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -18,7 +19,6 @@ import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import tech.ailef.dbadmin.external.DbAdmin;
|
||||
import tech.ailef.dbadmin.external.annotations.ComputedColumn;
|
||||
import tech.ailef.dbadmin.external.annotations.Filterable;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
import tech.ailef.dbadmin.external.misc.Utils;
|
||||
|
||||
@ -39,7 +39,7 @@ public class DbObjectSchema {
|
||||
/**
|
||||
* A JPA repository to operate on the database
|
||||
*/
|
||||
private AdvancedJpaRepository jpaRepository;
|
||||
private CustomJpaRepository jpaRepository;
|
||||
|
||||
private DbAdmin dbAdmin;
|
||||
|
||||
@ -113,11 +113,11 @@ public class DbObjectSchema {
|
||||
fields.add(f);
|
||||
}
|
||||
|
||||
public AdvancedJpaRepository getJpaRepository() {
|
||||
public CustomJpaRepository getJpaRepository() {
|
||||
return jpaRepository;
|
||||
}
|
||||
|
||||
public void setJpaRepository(AdvancedJpaRepository jpaRepository) {
|
||||
public void setJpaRepository(CustomJpaRepository jpaRepository) {
|
||||
this.jpaRepository = jpaRepository;
|
||||
}
|
||||
|
||||
@ -182,13 +182,37 @@ public class DbObjectSchema {
|
||||
public List<DbField> getFilterableFields() {
|
||||
return getSortedFields().stream().filter(f -> {
|
||||
return !f.isBinary() && !f.isPrimaryKey()
|
||||
&& f.getPrimitiveField().getAnnotation(Filterable.class) != null;
|
||||
&& f.isFilterable();
|
||||
}).toList();
|
||||
}
|
||||
|
||||
public List<DbObject> findAll() {
|
||||
List<?> r = jpaRepository.findAll();
|
||||
return r.stream().map(o -> new DbObject(o, this)).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DbObjectSchema [fields=" + fields + ", className=" + entityClass.getName() + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(tableName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
DbObjectSchema other = (DbObjectSchema) obj;
|
||||
return Objects.equals(tableName, other.tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -2,20 +2,22 @@ package tech.ailef.dbadmin.external.dto;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbField;
|
||||
|
||||
public class QueryFilter {
|
||||
private String field;
|
||||
private DbField field;
|
||||
|
||||
private CompareOperator op;
|
||||
|
||||
private String value;
|
||||
|
||||
public QueryFilter(String field, CompareOperator op, String value) {
|
||||
public QueryFilter(DbField field, CompareOperator op, String value) {
|
||||
this.field = field;
|
||||
this.op = op;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
public DbField getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
@ -27,20 +29,28 @@ public class QueryFilter {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (value != null && !value.toString().isBlank()) {
|
||||
String displayValue = value;
|
||||
if (value.length() > 10) {
|
||||
displayValue = value.substring(0, 4) + "..." + value.substring(value.length() - 4);
|
||||
}
|
||||
return "'" + field.getName() + "' " + op.getDisplayName() + " '" + displayValue + "'";
|
||||
} else {
|
||||
if (op != CompareOperator.STRING_EQ && op != CompareOperator.EQ) {
|
||||
return "'" + field.getName() + "' " + op.getDisplayName() + " NULL";
|
||||
} else {
|
||||
return "'" + field.getName() + "' IS NULL";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(field, op, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String displayValue = value;
|
||||
if (value.length() > 10) {
|
||||
displayValue = value.substring(0, 4) + "..." + value.substring(value.length() - 4);
|
||||
}
|
||||
return "'" + field + "' " + op.getDisplayName() + " '" + displayValue + "'";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
@ -50,8 +60,8 @@ public class QueryFilter {
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
QueryFilter other = (QueryFilter) obj;
|
||||
return Objects.equals(field, other.field) && Objects.equals(op, other.op) && Objects.equals(value, other.value);
|
||||
return Objects.equals(field, other.field) && op == other.op && Objects.equals(value, other.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import java.util.Set;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
|
||||
import tech.ailef.dbadmin.external.dto.CompareOperator;
|
||||
import tech.ailef.dbadmin.external.dto.QueryFilter;
|
||||
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
|
||||
@ -32,7 +33,7 @@ public interface Utils {
|
||||
r.put("filter_value", new ArrayList<>());
|
||||
|
||||
for (QueryFilter filter : filters) {
|
||||
r.get("filter_field").add(filter.getField());
|
||||
r.get("filter_field").add(filter.getField().getJavaName());
|
||||
r.get("filter_op").add(filter.getOp().toString());
|
||||
r.get("filter_value").add(filter.getValue());
|
||||
}
|
||||
@ -40,7 +41,7 @@ public interface Utils {
|
||||
return r;
|
||||
}
|
||||
|
||||
public static Set<QueryFilter> computeFilters(MultiValueMap<String, String> params) {
|
||||
public static Set<QueryFilter> computeFilters(DbObjectSchema schema, MultiValueMap<String, String> params) {
|
||||
if (params == null)
|
||||
return new HashSet<>();
|
||||
|
||||
@ -62,7 +63,7 @@ public interface Utils {
|
||||
String field = fields.get(i);
|
||||
String value = values.get(i);
|
||||
|
||||
QueryFilter queryFilter = new QueryFilter(field, CompareOperator.valueOf(op.toUpperCase()), value);
|
||||
QueryFilter queryFilter = new QueryFilter(schema.getFieldByJavaName(field), CompareOperator.valueOf(op.toUpperCase()), value);
|
||||
filters.add(queryFilter);
|
||||
}
|
||||
|
||||
|
@ -181,6 +181,19 @@ AUTOCOMPLETE
|
||||
background-color: #F0F0F0;
|
||||
}
|
||||
|
||||
ul.categorical-select {
|
||||
list-style-type: none;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
ul.categorical-select button {
|
||||
color: #007fd0;
|
||||
text-decoration: underline;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Images
|
||||
*/
|
||||
|
@ -8,8 +8,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
let activeFilters = root.querySelectorAll(".active-filter");
|
||||
activeFilters.forEach(activeFilter => {
|
||||
activeFilter.addEventListener('click', function(e) {
|
||||
let formId = e.target.dataset.formid;
|
||||
document.getElementById(formId).submit()
|
||||
let formId = activeFilter.dataset.formid;
|
||||
document.getElementById(formId).submit();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -46,64 +46,114 @@
|
||||
<span class="fw-bold align-middle" th:text="${field.getName()}"></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<form action="" method="GET">
|
||||
<!-- Reset page when applying filter to start back at page 1 -->
|
||||
<input type="hidden" name="page" value="1">
|
||||
<input type="hidden" name="pageSize" th:value="${page.getPagination().getPageSize()}">
|
||||
<input type="hidden" name="query" th:value="${query}">
|
||||
<input type="hidden" name="filter_field" th:value="${field.getJavaName()}">
|
||||
|
||||
<div class="input-group pe-2">
|
||||
<th:block th:if="${field.isForeignKey()}">
|
||||
<span class="input-group-text w-25">
|
||||
<input type="hidden" name="filter_op" value="string_eq">
|
||||
Equals
|
||||
</span>
|
||||
<div class="autocomplete-input position-relative w-50">
|
||||
<input class="autocomplete form-control" type="text" name="filter_value"
|
||||
th:data-classname="${field.getConnectedType().getName()}"
|
||||
autocomplete="off"
|
||||
placeholder="NULL">
|
||||
</input>
|
||||
<div class="suggestions d-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</th:block>
|
||||
<th:block th:unless="${field.isForeignKey()}">
|
||||
<select class="form-select w-25" name="filter_op">
|
||||
<option th:value="${op}" th:each="op : ${field.getType().getCompareOperators()}"
|
||||
th:text="${op.getDisplayName()}">
|
||||
</select>
|
||||
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||
name="filter_value"
|
||||
class="form-control w-50" th:id="|__id_${field.getName()}|"
|
||||
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
|
||||
th:required="${!field.isNullable() && !field.isPrimaryKey()}"
|
||||
step="any"
|
||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||
oninput="this.setCustomValidity('')">
|
||||
</th:block>
|
||||
|
||||
<!--/*--> Handle non categorical filter <!--*/-->
|
||||
<th:block th:if="${!field.isFilterableCategorical()}">
|
||||
<form action="" method="GET">
|
||||
<!--/*--> Propagate queryParams containing other filters with hidden fields <!--*/-->
|
||||
<th:block th:each="p : ${queryParams.keySet()}">
|
||||
<input th:each="v : ${queryParams.get(p)}"
|
||||
th:name="${p}" th:value="${v}" type="hidden"
|
||||
th:if="${p.startsWith('filter_')}">
|
||||
</th:block>
|
||||
|
||||
<input th:each="v : ${queryParams.get(p)}"
|
||||
th:name="${p}" th:value="${v}" type="hidden"
|
||||
th:if="${p.startsWith('filter_')}">
|
||||
</th:block>
|
||||
|
||||
<button class="ui-btn btn btn-primary"><i class="bi bi-search text-white"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!--
|
||||
<th:block th:if="${field.getConnectedType() != null}">
|
||||
<div th:each="val : ${schema.getFieldValues(field)}">
|
||||
<span th:text="${val}"></span>
|
||||
</div>
|
||||
</th:block>
|
||||
-->
|
||||
<div class="input-group pe-2">
|
||||
<!-- Reset page when applying filter to start back at page 1 -->
|
||||
<input type="hidden" name="page" value="1">
|
||||
<input type="hidden" name="pageSize" th:value="${page.getPagination().getPageSize()}">
|
||||
<input type="hidden" name="query" th:value="${query}">
|
||||
<input type="hidden" name="filter_field" th:value="${field.getJavaName()}">
|
||||
|
||||
<th:block th:if="${field.isForeignKey()}">
|
||||
<span class="input-group-text w-25">
|
||||
<input type="hidden" name="filter_op" value="string_eq">
|
||||
Equals
|
||||
</span>
|
||||
<div class="autocomplete-input position-relative w-50">
|
||||
<input class="autocomplete form-control" type="text" name="filter_value"
|
||||
th:data-classname="${field.getConnectedType().getName()}"
|
||||
autocomplete="off"
|
||||
placeholder="NULL">
|
||||
</input>
|
||||
<div class="suggestions d-none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</th:block>
|
||||
<th:block th:unless="${field.isForeignKey()}">
|
||||
<select class="form-select w-25" name="filter_op">
|
||||
<option th:value="${op}" th:each="op : ${field.getType().getCompareOperators()}"
|
||||
th:text="${op.getDisplayName()}">
|
||||
</select>
|
||||
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||
name="filter_value"
|
||||
class="form-control w-50" th:id="|__id_${field.getName()}|"
|
||||
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
|
||||
th:required="${!field.isNullable() && !field.isPrimaryKey()}"
|
||||
step="any"
|
||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||
oninput="this.setCustomValidity('')">
|
||||
</th:block>
|
||||
<button class="ui-btn btn btn-primary"><i class="bi bi-search text-white"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</th:block>
|
||||
<!--/*--> Handle categorical filter <!--*/-->
|
||||
<th:block th:if="${field.isFilterableCategorical()}">
|
||||
<th:block th:if="${field.isForeignKey()}">
|
||||
<ul class="categorical-select">
|
||||
<li th:each="categoricalValue : ${field.getConnectedSchema().findAll()}">
|
||||
<form action="" method="GET">
|
||||
<!--/*--> Propagate queryParams containing other filters with hidden fields <!--*/-->
|
||||
<th:block th:each="p : ${queryParams.keySet()}">
|
||||
<input th:each="v : ${queryParams.get(p)}"
|
||||
th:name="${p}" th:value="${v}" type="hidden"
|
||||
th:if="${p.startsWith('filter_')}">
|
||||
</th:block>
|
||||
|
||||
<!-- Reset page when applying filter to start back at page 1 -->
|
||||
<input type="hidden" name="page" value="1">
|
||||
<input type="hidden" name="pageSize" th:value="${page.getPagination().getPageSize()}">
|
||||
<input type="hidden" name="query" th:value="${query}">
|
||||
<input type="hidden" name="filter_field" th:value="${field.getJavaName()}">
|
||||
|
||||
<input type="hidden" name="filter_op" value="string_eq">
|
||||
<input type="hidden" name="filter_value"
|
||||
th:value="${categoricalValue.getPrimaryKeyValue()}">
|
||||
<button class="mb-2">
|
||||
[[ ${categoricalValue.getDisplayName()} ]]
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</th:block>
|
||||
<th:block th:unless="${field.isForeignKey()}">
|
||||
<ul class="categorical-select">
|
||||
<li th:each="categoricalValue : ${field.getAllValues()}">
|
||||
<form action="" method="GET">
|
||||
<!--/*--> Propagate queryParams containing other filters with hidden fields <!--*/-->
|
||||
<th:block th:each="p : ${queryParams.keySet()}">
|
||||
<input th:each="v : ${queryParams.get(p)}"
|
||||
th:name="${p}" th:value="${v}" type="hidden"
|
||||
th:if="${p.startsWith('filter_')}">
|
||||
</th:block>
|
||||
|
||||
<!-- Reset page when applying filter to start back at page 1 -->
|
||||
<input type="hidden" name="page" value="1">
|
||||
<input type="hidden" name="pageSize" th:value="${page.getPagination().getPageSize()}">
|
||||
<input type="hidden" name="query" th:value="${query}">
|
||||
<input type="hidden" name="filter_field" th:value="${field.getJavaName()}">
|
||||
|
||||
<input type="hidden" name="filter_op" value="string_eq">
|
||||
<input type="hidden" name="filter_value"
|
||||
th:value="${categoricalValue.getValue()}">
|
||||
<button class="mb-2">
|
||||
[[ ${categoricalValue.getFormattedValue()} ]]
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</th:block>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -43,18 +43,6 @@
|
||||
<div class="sidebar-top">
|
||||
<h6 class="fw-bold pt-2 ms-3 menu-subheading d-none d-md-block">MENU</h6>
|
||||
<ul class="sidebar-menu pb-0 mb-0 ">
|
||||
<li th:class="${#strings.equals(activePage, 'home') ? 'active' : ''}">
|
||||
<a th:href="|/${baseUrl}|">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="menu-icon">
|
||||
<i class="bi bi-house"></i>
|
||||
</div>
|
||||
<div class="menu-entry-text d-none d-md-block">
|
||||
Home
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li th:class="${#strings.equals(activePage, 'entities') ? 'active' : ''}">
|
||||
<a th:href="|/${baseUrl}|">
|
||||
<div class="d-flex align-items-center">
|
||||
|
@ -8,8 +8,8 @@
|
||||
<div class="d-flex">
|
||||
<div th:replace="~{fragments/resources :: sidebar('entities')}"></div>
|
||||
<div class="main-content bg-lighter">
|
||||
<h1 class="fw-bold mb-4"><i class="align-middle bi bi-house"></i><span class="align-middle"> Home</span></h1>
|
||||
<form th:action="${baseUrl}" method="GET">
|
||||
<h1 class="fw-bold mb-4"><i class="align-middle bi bi-database"></i><span class="align-middle"> Entities</span></h1>
|
||||
<form th:action="|/${baseUrl}|" method="GET">
|
||||
<div class="input-group">
|
||||
<input type="text" th:value="${query}"
|
||||
placeholder="Type a class or a table name and press ENTER to search"
|
||||
@ -58,108 +58,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <h1 class="fw-bold mb-4"><i class="bi bi-bounding-box-circles"></i> Dashboard</h1>
|
||||
<div class="alert mt-4 alert-warning" role="alert">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<span>
|
||||
In some sections, this website displays unfiltered war footage from Telegram channels.
|
||||
This might be NSFW and/or hurt your sensibility. Proceed at your own discretion.
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col pb-4">
|
||||
<form action="/search" method="get" class="form">
|
||||
<div class="input-group">
|
||||
<input type="text" name="query" class="form-control ui-text-input" placeholder="Quick search">
|
||||
<button class="ui-btn btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="row mt-4">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="box with-footer-button">
|
||||
<h3 class="fw-bold"><i class="bi bi-chat-dots"></i> Total messages</h3>
|
||||
<p class="fine">The total number of messages indexed since Feb 23, 2022.</p>
|
||||
<p class="fs-bigger" th:text="${countMessages}"></p>
|
||||
<div class="separator mb-3 mt-3"></div>
|
||||
<div class="row mt-1">
|
||||
<div class="col-6 text-center">
|
||||
<h4> <span th:text="${countMessagesOneDay}"></span></h4>
|
||||
<p class="mb-0 text-center fw-bold">LAST 24 HOURS</p>
|
||||
</div>
|
||||
<div class="col-6 text-center">
|
||||
<h4> <span th:text="${countMessagesOneHour}"></span></h4>
|
||||
<p class="mb-0 text-center fw-bold">LAST HOUR</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/search"
|
||||
class="text-decoration-none color-black">
|
||||
<div class="explore-channel text-center">
|
||||
<p class="m-0 p-0">
|
||||
SEARCH MESSAGES
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 mt-4 mt-lg-0">
|
||||
<div class="box with-footer-button">
|
||||
<h3 class="fw-bold"><i class="bi bi-megaphone"></i> Total channels</h3>
|
||||
<p class="fine">The total number of channels, a fraction of which is actively indexed.</p>
|
||||
<p class="fs-bigger" th:text="${countChannels}"></p>
|
||||
<div class="separator mb-3 mt-3"></div>
|
||||
<div class="row mt-1">
|
||||
<div class="col-4 text-center">
|
||||
<h4> <span class="fw-bold" th:text="${countChannelsEn}"></span></h4>
|
||||
<p class="mb-0 text-center fw-bold">🇬🇧 English</p>
|
||||
</div>
|
||||
<div class="col-4 text-center">
|
||||
<h4> <span class="fw-bold" th:text="${countChannelsRu}"></span></h4>
|
||||
<p class="mb-0 text-center fw-bold">🇷🇺 Russian</p>
|
||||
</div>
|
||||
<div class="col-4 text-center">
|
||||
<h4> <span class="fw-bold" th:text="${countChannelsUa}"></span></h4>
|
||||
<p class="mb-0 text-center fw-bold">🇺🇦 Ukrainian</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/channels"
|
||||
class="text-decoration-none color-black">
|
||||
<div class="explore-channel text-center">
|
||||
<p class="m-0 p-0">
|
||||
EXPLORE CHANNELS
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col-lg-12 col-xxl-8">
|
||||
<div class="box d-none d-sm-block">
|
||||
<h3 class="fw-bold"><i class="bi bi-graph-up"></i> Messages per day</h3>
|
||||
A
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-xxl-4 mt-4 mt-xxl-0 ">
|
||||
<div class="box">
|
||||
<h3 class="fw-bold mb-4"><i class="bi bi-graph-up-arrow"></i> Trending topics</h3>
|
||||
CIAO
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
@ -43,19 +43,33 @@
|
||||
</div>
|
||||
</th:block>
|
||||
<th:block th:unless="${field.isForeignKey()}">
|
||||
|
||||
<input th:if="${!field.isBinary()}" placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||
th:name="${field.getName()}"
|
||||
th:value="
|
||||
${create ? (params != null ? params.getOrDefault(field.getName(), '') : '')
|
||||
: (object != null ? object.get(field).getValue() : '' )}
|
||||
"
|
||||
class="form-control" th:id="|__id_${field.getName()}|"
|
||||
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
|
||||
th:required="${!field.isNullable() && !field.isPrimaryKey()}"
|
||||
step="any"
|
||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||
oninput="this.setCustomValidity('')">
|
||||
<th:block th:if="${field.isText()}">
|
||||
<textarea placeholder="NULL"
|
||||
th:name="${field.getName()}"
|
||||
th:text="
|
||||
${create ? (params != null ? params.getOrDefault(field.getName(), '') : '')
|
||||
: (object != null ? object.get(field).getValue() : '' )}
|
||||
"
|
||||
class="form-control" th:id="|__id_${field.getName()}|"
|
||||
th:required="${!field.isNullable() && !field.isPrimaryKey()}"
|
||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||
rows="5"
|
||||
oninput="this.setCustomValidity('')"></textarea>
|
||||
</th:block>
|
||||
<th:block th:if="${!field.isText()}">
|
||||
<input th:if="${!field.isBinary()}" placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||
th:name="${field.getName()}"
|
||||
th:value="
|
||||
${create ? (params != null ? params.getOrDefault(field.getName(), '') : '')
|
||||
: (object != null ? object.get(field).getValue() : '' )}
|
||||
"
|
||||
class="form-control" th:id="|__id_${field.getName()}|"
|
||||
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
|
||||
th:required="${!field.isNullable() && !field.isPrimaryKey()}"
|
||||
step="any"
|
||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||
oninput="this.setCustomValidity('')">
|
||||
</th:block>
|
||||
|
||||
<!--/*--> Binary field <!--*/-->
|
||||
<th:block th:if="${field.isBinary()}">
|
||||
|
@ -81,13 +81,14 @@
|
||||
<span title="Click to remove this filter"
|
||||
class="active-filter badge bg-primary me-1 mb-2 p-2 font-monospace cursor-pointer noselect"
|
||||
th:data-formid="${filter.toString()}"
|
||||
th:text="${filter}">
|
||||
>
|
||||
[[ ${filter}]] <i class="bi bi-x-circle"></i>
|
||||
</span>
|
||||
<form action="" th:id="${filter.toString()}" method="GET">
|
||||
<th:block th:each="p : ${queryParams.keySet()}">
|
||||
<input th:each="v : ${queryParams.get(p)}" th:name="${p}" th:value="${v}" type="hidden">
|
||||
</th:block>
|
||||
<input type="hidden" name="remove_field" th:value="${filter.getField()}">
|
||||
<input type="hidden" name="remove_field" th:value="${filter.getField().getJavaName()}">
|
||||
<input type="hidden" name="remove_op" th:value="${filter.getOp()}">
|
||||
<input type="hidden" name="remove_value" th:value="${filter.getValue()}">
|
||||
</form>
|
||||
|
Loading…
x
Reference in New Issue
Block a user