0.0.1 Alpha version

This commit is contained in:
Francesco
2023-09-18 09:25:25 +02:00
commit 348408a3e1
45 changed files with 4339 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
spring.datasource.url=jdbc:h2:file:./dbadmin
spring.datasource.username=sa
spring.datasource.password=password
#spring.h2.console.enabled=true

View File

@@ -0,0 +1,157 @@
form.delete-form {
display: inline-block;
}
form.delete-form button {
background: transparent;
border: none;
}
.dbfieldtype {
font-weight: normal;
font-family: monospace;
font-size: 0.9rem;
}
a .bi, button .bi {
color: #007fd0;
}
a {
color: #007fd0;
}
.disable {
pointer-events: none;
background: #EDECEF;
}
.null-label {
background-color: #EEE;
border-radius: 5px;
border: 1px solid #DDD;
display: inline-block;
padding: 3px;
color: #333;
padding-left: 6px;
padding-right: 6px;
}
ul.pagination {
padding-bottom: 0;
margin-bottom: 0;
}
tr.table-data-row td, tr.table-data-row th {
border-right: 1px solid #DDD;
}
tr.table-data-row td:last-child, tr.table-data-row th:last-child {
border-right: 0px;
}
.row-icons {
font-size: 1.2rem;
width: 128px;
}
h1 .bi {
font-size: 2rem;
}
h1 a {
color: #222;
text-decoration: none;
}
h1 a:hover {
color: #007fd0;
}
.inner-navigation {
border-top-right-radius: 5px;
border-top-left-radius: 5px;
background-color: #FAFAFA;
}
.inner-navigation a:first-child {
border-top-left-radius: 5px;
background-color: #FAFAFA;
}
.inner-navigation-border {
border-bottom: 4px solid #F0F0F0;
}
.inner-navigation a {
text-decoration: none;
font-weight: bold;
border-bottom: 4px solid #F0F0F0;
}
.inner-navigation a.active {
border-bottom: 4px solid #007fd0 !important;
}
.inner-navigation a:hover {
background-color: #FFF;
border-bottom: 4px solid #ADDEFF;
}
.suggestions {
position: absolute;
width: 100%;
background-color: white;
border: 1px solid #DADADA;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
top: 100%;
max-height: 300px;
overflow: auto;
z-index: 999;
}
.suggestion {
padding: 1rem;
background-color: #FAFAFA;
padding-bottom: 0;
margin: 0;
border-bottom: 2px solid #FAFAFA;
}
.suggestion:hover {
cursor: pointer;
background-color: #FFF;
border-bottom: 2px solid #ADDEFF;
}
td.table-checkbox, th.table-checkbox {
width: 36px;
}
/**
AUTOCOMPLETE
**/
.badge-checkbox {
display: none;
}
.value-badge {
cursor: pointer; -webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.value-badge .badge {
font-size: 1rem;
padding: 8px;
margin-top: 5px;
}
.clear-all-badge {
padding: 0.4rem;
}

View File

@@ -0,0 +1,372 @@
body {
font-family: "Poppins", "Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
font-weight: 300;
}
h1, h2, h3, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
font-family: "Roboto", "Segoe UI", sans-serif;
}
.bg-lighter {
background-color: #F2F2F2;
}
#map { height: 100%; }
.bg-lightest {
background-color: #F8F8F8;
}
.bg-red {
background-color: red;
}
.bg-white {
background-color: white;
}
.box {
border-radius: 5px;
width: 100%;
background-color: #FAFAFA;
padding: 25px;
-webkit-box-shadow: 10px 10px 84px -44px rgba(0,0,0,0.27);
-moz-box-shadow: 10px 10px 84px -44px rgba(0,0,0,0.27);
box-shadow: 10px 10px 84px -44px rgba(0,0,0,0.27);
}
.box.with-navigation {
border-radius: 5px;
border-top-right-radius: 0px;
border-top-left-radius: 0px;
width: 100%;
background-color: #FAFAFA;
padding: 25px;
-webkit-box-shadow: 10px 10px 84px -44px rgba(0,0,0,0.27);
-moz-box-shadow: 10px 10px 84px -74px rgba(0,0,0,0.27);
box-shadow: 10px 10px 84px -74px rgba(0,0,0,0.27);
}
.box.with-telegram-embed {
padding: 10px;
padding-left: 5px;
}
.box.with-footer-button {
position: relative;
padding-bottom: 70px;
}
.box.box-h-300 {
height: 300px;
overflow: auto;
}
.navbar-brand .bi-activity {
color: green;
}
.navbar {
opacity: 0.90;
}
.navbar-brand .bi-hexagon-fill {
color: #3fb95f;
}
.bg-complementary {
background-color: #3fb95f;
}
div.main-wrapper {
height: 100vh;
overflow: auto;
}
div.sidebar {
width: 200px;
min-height: 100vh;
padding: 56px 0px 25px 0px;
-webkit-box-shadow: 10px 2px 67px -30px rgba(0,0,0,0.25);
-moz-box-shadow: 10px 2px 67px -30px rgba(0,0,0,0.25);
box-shadow: 10px 2px 67px -30px rgba(0,0,0,0.25);
}
.sidebar-top {
width: 200px;
position: fixed;
}
div.main-content {
padding: 50px;
width: calc(100% - 200px);
padding-top: 80px;
}
div.main-content.full-screen {
padding: 0px;
padding-top: 56px;
}
ul.sidebar-menu {
list-style-type: none;
padding-left: 0px;
}
ul.sidebar-menu li {
background-color: #F8F8F8;
border-left: 5px solid #F2F2F2;
}
ul.sidebar-menu li.active {
border-left: 5px solid #0092ee;
}
ul.sidebar-menu li:hover {
border-left: 5px solid #7fc8f6;
background-color: white;
}
ul.sidebar-menu li a {
text-decoration: none;
color: #444;
}
ul.sidebar-menu .menu-icon {
padding: 8px;
font-size: 1.6rem;
}
.navbar.fixed-top {
-webkit-box-shadow: 0px 5px 67px -30px rgba(0,0,0,1);
-moz-box-shadow: 0px 5px 67px -30px rgba(0,0,0,1);
box-shadow: 0px 5px 67px -30px rgba(0,0,0,1);
}
em {
background-color: #f1f4d0;
}
.label {
font-weight: bold;
color: #444
}
.label a {
text-decoration: none;
color: #0293c9;
}
.label-gray {
background-color: #EEE;
border-radius: 5px;
border: 1px solid #DDD;
padding: 3px 6px 0px 6px;
display: inline-block;
}
.message-text {
color: #333;
}
.form-date {
border: 1px solid #CCC;
}
.form-select {
border: 1px solid #CCC;
}
.separator {
height: 2px;
border-bottom: 1px solid #CCC;
}
.menu-subheading {
text-transform: uppercase;
color: #444;
font-size: 0.8rem;
}
.btn-outline-left {
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-left: 0px;
}
.channel-about {
font-size: 0.9rem;
color: #555;
text-align: left;
}
p.fine {
font-size: 0.9rem;
color: #444;
}
div.sidebar-bottom {
position: fixed;
bottom: 0px;
width: 200px;
}
.fs-smaller {
font-size: 0.8rem;
}
.fs-bigger {
text-align: center;
color: #0092ee;
font-weight: bold;
font-size: 2.3rem;
}
.fs-big {
font-size: 1.1rem;
}
.form-control:focus {
box-shadow: none !important;
}
.btn-check:focus+.btn-outline-primary, .btn-outline-primary:focus {
box-shadow: none !important;
}
.color-telegram {
color: #0092ee;
}
.bg-accent {
background-color: #007fd0;
}
.bg-accent a.navbar-brand {
color: #EEE;
}
.bg-accent a.navbar-brand:hover {
color: white;
}
/**
* Utility height classes
*/
.h-200 {
height: 200px;
}
.h-300 {
height: 300px;
}
.h-400 {
height: 400px;
}
.h-500 {
height: 500px;
}
.h-600 {
height: 600px;
}
.h-700 {
height: 700px;
}
.h-800 {
height: 800px;
}
.h-1000 {
height: 1000px;
}
.h-fill {
height: calc(100vh - 56px);
}
.overflow-auto {
overflow: auto;
}
.color-white {
color:white !important;
}
.ui-text-input {
border: 0px;
border-bottom: 2px solid #52b5f3;
background-color: white;
border-radius: 0px;
}
span.input-group-text {
background-color: #F0F0F0;
border:0px;
border-bottom: 2px solid #0768a5;
border-radius: 0px;
}
.ui-btn {
border-radius: 0px;
font-weight: bold;
}
.btn-primary {
background-color: #21a0f0;
border: 1px solid #21a0f0;
}
.w-10 {
width: 10%;
}
.w-5 {
width: 5%;
}
@media (max-width: 767px) {
div.sidebar {
width: 53px;
}
.sidebar-top {
width: 53px;
position: fixed;
}
div.main-content {
width: calc(100% - 53px);
padding: 15px;
padding-top: 70px;
}
div.box {
padding: 15px;
overflow: auto;
}
div.sidebar-bottom {
width: 53px;
}
.extra-toolbar {
left: 53px;
}
.extra-toolbar ul {
padding-left: 0px;
}
}

