This commit is contained in:
Francesco 2023-09-26 16:38:42 +02:00
parent ca657340c0
commit ca1931cda6
11 changed files with 202 additions and 166 deletions

View File

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

View File

@ -19,7 +19,6 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;

View File

@ -90,7 +90,7 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
.collect(Collectors.toList()); .collect(Collectors.toList());
List<Predicate> queryPredicates = new ArrayList<>(); List<Predicate> queryPredicates = new ArrayList<>();
if (q != null) { if (q != null && !q.isBlank()) {
for (DbField f : stringFields) { for (DbField f : stringFields) {
Path path = root.get(f.getJavaName()); Path path = root.get(f.getJavaName());
queryPredicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%")); queryPredicates.add(cb.like(cb.lower(cb.toString(path)), "%" + q.toLowerCase() + "%"));
@ -118,15 +118,15 @@ public class AdvancedJpaRepository extends SimpleJpaRepository {
); );
} else if (op == CompareOperator.EQ) { } else if (op == CompareOperator.EQ) {
finalPredicates.add( finalPredicates.add(
cb.equal(root.get(field), value) cb.equal(root.get(field), Double.parseDouble(value.toString()))
); );
} else if (op == CompareOperator.GT) { } else if (op == CompareOperator.GT) {
finalPredicates.add( finalPredicates.add(
cb.greaterThan(root.get(field), value.toString()) cb.greaterThan(root.get(field), Double.parseDouble(value.toString()))
); );
} else if (op == CompareOperator.LT) { } else if (op == CompareOperator.LT) {
finalPredicates.add( finalPredicates.add(
cb.lessThan(root.get(field), value.toString()) cb.lessThan(root.get(field), Double.parseDouble(value.toString()))
); );
} else if (op == CompareOperator.AFTER) { } else if (op == CompareOperator.AFTER) {
if (value instanceof LocalDate) if (value instanceof LocalDate)

View File

@ -1,10 +1,15 @@
package tech.ailef.dbadmin.external.dbmapping; package tech.ailef.dbadmin.external.dbmapping;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import tech.ailef.dbadmin.external.annotations.DisplayImage; import tech.ailef.dbadmin.external.annotations.DisplayImage;
import tech.ailef.dbadmin.external.annotations.Filterable;
public class DbField { public class DbField {
protected String dbName; protected String dbName;
@ -128,6 +133,25 @@ public class DbField {
return type == DbFieldType.TEXT; 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().equalsIgnoreCase("categorical");
}
public Set<DbFieldValue> getAllValues() {
List findAll = schema.getJpaRepository().findAll();
Set<DbFieldValue> allValues = new HashSet<>();
for (Object o : findAll) {
DbFieldValue val = new DbObject(o, schema).get(this);
allValues.add(val);
}
return allValues;
}
@Override @Override
public String toString() { public String toString() {
return "DbField [name=" + dbName + ", javaName=" + javaName + ", type=" + type + ", field=" + field return "DbField [name=" + dbName + ", javaName=" + javaName + ", type=" + type + ", field=" + field
@ -135,6 +159,23 @@ public class DbField {
+ ", schema=" + schema.getClassName() + "]"; + ", 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

@ -1,5 +1,7 @@
package tech.ailef.dbadmin.external.dbmapping; package tech.ailef.dbadmin.external.dbmapping;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
public class DbFieldValue { public class DbFieldValue {
@ -39,6 +41,22 @@ public class DbFieldValue {
public String toString() { public String toString() {
return "DbFieldValue [value=" + value + ", field=" + field + "]"; 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -18,7 +19,6 @@ import jakarta.persistence.OneToOne;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import tech.ailef.dbadmin.external.DbAdmin; import tech.ailef.dbadmin.external.DbAdmin;
import tech.ailef.dbadmin.external.annotations.ComputedColumn; 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.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.misc.Utils; import tech.ailef.dbadmin.external.misc.Utils;
@ -182,13 +182,41 @@ public class DbObjectSchema {
public List<DbField> getFilterableFields() { public List<DbField> getFilterableFields() {
return getSortedFields().stream().filter(f -> { return getSortedFields().stream().filter(f -> {
return !f.isBinary() && !f.isPrimaryKey() return !f.isBinary() && !f.isPrimaryKey()
&& f.getPrimitiveField().getAnnotation(Filterable.class) != null; && f.isFilterable();
}).toList(); }).toList();
} }
public List<DbObject> findAll() {
List r = jpaRepository.findAll();
List<DbObject> results = new ArrayList<>();
for (Object o : r) {
results.add(new DbObject(o, this));
}
return results;
}
@Override @Override
public String toString() { public String toString() {
return "DbObjectSchema [fields=" + fields + ", className=" + entityClass.getName() + "]"; 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

@ -181,6 +181,19 @@ AUTOCOMPLETE
background-color: #F0F0F0; 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 * Images
*/ */

View File

@ -9,7 +9,7 @@ document.addEventListener("DOMContentLoaded", () => {
activeFilters.forEach(activeFilter => { activeFilters.forEach(activeFilter => {
activeFilter.addEventListener('click', function(e) { activeFilter.addEventListener('click', function(e) {
let formId = e.target.dataset.formid; let formId = e.target.dataset.formid;
document.getElementById(formId).submit() document.getElementById(formId).submit();
}); });
}); });

View File

@ -46,64 +46,99 @@
<span class="fw-bold align-middle" th:text="${field.getName()}"></span> <span class="fw-bold align-middle" th:text="${field.getName()}"></span>
</div> </div>
<div class="card-body"> <div class="card-body">
<!--/*--> Propagate queryParams containing other filters with hidden fields <!--*/-->
<form action="" method="GET"> <th:block th:each="p : ${queryParams.keySet()}">
<!-- Reset page when applying filter to start back at page 1 --> <input th:each="v : ${queryParams.get(p)}"
<input type="hidden" name="page" value="1"> th:name="${p}" th:value="${v}" type="hidden"
<input type="hidden" name="pageSize" th:value="${page.getPagination().getPageSize()}"> th:if="${p.startsWith('filter_')}">
<input type="hidden" name="query" th:value="${query}"> </th:block>
<input type="hidden" name="filter_field" th:value="${field.getJavaName()}"> <!--/*--> Handle non categorical filter <!--*/-->
<th:block th:if="${!field.isFilterableCategorical()}">
<div class="input-group pe-2"> <form action="" method="GET">
<th:block th:if="${field.isForeignKey()}"> <div class="input-group pe-2">
<span class="input-group-text w-25"> <!-- Reset page when applying filter to start back at page 1 -->
<input type="hidden" name="filter_op" value="string_eq"> <input type="hidden" name="page" value="1">
Equals <input type="hidden" name="pageSize" th:value="${page.getPagination().getPageSize()}">
</span> <input type="hidden" name="query" th:value="${query}">
<div class="autocomplete-input position-relative w-50"> <input type="hidden" name="filter_field" th:value="${field.getJavaName()}">
<input class="autocomplete form-control" type="text" name="filter_value"
th:data-classname="${field.getConnectedType().getName()}" <th:block th:if="${field.isForeignKey()}">
autocomplete="off" <span class="input-group-text w-25">
placeholder="NULL"> <input type="hidden" name="filter_op" value="string_eq">
</input> Equals
<div class="suggestions d-none"> </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> </div>
</div>
</th:block>
</th:block> <th:block th:unless="${field.isForeignKey()}">
<th:block th:unless="${field.isForeignKey()}"> <select class="form-select w-25" name="filter_op">
<select class="form-select w-25" name="filter_op"> <option th:value="${op}" th:each="op : ${field.getType().getCompareOperators()}"
<option th:value="${op}" th:each="op : ${field.getType().getCompareOperators()}" th:text="${op.getDisplayName()}">
th:text="${op.getDisplayName()}"> </select>
</select> <input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}" name="filter_value"
name="filter_value" class="form-control w-50" th:id="|__id_${field.getName()}|"
class="form-control w-50" th:id="|__id_${field.getName()}|" th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}"
th:classAppend="${field.isPrimaryKey() && object != null ? 'disable' : ''}" th:required="${!field.isNullable() && !field.isPrimaryKey()}"
th:required="${!field.isNullable() && !field.isPrimaryKey()}" step="any"
step="any" oninvalid="this.setCustomValidity('This field is not nullable.')"
oninvalid="this.setCustomValidity('This field is not nullable.')" oninput="this.setCustomValidity('')">
oninput="this.setCustomValidity('')"> </th:block>
</th:block> <button class="ui-btn btn btn-primary"><i class="bi bi-search text-white"></i></button>
</div>
<th:block th:each="p : ${queryParams.keySet()}"> </form>
<input th:each="v : ${queryParams.get(p)}" </th:block>
th:name="${p}" th:value="${v}" type="hidden" <!--/*--> Handle categorical filter <!--*/-->
th:if="${p.startsWith('filter_')}"> <th:block th:if="${field.isFilterableCategorical()}">
</th:block> <th:block th:if="${field.isForeignKey()}">
<ul class="categorical-select">
<li th:each="categoricalValue : ${field.getConnectedSchema().findAll()}">
<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()}">
<input type="hidden" name="filter_op" value="string_eq">
<button class="ui-btn btn btn-primary"><i class="bi bi-search text-white"></i></button> <input type="hidden" name="filter_value"
</div> th:value="${categoricalValue.getPrimaryKeyValue()}">
</form> <button class="mb-2">
[[ ${categoricalValue.getDisplayName()} ]]
<!-- </button>
<th:block th:if="${field.getConnectedType() != null}"> </form>
<div th:each="val : ${schema.getFieldValues(field)}"> </li>
<span th:text="${val}"></span> </ul>
</div> </th:block>
</th:block> <th:block th:unless="${field.isForeignKey()}">
--> <ul class="categorical-select">
<li th:each="categoricalValue : ${field.getAllValues()}">
<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()}">
<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>
</div> </div>
</body> </body>

View File

@ -58,108 +58,8 @@
</div> </div>
</div> </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> </div>
</div> </div>
</body> </body>
</html> </html>
</body>
</html>

View File

@ -81,7 +81,8 @@
<span title="Click to remove this filter" <span title="Click to remove this filter"
class="active-filter badge bg-primary me-1 mb-2 p-2 font-monospace cursor-pointer noselect" class="active-filter badge bg-primary me-1 mb-2 p-2 font-monospace cursor-pointer noselect"
th:data-formid="${filter.toString()}" th:data-formid="${filter.toString()}"
th:text="${filter}"> >
[[ ${filter}]] <!-- <i class="bi bi-x-circle"></i> -->
</span> </span>
<form action="" th:id="${filter.toString()}" method="GET"> <form action="" th:id="${filter.toString()}" method="GET">
<th:block th:each="p : ${queryParams.keySet()}"> <th:block th:each="p : ${queryParams.keySet()}">