mirror of
https://github.com/dalbodeule/snap-admin.git
synced 2025-08-13 15:11:14 +00:00
0.0.2
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
|
||||
spring.datasource.url=jdbc:h2:file:./dbadmin
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=password
|
||||
#spring.datasource.url=jdbc:h2:file:./database
|
||||
#spring.datasource.username=sa
|
||||
#spring.datasource.password=password
|
||||
#spring.h2.console.enabled=true
|
||||
|
||||
|
||||
#spring.jpa.show-sql=true
|
||||
|
@@ -133,6 +133,17 @@ td.table-checkbox, th.table-checkbox {
|
||||
}
|
||||
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.noselect {
|
||||
cursor: pointer; -webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/**
|
||||
AUTOCOMPLETE
|
||||
**/
|
||||
@@ -154,4 +165,12 @@ AUTOCOMPLETE
|
||||
|
||||
.clear-all-badge {
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters
|
||||
*/
|
||||
|
||||
.filterable-field .card-header:hover {
|
||||
background-color: #F0F0F0;
|
||||
}
|
34
src/main/resources/static/js/filters.js
Normal file
34
src/main/resources/static/js/filters.js
Normal file
@@ -0,0 +1,34 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
let rootElements = document.querySelectorAll('.filterable-fields');
|
||||
|
||||
|
||||
rootElements.forEach(root => {
|
||||
let fields = root.querySelectorAll('.filterable-field');
|
||||
|
||||
let activeFilters = root.querySelectorAll(".active-filter");
|
||||
activeFilters.forEach(activeFilter => {
|
||||
activeFilter.addEventListener('click', function(e) {
|
||||
let formId = e.target.dataset.formid;
|
||||
document.getElementById(formId).submit()
|
||||
});
|
||||
});
|
||||
|
||||
fields.forEach(field => {
|
||||
field.querySelector(".card-header").addEventListener('click', function(e) {
|
||||
if (field.querySelector(".card-body").classList.contains('d-none')) {
|
||||
field.querySelector(".card-body").classList.remove('d-none');
|
||||
field.querySelector(".card-body").classList.add('d-block');
|
||||
field.querySelector(".card-header i.bi").classList.remove('bi-caret-right');
|
||||
field.querySelector(".card-header i.bi").classList.add('bi-caret-down');
|
||||
} else {
|
||||
field.querySelector(".card-body").classList.remove('d-block');
|
||||
field.querySelector(".card-body").classList.add('d-none');
|
||||
field.querySelector(".card-header i.bi").classList.remove('bi-caret-down');
|
||||
field.querySelector(".card-header i.bi").classList.add('bi-caret-right');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
@@ -48,13 +48,24 @@
|
||||
</a>
|
||||
</th:block>
|
||||
<th:block th:if="${!field.isPrimaryKey()}">
|
||||
<span th:text="${object.get(field).getFormattedValue()}" th:if="${!field.isBinary()}">
|
||||
</span>
|
||||
|
||||
<th:block th:if="${!field.isBinary()}">
|
||||
<span th:if="${object.get(field).getFormattedValue() == null}" class="font-monospace null-label">
|
||||
NULL
|
||||
</span>
|
||||
<span th:unless="${object.get(field).getFormattedValue() == null}"
|
||||
th:text="${object.get(field).getFormattedValue()}">
|
||||
|
||||
</span>
|
||||
|
||||
</th:block>
|
||||
<span th:unless="${!field.isBinary()}">
|
||||
<th:block th:if="${object.get(field).getValue()}">
|
||||
<a class="text-decoration-none null-label"
|
||||
th:href="|/dbadmin/download/${schema.getJavaClass().getName()}/${field.getName()}/${object.get(schema.getPrimaryKey()).getValue()}|">
|
||||
<i class="align-middle bi bi-box-arrow-down"></i><span class="align-middle"> Download</span>
|
||||
<i class="align-middle bi bi-box-arrow-down"></i><span class="align-middle"> Download
|
||||
<!--/*--> <span class="text-muted">([[ ${object.get(field).getValue().length} ]] bytes)</span> <!--*/-->
|
||||
</span>
|
||||
</a>
|
||||
</th:block>
|
||||
<th:block th:unless="${object.get(field).getValue()}">
|
||||
|
@@ -38,5 +38,73 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="card mb-3 filterable-field" th:fragment="filter_field(field)">
|
||||
<div class="card-header noselect cursor-pointer">
|
||||
<i class="bi bi-caret-right filter-icon align-middle"></i>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<script type="text/javascript" src="/js/table.js"></script>
|
||||
<script type="text/javascript" src="/js/autocomplete.js"></script>
|
||||
<script type="text/javascript" src="/js/autocomplete-multi.js"></script>
|
||||
<script type="text/javascript" src="/js/filters.js"></script>
|
||||
<title th:text="${title != null ? title + ' | Spring Boot DB Admin Panel' : 'Spring Boot DB Admin Panel'}"></title>
|
||||
</head>
|
||||
|
||||
@@ -124,14 +125,17 @@
|
||||
<div th:if="${page != null && page.getPagination().getMaxPage() != 1}" class="d-flex">
|
||||
<ul class="pagination me-3">
|
||||
<li class="page-item" th:if="${page.getPagination().getCurrentPage() != 1}">
|
||||
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${page.getPagination().getCurrentPage() - 1},pageSize=${page.getPagination().getPageSize()})}" aria-label="Previous">
|
||||
<a class="page-link"
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(page.getPagination.getCurrentPage() - 1)}|}"
|
||||
aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
<span class="sr-only">Previous</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="page-item" th:each="p : ${page.getPagination().getBeforePages()}">
|
||||
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${p},pageSize=${page.getPagination().getPageSize()})}" th:text="${p}"></a>
|
||||
<a class="page-link"
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(p)}|}" th:text="${p}"></a>
|
||||
</li>
|
||||
|
||||
<li class="page-item active">
|
||||
@@ -139,12 +143,15 @@
|
||||
</li>
|
||||
|
||||
<li class="page-item" th:each="p : ${page.getPagination().getAfterPages()}">
|
||||
<a class="page-link" th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${p},pageSize=${page.getPagination().getPageSize()})}" th:text="${p}"></a>
|
||||
<a class="page-link"
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(p)}|}"
|
||||
th:text="${p}"></a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
th:if="${!page.getPagination().isLastPage()}"
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}|(query=${query},page=${page.getPagination().getCurrentPage() + 1},pageSize=${page.getPagination().getPageSize()})}" aria-label="Next">
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getLink(page.getPagination.getCurrentPage() + 1)}|}"
|
||||
aria-label="Next">
|
||||
<span class="sr-only">Next</span>
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
@@ -155,6 +162,11 @@
|
||||
<input type="hidden" th:value="${page.getPagination().getCurrentPage()}" th:name="page">
|
||||
<input type="hidden" th:value="${query}" th:name="query">
|
||||
<input type="hidden" name="pageSize">
|
||||
<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>
|
||||
<select class="form-select page-size">
|
||||
<option disabled>Page size</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 50}">50</option>
|
||||
@@ -172,7 +184,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center" th:if="${page.getPagination().getMaxPage() == 1}">
|
||||
<div class="d-flex align-items-center" th:if="${page.getPagination().getMaxPage() == 1}">
|
||||
<div class="me-3">
|
||||
<form method="GET" th:action="@{|/dbadmin/model/${schema.getClassName()}|}">
|
||||
<input type="hidden" th:value="${page.getPagination().getCurrentPage()}" th:name="page">
|
||||
<input type="hidden" th:value="${query}" th:name="query">
|
||||
<input type="hidden" name="pageSize">
|
||||
<select class="form-select page-size">
|
||||
<option disabled>Page size</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 50}">50</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 100}">100</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 150}">150</option>
|
||||
<option th:selected="${page.getPagination().getPageSize() == 200}">200</option>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<p class="m-0 p-0">
|
||||
<i>Showing [[ ${page.getActualResults()} ]] of [[ ${page.getPagination().getMaxElement()} ]] results</i>
|
||||
</p>
|
||||
@@ -180,7 +206,6 @@
|
||||
|
||||
|
||||
<div class="bulk-actions">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
@@ -27,21 +27,18 @@
|
||||
</div>
|
||||
<div class="align-items-center">
|
||||
<h4 class="m-0" th:if="${page}">
|
||||
<th:block th:if="${sortKey != field.getName()}" >
|
||||
<a th:href="@{|/dbadmin/model/${schema.getClassName()}|(page=${page.getPagination().getCurrentPage()},
|
||||
pageSize=${page.getPagination().getPageSize()},sortKey=${field.getName()},sortOrder=DESC)}">
|
||||
<th:block th:if="${sortKey != field.getJavaName()}" >
|
||||
<a th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'DESC')}|}">
|
||||
<i title="Sort" class="bi bi-caret-up"></i>
|
||||
</a>
|
||||
</th:block>
|
||||
<th:block th:unless="${sortKey != field.getName()}">
|
||||
<th:block th:unless="${sortKey != field.getJavaName()}">
|
||||
<a th:if="${sortOrder == 'DESC'}"
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}|(page=${page.getPagination().getCurrentPage()},
|
||||
pageSize=${page.getPagination().getPageSize()},sortKey=${field.getName()},sortOrder=ASC)}">
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'ASC')}|}">
|
||||
<i title="Sort" class="bi bi-caret-down-fill"></i>
|
||||
</a>
|
||||
<a th:if="${sortOrder == 'ASC'}"
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}|(page=${page.getPagination().getCurrentPage()},
|
||||
pageSize=${page.getPagination().getPageSize()},sortKey=${field.getName()},sortOrder=DESC)}">
|
||||
th:href="@{|/dbadmin/model/${schema.getClassName()}${page.getPagination().getSortedPageLink(field.getJavaName(), 'DESC')}|}">
|
||||
<i title="Sort" class="bi bi-caret-up-fill"></i>
|
||||
</a>
|
||||
</th:block>
|
||||
|
@@ -39,7 +39,7 @@
|
||||
: (object != null ? object.traverse(field).getPrimaryKeyValue() : '' )
|
||||
})}">
|
||||
</div>
|
||||
<input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|">
|
||||
<!-- <input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|"> -->
|
||||
</th:block>
|
||||
<th:block th:unless="${field.isForeignKey()}">
|
||||
<input placeholder="NULL" th:type="${field.getType().getHTMLName()}"
|
||||
@@ -55,7 +55,7 @@
|
||||
step="any"
|
||||
oninvalid="this.setCustomValidity('This field is not nullable.')"
|
||||
oninput="this.setCustomValidity('')">
|
||||
<input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|">
|
||||
<!-- <input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|"> -->
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
|
@@ -15,7 +15,7 @@
|
||||
<span class="align-middle"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span>
|
||||
</h1>
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<div th:class="${schema.getFilterableFields().isEmpty() ? 'col' : 'col-9'}">
|
||||
<div class="w-100 d-flex inner-navigation">
|
||||
<a th:href="|/dbadmin/model/${className}|" class="active">
|
||||
<div class="ui-tab ps-5 pe-5 p-3">
|
||||
@@ -31,36 +31,82 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="box with-navigation">
|
||||
<form th:action="|/dbadmin/model/${className}|" method="GET" class="mb-3">
|
||||
<div class="input-group">
|
||||
<input type="text" th:value="${query}"
|
||||
placeholder="Type and press ENTER to search"
|
||||
class="ui-text-input form-control" name="query" autofocus>
|
||||
<button class="ui-btn btn btn-primary">Search</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form th:action="|/dbadmin/model/${className}|" method="GET" class="mb-3">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" th:value="${query}"
|
||||
placeholder="Type and press ENTER to search"
|
||||
class="ui-text-input form-control" name="query" autofocus>
|
||||
<button class="ui-btn btn btn-primary">Search</button>
|
||||
</div>
|
||||
<th:block th:each="queryParam : ${queryParams.keySet()}">
|
||||
<input th:each="paramValue : ${queryParams.get(queryParam)}"
|
||||
th:if="${queryParam.startsWith('filter_')}"
|
||||
th:name="${queryParam}" th:value="${paramValue}" type="hidden">
|
||||
</th:block>
|
||||
|
||||
<input type="hidden" name="page" value="1">
|
||||
<input type="hidden" name="pageSize"
|
||||
th:value="${page.getPagination().getPageSize()}">
|
||||
</form>
|
||||
<div class="separator mb-4 mt-4"></div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
|
||||
|
||||
<h3 class="fw-bold mb-4 align-baseline w-100">
|
||||
<h3 class="fw-bold mb-4 align-baseline flex-grow-1">
|
||||
<span title="Java class name"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span>
|
||||
<span title="Database table name" class="ms-3 label label-primary label-gray font-monospace">
|
||||
[[ ${schema.getTableName()} ]]
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</h3>
|
||||
<h3><a title="Create new item"
|
||||
th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a></h3>
|
||||
|
||||
<h3>
|
||||
<a title="Create new item"
|
||||
th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div th:replace="~{fragments/table_selectable :: table(results=${page.getResults()}, schema=${schema})}"
|
||||
|
||||
<div th:replace="~{fragments/table_selectable :: table(results=${page.getResults()}, schema=${schema})}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${!schema.getFilterableFields().isEmpty()}" class="col-3">
|
||||
<div class="box filterable-fields">
|
||||
<h3 class="fw-bold mb-3"><i class="bi bi-funnel"></i> Filters</h3>
|
||||
|
||||
<div class="mb-2">
|
||||
<div th:each="filter : ${activeFilters}">
|
||||
|
||||
<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}">
|
||||
</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_op" th:value="${filter.getOp()}">
|
||||
<input type="hidden" name="remove_value" th:value="${filter.getValue()}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<th:block th:each="field : ${schema.getFilterableFields()}">
|
||||
<div th:replace="~{fragments/forms :: filter_field(field=${field})}"></div>
|
||||
</th:block>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user