View File

@@ -0,0 +1,123 @@
/* Request to the autocomplete REST endpoit */
async function getSuggestions(className, query) {
const response = await fetch(`/dbadmin/api/autocomplete/${className}?query=${query}`);
const suggestions = await response.json();
return suggestions;
}
function hideSuggestions(inputElement) {
let suggestionsDiv = inputElement.parentElement.querySelector("div.suggestions");
suggestionsDiv.classList.remove('d-block');
suggestionsDiv.classList.add('d-none');
}
function showSuggestions(inputElement) {
let suggestionsDiv = inputElement.parentElement.querySelector("div.suggestions");
suggestionsDiv.classList.remove('d-none');
suggestionsDiv.classList.add('d-block');
}
document.addEventListener("DOMContentLoaded", () => {
let rootElements = document.querySelectorAll(".autocomplete-multi-input");
/* Instead of using onBlur, which takes precedence over onClick
/* and causes the click event to disappear, we detect click
/* on outside elements and close all the autocomplete manually */
document.querySelector("body").addEventListener('click', function(e) {
if (!e.target.classList.contains("suggestion") && !e.target.classList.contains("autocomplete")) {
rootElements.forEach(root => {
hideSuggestions(root.querySelector("input.autocomplete"));
});
}
});
rootElements.forEach(root => {
/* Event listener to delete badge on click
*/
root.querySelectorAll(".selected-values .value-badge").forEach(badge => {
badge.addEventListener('click', function() {
badge.remove();
});
});
root.querySelector(".clear-all-badge").addEventListener('click', function(e) {
e.target.classList.add('d-none');
e.target.classList.remove('d-inline-block');
root.querySelectorAll(".selected-values .value-badge").forEach(badge => {
badge.remove();
});
});
let input = root.querySelector("input.autocomplete");
if (input == undefined) return;
input.addEventListener('focus', function() {
showSuggestions(input);
});
let fieldName = input.dataset.fieldname;
input.parentElement.querySelector("div.suggestions").innerHTML =
`<div class="suggestion p-2 m-0">Start typing for suggestions</div>`;
input.addEventListener('keyup', async function(e) {
let suggestions = await getSuggestions(e.target.dataset.classname, e.target.value);
input.parentElement.querySelector("div.suggestions").innerHTML = "";
if (e.target.value.length <= 1) {
input.parentElement.querySelector("div.suggestions").innerHTML =
`<div class="suggestion p-2 m-0">Start typing for suggestions</div>`;
return;
}
suggestions.forEach(suggestion => {
let suggestionDiv = document.createElement('div');
suggestionDiv.innerHTML =
`<div class="suggestion p-2 m-0">
<strong>${suggestion.id}</strong>
<p class="p-0 m-0">${suggestion.value}</p>
</div>`;
input.parentElement.querySelector("div.suggestions").appendChild(suggestionDiv);
suggestionDiv.addEventListener('click', function(e) {
hideSuggestions(input);
input.value = '';
// Check if we need to add the 'Clear all' button back
root.querySelector(".clear-all-badge").classList.add('d-inline-block');
root.querySelector(".clear-all-badge").classList.remove('d-none');
root.querySelector(".selected-values")
.innerHTML += `
<span class="value-badge">
<input type="checkbox" class="badge-checkbox" checked="checked"
name="${fieldName}" value="${suggestion.id}">
<span class="badge bg-primary me-2">
${suggestion.value}
</span>
</span>`
root.querySelectorAll(".selected-values .value-badge").forEach(badge => {
badge.addEventListener('click', function() {
badge.remove();
});
});
});
});
if (suggestions.length == 0) {
let suggestionDiv = document.createElement('div');
suggestionDiv.innerHTML =
`<div class="suggestion p-2 m-0">
<p class="p-0 m-0">No results</p>
</div>`;
input.parentElement.querySelector("div.suggestions").appendChild(suggestionDiv);
}
});
});
});

