This commit is contained in:
Francesco
2023-09-25 10:10:30 +02:00
parent 95e8dea90f
commit cdbad51641
50 changed files with 1171 additions and 218 deletions

View File

@@ -1,14 +0,0 @@
package tech.ailef.dbadmin;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
@ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true)
@ComponentScan
@EnableConfigurationProperties(DbAdminProperties.class)
@AutoConfiguration
public class DbAdminAutoConfiguration {
}

View File

@@ -1,37 +0,0 @@
//package tech.ailef.dbadmin.dto;
//
//import java.util.Set;
//
//public class ListModelRequest {
// private String className;
//
// private String query;
//
// private Integer page;
//
// private Integer pageSize;
//
// private String sortKey;
//
// private String sortOrder;
//
// private Set<QueryFilter> queryFilters;
//
// private PaginationInfo paginationInfo;
//
// public ListModelRequest(String className, String query, Integer page, Integer pageSize, String sortKey,
// String sortOrder, Set<QueryFilter> queryFilters, PaginationInfo paginationInfo) {
// super();
// this.className = className;
// this.query = query;
// this.page = page;
// this.pageSize = pageSize;
// this.sortKey = sortKey;
// this.sortOrder = sortOrder;
// this.queryFilters = queryFilters;
// this.paginationInfo = paginationInfo;
// }
//
//
//// @RequestParam MultiValueMap<String, String> otherParams,
//}

View File

