This commit is contained in:
Francesco
2023-09-27 10:58:51 +02:00
parent ef99c3e0ed
commit 1b4f91a168
23 changed files with 418 additions and 422 deletions

View File

@@ -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
}

View File

@@ -16,4 +16,5 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Filterable {
public FilterableType type() default FilterableType.DEFAULT;
}

View File

@@ -0,0 +1,5 @@
package tech.ailef.dbadmin.external.annotations;
public enum FilterableType {
DEFAULT, CATEGORICAL;
}

View File

@@ -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()) {

View File

@@ -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.");
}
}

View File

@@ -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)
);
}

View File

@@ -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))

View File

@@ -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;
}
}

View File

@@ -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() {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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
*/

View File

@@ -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();
});
});

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">&#x1F1EC;&#x1F1E7; 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">&#x1F1F7;&#x1F1FA; 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">&#x1F1FA;&#x1F1E6; 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>

View File

@@ -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()}">

View File

@@ -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>