View File

@@ -0,0 +1,83 @@
/* Request to the autocomplete REST endpoit */
async function getSuggestions(className, query) {
const response = await fetch(`/dbadmin/api/autocomplete/${className}?query=${query}`);
const suggestions = await response.json();
return suggestions;
}
function hideSuggestions(inputElement) {
let suggestionsDiv = inputElement.parentElement.querySelector("div.suggestions");
suggestionsDiv.classList.remove('d-block');
suggestionsDiv.classList.add('d-none');
}
function showSuggestions(inputElement) {
let suggestionsDiv = inputElement.parentElement.querySelector("div.suggestions");
suggestionsDiv.classList.remove('d-none');
suggestionsDiv.classList.add('d-block');
}
document.addEventListener("DOMContentLoaded", () => {
/* Instead of using onBlur, which takes precedence over onClick
/* and causes the click event to disappear, we detect click
/* on outside elements and close all the autocomplete manually */
document.querySelector("body").addEventListener('click', function(e) {
if (!e.target.classList.contains("suggestion") && !e.target.classList.contains("autocomplete")) {
rootElements.forEach(root => {
hideSuggestions(root.querySelector("input.autocomplete"));
});
}
});
let rootElements = document.querySelectorAll(".autocomplete-input");
rootElements.forEach(root => {
let input = root.querySelector("input.autocomplete");
if (input == undefined) return;
input.addEventListener('focus', function() {
showSuggestions(input);
});
input.parentElement.querySelector("div.suggestions").innerHTML =
`<div class="suggestion p-2 m-0">Enter a valid ID or start typing for suggestions</div>`;
input.addEventListener('keyup', async function(e) {
let suggestions = await getSuggestions(e.target.dataset.classname, e.target.value);
input.parentElement.querySelector("div.suggestions").innerHTML = "";
if (e.target.value.length <= 0) {
input.parentElement.querySelector("div.suggestions").innerHTML =
`<div class="suggestion p-2 m-0">Enter a valid ID or start typing for suggestions</div>`;
return;
}
suggestions.forEach(suggestion => {
let suggestionDiv = document.createElement('div');
suggestionDiv.innerHTML =
`<div class="suggestion p-2 m-0">
<strong>${suggestion.id}</strong>
<p class="p-0 m-0">${suggestion.value}</p>
</div>`;
input.parentElement.querySelector("div.suggestions").appendChild(suggestionDiv);
suggestionDiv.addEventListener('click', function(e) {
input.value = suggestion.id;
hideSuggestions(input);
});
});
if (suggestions.length == 0) {
let suggestionDiv = document.createElement('div');
suggestionDiv.innerHTML =
`<div class="suggestion p-2 m-0">
<p class="p-0 m-0">No results</p>
</div>`;
input.parentElement.querySelector("div.suggestions").appendChild(suggestionDiv);
}
});
});
});