@@ -1,30 +0,0 @@
package tech.ailef.dbadmin.dto;
import java.util.List;
import tech.ailef.dbadmin.dbmapping.DbObject;
public class PaginatedResult {
private PaginationInfo pagination;
private List<DbObject> results;
public PaginatedResult(PaginationInfo pagination, List<DbObject> page) {
this.pagination = pagination;
this.results = page;
}
public PaginationInfo getPagination() {
return pagination;
}
public List<DbObject> getResults() {
return results;
}
public int getActualResults() {
return getResults().size();
}
}

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin;
package tech.ailef.dbadmin.external;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -10,9 +10,7 @@ import java.util.Set;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Component;
@@ -26,14 +24,13 @@ import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.PersistenceContext;
import tech.ailef.dbadmin.annotations.DisplayFormat;
import tech.ailef.dbadmin.dbmapping.AdvancedJpaRepository;
import tech.ailef.dbadmin.dbmapping.DbField;
import tech.ailef.dbadmin.dbmapping.DbFieldType;
import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.misc.Utils;
import tech.ailef.dbadmin.external.annotations.DisplayFormat;
import tech.ailef.dbadmin.external.dbmapping.AdvancedJpaRepository;
import tech.ailef.dbadmin.external.dbmapping.DbField;
import tech.ailef.dbadmin.external.dbmapping.DbFieldType;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.misc.Utils;
/**
* The main DbAdmin class responsible for the initialization phase. This class scans
@@ -48,7 +45,7 @@ import tech.ailef.dbadmin.misc.Utils;
public class DbAdmin {
private static final Logger logger = Logger.getLogger(DbAdmin.class.getName());
@PersistenceContext
// @PersistenceContext
private EntityManager entityManager;
private List<DbObjectSchema> schemas = new ArrayList<>();
@@ -91,6 +88,18 @@ public class DbAdmin {
});
}
/**
* Finds a schema by its table name
* @param tableName the table name on the database
* @return
* @throws DbAdminException if corresponding schema not found
*/
public DbObjectSchema findSchemaByTableName(String tableName) {
return schemas.stream().filter(s -> s.getTableName().equals(tableName)).findFirst().orElseThrow(() -> {
return new DbAdminException("Schema " + tableName + " not found.");
});
}
/**
* Finds a schema by its class
* @param klass

View File

@@ -0,0 +1,76 @@
package tech.ailef.dbadmin.external;
import java.util.Properties;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import tech.ailef.dbadmin.internal.InternalDbAdminConfiguration;
@ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true)
@ComponentScan
@EnableConfigurationProperties(DbAdminProperties.class)
@Configuration
@EnableJpaRepositories(
entityManagerFactoryRef = "internalEntityManagerFactory",
transactionManagerRef = "internalTransactionManager",
basePackages = { "tech.ailef.dbadmin.internal.repository" }
)
@EnableTransactionManagement
@Import(InternalDbAdminConfiguration.class)
public class DbAdminAutoConfiguration {
@Autowired
private DbAdminProperties props;
@Bean
public DataSource internalDataSource() {
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("org.h2.Driver");
if (props.isTestMode()) {
dataSourceBuilder.url("jdbc:h2:mem:test");
} else {
dataSourceBuilder.url("jdbc:h2:file:./dbadmin_internal");
}
dataSourceBuilder.username("sa");
dataSourceBuilder.password("password");
return dataSourceBuilder.build();
}
@Bean
public LocalContainerEntityManagerFactoryBean internalEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(internalDataSource());
factoryBean.setPersistenceUnitName("internal");
factoryBean.setPackagesToScan("tech.ailef.dbadmin.internal.model"); // , "tech.ailef.dbadmin.repository");
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Properties properties = new Properties();
properties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
properties.setProperty("hibernate.hbm2ddl.auto", "update");
factoryBean.setJpaProperties(properties);
factoryBean.afterPropertiesSet();
return factoryBean;
}
@Bean
public PlatformTransactionManager internalTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(internalEntityManagerFactory().getObject());
return transactionManager;
}
}

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin;
package tech.ailef.dbadmin.external;
import java.util.HashMap;
import java.util.Map;
@@ -26,6 +26,8 @@ public class DbAdminProperties {
*/
private String modelsPackage;
private boolean testMode = false;
public boolean isEnabled() {
return enabled;
}
@@ -50,11 +52,20 @@ public class DbAdminProperties {
this.modelsPackage = modelsPackage;
}
public boolean isTestMode() {
return testMode;
}
public void setTestMode(boolean testMode) {
this.testMode = testMode;
}
public Map<String, String> toMap() {
Map<String, String> conf = new HashMap<>();
conf.put("enabled", enabled + "");
conf.put("baseUrl", baseUrl);
conf.put("modelsPackage", modelsPackage);
conf.put("testMode", testMode + "");
return conf;
}

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.annotations;
package tech.ailef.dbadmin.external.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.annotations;
package tech.ailef.dbadmin.external.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.annotations;
package tech.ailef.dbadmin.external.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.annotations;
package tech.ailef.dbadmin.external.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.annotations;
package tech.ailef.dbadmin.external.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.controller;
package tech.ailef.dbadmin.external.controller;
import java.util.ArrayList;
import java.util.HashMap;
@@ -19,6 +19,7 @@ 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;
@@ -27,16 +28,21 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import tech.ailef.dbadmin.DbAdmin;
import tech.ailef.dbadmin.DbAdminProperties;
import tech.ailef.dbadmin.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.dbmapping.DbObject;
import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.dto.CompareOperator;
import tech.ailef.dbadmin.dto.PaginatedResult;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.InvalidPageException;
import tech.ailef.dbadmin.misc.Utils;
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.DbObject;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.external.dto.CompareOperator;
import tech.ailef.dbadmin.external.dto.LogsSearchRequest;
import tech.ailef.dbadmin.external.dto.PaginatedResult;
import tech.ailef.dbadmin.external.dto.QueryFilter;
import tech.ailef.dbadmin.external.exceptions.InvalidPageException;
import tech.ailef.dbadmin.external.misc.Utils;
import tech.ailef.dbadmin.internal.model.UserAction;
import tech.ailef.dbadmin.internal.model.UserSetting;
import tech.ailef.dbadmin.internal.repository.UserSettingsRepository;
import tech.ailef.dbadmin.internal.service.UserActionService;
/**
* The main DbAdmin controller that register most of the routes of the web interface.
@@ -52,7 +58,14 @@ public class DefaultDbAdminController {
@Autowired
private DbAdmin dbAdmin;
@Autowired
private UserActionService userActionService;
@Autowired
private UserSettingsRepository userSettingsRepo;
/**
* Home page with list of schemas
* @param model
@@ -61,6 +74,7 @@ public class DefaultDbAdminController {
*/
@GetMapping
public String index(Model model, @RequestParam(required = false) String query) {
List<DbObjectSchema> schemas = dbAdmin.getSchemas();
if (query != null && !query.isBlank()) {
schemas = schemas.stream().filter(s -> {
@@ -149,7 +163,7 @@ public class DefaultDbAdminController {
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
try {
PaginatedResult result = null;
PaginatedResult<DbObject> result = null;
if (query != null || !otherParams.isEmpty()) {
result = repository.search(schema, query, page, pageSize, sortKey, sortOrder, queryFilters);
} else {
@@ -264,6 +278,8 @@ public class DefaultDbAdminController {
attr.addFlashAttribute("error", e.getMessage());
}
saveAction(new UserAction(schema.getTableName(), id, "DELETE"));
return "redirect:/" + properties.getBaseUrl() + "/model/" + className;
}
@@ -291,6 +307,10 @@ public class DefaultDbAdminController {
if (countDeleted > 0)
attr.addFlashAttribute("message", "Deleted " + countDeleted + " of " + ids.length + " items");
for (String id : ids) {
saveAction(new UserAction(schema.getTableName(), id, "DELETE"));
}
return "redirect:/" + properties.getBaseUrl() + "/model/" + className;
}
@@ -353,6 +373,7 @@ public class DefaultDbAdminController {
repository.attachManyToMany(schema, newPrimaryKey, multiValuedParams);
pkValue = newPrimaryKey.toString();
attr.addFlashAttribute("message", "Item created successfully.");
saveAction(new UserAction(schema.getTableName(), pkValue, "CREATE"));
} catch (DataIntegrityViolationException e) {
attr.addFlashAttribute("errorTitle", "Unable to INSERT row");
attr.addFlashAttribute("error", e.getMessage());
@@ -376,6 +397,7 @@ public class DefaultDbAdminController {
repository.update(schema, params, files);
repository.attachManyToMany(schema, pkValue, multiValuedParams);
attr.addFlashAttribute("message", "Item saved successfully.");
saveAction(new UserAction(schema.getTableName(), pkValue, "EDIT"));
} catch (DataIntegrityViolationException e) {
attr.addFlashAttribute("errorTitle", "Unable to UPDATE row (no changes applied)");
attr.addFlashAttribute("error", e.getMessage());
@@ -391,6 +413,7 @@ public class DefaultDbAdminController {
Object newPrimaryKey = repository.create(schema, params, files, pkValue);
repository.attachManyToMany(schema, newPrimaryKey, multiValuedParams);
attr.addFlashAttribute("message", "Item created successfully");
saveAction(new UserAction(schema.getTableName(), pkValue, "CREATE"));
} catch (DataIntegrityViolationException e) {
attr.addFlashAttribute("errorTitle", "Unable to INSERT row (no changes applied)");
attr.addFlashAttribute("error", e.getMessage());
@@ -409,6 +432,23 @@ public class DefaultDbAdminController {
}
}
@GetMapping("/logs")
public String logs(Model model, LogsSearchRequest searchRequest) {
model.addAttribute("activePage", "logs");
model.addAttribute(
"page",
userActionService.findActions(
searchRequest.getTable(),
searchRequest.getActionType(),
searchRequest.getItemId(),
searchRequest.toPageRequest()
)
);
model.addAttribute("schemas", dbAdmin.getSchemas());
model.addAttribute("searchRequest", searchRequest);
return "logs";
}
@GetMapping("/settings")
public String settings(Model model) {
@@ -416,5 +456,16 @@ public class DefaultDbAdminController {
return "settings";
}
@PostMapping("/settings")
public String settings(@RequestParam Map<String, String> params, Model model) {
for (String paramName : params.keySet()) {
userSettingsRepo.save(new UserSetting(paramName, params.get(paramName)));
}
model.addAttribute("activePage", "settings");
return "settings";
}
private UserAction saveAction(UserAction action) {
return userActionService.save(action);
}
}

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.controller;
package tech.ailef.dbadmin.external.controller;
import java.util.Optional;
@@ -17,12 +17,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.server.ResponseStatusException;
import tech.ailef.dbadmin.DbAdmin;
import tech.ailef.dbadmin.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.dbmapping.DbFieldValue;
import tech.ailef.dbadmin.dbmapping.DbObject;
import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.DbAdmin;
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.external.dbmapping.DbFieldValue;
import tech.ailef.dbadmin.external.dbmapping.DbObject;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
/**
* Controller to serve file or images (`@DisplayImage`)

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.controller;
package tech.ailef.dbadmin.external.controller;
import java.util.Map;
@@ -7,7 +7,8 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import jakarta.servlet.http.HttpServletRequest;
import tech.ailef.dbadmin.DbAdminProperties;
import tech.ailef.dbadmin.external.DbAdminProperties;
import tech.ailef.dbadmin.internal.UserConfiguration;
/**
* This class registers some ModelAttribute objects that are
@@ -18,6 +19,9 @@ public class GlobalController {
@Autowired
private DbAdminProperties props;
@Autowired
private UserConfiguration userConf;
/**
* A multi valued map containing the query parameters. It is used primarily
@@ -36,7 +40,18 @@ public class GlobalController {
* @return
*/
@ModelAttribute("baseUrl")
public String getBaseUrl(HttpServletRequest request) {
public String getBaseUrl() {
return props.getBaseUrl();
}
}
@ModelAttribute("requestUrl")
public String getRequestUrl(HttpServletRequest request) {
return request.getRequestURI();
}
@ModelAttribute("userConf")
public UserConfiguration getUserConf() {
return userConf;
}
}

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.controller.rest;
package tech.ailef.dbadmin.external.controller.rest;
import java.util.List;
import java.util.stream.Collectors;
@@ -11,10 +11,10 @@ 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.DbAdmin;
import tech.ailef.dbadmin.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.dto.AutocompleteSearchResult;
import tech.ailef.dbadmin.external.DbAdmin;
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.external.dto.AutocompleteSearchResult;
/**
* API controller for autocomplete results

View File

@@ -1,8 +1,6 @@
package tech.ailef.dbadmin.controller.rest;
package tech.ailef.dbadmin.external.controller.rest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
@@ -14,12 +12,12 @@ 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.DbAdmin;
import tech.ailef.dbadmin.DbAdminProperties;
import tech.ailef.dbadmin.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.dto.PaginatedResult;
import tech.ailef.dbadmin.exceptions.DbAdminException;
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/"})

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dbmapping;
package tech.ailef.dbadmin.external.dbmapping;
import java.io.IOException;
import java.time.LocalDate;
@@ -21,9 +21,9 @@ import jakarta.persistence.criteria.CriteriaUpdate;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import tech.ailef.dbadmin.dto.CompareOperator;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.dto.CompareOperator;
import tech.ailef.dbadmin.external.dto.QueryFilter;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
@SuppressWarnings("rawtypes")
public class AdvancedJpaRepository extends SimpleJpaRepository {

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dbmapping;
package tech.ailef.dbadmin.external.dbmapping;
import java.io.IOException;
import java.util.ArrayList;
@@ -18,14 +18,14 @@ import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import jakarta.transaction.Transactional;
import tech.ailef.dbadmin.dto.PaginatedResult;
import tech.ailef.dbadmin.dto.PaginationInfo;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.exceptions.InvalidPageException;
import tech.ailef.dbadmin.external.dto.PaginatedResult;
import tech.ailef.dbadmin.external.dto.PaginationInfo;
import tech.ailef.dbadmin.external.dto.QueryFilter;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.exceptions.InvalidPageException;
/**
* Implements the basic CRUD operations (and some more)
@@ -117,7 +117,7 @@ public class DbAdminRepository {
}
return new PaginatedResult(
return new PaginatedResult<DbObject>(
new PaginationInfo(page, maxPage, pageSize, maxElement, null, new HashSet<>()),
results
);
@@ -128,18 +128,18 @@ public class DbAdminRepository {
* @param schema
* @param params
*/
@Transactional
@Transactional("transactionManager")
public void update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {
schema.getJpaRepository().update(schema, params, files);
}
@SuppressWarnings("unchecked")
@Transactional
@Transactional("transactionManager")
private void save(DbObjectSchema schema, DbObject o) {
schema.getJpaRepository().save(o.getUnderlyingInstance());
}
@Transactional
@Transactional("transactionManager")
public void attachManyToMany(DbObjectSchema schema, Object id, Map<String, List<String>> params) {
Optional<DbObject> optional = findById(schema, id);
@@ -215,7 +215,7 @@ public class DbAdminRepository {
* @param query
* @return
*/
public PaginatedResult search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey,
public PaginatedResult<DbObject> search(DbObjectSchema schema, String query, int page, int pageSize, String sortKey,
String sortOrder, Set<QueryFilter> queryFilters) {
AdvancedJpaRepository jpaRepository = schema.getJpaRepository();
@@ -227,7 +227,7 @@ public class DbAdminRepository {
throw new InvalidPageException();
}
return new PaginatedResult(
return new PaginatedResult<DbObject>(
new PaginationInfo(page, maxPage, pageSize, maxElement, query, queryFilters),
jpaRepository.search(query, page, pageSize, sortKey, sortOrder, queryFilters).stream()
.map(o -> new DbObject(o, schema))
@@ -256,7 +256,7 @@ public class DbAdminRepository {
* @return
*/
@SuppressWarnings("unchecked")
@Transactional
@Transactional("transactionManager")
public void delete(DbObjectSchema schema, String id) {
schema.getJpaRepository().deleteById(id);
}

View File

@@ -1,10 +1,10 @@
package tech.ailef.dbadmin.dbmapping;
package tech.ailef.dbadmin.external.dbmapping;
import java.lang.reflect.Field;
import com.fasterxml.jackson.annotation.JsonIgnore;
import tech.ailef.dbadmin.annotations.DisplayImage;
import tech.ailef.dbadmin.external.annotations.DisplayImage;
public class DbField {
protected String dbName;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dbmapping;
package tech.ailef.dbadmin.external.dbmapping;
import java.io.IOException;
import java.math.BigDecimal;
@@ -11,8 +11,8 @@ import org.springframework.web.multipart.MultipartFile;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import tech.ailef.dbadmin.dto.CompareOperator;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.dto.CompareOperator;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
public enum DbFieldType {
INTEGER {

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dbmapping;
package tech.ailef.dbadmin.external.dbmapping;
import com.fasterxml.jackson.annotation.JsonIgnore;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dbmapping;
package tech.ailef.dbadmin.external.dbmapping;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -11,9 +11,9 @@ import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import tech.ailef.dbadmin.annotations.DisplayName;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.misc.Utils;
import tech.ailef.dbadmin.external.annotations.DisplayName;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.misc.Utils;
public class DbObject {
private Object instance;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dbmapping;
package tech.ailef.dbadmin.external.dbmapping;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -16,11 +16,11 @@ import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import tech.ailef.dbadmin.DbAdmin;
import tech.ailef.dbadmin.annotations.ComputedColumn;
import tech.ailef.dbadmin.annotations.Filterable;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.misc.Utils;
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;
public class DbObjectSchema {
/**

View File

@@ -1,6 +1,6 @@
package tech.ailef.dbadmin.dto;
package tech.ailef.dbadmin.external.dto;
import tech.ailef.dbadmin.dbmapping.DbObject;
import tech.ailef.dbadmin.external.dbmapping.DbObject;
public class AutocompleteSearchResult {
private Object id;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dto;
package tech.ailef.dbadmin.external.dto;
public enum CompareOperator {
GT {

View File

@@ -0,0 +1,98 @@
package tech.ailef.dbadmin.external.dto;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
public class LogsSearchRequest {
private String table;
private String actionType;
private String itemId;
private int page;
private int pageSize;
private String sortKey;
private String sortOrder;
public String getTable() {
return table == null || table.isBlank() || table.equalsIgnoreCase("Any") ? null : table;
}
public void setTable(String table) {
this.table = table;
}
public String getActionType() {
return actionType == null || actionType.isBlank() || actionType.equalsIgnoreCase("Any") ? null : actionType;
}
public void setActionType(String actionType) {
this.actionType = actionType;
}
public String getItemId() {
return itemId == null || itemId.isBlank() ? null : itemId;
}
public void setItemId(String itemId) {
this.itemId = itemId;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public String getSortKey() {
return sortKey;
}
public void setSortKey(String sortKey) {
this.sortKey = sortKey;
}
public String getSortOrder() {
return sortOrder;
}
public void setSortOrder(String sortOrder) {
this.sortOrder = sortOrder;
}
@Override
public String toString() {
return "LogsSearchRequest [table=" + table + ", actionType=" + actionType + ", itemId=" + itemId + ", page="
+ page + ", pageSize=" + pageSize + ", sortKey=" + sortKey + ", sortOrder=" + sortOrder + "]";
}
public PageRequest toPageRequest() {
int actualPage = page - 1 < 0 ? 0 : page - 1;
int actualPageSize = pageSize <= 0 ? 50 : pageSize;
if (sortKey == null)
return PageRequest.of(actualPage, actualPageSize);
if (sortOrder == null) sortOrder = "ASC";
if (sortOrder.equals("DESC")) {
return PageRequest.of(actualPage, actualPageSize, Sort.by(sortKey).descending());
} else {
return PageRequest.of(actualPage, actualPageSize, Sort.by(sortKey).ascending());
}
}
}

View File

@@ -0,0 +1,32 @@
package tech.ailef.dbadmin.external.dto;
import java.util.List;
public class PaginatedResult<T> {
private PaginationInfo pagination;
private List<T> results;
public PaginatedResult(PaginationInfo pagination, List<T> page) {
this.pagination = pagination;
this.results = page;
}
public PaginationInfo getPagination() {
return pagination;
}
public List<T> getResults() {
return results;
}
public boolean isEmpty() {
return results.isEmpty();
}
public int getNumberOfResults() {
return getResults().size();
}
}

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dto;
package tech.ailef.dbadmin.external.dto;
import java.util.ArrayList;
import java.util.List;
@@ -8,7 +8,7 @@ import java.util.stream.IntStream;
import org.springframework.util.MultiValueMap;
import tech.ailef.dbadmin.misc.Utils;
import tech.ailef.dbadmin.external.misc.Utils;
/**
* Attached as output to requests that have a paginated response,
@@ -35,6 +35,7 @@ public class PaginationInfo {
*/
private int pageSize;
// TODO: Check if used
private long maxElement;
private Set<QueryFilter> queryFilters;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.dto;
package tech.ailef.dbadmin.external.dto;
import java.util.Objects;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.exceptions;
package tech.ailef.dbadmin.external.exceptions;
public class DbAdminException extends RuntimeException {
private static final long serialVersionUID = 8120227031645804467L;

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.exceptions;
package tech.ailef.dbadmin.external.exceptions;
/**
* Thrown during the computation of pagination if the requested

View File

@@ -1,4 +1,4 @@
package tech.ailef.dbadmin.misc;
package tech.ailef.dbadmin.external.misc;
import java.util.ArrayList;
import java.util.HashSet;
@@ -8,9 +8,9 @@ import java.util.Set;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import tech.ailef.dbadmin.dto.CompareOperator;
import tech.ailef.dbadmin.dto.QueryFilter;
import tech.ailef.dbadmin.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.dto.CompareOperator;
import tech.ailef.dbadmin.external.dto.QueryFilter;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
public interface Utils {
public static String camelToSnake(String v) {
@@ -24,6 +24,8 @@ public interface Utils {
public static MultiValueMap<String, String> computeParams(Set<QueryFilter> filters) {
MultiValueMap<String, String> r = new LinkedMultiValueMap<>();
if (filters == null)
return r;
r.put("filter_field", new ArrayList<>());
r.put("filter_op", new ArrayList<>());

View File

@@ -0,0 +1,12 @@
package tech.ailef.dbadmin.internal;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@ConditionalOnProperty(name = "dbadmin.enabled", matchIfMissing = true)
@ComponentScan
@Configuration
public class InternalDbAdminConfiguration {
}

View File

@@ -0,0 +1,30 @@
package tech.ailef.dbadmin.internal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import tech.ailef.dbadmin.internal.model.UserSetting;
import tech.ailef.dbadmin.internal.repository.UserSettingsRepository;
@Component
public class UserConfiguration {
@Autowired
private UserSettingsRepository repo;
public String get(String settingName) {
Optional<UserSetting> setting = repo.findById(settingName);
if (setting.isPresent())
return setting.get().getSettingValue();
return defaultValues().get(settingName);
}
private Map<String, String> defaultValues() {
Map<String, String> values = new HashMap<>();
values.put("brandName", "Spring Boot Database Admin");
return values;
}
}

View File

@@ -0,0 +1,98 @@
package tech.ailef.dbadmin.internal.model;
import java.time.LocalDateTime;
import org.springframework.format.datetime.standard.DateTimeFormatterFactory;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
@Entity
public class UserAction {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false)
private LocalDateTime createdAt;
@Lob
@Column(nullable = false)
private String sql;
@Column(nullable = false)
private String onTable;
@Column(nullable = false)
private String primaryKey;
@Column(nullable = false)
private String actionType;
public UserAction() {
}
public UserAction(String onTable, String primaryKey, String actionType) {
this.createdAt = LocalDateTime.now();
this.sql = "SQL TODO";
this.onTable = onTable;
this.actionType = actionType;
this.primaryKey = primaryKey;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getOnTable() {
return onTable;
}
public void setOnTable(String onTable) {
this.onTable = onTable;
}
public String getPrimaryKey() {
return primaryKey;
}
public void setPrimaryKey(String primaryKey) {
this.primaryKey = primaryKey;
}
public String getActionType() {
return actionType;
}
public void setActionType(String actionType) {
this.actionType = actionType;
}
public String getFormattedDate() {
return new DateTimeFormatterFactory("YYYY-MM-dd HH:mm:ss").createDateTimeFormatter().format(createdAt);
}
}

View File

@@ -0,0 +1,37 @@
package tech.ailef.dbadmin.internal.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class UserSetting {
@Id
private String id;
private String settingValue;
public UserSetting() {
}
public UserSetting(String id, String settingValue) {
this.id = id;
this.settingValue = settingValue;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSettingValue() {
return settingValue;
}
public void setSettingValue(String settingValue) {
this.settingValue = settingValue;
}
}

View File

@@ -0,0 +1,14 @@
package tech.ailef.dbadmin.internal.repository;
import java.util.List;
import org.springframework.data.domain.PageRequest;
import tech.ailef.dbadmin.internal.model.UserAction;
public interface CustomActionRepository {
public List<UserAction> findActions(String table, String actionType, String itemId, PageRequest pageRequest);
public long countActions(String table, String actionType, String itemId);
}

View File

@@ -0,0 +1,76 @@
package tech.ailef.dbadmin.internal.repository;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import tech.ailef.dbadmin.internal.model.UserAction;
@Component
public class CustomActionRepositoryImpl implements CustomActionRepository {
@PersistenceContext(unitName = "internal")
private EntityManager entityManager;
@Override
public List<UserAction> findActions(String table, String actionType, String itemId, PageRequest page) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<UserAction> query = cb.createQuery(UserAction.class);
Root<UserAction> userAction = query.from(UserAction.class);
List<Predicate> predicates = new ArrayList<Predicate>();
if (table != null)
predicates.add(cb.equal(userAction.get("onTable"), table));
if (actionType != null)
predicates.add(cb.equal(userAction.get("actionType"), actionType));
if (itemId != null)
predicates.add(cb.equal(userAction.get("primaryKey"), itemId));
if (!predicates.isEmpty()) {
query.select(userAction)
.where(cb.and(
predicates.toArray(new Predicate[predicates.size()])));
}
return entityManager.createQuery(query)
.setMaxResults(page.getPageSize())
.setFirstResult((int)page.getOffset())
.getResultList();
}
@Override
public long countActions(String table, String actionType, String itemId) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> query = cb.createQuery(Long.class);
Root<UserAction> userAction = query.from(UserAction.class);
List<Predicate> predicates = new ArrayList<>();
if (table != null)
predicates.add(cb.equal(userAction.get("onTable"), table));
if (actionType != null)
predicates.add(cb.equal(userAction.get("actionType"), actionType));
if (itemId != null)
predicates.add(cb.equal(userAction.get("primaryKey"), itemId));
if (!predicates.isEmpty()) {
query.select(cb.count(userAction))
.where(cb.and(
predicates.toArray(new Predicate[predicates.size()])));
} else {
query.select(cb.count(userAction));
}
return entityManager.createQuery(query).getSingleResult();
}
}

View File

@@ -0,0 +1,15 @@
package tech.ailef.dbadmin.internal.repository;
import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import tech.ailef.dbadmin.internal.model.UserAction;
@Repository
public interface UserActionRepository extends JpaRepository<UserAction, Integer>, CustomActionRepository {
public List<UserAction> findAllByOnTableAndActionTypeAndPrimaryKey(String table, String actionType, String primaryKey, PageRequest pageRequest);
}

View File

@@ -0,0 +1,11 @@
package tech.ailef.dbadmin.internal.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import tech.ailef.dbadmin.internal.model.UserSetting;
@Repository
public interface UserSettingsRepository extends JpaRepository<UserSetting, String> {
}

View File

@@ -0,0 +1,40 @@
package tech.ailef.dbadmin.internal.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tech.ailef.dbadmin.external.dto.PaginatedResult;
import tech.ailef.dbadmin.external.dto.PaginationInfo;
import tech.ailef.dbadmin.internal.model.UserAction;
import tech.ailef.dbadmin.internal.repository.CustomActionRepositoryImpl;
import tech.ailef.dbadmin.internal.repository.UserActionRepository;
@Service
public class UserActionService {
@Autowired
private UserActionRepository repo;
@Autowired
private CustomActionRepositoryImpl customRepo;
@Transactional("internalTransactionManager")
public UserAction save(UserAction a) {
return repo.save(a);
}
public PaginatedResult<UserAction> findActions(String table, String actionType, String userId, PageRequest page) {
long count = customRepo.countActions(table, actionType, userId);
List<UserAction> actions = customRepo.findActions(table, actionType, userId, page);
int maxPage = (int)(Math.ceil ((double)count / page.getPageSize()));
return new PaginatedResult<>(
new PaginationInfo(page.getPageNumber() + 1, maxPage, page.getPageSize(), count, null, null),
actions
);
}
}

View File

@@ -1,7 +1,8 @@
#spring.jpa.hibernate.ddl-auto=create
#spring.datasource.url=jdbc:h2:file:./database
#spring.datasource.username=sa
#spring.datasource.password=password
#spring.datasource.dbadmin.url=jdbc:h2:file:./database
#spring.datasource.dbadmin.username=sa
#spring.datasource.dbadmin.password=password
#spring.h2.console.enabled=true
#spring.jpa.show-sql=true

View File

@@ -0,0 +1,12 @@
document.addEventListener("DOMContentLoaded", () => {
let form = document.getElementById('log-filter-form');
if (form == null) return;
let selects = form.querySelectorAll('select');
selects.forEach(select => {
select.addEventListener('change', function(e) {
form.submit();
});
});
});

View File

@@ -10,6 +10,7 @@
<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>
<script type="text/javascript" src="/js/logs.js"></script>
<script type="text/javascript" src="/js/create.js"></script>
<title th:text="${title != null ? title + ' | Spring Boot DB Admin Panel' : 'Spring Boot DB Admin Panel'}"></title>
<script th:inline="javascript">
@@ -30,7 +31,9 @@
<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>
<a class=" fw-bold navbar-brand" href="/"><i class="bi bi-hexagon-fill"></i>
[[ ${userConf.get('brandName')} ]]
</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>
@@ -64,6 +67,30 @@
</div>
</a>
</li>
<li th:class="${#strings.equals(activePage, 'logs') ? 'active' : ''}">
<a th:href="|/${baseUrl}/logs|">
<div class="d-flex align-items-center">
<div class="menu-icon">
<i class="bi bi-file-text"></i>
</div>
<div class="menu-entry-text d-none d-md-block">
Logs
</div>
</div>
</a>
</li>
<li th:class="${#strings.equals(activePage, 'settings') ? 'active' : ''}">
<a th:href="|/${baseUrl}/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>
<!--
<li th:class="${#strings.equals(activePage, 'console') ? 'active' : ''}">
<a href="/live">
@@ -89,18 +116,6 @@
</div>
</a>
</li>
<li th:class="${#strings.equals(activePage, 'settings') ? 'active' : ''}">
<a th:href="|/${baseUrl}/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>
@@ -123,14 +138,15 @@
</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="@{|/${baseUrl}/model/${schema.getClassName()}${page.getPagination().getLink(page.getPagination.getCurrentPage() - 1)}|}"
th:href="@{|${requestUrl}${page.getPagination().getLink(page.getPagination.getCurrentPage() - 1)}|}"
aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">Previous</span>
@@ -139,7 +155,7 @@
<li class="page-item" th:each="p : ${page.getPagination().getBeforePages()}">
<a class="page-link"
th:href="@{|/${baseUrl}/model/${schema.getClassName()}${page.getPagination().getLink(p)}|}" th:text="${p}"></a>
th:href="@{|${requestUrl}${page.getPagination().getLink(p)}|}" th:text="${p}"></a>
</li>
<li class="page-item active">
@@ -148,13 +164,13 @@
<li class="page-item" th:each="p : ${page.getPagination().getAfterPages()}">
<a class="page-link"
th:href="@{|/${baseUrl}/model/${schema.getClassName()}${page.getPagination().getLink(p)}|}"
th:href="@{|${requestUrl}${page.getPagination().getLink(p)}|}"
th:text="${p}"></a>
</li>
<li class="page-item">
<a class="page-link"
th:if="${!page.getPagination().isLastPage()}"
th:href="@{|/${baseUrl}/model/${schema.getClassName()}${page.getPagination().getLink(page.getPagination.getCurrentPage() + 1)}|}"
th:href="@{|${requestUrl}${page.getPagination().getLink(page.getPagination.getCurrentPage() + 1)}|}"
aria-label="Next">
<span class="sr-only">Next</span>
<span aria-hidden="true">&raquo;</span>
@@ -162,15 +178,15 @@
</li>
</ul>
<div class="me-3">
<form method="GET" th:action="@{|/${baseUrl}/model/${schema.getClassName()}|}">
<form method="GET" th:action="@{|${requestUrl}|}">
<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>
<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>
@@ -183,14 +199,14 @@
<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>
<i>Showing [[ ${page.getNumberOfResults()} ]] of [[ ${page.getPagination().getMaxElement()} ]] results</i>
</p>
</div>
</div>
<div class="d-flex align-items-center" th:if="${page.getPagination().getMaxPage() == 1}">
<div class="me-3">
<form method="GET" th:action="@{|/${baseUrl}/model/${schema.getClassName()}|}">
<form method="GET" th:action="@{|${requestUrl}|}">
<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">
@@ -204,13 +220,14 @@
</form>
</div>
<p class="m-0 p-0">
<i>Showing [[ ${page.getActualResults()} ]] of [[ ${page.getPagination().getMaxElement()} ]] results</i>
<i>Showing [[ ${page.getNumberOfResults()} ]] of [[ ${page.getPagination().getMaxElement()} ]] results</i>
</p>
</div>
<div class="bulk-actions">
</div>
</div>
</nav>
</html>

View File

@@ -0,0 +1,101 @@
<!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('logs')}"></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">Logs</span>
</h1>
<div class="row mt-4">
<div class="col">
<div class="box">
<h3 class="fw-bold">Logs</h3>
<div class="w-75">
<form th:action="|/${baseUrl}/logs|" class="mt-3" id="log-filter-form" method="GET">
<div class="input-group">
<span class="input-group-text">Action type</span>
<select name="actionType" class="form-select">
<option>Any</option>
<option value="CREATE"
th:selected="${searchRequest.getActionType() != null
&& searchRequest.getActionType().equalsIgnoreCase('CREATE') }">Create</option>
<option value="EDIT"
th:selected="${searchRequest.getActionType() != null
&& searchRequest.getActionType().equalsIgnoreCase('EDIT') }">Edit</option>
<option value="DELETE"
th:selected="${searchRequest.getActionType() != null
&& searchRequest.getActionType().equalsIgnoreCase('DELETE') }">Delete</option>
</select>
<span class="input-group-text ms-3">Table</span>
<select name="table" class="form-select">
<option value="Any">Any</option>
<option th:each="schema : ${schemas}"
th:value="${schema.getTableName()}"
th:text="${schema.getTableName()}"
th:selected="${schema.getTableName().equals(searchRequest.getTable())}">
</option>
</select>
<span class="input-group-text ms-3">Item ID</span>
<input type="text" class="form-control" name="itemId"
th:value="${searchRequest.getItemId()}">
<button class="ui-btn btn btn-primary ms-3">Filter</button>
</div>
</form>
</div>
<div class="separator mt-3 mb-3"></div>
<div class="mt-3" th:if="${page.isEmpty()}">
<div class="alert alert-warning">There are no results for your filtering criteria</div>
</div>
<div class="table-responsive mt-3" th:if="${!page.isEmpty()}">
<nav th:replace="~{fragments/resources :: pagination(page=${page})}"></nav>
<table class="table table-striped mt-3">
<tr class="table-data-row">
<th>Action type</th>
<th>Table</th>
<th>Item ID</th>
<th>Time</th>
<th></th>
</tr>
<tr th:each="entry : ${page.getResults()}" class="table-data-row align-middle">
<td th:text="${entry.getActionType()}">
</td>
<td th:text="${entry.getOnTable()}">
</td>
<td th:text="${entry.getPrimaryKey()}">
</td>
<td th:text="${entry.getFormattedDate()}">
</td>
<td>
<!--
<a href="#" class="ui-btn btn btn-primary">
Diff <i class="text-white bi bi-search"></i>
</a>
-->
</td>
</tr>
</table>
<nav th:replace="~{fragments/resources :: pagination(page=${page})}"></nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -17,19 +17,27 @@
<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
<i class="bi bi-database pe-2"></i> GENERAL
</div>
</a>
<a href="#">
<!-- <a href="#">
<div class="ui-tab ps-5 pe-5 p-3">
<i class="bi bi-table pe-2"></i> DATA
</div>
</a>
</a> -->
<div class="inner-navigation-border flex-grow-1">
</div>
</div>
<div class="box with-navigation">
SETTINGS
<h3 class="fw-bold">General</h3>
<form th:action="|/${baseUrl}/settings|" method="POST">
<label for="brandName">Brand name</label>
<span class="m-0 p-0 text-muted">What appears in the top bar</span>
<input class="form-control mt-2" type="text"
id="brandName" name="brandName"
th:value="${userConf.get('brandName')}">
<button class="ui-btn btn btn-primary mt-3">Save</button>
</form>
</div>
</div>
</div>