Add build management features and views
All checks were successful
Build / build (push) Successful in 2m10s
All checks were successful
Build / build (push) Successful in 2m10s
This commit is contained in:
@@ -43,6 +43,11 @@ public class WebController {
|
||||
private static final String INPUT_LIST = "inputLists";
|
||||
/// The name of the model attribute that holds the current work log entries.
|
||||
private static final String WORKLOG = "worklog";
|
||||
/// The name of the model attribute that holds the current build being viewed or edited.
|
||||
private static final String BUILD = "build";
|
||||
/// The name of the model attribute that holds the build information.
|
||||
private static final String BUILD_INFO = "buildInfo";
|
||||
/// The name of the model attribute that holds the builds available for selection.
|
||||
private static final String BUILDS = "builds";
|
||||
|
||||
/// The name of the input field for the current size of the work log.
|
||||
@@ -93,6 +98,54 @@ public class WebController {
|
||||
return "browse_type";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the browsing of all builds.
|
||||
* Displays a list of all builds available in the system.
|
||||
*/
|
||||
@GetMapping("/builds")
|
||||
public String browseBuilds(Model model) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
model.addAttribute(BUILDS, buildService.getAllBuilds());
|
||||
return "builds";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the viewing of a specific build by its ID.
|
||||
* If the build does not exist, it redirects to the builds page.
|
||||
*/
|
||||
@GetMapping("/build/{id}")
|
||||
public String viewBuild(Model model, @PathVariable long id) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
var build = buildService.getBuildById(id);
|
||||
if (build == null) {
|
||||
return "redirect:/builds";
|
||||
}
|
||||
model.addAttribute(BUILD, build);
|
||||
model.addAttribute(BUILD_INFO, buildService.getBuildInfo(build));
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors());
|
||||
return "build_view";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation of a new build.
|
||||
*/
|
||||
@PostMapping("/create_build")
|
||||
public String createBuild(Model model, @RequestBody MultiValueMap<String, String> formData) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
var build = buildService.createBuild(formData.getFirst("name"), formData.getFirst("description"));
|
||||
return "redirect:/build/" + build.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a build by its ID.
|
||||
*/
|
||||
@GetMapping("/delete_build/{id}")
|
||||
public String deleteBuild(Model model, @PathVariable long id) {
|
||||
model.addAttribute(TIME, System.currentTimeMillis());
|
||||
buildService.deleteBuild(id);
|
||||
return "redirect:/builds";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the view of an asset by its QR code.
|
||||
* If the asset does not exist, it redirects to the index page.
|
||||
@@ -227,6 +280,7 @@ public class WebController {
|
||||
model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(assetType));
|
||||
model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(assetType));
|
||||
model.addAttribute(INPUT_LIST, assetService.getInputList(assetType));
|
||||
model.addAttribute(BUILDS, buildService.getAllBuilds());
|
||||
return "create_asset";
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,19 @@ public class AssetDescriptor {
|
||||
return String.format("%s-%s", type, property.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the property with the specified name.
|
||||
*
|
||||
* @param name The name of the property to retrieve.
|
||||
* @return The AssetProperty with the given name.
|
||||
*/
|
||||
public AssetProperty getProperty(String name) {
|
||||
return properties.stream()
|
||||
.filter(property -> property.getName().equals(name))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No property found with name: " + name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of the asset type described by this descriptor.
|
||||
*
|
||||
|
||||
@@ -24,6 +24,37 @@ public class AssetDescriptors {
|
||||
assets.add(property);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the descriptor for a specific asset type.
|
||||
*
|
||||
* @param type The type of the asset to retrieve the descriptor for.
|
||||
*/
|
||||
public AssetDescriptor getDescriptorForType(String type) {
|
||||
return assets.stream()
|
||||
.filter(assetDescriptor -> assetDescriptor.getType().equals(type))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No asset descriptor found for type: " + type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the property for a specific asset type and property name.
|
||||
*
|
||||
* @param type The type of the asset to retrieve the property for.
|
||||
* @param propertyName The name of the property to retrieve.
|
||||
*/
|
||||
public AssetProperty getPropertyForType(String type, String propertyName) {
|
||||
return getDescriptorForType(type).getProperty(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the generic property for a specific property name.
|
||||
*
|
||||
* @param propertyName The name of the property to retrieve.
|
||||
*/
|
||||
public AssetProperty getGenericProperty(String propertyName) {
|
||||
return getPropertyForType("asset", propertyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
var builder = new StringBuilder();
|
||||
|
||||
@@ -183,6 +183,9 @@ public class AssetProperty {
|
||||
return value.toString();
|
||||
} else if (type == PropertyType.CAPACITY) {
|
||||
return convertCapacity((Long) value).toString();
|
||||
} else if (type == PropertyType.BUILD) {
|
||||
var build = (Build) value;
|
||||
return build.getName();
|
||||
} else if (type.isEnum) {
|
||||
if (value instanceof AssetEnum assetEnum) {
|
||||
return assetEnum.getDisplayName();
|
||||
|
||||
@@ -21,10 +21,30 @@ public class Build
|
||||
@GeneratedValue
|
||||
private long id;
|
||||
|
||||
/**
|
||||
* Indicates whether this build is a meta build.
|
||||
* A meta build is a build that does not represent a physical computer,
|
||||
* but rather a collection of parts that can be used in other builds.
|
||||
*
|
||||
* It is used internally to represents parts that are explicitly not part of a build.
|
||||
*/
|
||||
private boolean meta;
|
||||
|
||||
/**
|
||||
* The name of the build.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* A description of the build.
|
||||
* This can be used to provide additional information about the build.
|
||||
*/
|
||||
private String description;
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL)
|
||||
/**
|
||||
* A list of parts that are included in the build.
|
||||
*/
|
||||
@OneToMany(mappedBy = "build")
|
||||
@OrderBy("type, brand, model, qr")
|
||||
private List<GenericAsset> parts;
|
||||
}
|
||||
|
||||
20
src/main/java/be/seeseepuff/pcinv/models/BuildInfo.java
Normal file
20
src/main/java/be/seeseepuff/pcinv/models/BuildInfo.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package be.seeseepuff.pcinv.models;
|
||||
|
||||
import be.seeseepuff.pcinv.meta.CapacityInfo;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class BuildInfo {
|
||||
private long totalRam;
|
||||
|
||||
/**
|
||||
* Calculates the total RAM capacity of the build.
|
||||
*
|
||||
* @return A CapacityInfo object representing the total RAM capacity.
|
||||
*/
|
||||
public CapacityInfo getTotalRamCapacity() {
|
||||
return CapacityInfo.of(totalRam);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,14 @@ import be.seeseepuff.pcinv.models.Build;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface BuildRepository extends JpaRepository<Build, Long>
|
||||
{
|
||||
Build getBuildByNameAndMeta(String name, boolean meta);
|
||||
|
||||
Build getBuildById(long id);
|
||||
|
||||
List<Build> findAllByMeta(boolean meta);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ package be.seeseepuff.pcinv.services;
|
||||
|
||||
import be.seeseepuff.pcinv.meta.*;
|
||||
import be.seeseepuff.pcinv.models.Asset;
|
||||
import be.seeseepuff.pcinv.models.Build;
|
||||
import be.seeseepuff.pcinv.models.GenericAsset;
|
||||
import be.seeseepuff.pcinv.models.WorkLogEntry;
|
||||
import be.seeseepuff.pcinv.repositories.AssetRepository;
|
||||
import be.seeseepuff.pcinv.repositories.BuildRepository;
|
||||
import be.seeseepuff.pcinv.repositories.GenericAssetRepository;
|
||||
import be.seeseepuff.pcinv.repositories.WorkLogRepository;
|
||||
import jakarta.persistence.EntityManager;
|
||||
@@ -30,6 +28,7 @@ public class AssetService {
|
||||
private final WorkLogRepository workLogRepository;
|
||||
private final Collection<AssetRepository<?>> repositories;
|
||||
private final EntityManager entityManager;
|
||||
private final BuildService buildService;
|
||||
|
||||
/**
|
||||
* Returns the count of all assets in the repository.
|
||||
@@ -253,6 +252,12 @@ public class AssetService {
|
||||
case "false" -> false;
|
||||
default -> null;
|
||||
};
|
||||
} else if (property.getType() == PropertyType.BUILD) {
|
||||
var build = buildService.getBuildById(Integer.parseInt(stringValue));
|
||||
if (build == null) {
|
||||
throw new IllegalArgumentException("Invalid build ID for property '" + property.getName() + "': " + stringValue);
|
||||
}
|
||||
return build;
|
||||
} else if (property.getType().isEnum) {
|
||||
for (var option : property.getOptions()) {
|
||||
if (option.getValue().equals(stringValue)) {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package be.seeseepuff.pcinv.services;
|
||||
|
||||
import be.seeseepuff.pcinv.models.Build;
|
||||
import be.seeseepuff.pcinv.models.BuildInfo;
|
||||
import be.seeseepuff.pcinv.repositories.BuildRepository;
|
||||
import be.seeseepuff.pcinv.repositories.RamRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -13,14 +16,22 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BuildService
|
||||
{
|
||||
public class BuildService {
|
||||
private final BuildRepository buildRepository;
|
||||
public static final Build EMPTY = Build.builder()
|
||||
.id(0)
|
||||
.name("(None)")
|
||||
.description("A meta build used to indicate that the part is not being used by anything")
|
||||
.build();
|
||||
private final RamRepository ramRepository;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
Build empty = buildRepository.getBuildByNameAndMeta("None", true);
|
||||
if (empty == null) {
|
||||
empty = Build.builder()
|
||||
.name("None")
|
||||
.meta(true)
|
||||
.build();
|
||||
}
|
||||
empty.setDescription("A meta build to hold unused parts.");
|
||||
empty = buildRepository.save(empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all computer builds, including meta builds.
|
||||
@@ -28,10 +39,7 @@ public class BuildService
|
||||
* @return A list of all builds.
|
||||
*/
|
||||
public List<Build> getAllBuilds() {
|
||||
var build = new ArrayList<Build>();
|
||||
build.add(EMPTY);
|
||||
build.addAll(getAllRealBuilds());
|
||||
return build;
|
||||
return buildRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,6 +48,66 @@ public class BuildService
|
||||
* @return A list of all builds.
|
||||
*/
|
||||
public List<Build> getAllRealBuilds() {
|
||||
return buildRepository.findAll();
|
||||
return buildRepository.findAllByMeta(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a build by its ID.
|
||||
*
|
||||
* @param id The ID of the build to retrieve.
|
||||
* @return The build with the given ID, or null if not found.
|
||||
*/
|
||||
public Build getBuildById(long id) {
|
||||
return buildRepository.getBuildById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new build with the given name and description.
|
||||
*
|
||||
* @param name The name of the build.
|
||||
* @param description The description of the build.
|
||||
* @return The created build.
|
||||
*/
|
||||
@Transactional
|
||||
public Build createBuild(String name, String description) {
|
||||
Build build = Build.builder()
|
||||
.name(name)
|
||||
.description(description)
|
||||
.build();
|
||||
return buildRepository.saveAndFlush(build);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a build by its ID.
|
||||
*
|
||||
* @param id The ID of the build to delete.
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteBuild(long id) {
|
||||
Build build = buildRepository.getBuildById(id);
|
||||
for (var part : build.getParts()) {
|
||||
part.setBuild(null);
|
||||
}
|
||||
buildRepository.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the build information for a given build ID.
|
||||
*
|
||||
* @param build The build object for which to retrieve the information.
|
||||
* @return The BuildInfo object containing the build information.
|
||||
* @throws IllegalArgumentException if the build with the given ID does not exist.
|
||||
*/
|
||||
public BuildInfo getBuildInfo(Build build) {
|
||||
var buildInfo = new BuildInfo();
|
||||
for (var part : build.getParts()) {
|
||||
if (part.getType().equals("ram")) {
|
||||
var asset = ramRepository.findByAsset(part);
|
||||
if (asset.getCapacity() != null) {
|
||||
buildInfo.setTotalRam(buildInfo.getTotalRam() + asset.getCapacity());
|
||||
}
|
||||
}
|
||||
}
|
||||
return buildInfo;
|
||||
}
|
||||
}
|
||||
|
||||
24
src/main/resources/templates/build_view.html
Normal file
24
src/main/resources/templates/build_view.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<body th:replace="~{fragments :: base(title=${'Build ' + build.getName()}, content=~{::content})}">
|
||||
<div th:fragment="content">
|
||||
<ul>
|
||||
<li><b>Name: </b><span th:text="${build.name}"></span></li>
|
||||
<li><b>Description: </b><span th:text="${build.description}"></span></li>
|
||||
<li><b>Part Count: </b><span th:text="${build.getParts()?.size() ?: 0}"></span></li>
|
||||
<li><b>Total RAM: </b><span th:text="${buildInfo.getTotalRamCapacity().getCapacityInUnit()} + ' ' + ${buildInfo.getTotalRamCapacity().getIdealUnit().displayName}"></span></li>
|
||||
</ul>
|
||||
<table border="1" cellpadding="4">
|
||||
<tr bgcolor="#d3d3d3">
|
||||
<th>QR</th>
|
||||
<th>Type</th>
|
||||
<th>Brand</th>
|
||||
<th>Model</th>
|
||||
</tr>
|
||||
<tr th:each="p : ${build.getParts()}">
|
||||
<td><a th:href="'/view/' + ${p.qr}" th:text="${p.qr}"></a></td>
|
||||
<td th:text="${descriptors.getDescriptorForType(p.type).displayName}"></td>
|
||||
<td th:text="${descriptors.getGenericProperty('brand').renderValue(p)}"></td>
|
||||
<td th:text="${descriptors.getGenericProperty('model').renderValue(p)}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
28
src/main/resources/templates/builds.html
Normal file
28
src/main/resources/templates/builds.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<body th:replace="~{fragments :: base(title='Builds', content=~{::content})}">
|
||||
<div th:fragment="content">
|
||||
<table border="1" cellpadding="4">
|
||||
<tr bgcolor="#d3d3d3">
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Part Count</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
<tr th:each="b : ${builds}">
|
||||
<td><a th:href="'/build/' + ${b.id}" th:text="${b.getName()}"></a></td>
|
||||
<td th:text="${b.getDescription()}"></td>
|
||||
<td th:text="${b.getParts()?.size() ?: 0}"></td>
|
||||
<td>
|
||||
<a th:if="${!b.isMeta()}" th:href="${'/delete_build/' + b.id}">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
<form action="/create_build" method="post">
|
||||
<tr>
|
||||
<td><label><input type="text" name="name" placeholder="Name"></label></td>
|
||||
<td><label><input type="text" name="description" placeholder="Description"></label></td>
|
||||
<td></td>
|
||||
<td><input type="submit" value="Create"></td>
|
||||
</tr>
|
||||
</form>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
@@ -7,7 +7,8 @@
|
||||
<table border="1" cellpadding="4">
|
||||
<tr th:each="p : ${d.properties}">
|
||||
<th bgcolor="lightgray"><b th:text="${p.displayName}"></b></th>
|
||||
<td th:text="${p.renderValue(asset)}"></td>
|
||||
<td th:if="${p.name == 'build'}"><a th:href="'/build/' + ${asset.getAsset().getBuild().id}" th:text="${p.renderValue(asset)}"></a></td>
|
||||
<td th:if="${p.name != 'build'}" th:text="${p.renderValue(asset)}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user