View File

@@ -0,0 +1,66 @@
function updateBulkActions(table, selected) {
let divs = document.querySelectorAll(".bulk-actions");
divs.forEach(div => {
div.innerHTML = `${selected} items selected <input type="submit" form="delete-form" class="ui-btn btn btn-secondary" value="Delete">`;
});
}
document.addEventListener("DOMContentLoaded", () => {
let selected = 0;
if (document.getElementById('delete-form') != null) {
document.getElementById('delete-form').addEventListener('submit', function(e) {
if (selected == 0) {
e.preventDefault();
alert('No items selected');
return;
}
if (!confirm('Are you sure you want to delete these items?')) {
e.preventDefault();
}
});
}
document.querySelectorAll("div.table-selectable").forEach(table => {
let tableInputs = table.querySelectorAll("table input[type=\"checkbox\"]");
tableInputs.forEach(input => {
if (input.checked && !input.classList.contains('check-all')) selected++;
input.addEventListener('change', function(e) {
if (e.target.classList.contains('check-all')) {
if (e.target.checked) {
selected = tableInputs.length - 1;
tableInputs.forEach(input => {
input.checked = true;
});
} else {
selected = 0;
tableInputs.forEach(input => {
input.checked = false;
});
}
} else {
if (e.target.checked) {
selected++;
} else {
selected--;
}
}
updateBulkActions(table, selected);
});
});
updateBulkActions(table, selected);
});
if (document.querySelector("div.table-selectable select.page-size") != null) {
document.querySelector("div.table-selectable select.page-size").addEventListener('change', function(e) {
this.parentElement.querySelector("input[name=\"pageSize\"]").value = e.target.value;
this.parentElement.submit();
});
}
});

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head th:replace="~{fragments/resources::head}">
</head>
<body>
<div class="bg-light main-wrapper">
<nav th:replace="~{fragments/resources :: navbar}"></nav>
<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-exclamation-octagon"></i>
<span class="align-middle">Error</span>
</h1>
<div class="row mt-4">
<div class="col">
<div class="box">
<h3 class="fw-bold"><p th:text="|${status} ${error}|"></p></h3>
<p th:text="${message}"></p>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head></head>
<body>
<tr th:fragment="data_row(row, selectable)" class="table-data-row">
<td th:if=${selectable} class="table-checkbox">
<input type="checkbox" class="form-check-input" name="ids"
th:value="${row.getPrimaryKeyValue()}" form="delete-form">
</td>
<td th:each="field : ${schema.getSortedFields()}">
<th:block th:if="${!row.has(field)}">
<span class="font-monospace null-label">NULL</span>
</th:block>
<th:block th:if="${row.has(field)}">
<th:block th:replace="~{fragments/data_row :: data_row_field(field=${field}, object=${row})}"></th:block>
</th:block>
</td>
<td th:each="colName : ${schema.getComputedColumnNames()}">
<span th:text="${row.compute(colName)}"></span>
</td>
<td class="text-center row-icons" th:if="${selectable}">
<a class="ps-1" th:href="|/dbadmin/model/${schema.getJavaClass().getName()}/edit/${row.getPrimaryKeyValue()}|">
<i class="bi bi-pencil-square"></i></a>
<form class="delete-form" method="POST"
th:action="|/dbadmin/model/${schema.getJavaClass().getName()}/delete/${row.getPrimaryKeyValue()}|">
<button><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
<!-- data-row-field fragment -->
<th:block th:fragment="data_row_field(field, object)">
<th:block th:if="${field.getConnectedType() != null}">
<a th:href="|/dbadmin/model/${field.getConnectedType().getName()}/show/${object.traverse(field).getPrimaryKeyValue()}|">
<span th:text="${object.has(field) ? object.traverse(field).getPrimaryKeyValue() : 'NULL'}"></span>
</a>
<p class="p-0 m-0"
th:text="${object.traverse(field).getDisplayName()}"></p>
</th:block>
<th:block th:if="${field.getConnectedType() == null}">
<th:block th:if="${field.isPrimaryKey()}">
<a th:href="|/dbadmin/model/${schema.getClassName()}/show/${object.get(field).getValue()}|">
<span th:text="${object.get(field).getFormattedValue()}">
</span>
</a>
</th:block>
<th:block th:if="${!field.isPrimaryKey()}">
<span th:text="${object.get(field).getFormattedValue()}" th:if="${!field.isBinary()}">
</span>
<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>
</a>
</th:block>
<th:block th:unless="${object.get(field).getValue()}">
<span class="font-monospace null-label">NULL</span>
</th:block>
</span>
</th:block>
</th:block>
</th:block>
<!-- end data-row-field fragment -->
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head></head>
<body>
<div class="autocomplete-input position-relative" th:fragment="input_autocomplete(field, value)">
<input class="autocomplete form-control" type="text" th:name="${field.getName()}"
th:data-classname="${field.getConnectedType().getName()}"
autocomplete="off"
th:value="${value}"
placeholder="NULL">
</input>
<div class="suggestions d-none">
</div>
</div>
<div class="autocomplete-multi-input position-relative" th:fragment="input_autocomplete_multi(field, values)">
<div class="position-relative">
<input class="autocomplete form-control" type="text"
autocomplete="off"
th:name="|${field.getName()}[]|"
th:data-fieldname="|${field.getName()}[]|"
th:data-classname="${field.getConnectedType().getName()}">
</input>
<div class="suggestions d-none">
</div>
</div>
<span class="badge bg-danger mb-0 mt-2 value-badge clear-all-badge"
th:classAppend="${values == null || values.isEmpty() ? 'd-none' : ''}">Clear all <i class="bi bi-trash"></i></span>
<div class="mt-0 mb-2 selected-values">
<th:block th:each="value : ${values}" th:if="${values}">
<span class="value-badge">
<input type="checkbox" checked="checked" class="badge-checkbox"
th:name="|${field.getName()}[]|" th:value="${value.getPrimaryKeyValue()}">
<span class="badge bg-primary me-2" th:text="${value.getDisplayName()}">
</span>
</span>
</th:block>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,186 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head th:fragment="head">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css" integrity="sha384-b6lVK+yci+bfDmaY1u0zE8YYJt0TZxLEAFyYSLHId4xoVvsrQu3INevFKo+Xir8e" crossorigin="anonymous">
<link rel="stylesheet" href="/css/dbadmin.css">
<link rel="stylesheet" href="/css/style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.3/js/bootstrap.min.js" integrity="sha512-1/RvZTcCDEUjY/CypiMz+iqqtaoQfAITmNSJY17Myp4Ms5mdxPS5UV7iOfdZoxcGhzFbOm6sntTKJppjvuhg4g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<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>
<title th:text="${title != null ? title + ' | Spring Boot DB Admin Panel' : 'Spring Boot DB Admin Panel'}"></title>
</head>
<th:block th:fragment="alerts">
<div th:if="${message}" class="alert alert-success alert-dismissible fade show" role="alert">
<p th:text="${message}" class="mb-0"></p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div th:if="${error}" class="alert alert-danger">
<h6 class="fw-bold" th:if="${errorTitle}" th:text="${errorTitle}"></h6>
<p class="mb-0" th:text="${error}"></p>
</div>
</th:block>
<nav class="navbar fixed-top navbar-expand-lg bg-accent color-white" th:fragment="navbar">
<div class="container-fluid">
<a class=" fw-bold navbar-brand" href="/"><i class="bi bi-hexagon-fill"></i> Spring Boot DB Admin Panel</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="sidebar bg-lightest position-relative" th:fragment="sidebar(page)">
<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 href="/dbadmin">
<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 href="/dbadmin">
<div class="d-flex align-items-center">
<div class="menu-icon">
<i class="bi bi-database"></i>
</div>
<div class="menu-entry-text d-none d-md-block">
Entities
</div>
</div>
</a>
</li>
<li th:class="${#strings.equals(activePage, 'console') ? 'active' : ''}">
<a href="/live">
<div class="d-flex align-items-center">
<div class="menu-icon">
<i class="bi bi-terminal"></i>
</div>
<div class="menu-entry-text d-none d-md-block">
SQL Console
</div>
</div>
</a>
</li>
<li th:class="${#strings.equals(activePage, 'ai') ? 'active' : ''}">
<a href="/search">
<div class="d-flex align-items-center">
<div class="menu-icon">
<i class="bi bi-share"></i>
</div>
<div class="menu-entry-text d-none d-md-block">
AI console
</div>
</div>
</a>
</li>
<li th:class="${#strings.equals(activePage, 'settings') ? 'active' : ''}">
<a href="/dbadmin/settings">
<div class="d-flex align-items-center">
<div class="menu-icon">
<i class="bi bi-gear"></i>
</div>
<div class="menu-entry-text d-none d-md-block">
Settings
</div>
</div>
</a>
</li>
</ul>
<!-- <div class="separator"></div> -->
</div>
<div class="sidebar-bottom">
<div class="separator"></div>
<ul class="sidebar-menu mb-0 pb-0">
<li th:class="${#strings.equals(page, 'about') ? 'active' : ''}">
<a href="/about">
<div class="d-flex align-items-center">
<div class="menu-icon">
<i class="bi bi-question-circle"></i>
</div>
<div class="menu-entry-text d-none d-md-block">
About
</div>
</div>
</a>
</li>
</ul>
</div>
</div>
</div>
<nav aria-label="Results pagination" th:fragment="pagination(page)">
<div class="d-flex justify-content-between">
<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">
<span aria-hidden="true">&laquo;</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>
</li>
<li class="page-item active">
<a class="page-link" href="#" th:text="${page.getPagination().getCurrentPage()}"></a>
</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>
</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">
<span class="sr-only">Next</span>
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
<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>
<div class="d-flex align-items-center" th:if="${page.getPagination().getMaxPage() > 1}">
<p class="m-0 p-0">
<i>Showing [[ ${page.getActualResults()} ]] of [[ ${page.getPagination().getMaxElement()} ]] results</i>
</p>
</div>
</div>
<div class="d-flex align-items-center" th:if="${page.getPagination().getMaxPage() == 1}">
<p class="m-0 p-0">
<i>Showing [[ ${page.getActualResults()} ]] of [[ ${page.getPagination().getMaxElement()} ]] results</i>
</p>
</div>
<div class="bulk-actions">
</div>
</div>
</nav>
</html>

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head></head>
<body>
<div th:fragment="table(results, schema)">
<div th:if="${results == null || results.isEmpty()}">
<p class="alert alert-warning">This table contains no data.</p>
</div>
<div th:if="${results != null && results.size() > 0}">
<table class="table table-striped align-middle mt-3">
<tr class="table-data-row">
<th th:each="field : ${schema.getSortedFields()}">
<div class="m-0 p-0 d-flex justify-content-between">
<div class="column-title">
<span th:if="${field.isPrimaryKey()}">
<i title="Primary Key" class="bi bi-key"></i>
</span>
<span th:if="${field.isForeignKey()}">
<i title="Foreign Key" class="bi bi-link"></i>
</span>
<span class="m-0 p-0" th:text="${field.getName()}"></span>
</div>
</div>
<p class="m-0 p-0 dbfieldtype"><small th:text="${field.getType()}"></small></p>
</th>
<th class="table-data-row" th:each="colName : ${schema.getComputedColumnNames()}">
<div class="m-0 p-0 d-flex justify-content-between">
<div class="column-title">
<i title="Primary Key" class="bi bi-cpu"></i>
<span class="m-0 p-0" th:text="${colName}"></span>
</div>
</div>
<p class="m-0 p-0 dbfieldtype"><small>COMPUTED</small></p>
</th>
</tr>
<th:block th:each="r : ${results}">
<tr th:replace="~{fragments/data_row :: data_row(row=${r}, selectable=${false})}">
</tr>
</th:block>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head></head>
<body>
<div class="table-selectable" th:fragment="table(results, schema)">
<div th:if="${results.isEmpty()}">
<p>This table contains no data.</p>
</div>
<div th:if="${results.size() > 0}">
<form id="delete-form" th:action="|/dbadmin/model/${schema.getClassName()}/delete|" method="POST">
</form>
<nav th:replace="~{fragments/resources :: pagination(${page})}">
</nav>
<table class="table table-striped align-middle mt-3">
<tr class="table-data-row">
<th class="table-checkbox"><input type="checkbox" class="form-check-input check-all"></th>
<th class="table-data-row" th:each="field : ${schema.getSortedFields()}">
<div class="m-0 p-0 d-flex justify-content-between">
<div class="column-title">
<span th:if="${field.isPrimaryKey()}">
<i title="Primary Key" class="bi bi-key"></i>
</span>
<span th:if="${field.isForeignKey()}">
<i title="Foreign Key" class="bi bi-link"></i>
</span>
<span class="m-0 p-0" th:text="${field.getName()}"></span>
</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)}">
<i title="Sort" class="bi bi-caret-up"></i>
</a>
</th:block>
<th:block th:unless="${sortKey != field.getName()}">
<a th:if="${sortOrder == 'DESC'}"
th:href="@{|/dbadmin/model/${schema.getClassName()}|(page=${page.getPagination().getCurrentPage()},
pageSize=${page.getPagination().getPageSize()},sortKey=${field.getName()},sortOrder=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)}">
<i title="Sort" class="bi bi-caret-up-fill"></i>
</a>
</th:block>
</h4>
</div>
</div>
<p class="m-0 p-0 dbfieldtype"><small th:text="${field.getType()}"></small></p>
</th>
<th class="table-data-row" th:each="colName : ${schema.getComputedColumnNames()}">
<div class="m-0 p-0 d-flex justify-content-between">
<div class="column-title">
<i title="Primary Key" class="bi bi-cpu"></i>
<span class="m-0 p-0" th:text="${colName}"></span>
</div>
</div>
<p class="m-0 p-0 dbfieldtype"><small>COMPUTED</small></p>
</th>
<th></th>
</tr>
<th:block th:each="r : ${results}">
<tr th:replace="~{fragments/data_row :: data_row(row=${r},selectable=${true})}"></tr>
</th:block>
</table>
<nav th:replace="~{fragments/resources :: pagination(${page})}">
</nav>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,165 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head th:replace="~{fragments/resources::head}">
</head>
<body>
<div class="bg-light main-wrapper">
<nav th:replace="~{fragments/resources :: navbar}"></nav>
<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 action="/dbadmin" 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"
class="ui-text-input form-control" name="query" autofocus>
<button class="ui-btn btn btn-primary">Search</button>
</div>
</form>
<div class="row mt-4">
<div class="col">
<div class="box">
<table class="table table-striped mt-4" th:if="${!schemas.isEmpty()}">
<tr>
<th>Table</th>
<th>Rows</th>
<th>Java class</th>
<th></th>
</tr>
<tr th:each="schema : ${schemas}">
<td>
<a th:text="${schema.getTableName()}"
th:href="|/dbadmin/model/${schema.getClassName()}|"></a>
</td>
<td>
<span th:text="${counts.get(schema.getClassName())}"></span>
</td>
<td>
<span th:text="${schema.getClassName()}"></span>
</td>
<td class="text-end row-icons">
<a title="List all" th:href="|/dbadmin/model/${schema.getClassName()}|"><i class="bi bi-list"></i></i></a>
<a title="Create new" th:href="|/dbadmin/model/${schema.getClassName()}/create|"><i class="bi bi-plus-square"></i></a>
</td>
</tr>
</table>
<div class="p-0 m-0" th:if="${schemas.isEmpty()}">
No entities have been loaded from Java classes.
<ul class="mt-3">
<li>Make sure you are initializing Spring Boot DB Admin Panel correctly and double check
that the package you have set in the CommandLineRunner is the correct one.</li>
<li>Check that the Java classes in the package have been correctly marked with
the <code>@Entity</code> annotation.</li>
</ul>
</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>
</body>
</html>
</body>
</html>

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head th:replace="~{fragments/resources::head}">
</head>
<body>
<div class="bg-light main-wrapper">
<nav th:replace="~{fragments/resources :: navbar}"></nav>
<div class="d-flex">
<div th:replace="~{fragments/resources :: sidebar('entities')}"></div>
<div class="main-content bg-lighter">
<th:block th:replace="~{fragments/resources :: alerts}"></th:block>
<h1 class="fw-bold mb-4">
<i class="align-middle bi bi-database"></i>
<span class="align-middle">Entities</span>
<i class="align-middle bi bi-chevron-double-right"></i>
<span class="align-middle"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span>
<i class="align-middle bi bi-chevron-double-right"></i>
<span class="align-middle" th:text="${create ? 'Create' : 'Edit'}"></span>
<th:block th:if="${!create}">
<i class="align-middle bi bi-chevron-double-right"></i>
<span class="align-middle" th:text="${object.getDisplayName()}"></span>
</th:block>
</h1>
<div class="row mt-4">
<div class="col">
<div class="box">
<h3 class="fw-bold mb-4" th:text="${create ? schema.getJavaClass().getSimpleName() : object.getDisplayName()}"></h3>
<form class="form" enctype="multipart/form-data" method="post" th:action="|/dbadmin/model/${className}/create|">
<input type="hidden" name="__dbadmin_create" th:value="${create}">
<div th:each="field : ${schema.getSortedFields()}" class="mt-2">
<label th:for="|__id_${field.getName()}|" class="mb-1">[[ ${field.getName()} ]]</label>
<th:block th:if="${field.isForeignKey()}">
<div th:replace="~{fragments/forms :: input_autocomplete(field=${field}, value=${
create ? (params != null ? params.getOrDefault(field.getName(), '') : '')
: (object != null ? object.traverse(field).getPrimaryKeyValue() : '' )
})}">
</div>
<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()}"
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('')">
<input type="hidden" th:value="${field.getType()}" th:name="|__dbadmin_${field.getName()}_type|">
</th:block>
</div>
<div th:each="field : ${schema.getManyToManyOwnedFields()}" class="mt-3">
<h2><span th:title="|${field.getType()} relationship|"><i class="bi bi-share"></i> [[ ${field.getJavaName()} ]]</span></h2>
<div th:replace="~{fragments/forms :: input_autocomplete_multi(field=${field},
values=${object != null ? object.traverseMany(field) : null } )}">
</div>
</div>
<div class="d-flex mt-4 justify-content-between">
<a th:href="|/dbadmin/model/${schema.getClassName()}|" class="ui-btn btn btn-secondary">Cancel</a>
<input type="submit" class="ui-btn btn btn-primary" th:value="${object != null ? 'Save' : 'Create'}">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head th:replace="~{fragments/resources::head}">
</head>
<body>
<div class="bg-light main-wrapper">
<nav th:replace="~{fragments/resources :: navbar}"></nav>
<div class="d-flex">
<div th:replace="~{fragments/resources :: sidebar('entities')}"></div>
<div class="main-content bg-lighter">
<th:block th:replace="~{fragments/resources :: alerts}"></th:block>
<h1 class="fw-bold mb-4"><i class="align-middle bi bi-database"></i>
<span class="align-middle"><a href="/dbadmin">Entities</a></span>
<i class="align-middle bi bi-chevron-double-right"></i>
<span class="align-middle"> [[ ${schema.getJavaClass().getSimpleName()} ]] </span>
</h1>
<div class="row mt-4">
<div class="col">
<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">
<i class="bi bi-database pe-2"></i> DATA
</div>
</a>
<a th:href="|/dbadmin/model/${className}/schema|">
<div class="ui-tab ps-5 pe-5 p-3">
<i class="bi bi-table pe-2"></i> SCHEMA
</div>
</a>
<div class="inner-navigation-border flex-grow-1">
</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>
<div class="d-flex justify-content-between">
<h3 class="fw-bold mb-4 align-baseline w-100">
<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>
</div>
<div th:replace="~{fragments/table_selectable :: table(results=${page.getResults()}, schema=${schema})}"
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head th:replace="~{fragments/resources::head}">
</head>
<body>
<div class="bg-light main-wrapper">
<nav th:replace="~{fragments/resources :: navbar}"></nav>
<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="bi bi-database"></i>
<a class="align-middle" href="/dbadmin">Entities</a>
<i class="align-middle bi bi-chevron-double-right"></i>
<a class="align-middle" th:href="|/dbadmin/model/${schema.getJavaClass().getName()}|"> [[ ${schema.getJavaClass().getSimpleName()} ]]</a>
<i class="align-middle bi bi-chevron-double-right"></i><span class="align-middle"> Schema</span>
</h1>
<div class="row mt-4">
<div class="col">
<div class="w-100 d-flex inner-navigation">
<a th:href="|/dbadmin/model/${schema.getJavaClass().getName()}|">
<div class="ui-tab ps-5 pe-5 p-3">
<i class="bi bi-database pe-2"></i> DATA
</div>
</a>
<a th:href="|/dbadmin/model/${schema.getJavaClass().getName()}/schema|" class="active">
<div class="ui-tab ps-5 pe-5 p-3">
<i class="bi bi-table pe-2"></i> SCHEMA
</div>
</a>
<div class="inner-navigation-border flex-grow-1">
</div>
</div>
<div class="box with-navigation">
<div class="d-flex justify-content-between">
<h3 class="fw-bold mb-4 align-baseline">
<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>
</div>
<table class="table table-striped align-middle">
<tr class="table-data-row">
<th style="width: 32px"></th>
<th>Column</th>
<th>Type</th>
<th>Nullable</th>
</tr>
<tr th:each="field : ${schema.getSortedFields()}" class="table-data-row">
<td>
<span th:if="${field.isPrimaryKey()}">
<i title="Primary Key" class="bi bi-key"></i>
</span>
<span th:if="${field.isForeignKey()}">
<i title="Foreign Key" class="bi bi-link"></i>
</span>
</td>
<td>
<span class="m-0 p-0" th:text="${field.getName()}"></span>
</td>
<td th:text="${field.getType()}">
</td>
<td th:text="${field.isNullable()}"></td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head th:replace="~{fragments/resources::head}">
</head>
<body>
<div class="bg-light main-wrapper">
<nav th:replace="~{fragments/resources :: navbar}"></nav>
<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-database"></i>
<a class="align-middle" href="/dbadmin">Entities</a>
<i class="align-middle bi bi-chevron-double-right"></i>
<a class="align-middle "th:href="|/dbadmin/model/${schema.getJavaClass().getName()}|">
[[ ${schema.getJavaClass().getSimpleName()} ]]</a>
<i class="align-middle bi bi-chevron-double-right"></i>
<span class="align-middle"> [[ ${object.getDisplayName()} ]]</span>
</h1>
<div class="row mt-4">
<div class="col">
<div class="box">
<div class="d-flex justify-content-between">
<h3 class="mb-3 fw-bold" th:text="${object.getDisplayName()}"></h3>
<h3><a th:href="|/dbadmin/model/${schema.getClassName()}/edit/${object.getPrimaryKeyValue()}|">
<i class="bi bi-pencil"></i></a>
</h3>
</div>
<table class="table table-striped align-middle show-table">
<tr class="table-data-row">
<th style="width: 32px"></th>
<th>Column</th>
<th>Value</th>
<th>Type</th>
</tr>
<tr th:each="field : ${schema.getSortedFields()}" class="table-data-row">
<td>
<span th:if="${field.isPrimaryKey()}">
<i title="Primary Key" class="bi bi-key"></i>
</span>
<span th:if="${field.isForeignKey()}">
<i title="Foreign Key" class="bi bi-link"></i>
</span>
</td>
<td>
<span class="m-0 p-0" th:text="${field.getName()}"></span>
</td>
<td>
<th:block th:if="${!object.has(field)}">
<span class="font-monospace null-label">NULL</span>
</th:block>
<th:block th:if="${object.has(field)}">
<th:block th:replace="~{fragments/data_row :: data_row_field(field=${field}, object=${object})}"></th:block>
</th:block>
</td>
<td class="dbfieldtype" th:text="${field.getType()}">
</td>
</tr>
<tr class="table-data-row" th:each="colName : ${schema.getComputedColumnNames()}">
<td>
<i class="bi bi-cpu"></i>
</td>
<td th:text="${colName}">
</td>
<td th:text="${object.compute(colName)}">
</td>
<td>
<span class="dbfieldtype">COMPUTED</span>
</td>
</tr>
</table>
<div th:each="field : ${schema.getRelationshipFields()}">
<h2>
<span th:title="|${field.getType()} relationship|">
<i class="align-middle bi bi-share"></i>
<span class="align-middle">[[ ${field.getJavaName()} ]]</span>
</span>
</h2>
<div th:replace="~{fragments/table :: table(schema=${field.getConnectedSchema()},
results=${object.getValues(field)})}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head th:replace="~{fragments/resources::head}">
</head>
<body>
<div class="bg-light main-wrapper">
<nav th:replace="~{fragments/resources :: navbar}"></nav>
<div class="d-flex">
<div th:replace="~{fragments/resources :: sidebar('entities')}"></div>
<div class="main-content bg-lighter">
<th:block th:replace="~{fragments/resources :: alerts}"></th:block>
<h1 class="fw-bold mb-4"><i class="align-middle bi bi-gear"></i>
<span class="align-middle"><a href="/dbadmin">Settings</a></span>
</h1>
<div class="row mt-4">
<div class="col">
<div class="w-100 d-flex inner-navigation">
<a href="#" class="active">
<div class="ui-tab ps-5 pe-5 p-3">
<i class="bi bi-database pe-2"></i> APPEARANCE
</div>
</a>
<a href="#">
<div class="ui-tab ps-5 pe-5 p-3">
<i class="bi bi-table pe-2"></i> DATA
</div>
</a>
<div class="inner-navigation-border flex-grow-1">
</div>
</div>
<div class="box with-navigation">
SETTINGS
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>