WIP CSV export

This commit is contained in:
Francesco 2023-10-13 17:28:05 +02:00
parent a86a369120
commit 7250a2433c
6 changed files with 161 additions and 1 deletions

View File

@ -0,0 +1,83 @@
package tech.ailef.dbadmin.external.controller;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import tech.ailef.dbadmin.external.DbAdmin;
import tech.ailef.dbadmin.external.dbmapping.DbAdminRepository;
import tech.ailef.dbadmin.external.dbmapping.DbField;
import tech.ailef.dbadmin.external.dbmapping.DbObject;
import tech.ailef.dbadmin.external.dbmapping.DbObjectSchema;
import tech.ailef.dbadmin.external.dto.QueryFilter;
import tech.ailef.dbadmin.external.exceptions.DbAdminException;
import tech.ailef.dbadmin.external.misc.Utils;
@Controller
@RequestMapping(value= {"/${dbadmin.baseUrl}/export", "/${dbadmin.baseUrl}/export/"})
public class DataExportController {
@Autowired
private DbAdmin dbAdmin;
@Autowired
private DbAdminRepository repository;
@GetMapping("/{className}")
@ResponseBody
public ResponseEntity<byte[]> export(@PathVariable String className,
@RequestParam(required=false) String query,
@RequestParam MultiValueMap<String, String> otherParams) {
DbObjectSchema schema = dbAdmin.findSchemaByClassName(className);
Set<QueryFilter> queryFilters = Utils.computeFilters(schema, otherParams);
System.out.println("QF = " + queryFilters);
List<DbObject> results = repository.search(schema, query, queryFilters);
String result = toCsv(results, schema.getSortedFields());
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"export_" + schema.getClass().getSimpleName() + ".csv\"")
.body(result.getBytes());
}
private String toCsv(List<DbObject> items, List<DbField> fields) {
if (items.isEmpty()) return "";
StringWriter sw = new StringWriter();
CSVFormat csvFormat = CSVFormat.DEFAULT.builder()
// .setHeader(HEADERS)
.build();
try (final CSVPrinter printer = new CSVPrinter(sw, csvFormat)) {
for (DbObject item : items) {
printer.printRecord(fields.stream().map(f -> {
return item.get(f).getFormattedValue();
}));
}
return sw.toString();
} catch (IOException e) {
throw new DbAdminException(e);
}
}
}

View File

@ -48,7 +48,7 @@ import tech.ailef.dbadmin.external.exceptions.DbAdminException;
*/ */
@Controller @Controller
@RequestMapping(value = {"/${dbadmin.baseUrl}/download", "/${dbadmin.baseUrl}/download/"}) @RequestMapping(value = {"/${dbadmin.baseUrl}/download", "/${dbadmin.baseUrl}/download/"})
public class DownloadController { public class FileDownloadController {
@Autowired @Autowired
private DbAdminRepository repository; private DbAdminRepository repository;

View File

@ -101,6 +101,12 @@ public class CustomJpaRepository extends SimpleJpaRepository {
.setFirstResult((page - 1) * pageSize).getResultList(); .setFirstResult((page - 1) * pageSize).getResultList();
} }
public List<Object> search(String query, Set<QueryFilter> filters) {
return search(query, 1, Integer.MAX_VALUE, null, null, filters);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public int update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) { public int update(DbObjectSchema schema, Map<String, String> params, Map<String, MultipartFile> files) {

View File

@ -87,6 +87,17 @@ public class DbAdminRepository {
public long count(DbObjectSchema schema, String query, Set<QueryFilter> queryFilters) { public long count(DbObjectSchema schema, String query, Set<QueryFilter> queryFilters) {
return schema.getJpaRepository().count(query, queryFilters); return schema.getJpaRepository().count(query, queryFilters);
} }
public List<DbObject> search(DbObjectSchema schema, String query, Set<QueryFilter> queryFilters) {
CustomJpaRepository jpaRepository = schema.getJpaRepository();
long maxElement = count(schema, query, queryFilters);
return jpaRepository.search(query, queryFilters).stream()
.map(o -> new DbObject(o, schema))
.toList();
}
/** /**

View File

@ -227,6 +227,16 @@ public class DbField {
&& getPrimitiveField().getAnnotation(ManyToMany.class) == null; && getPrimitiveField().getAnnotation(ManyToMany.class) == null;
} }
/**
* Returns whether this field is exportable into a CSV file.
*
* @return
*/
public boolean isExportable() {
return !isBinary();
}
public boolean isGeneratedValue() { public boolean isGeneratedValue() {
return getPrimitiveField().getAnnotation(GeneratedValue.class) != null; return getPrimitiveField().getAnnotation(GeneratedValue.class) != null;
} }

View File

@ -3,6 +3,53 @@
<head th:replace="~{fragments/resources::head}"> <head th:replace="~{fragments/resources::head}">
</head> </head>
<body> <body>
<!-- Modal -->
<div class="modal fade" id="csvExportModal" tabindex="-1" aria-labelledby="csvExportModalLabel" aria-hidden="true">
<form th:action="|/${dbadmin_baseUrl}/export/${schema.getClassName()}|" method="GET">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="csvExportModalLabel">Export settings</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<h5 class="fw-bold">Include columns</h5>
<div th:each="field : ${schema.getSortedFields()}" th:if="${field.isExportable()}">
<div class="form-check">
<input class="form-check-input" type="checkbox"
th:value="${field.getName()}" th:id="|__check_${field.getName()}|"
th:name="fields[]"
checked>
<label class="form-check-label" th:for="|__check_${field.getName()}|">
[[ ${field.getName()} ]]
</label>
</div>
</div>
<h5 class="fw-bold mt-3" th:if="${!activeFilters.isEmpty()}">Active filters</h5>
<div th:each="filter : ${activeFilters}">
<span class="active-filter badge bg-primary me-1 mb-2 p-2 font-monospace noselect">
[[ ${filter}]]
</span>
</div>
<h5 class="fw-bold mt-3">Export format</h3>
<select name="format" class="form-select">
<option value="csv">CSV</option>
<option value="xlsx">XLSX</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</form>
</div>
<!-- End modal -->
<div class="bg-light main-wrapper"> <div class="bg-light main-wrapper">
<nav th:replace="~{fragments/resources :: navbar}"></nav> <nav th:replace="~{fragments/resources :: navbar}"></nav>
<div class="d-flex"> <div class="d-flex">
@ -59,6 +106,9 @@
<span title="Database table name" class="ms-3 label label-primary label-gray font-monospace"> <span title="Database table name" class="ms-3 label label-primary label-gray font-monospace">
[[ ${schema.getTableName()} ]] [[ ${schema.getTableName()} ]]
</span> </span>
<button type="button" class="btn p-0 m-0 ms-3" data-bs-toggle="modal" data-bs-target="#csvExportModal">
<i class="bi bi-filetype-csv" style="font-size: 1.5rem;"></i>
</button>
</h3> </h3>
<h3 class="create-button"> <h3 class="create-button">