diff --git a/deployment/Kubernetes/argocd/docs/devSetup.md b/deployment/Kubernetes/argocd/docs/devSetup.md new file mode 100644 index 000000000..acd5a75c3 --- /dev/null +++ b/deployment/Kubernetes/argocd/docs/devSetup.md @@ -0,0 +1,13 @@ +# Developer Setup + +## Argo Setup + + 1. Have minikube running + 2. `kubectl create ns argocd` + 3. `kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.5.8/manifests/install.yaml` + - Get latest yaml from: https://github.com/argoproj/argo-cd/releases + 4. Verify: `kubectl get all -n argocd` + 5. Access by: `kubectl port-forward svc/argocd-server -n argocd 8080:443` + - username: `admin` + - pass via: `kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo` + 6. diff --git a/deployment/Kubernetes/docs/development.md b/deployment/Kubernetes/docs/development.md index 4dc8d579d..5084c5245 100644 --- a/deployment/Kubernetes/docs/development.md +++ b/deployment/Kubernetes/docs/development.md @@ -5,7 +5,12 @@ Prerequisites: - Working Kubernetes cluster - Minikube is a good option for local dev +## MiniKube Setup + 1. Download minikube from latest docs, install + 2. Start cluster with "minikube start" + +You now have an operational kube cluster. ## Resources diff --git a/deployment/Single Host/Station-Captain/docs/Features.adoc b/deployment/Single Host/Station-Captain/docs/Features.adoc index 8946bc2ef..50b0fb61c 100644 --- a/deployment/Single Host/Station-Captain/docs/Features.adoc +++ b/deployment/Single Host/Station-Captain/docs/Features.adoc @@ -27,7 +27,8 @@ These installers can be used to install the `oqm-captain` management script. ==== First time run -If the Open Quarter Base station package is not installed, the script will prompt the user if they want it installed. +If the Open Quarter Base station package is not installed, the script will prompt the user with a setup wizard. The user +can choose to use this wizard to help them get started. If no, continue to the menu. diff --git a/deployment/Single Host/Station-Captain/docs/User Guide.adoc b/deployment/Single Host/Station-Captain/docs/User Guide.adoc index 2c9d26ec5..034eccc91 100644 --- a/deployment/Single Host/Station-Captain/docs/User Guide.adoc +++ b/deployment/Single Host/Station-Captain/docs/User Guide.adoc @@ -100,6 +100,11 @@ TODO- document better .. *Plugins* TODO .. *Uninstall All* TODO +. *Plugins* +.. *Review Available Plugins* + + Select from a list of plugins to install +.. *Select Plugins* + + Select from a list of plugins to uninstall . *Snapshots* + Snapshots allow you to take a current state of you system, and save it or back it up for later. diff --git a/deployment/Single Host/Station-Captain/properties.json b/deployment/Single Host/Station-Captain/properties.json index 56e4a86ad..9b2ab431d 100644 --- a/deployment/Single Host/Station-Captain/properties.json +++ b/deployment/Single Host/Station-Captain/properties.json @@ -1,6 +1,6 @@ { "packageName":"oqm-manager-station+captain", - "version":"2.2.2", + "version":"2.3.0", "description":"Utility for setting up and maintaining an instance of Open QuarterMaster.", "maintainer": { "name":"EBP" diff --git a/deployment/Single Host/Station-Captain/src/lib/LogManagement.py b/deployment/Single Host/Station-Captain/src/lib/LogManagement.py index ee486c085..28b53bd2c 100644 --- a/deployment/Single Host/Station-Captain/src/lib/LogManagement.py +++ b/deployment/Single Host/Station-Captain/src/lib/LogManagement.py @@ -85,7 +85,7 @@ def packageLogs() -> (bool, str): sysInfoFile.write(LogManagement.getSystemInfo()) with open(compilingDir + "/01-installed.txt", "w") as sysInfoFile: - sysInfoFile.write(PackageManagement.getInstalledPackages()) + sysInfoFile.write(PackageManagement.getOqmPackagesStr(installed=True, notInstalled=False)) logging.info("Writing log messages.") result, services = ServiceUtils.getServiceNames() diff --git a/deployment/Single Host/Station-Captain/src/lib/PackageManagement.py b/deployment/Single Host/Station-Captain/src/lib/PackageManagement.py index 432938840..8e917fb97 100644 --- a/deployment/Single Host/Station-Captain/src/lib/PackageManagement.py +++ b/deployment/Single Host/Station-Captain/src/lib/PackageManagement.py @@ -3,6 +3,7 @@ import logging import platform +from ServiceUtils import * class PackageManagement: """ @@ -16,6 +17,7 @@ class PackageManagement: """ BASE_STATION_PACKAGE = "oqm-core-base+station" ALL_OQM = "oqm-*" + OQM_PLUGINS = "oqm-plugin-*" SYSTEM_PACKAGE_MANAGER = None @staticmethod @@ -25,7 +27,8 @@ def getSystemPackageManager() -> str: logging.debug("Determining the system's package manager.") systemReleaseInfo = platform.freedesktop_os_release() - if ("ID_LIKE" in systemReleaseInfo and systemReleaseInfo['ID_LIKE'].casefold() == "debian".casefold()) or systemReleaseInfo['ID'].casefold() == "Debian".casefold(): + if ("ID_LIKE" in systemReleaseInfo and systemReleaseInfo['ID_LIKE'].casefold() == "debian".casefold()) or \ + systemReleaseInfo['ID'].casefold() == "Debian".casefold(): PackageManagement.SYSTEM_PACKAGE_MANAGER = "apt" logging.info("Determined system using %s", PackageManagement.SYSTEM_PACKAGE_MANAGER) @@ -51,8 +54,37 @@ def coreInstalled() -> bool: logging.debug("Error Output of listing core components: " + result.stderr) return "installed" in result.stdout + @staticmethod + def installPackages(packages:list) -> (bool, str): + logging.info("Installing packages: %s", packages) + command:list = ["apt-get", "install", "-y"] + command.extend(packages) + result = subprocess.run( + command, + shell=False, capture_output=True, text=True, check=False + ) + if result.returncode != 0: + logging.error("Failed to run install packages command: %s", result.stderr) + return False, result.stderr + return True + + @staticmethod + def removePackages(packages:list) -> (bool, str): + logging.info("Removing packages: %s", packages) + command:list = ["apt-get", "remove", "-y", "--purge"] + command.extend(packages) + result = subprocess.run( + command, + shell=False, capture_output=True, text=True, check=False + ) + if result.returncode != 0: + logging.error("Failed to run remove packages command: %s", result.stderr) + return False, result.stderr + return True + @staticmethod def installCore(): + # TODO:: update to use new install, package get features # TODO:: update with error handling, return logging.info("Installing core components.") # TODO:: will likely need updated for yum @@ -72,7 +104,8 @@ def updateSystem() -> (bool, str): return False, result.stderr logging.debug("Upgrading apt packages.") subprocess.run(["clear"], shell=False, capture_output=False, text=True, check=False) - result = subprocess.run(["apt-get", "dist-upgrade"], shell=False, capture_output=False, text=True, check=False) + result = subprocess.run(["apt-get", "dist-upgrade"], shell=False, capture_output=False, text=True, + check=False) if result.returncode != 0: logging.error("Failed to run upgrade command: %s", result.stderr) return False, result.stderr @@ -90,7 +123,8 @@ def updateSystem() -> (bool, str): def promptForAutoUpdates() -> (bool, str): if "Ubuntu" in platform.version(): logging.debug("Prompting user through unattended-upgrades.") - subprocess.run(["dpkg-reconfigure", "-plow", "unattended-upgrades"], shell=False, capture_output=False, text=True, check=True) + subprocess.run(["dpkg-reconfigure", "-plow", "unattended-upgrades"], shell=False, capture_output=False, + text=True, check=True) logging.info("Done.") # TODO:: doublecheck automatic restart, setting alert email else: @@ -98,13 +132,89 @@ def promptForAutoUpdates() -> (bool, str): return True, None @staticmethod - def getInstalledPackages() -> (bool, str): - logging.debug("Ensuring core components are installed.") - # TODO:: will likely need updated for yum - result = PackageManagement.runPackageCommand("list", PackageManagement.ALL_OQM, "-qq") + def getOqmPackagesStr(filter: str = ALL_OQM, installed: bool = True, notInstalled: bool = True): + logging.debug("Getting OQM packages.") + result = PackageManagement.runPackageCommand("list", filter, "-qq") logging.debug("Output of listing core components: " + result.stdout) logging.debug("Error Output of listing core components: " + result.stderr) - result = os.linesep.join([s for s in result.stdout.splitlines() if "installed" in s]) + result = result.stdout + output = [] + for curLine in result.splitlines(): + if installed and notInstalled: + output.append(curLine) + continue + if installed: + if "installed" in curLine: + output.append(curLine) + continue + if notInstalled: + if not "installed" in curLine: + output.append(curLine) + return os.linesep.join(output) + + @staticmethod + def getPluginDisplayName(package:str): + # print("Package: " + package) + return package.split("-")[2].replace("+", " ") + + @staticmethod + def getPackageInfo(package:str) -> (bool, str): + output = {} + packageShow = subprocess.run(['apt-cache', 'show', package], shell=False, capture_output=True, text=True, check=False).stdout + packageShow = packageShow.splitlines() + + for curLine in packageShow: + if not curLine.strip(): + continue + split = curLine.split(": ", 1) + name = split[0] + value = split[1] + output[name] = value + return output + @staticmethod + def packageLineToArray(curLine:str) -> (dict): + output = {} + # print("cur line: ", curLine) + output['package'] = curLine.split("/")[0] + output['displayName'] = PackageManagement.getPluginDisplayName(output['package']) + lineParts = curLine.split(" ") + # print("lineParts: ", lineParts) + output['version'] = lineParts[1] + output['installed'] = "installed" in curLine + + packageInfo = PackageManagement.getPackageInfo(output['package']) + # print(packageInfo) + output['description'] = packageInfo['Description'] + output['fullInfo'] = packageInfo + + return output + + @staticmethod + def getOqmPackagesList(filter: str = ALL_OQM, installed: bool = True, notInstalled: bool = True): + logging.debug("Getting OQM packages.") + result = PackageManagement.getOqmPackagesStr(filter, installed, notInstalled) + # print("Package list str: " + result) + result = result.splitlines() + result = map(PackageManagement.packageLineToArray,result) + # TODO:: debug + # print("Package list: ", list(result)) return result + + @staticmethod + def ensureOnlyPluginsInstalled(pluginList:list) -> (bool, str): + logging.debug("Ensuring only plugins in list installed.") + + allInstalledPlugins = map( + lambda i: i['package'], + PackageManagement.getOqmPackagesList(PackageManagement.OQM_PLUGINS, installed=True) + ) + pluginsToRemove = [i for i in allInstalledPlugins if i not in pluginList] + + # TODO Try to figure out how to remove unwanted plugins while not bouncing dependency plugins + # TODO:: error check + PackageManagement.removePackages(pluginsToRemove) + PackageManagement.installPackages(pluginList) + + ServiceUtils.doServiceCommand(ServiceStateCommand.restart, ServiceUtils.SERVICE_ALL) diff --git a/deployment/Single Host/Station-Captain/src/lib/UserInteraction.py b/deployment/Single Host/Station-Captain/src/lib/UserInteraction.py index c5ddd4222..dd5bbc922 100644 --- a/deployment/Single Host/Station-Captain/src/lib/UserInteraction.py +++ b/deployment/Single Host/Station-Captain/src/lib/UserInteraction.py @@ -30,11 +30,8 @@ class UserInteraction: TALL_HEIGHT = 50 def __init__(self): - self.dialog = Dialog( - dialog="dialog", - autowidgetsize=True, + self.dialog = Dialog(dialog="dialog", autowidgetsize=True) - ) self.dialog.set_background_title(ScriptInfo.SCRIPT_TITLE) self.dialog.__setattr__("hfile", "oqm-station-captain-help.txt") @@ -147,9 +144,10 @@ def mainMenu(self): choices=[ ("(1)", "Info / Status"), ("(2)", "Manage Installation"), - ("(3)", "Snapshots"), - ("(4)", "Cleanup, Maintenance, and Updates"), - ("(5)", "Captain Settings"), + ("(3)", "Plugins"), + ("(4)", "Snapshots"), + ("(5)", "Cleanup, Maintenance, and Updates"), + # ("(6)", "Captain Settings"), ] ) UserInteraction.clearScreen() @@ -162,8 +160,10 @@ def mainMenu(self): if choice == "(2)": self.manageInstallationMenu() if choice == "(3)": - self.snapshotsMenu() + self.pluginsMenu() if choice == "(4)": + self.snapshotsMenu() + if choice == "(5)": self.cleanMaintUpdatesMenu() logging.debug("Done running main menu.") @@ -226,8 +226,7 @@ def manageInstallationMenu(self): ("(1)", "Setup Wizard"), ("(2)", "SSL/HTTPS Certs"), ("(3)", "Set E-mail Settings"), - ("(4)", "User Administration"), - ("(5)", "Plugins") + ("(4)", "User Administration") ] ) UserInteraction.clearScreen() @@ -268,11 +267,16 @@ def manageCertsMenu(self): choices.append(("(5)", f"Auto Regenerate certs ({autoRegenEnabled})")) choices.append(("(8)", "CA Private Key Location")) choices.append(("(9)", "CA Public Cert/Key Location")) - choices.append(("(10)", f"Cert Country Name ({mainCM.getConfigVal('cert.selfMode.certInfo.countryName')})")) - choices.append(("(11)", f"Cert State or Province Name ({mainCM.getConfigVal('cert.selfMode.certInfo.stateOrProvinceName')})")) - choices.append(("(12)", f"Cert Locality Name ({mainCM.getConfigVal('cert.selfMode.certInfo.localityName')})")) - choices.append(("(13)", f"Cert Organization Name ({mainCM.getConfigVal('cert.selfMode.certInfo.organizationName')})")) - choices.append(("(14)", f"Cert Organizational Unit Name ({mainCM.getConfigVal('cert.selfMode.certInfo.organizationalUnitName')})")) + choices.append( + ("(10)", f"Cert Country Name ({mainCM.getConfigVal('cert.selfMode.certInfo.countryName')})")) + choices.append(("(11)", + f"Cert State or Province Name ({mainCM.getConfigVal('cert.selfMode.certInfo.stateOrProvinceName')})")) + choices.append( + ("(12)", f"Cert Locality Name ({mainCM.getConfigVal('cert.selfMode.certInfo.localityName')})")) + choices.append(("(13)", + f"Cert Organization Name ({mainCM.getConfigVal('cert.selfMode.certInfo.organizationName')})")) + choices.append(("(14)", + f"Cert Organizational Unit Name ({mainCM.getConfigVal('cert.selfMode.certInfo.organizationalUnitName')})")) if certMode == "letsEncrypt": logging.debug("Setting up menu for let's encrypt mode") accepted = mainCM.getConfigVal('cert.letsEncryptMode.acceptTerms') @@ -287,7 +291,8 @@ def manageCertsMenu(self): logging.debug("Setting up menu for provided mode") choices.append(("(8)", "CA Private Key Location")) choices.append(("(9)", "CA Public Cert/Key Location")) - choices.append(("(16)", f"Provide CA Cert (Currently {mainCM.getConfigVal('cert.providedMode.caProvided')})")) + choices.append( + ("(16)", f"Provide CA Cert (Currently {mainCM.getConfigVal('cert.providedMode.caProvided')})")) if mainCM.getConfigVal('cert.providedMode.caProvided'): choices.append(("(17)", "Install CA on host")) @@ -303,12 +308,20 @@ def manageCertsMenu(self): if choice == "(1)": logging.debug("Showing current cert information") - certInfoReturn = subprocess.run(["openssl", "x509", "-in", mainCM.getConfigVal('cert.certs.systemCert'), "--text"], shell=False, capture_output=True, text=True, check=False) - self.dialog.scrollbox(mainCM.getConfigVal('cert.certs.systemCert') + "\n\n" + certInfoReturn.stdout, title="System Cert Info") - - if mainCM.getConfigVal("cert.mode") == "self" or (mainCM.getConfigVal("cert.mode") == "provided" and mainCM.getConfigVal("cert.providedMode.caProvided")): - certInfoReturn = subprocess.run(["openssl", "x509", "-in", mainCM.getConfigVal('cert.certs.CARootCert'), "--text"], shell=False, capture_output=True, text=True, check=False) - self.dialog.scrollbox(mainCM.getConfigVal('cert.certs.CARootCert') + "\n\n" + certInfoReturn.stdout, title="CA Cert Info") + certInfoReturn = subprocess.run( + ["openssl", "x509", "-in", mainCM.getConfigVal('cert.certs.systemCert'), "--text"], shell=False, + capture_output=True, text=True, check=False) + self.dialog.scrollbox(mainCM.getConfigVal('cert.certs.systemCert') + "\n\n" + certInfoReturn.stdout, + title="System Cert Info") + + if mainCM.getConfigVal("cert.mode") == "self" or ( + mainCM.getConfigVal("cert.mode") == "provided" and mainCM.getConfigVal( + "cert.providedMode.caProvided")): + certInfoReturn = subprocess.run( + ["openssl", "x509", "-in", mainCM.getConfigVal('cert.certs.CARootCert'), "--text"], shell=False, + capture_output=True, text=True, check=False) + self.dialog.scrollbox(mainCM.getConfigVal('cert.certs.CARootCert') + "\n\n" + certInfoReturn.stdout, + title="CA Cert Info") if choice == "(2)": logging.debug("Verifying current cert setup (TODO)") @@ -425,7 +438,8 @@ def manageCertsMenu(self): caProvided = False if code == self.dialog.OK: caProvided = True - mainCM.setConfigValInFile("cert.providedMode.caProvided", caProvided, ScriptInfo.CONFIG_DEFAULT_UPDATE_FILE) + mainCM.setConfigValInFile("cert.providedMode.caProvided", caProvided, + ScriptInfo.CONFIG_DEFAULT_UPDATE_FILE) mainCM.rereadConfigData() if choice == "(17)": logging.debug("Installing CA on host") @@ -690,7 +704,8 @@ def snapshotsMenu(self): else: logging.info("User chose not to take a preemptive snapshot.") - code = self.dialog.yesno("Are you want to restore the following snapshot?\n" + snapshotFile + "\n\nThis can't be undone.") + code = self.dialog.yesno( + "Are you want to restore the following snapshot?\n" + snapshotFile + "\n\nThis can't be undone.") if code != self.dialog.OK: logging.info("User chose not to do the restore after all.") continue @@ -702,7 +717,8 @@ def snapshotsMenu(self): if not result: self.dialog.msgbox(report, title="Error taking Snapshot") else: - self.dialog.msgbox("Snapshot was taken successfully.\n\nOutput File:\n" + report, title="Snapshot successful") + self.dialog.msgbox("Snapshot was taken successfully.\n\nOutput File:\n" + report, + title="Snapshot successful") if choice == "(3)": if SnapshotUtils.isAutomaticEnabled(): SnapshotUtils.disableAutomatic() @@ -779,7 +795,8 @@ def containerManagementMenu(self): choices=[ ("(1)", "Prune unused container resources"), ("(2)", - ("Disable" if ContainerUtils.isAutomaticEnabled() else "Enable") + " automatic pruning (recommend enabled)"), + ( + "Disable" if ContainerUtils.isAutomaticEnabled() else "Enable") + " automatic pruning (recommend enabled)"), ("(3)", "Set prune frequency"), ] ) @@ -812,7 +829,9 @@ def containerManagementMenu(self): def setupWizard(self): logging.debug("Running setup wizard.") - self.dialog.msgbox("Welcome to the setup wizard\n\nThis will guide you through a high-level setup of the OQM installation.\n\nYou can run this again later.", title="Setup Wizard") + self.dialog.msgbox( + "Welcome to the setup wizard\n\nThis will guide you through a high-level setup of the OQM installation.\n\nYou can run this again later.", + title="Setup Wizard") # Check if already installed, prompt to uninstall # if PackageManagement.coreInstalled(): @@ -893,4 +912,73 @@ def setupWizard(self): title="Setup Wizard" ) + def pluginsMenu(self): + logging.debug("Running Plugins menu.") + while True: + code, choice = self.dialog.menu( + "Please choose an option:", + title="Plugins Menu", + choices=[ + ("(1)", "Review Plugins"), + ("(2)", "Select Plugins") + ] + ) + UserInteraction.clearScreen() + logging.debug('Main menu choice: %s, code: %s', choice, code) + if code != self.dialog.OK: + break + if choice == "(1)": + self.showPlugins() + if choice == "(2)": + self.selectPluginsMenu() + + logging.debug("Done running manage install menu.") + + @staticmethod + def mapPluginSelection(pluginFromPm): + return ( + pluginFromPm['package'], + PackageManagement.getPluginDisplayName(pluginFromPm['package']), + pluginFromPm['installed'] + ) + + def getPluginSelectionArray(self): + logging.debug("Getting plugin selection array") + plugins = PackageManagement.getOqmPackagesList(PackageManagement.OQM_PLUGINS) + return map(UserInteraction.mapPluginSelection, plugins) + + def selectPluginsMenu(self): + # https://pythondialog.sourceforge.io/doc/widgets.html#build-list + code, installedPluginSelection = self.dialog.buildlist( + title="Select Installed Plugins", + text="Select which plugins to be installed. To be installed on right, not to be installed on left.", + visit_items=True, + items=self.getPluginSelectionArray() + ) + if code != self.dialog.OK: + return + self.dialog.infobox("Applying plugin selection. Please wait.") + PackageManagement.ensureOnlyPluginsInstalled(installedPluginSelection) + self.dialog.msgbox( + "Plugin Selection Complete!", + title="Plugin Selection" + ) + + def showPlugins(self): + toShow = "" + for curPackage in PackageManagement.getOqmPackagesList(PackageManagement.OQM_PLUGINS): + print(curPackage) + toShow += curPackage['displayName'] + "\n" + toShow += "\tVersion: " + curPackage['version'] + "\n" + toShow += "\tInstalled?: " + str(curPackage['installed']) + "\n" + toShow += "\tDescription: " + curPackage['description'] + "\n" + toShow += "\n\n\n" + self.dialog.scrollbox(toShow, title="Available Plugins", + # height=UserInteraction.TALL_HEIGHT, + # width=UserInteraction.WIDE_WIDTH, + # tab_correct=True, trim=False, + # cr_wrap=True + ) + + ui = UserInteraction() diff --git a/software/oqm-core-api/build.gradle b/software/oqm-core-api/build.gradle index 664c75bc9..c56bb677e 100644 --- a/software/oqm-core-api/build.gradle +++ b/software/oqm-core-api/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'com.ebp.openQuarterMaster' -version '2.1.1' +version '2.1.2-DEV' repositories { mavenCentral() @@ -67,7 +67,7 @@ dependencies { testImplementation group: 'io.rest-assured', name: 'rest-assured' testImplementation 'io.quarkus:quarkus-smallrye-jwt-build' testImplementation 'io.quarkus:quarkus-test-kafka-companion' - testImplementation 'net.datafaker:datafaker:2.2.2' + testImplementation 'net.datafaker:datafaker:2.3.0' testImplementation 'org.assertj:assertj-core:3.26.0' testImplementation 'io.jaegertracing:jaeger-testcontainers:0.7.0' } diff --git a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/config/BaseStationInteractingEntity.java b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/config/CoreApiInteractingEntity.java similarity index 88% rename from software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/config/BaseStationInteractingEntity.java rename to software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/config/CoreApiInteractingEntity.java index 59dbd47ff..275425536 100644 --- a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/config/BaseStationInteractingEntity.java +++ b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/config/CoreApiInteractingEntity.java @@ -17,7 +17,7 @@ @Singleton @NoArgsConstructor @BsonDiscriminator -public class BaseStationInteractingEntity extends InteractingEntity { +public class CoreApiInteractingEntity extends InteractingEntity { /** * Don't change this. We ue this very specific ObjectId to identify the Base Station's specific entry in the db. @@ -25,7 +25,7 @@ public class BaseStationInteractingEntity extends InteractingEntity { public static final ObjectId BS_ID = new ObjectId("00000000AAAAAAAAAAFFFFFF"); @Inject - public BaseStationInteractingEntity( + public CoreApiInteractingEntity( @ConfigProperty(name = "service.runBy.email", defaultValue = "") String email ){ @@ -42,12 +42,12 @@ public ObjectId getId() { @Override public String getName() { - return "Base Station"; + return "Core API"; } @Override public InteractingEntityType getInteractingEntityType() { - return InteractingEntityType.BASE_STATION; + return InteractingEntityType.CORE_API; } @Override diff --git a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/interactingEntity/InteractingEntityType.java b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/interactingEntity/InteractingEntityType.java index 9579aa398..6d927a1d3 100644 --- a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/interactingEntity/InteractingEntityType.java +++ b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/object/interactingEntity/InteractingEntityType.java @@ -4,5 +4,5 @@ public enum InteractingEntityType { USER, SERVICE_GENERAL, SERVICE_PLUGIN, - BASE_STATION + CORE_API } diff --git a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/rest/search/InteractingEntitySearch.java b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/rest/search/InteractingEntitySearch.java index 6a33211ab..bda29b193 100644 --- a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/rest/search/InteractingEntitySearch.java +++ b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/rest/search/InteractingEntitySearch.java @@ -2,6 +2,7 @@ import jakarta.ws.rs.QueryParam; import lombok.Getter; +import lombok.Setter; import lombok.ToString; import org.bson.conversions.Bson; import tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntity; @@ -12,9 +13,11 @@ import static com.mongodb.client.model.Filters.regex; @ToString(callSuper = true) +@Setter @Getter public class InteractingEntitySearch extends SearchKeyAttObject { @QueryParam("name") String name; + //TODO:: object specific fields, add to bson filter list @@ -23,8 +26,7 @@ public List getSearchFilters() { List output = super.getSearchFilters(); if (name != null && !name.isBlank()) { - //TODO:: handle first and last name properly - output.add(regex("firstName", SearchUtils.getSearchTermPattern(name))); + output.add(regex("name", SearchUtils.getSearchTermPattern(name))); } return output; diff --git a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/InteractingEntityReferenceValidator.java b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/InteractingEntityReferenceValidator.java index c6311e786..80d27ecc0 100644 --- a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/InteractingEntityReferenceValidator.java +++ b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/model/validation/validators/InteractingEntityReferenceValidator.java @@ -7,7 +7,7 @@ import java.util.ArrayList; import java.util.List; -import static tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntityType.BASE_STATION; +import static tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntityType.CORE_API; public class InteractingEntityReferenceValidator extends Validator { @@ -15,7 +15,7 @@ public class InteractingEntityReferenceValidator extends Validator errs = new ArrayList<>(); - if (reference.getId() == null && !BASE_STATION.equals(reference.getType())) { + if (reference.getId() == null && !CORE_API.equals(reference.getType())) { errs.add("Null entity id given."); } //TODO:: if object id, not BASE_STATION? diff --git a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/ExpiryProcessor.java b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/ExpiryProcessor.java index ca0975925..6a10646c9 100644 --- a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/ExpiryProcessor.java +++ b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/scheduled/ExpiryProcessor.java @@ -6,7 +6,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import lombok.extern.slf4j.Slf4j; -import tech.ebp.oqm.core.api.config.BaseStationInteractingEntity; +import tech.ebp.oqm.core.api.config.CoreApiInteractingEntity; import tech.ebp.oqm.core.api.model.object.history.events.item.expiry.ItemExpiryEvent; import tech.ebp.oqm.core.api.model.object.storage.items.InventoryItem; import tech.ebp.oqm.core.api.service.mongo.InventoryItemService; @@ -28,7 +28,7 @@ public class ExpiryProcessor { HistoryEventNotificationService eventNotificationService; @Inject - BaseStationInteractingEntity baseStationInteractingEntity; + CoreApiInteractingEntity coreApiInteractingEntity; @Inject InventoryItemService inventoryItemService; @@ -66,7 +66,7 @@ public void searchAndProcessExpiring() { if (!expiryEvents.isEmpty()) { this.inventoryItemService.update(curEntry.getDbId().toHexString(), cur); for (ItemExpiryEvent curEvent : expiryEvents) { - curEvent.setEntity(this.baseStationInteractingEntity.getId()); + curEvent.setEntity(this.coreApiInteractingEntity.getId()); this.inventoryItemService.addHistoryFor(curEntry.getDbId().toHexString(), cur, null, curEvent);//TODO:: pass BS entity? this.eventNotificationService.sendEvent(curEntry.getDbId(), this.inventoryItemService.getClazz(), curEvent);//TODO:: handle potential threadedness? } diff --git a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InteractingEntityService.java b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InteractingEntityService.java index 6863d3092..73b295664 100644 --- a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InteractingEntityService.java +++ b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InteractingEntityService.java @@ -13,7 +13,7 @@ import org.bson.types.ObjectId; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.jwt.JsonWebToken; -import tech.ebp.oqm.core.api.config.BaseStationInteractingEntity; +import tech.ebp.oqm.core.api.config.CoreApiInteractingEntity; import tech.ebp.oqm.core.api.model.collectionStats.CollectionStats; import tech.ebp.oqm.core.api.model.object.history.ObjectHistoryEvent; import tech.ebp.oqm.core.api.model.object.interactingEntity.InteractingEntity; @@ -21,6 +21,7 @@ import tech.ebp.oqm.core.api.service.mongo.exception.DbNotFoundException; import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; import static com.mongodb.client.model.Filters.and; import static com.mongodb.client.model.Filters.eq; @@ -32,34 +33,34 @@ public class InteractingEntityService extends TopLevelMongoService container = Arc.container().instance(BaseStationInteractingEntity.class)){ - baseStationInteractingEntityArc = container.get(); + CoreApiInteractingEntity coreApiInteractingEntityArc; + try(InstanceHandle container = Arc.container().instance(CoreApiInteractingEntity.class)){ + coreApiInteractingEntityArc = container.get(); } - if(baseStationInteractingEntityArc == null){ + if(coreApiInteractingEntityArc == null){ return; } //force getting around Arc subclassing out the injected class - BaseStationInteractingEntity baseStationInteractingEntity = new BaseStationInteractingEntity( - baseStationInteractingEntityArc.getEmail() + CoreApiInteractingEntity coreApiInteractingEntity = new CoreApiInteractingEntity( + coreApiInteractingEntityArc.getEmail() ); //ensure we have the base station in the db - try{ - this.get(baseStationInteractingEntity.getId()); - this.update(baseStationInteractingEntity); - log.info("Updated base station interacting entity entry."); - } catch(DbNotFoundException e){ - this.add(baseStationInteractingEntity); - log.info("Added base station interacting entity entry."); + CoreApiInteractingEntity gotten = (CoreApiInteractingEntity) this.get(coreApiInteractingEntity.getId()); + if(gotten == null){ + this.add(coreApiInteractingEntity); + log.info("Added core api interacting entity entry."); + } else { + this.update(coreApiInteractingEntity); + log.info("Updated core api interacting entity entry."); } } @@ -86,7 +87,7 @@ private Optional get(SecurityContext securityContext, JsonWeb ); } - public InteractingEntity get(ObjectId id){ + public InteractingEntity get(ObjectId id) { return this.getCollection().find(eq("_id", id)).limit(1).first(); } @@ -105,28 +106,32 @@ protected void update(InteractingEntity entity){ @WithSpan public InteractingEntity ensureEntity(SecurityContext context, JsonWebToken jwt) { InteractingEntity entity = null; - Optional returningEntityOp = this.get(context, jwt); - if(returningEntityOp.isEmpty()){ - log.info("New entity interacting with system."); - if(this.basicAuthEnabled){ - entity = InteractingEntity.createEntity(context); + try{ //TODO:: test this for performance. Any way around making the whole thing a critical section? + this.ensureUserLock.lock(); + + Optional returningEntityOp = this.get(context, jwt); + if(returningEntityOp.isEmpty()){ + log.info("New entity interacting with system."); + if(this.basicAuthEnabled){ + entity = InteractingEntity.createEntity(context); + } else { + entity = InteractingEntity.createEntity(jwt); + } } else { - entity = InteractingEntity.createEntity(jwt); - + log.debug("Existing entity interacting with system."); + entity = returningEntityOp.get(); } - - } else { - log.info("Returning entity interacting with system."); - entity = returningEntityOp.get(); - } - - if(entity.getId() == null){ - this.add(entity); - } else if (!this.basicAuthEnabled && entity.updateFrom(jwt)) { - this.update(entity); - log.info("Entity has been updated."); + + if(entity.getId() == null){ + this.add(entity); + } else if (!this.basicAuthEnabled && entity.updateFrom(jwt)) { + this.update(entity); + log.info("Entity has been updated."); + } + } finally { + this.ensureUserLock.unlock(); } - + return entity; } diff --git a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InventoryItemService.java b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InventoryItemService.java index 7bd534b78..beb0bb5b9 100644 --- a/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InventoryItemService.java +++ b/software/oqm-core-api/src/main/java/tech/ebp/oqm/core/api/service/mongo/InventoryItemService.java @@ -14,7 +14,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bson.types.ObjectId; -import tech.ebp.oqm.core.api.config.BaseStationInteractingEntity; +import tech.ebp.oqm.core.api.config.CoreApiInteractingEntity; import tech.ebp.oqm.core.api.model.collectionStats.InvItemCollectionStats; import tech.ebp.oqm.core.api.model.object.history.ObjectHistoryEvent; import tech.ebp.oqm.core.api.model.object.history.events.item.ItemAddEvent; @@ -60,7 +60,7 @@ public class InventoryItemService extends MongoHistoriedObjectService entityResult = this.interactingEntityService.search(new InteractingEntitySearch().setName(testUser.getName())); + log.debug("Initial entity state: {}", entityResult); + assertTrue(entityResult.isEmpty(), "Entity database was not clean."); + + ValidatableResponse response = TestRestUtils.setupJwtCall(given(), TestUserService.getInstance().getUserToken(testUser)) + .get() + .then() + .statusCode(200); + // TODO:: test user is in response + + entityResult = this.interactingEntityService.search(new InteractingEntitySearch().setName(testUser.getName())); + log.debug("Entity state after first call: {}", entityResult); + assertEquals(1, entityResult.getNumResults(), "Entity database Did not contain user."); + + response = TestRestUtils.setupJwtCall(given(), TestUserService.getInstance().getUserToken(testUser)) + .get() + .then() + .statusCode(200); + // TODO:: test user is in response + + entityResult = this.interactingEntityService.search(new InteractingEntitySearch().setName(testUser.getName())); + log.debug("Entity state after first call: {}", entityResult); + assertEquals(1, entityResult.getNumResults(), "Entity database Did not contain user only once."); + } + + @Test + public void ensureMultiThreadedTest(){ + User testUser = TestUserService.getInstance().getTestUser(true); + + SearchResult entityResult = this.interactingEntityService.search(new InteractingEntitySearch().setName(testUser.getName())); + log.debug("Initial entity state: {}", entityResult); + assertTrue(entityResult.isEmpty(), "Entity database was not clean."); + + //TODO:: unsure if actually properly multithreading + UniJoin.Builder multiThreadBuilder = Uni.join().builder(); + + for(int i = 0; i < 50; i++){ + int finalI = i; + multiThreadBuilder.add( + Uni.createFrom().item(()-> { + log.debug("Sending request {}", finalI); + return TestRestUtils.setupJwtCall(given(), TestUserService.getInstance().getUserToken(testUser)) + .get() + .then() + .statusCode(200); + }) + ); + } + + List responses = multiThreadBuilder.joinAll() + .andCollectFailures() + .runSubscriptionOn(this.executorService) + .await().indefinitely(); + + entityResult = this.interactingEntityService.search(new InteractingEntitySearch().setName(testUser.getName())); + log.debug("Entity state after calls: {}", entityResult); + assertEquals(1, entityResult.getNumResults(), "Entity database Did not contain user only once."); + } + + +} diff --git a/software/oqm-core-base-station/build.gradle b/software/oqm-core-base-station/build.gradle index 223129f9c..6810fb686 100644 --- a/software/oqm-core-base-station/build.gradle +++ b/software/oqm-core-base-station/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'com.ebp.openQuarterMaster' -version '1.3.1' +version '1.4.0-DEV' repositories { mavenCentral() @@ -17,11 +17,9 @@ dependencies { implementation 'io.quarkus:quarkus-rest' implementation 'io.quarkus:quarkus-rest-jackson' implementation 'io.quarkus:quarkus-rest-qute' -// implementation("io.quarkus:quarkus-websockets") implementation 'io.quarkus:quarkus-rest-client' implementation 'io.quarkus:quarkus-rest-client-jackson' implementation 'io.quarkus:quarkus-oidc' -// implementation 'io.quarkus:quarkus-oidc-token-propagation-reactive' implementation 'io.quarkus:quarkus-config-yaml' implementation 'io.quarkus:quarkus-smallrye-health' implementation 'io.quarkus:quarkus-container-image-jib' @@ -44,12 +42,12 @@ dependencies { implementation 'org.webjars:bootstrap:5.3.2' testImplementation 'io.quarkus:quarkus-junit5' - testImplementation "org.junit.jupiter:junit-jupiter-params:5.10.2" + testImplementation "org.junit.jupiter:junit-jupiter-params:5.10.3" testImplementation 'io.rest-assured:rest-assured' - testImplementation 'net.datafaker:datafaker:2.2.2' + testImplementation 'net.datafaker:datafaker:2.3.0' - implementation 'com.microsoft.playwright:playwright:1.44.0' - implementation 'com.deque.html.axe-core:playwright:4.9.1' + testImplementation 'com.microsoft.playwright:playwright:1.45.0' + testImplementation 'com.deque.html.axe-core:playwright:4.9.1' } java { diff --git a/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/rest/Printouts.java b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/rest/Printouts.java new file mode 100644 index 000000000..cb04fcf51 --- /dev/null +++ b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/rest/Printouts.java @@ -0,0 +1,63 @@ +package tech.ebp.oqm.core.baseStation.interfaces.rest; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.eclipse.microprofile.openapi.annotations.tags.Tags; +import tech.ebp.oqm.core.baseStation.model.printouts.InventorySheetsOptions; +import tech.ebp.oqm.core.baseStation.service.printout.StorageBlockInventorySheetService; +import tech.ebp.oqm.core.baseStation.utils.Roles; + +@Slf4j +@Path("/api/media/printouts") +@Tags({@Tag(name = "Media", description = "Endpoints for printouts")}) +@RolesAllowed(Roles.INVENTORY_VIEW) +@ApplicationScoped +public class Printouts extends ApiProvider { + + @Inject + StorageBlockInventorySheetService storageBlockInventorySheetService; + + @GET + @Path("storage-block/{storageBlockId}/storageSheet") + @Operation( + summary = "Creates a bundle of all inventory data stored." + ) + @APIResponse( + responseCode = "200", + description = "Export bundle created.", + content = @Content( + mediaType = "application/pdf" + ) + ) + @APIResponse( + responseCode = "400", + description = "Bad request given. Data given could not pass validation.", + content = @Content(mediaType = "text/plain") + ) + @RolesAllowed(Roles.INVENTORY_VIEW) + @Produces("application/pdf") + public Response getSheetPdf( + @PathParam("storageBlockId") String storageBlockId, + @BeanParam InventorySheetsOptions options + ) throws Throwable { + Response.ResponseBuilder response = Response.ok( + this.storageBlockInventorySheetService.getPdfInventorySheet( + this.getUserInfo(), + this.getBearerHeaderStr(), + this.getSelectedDb(), + storageBlockId, + options + ) + ); + response.header("Content-Disposition", "attachment;filename=storageSheet-" + storageBlockId + ".pdf"); + return response.build(); + } +} diff --git a/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/ui/pages/StorageBlockUi.java b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/ui/pages/StorageBlockUi.java index 4c641365b..82db483d8 100644 --- a/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/ui/pages/StorageBlockUi.java +++ b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/ui/pages/StorageBlockUi.java @@ -19,6 +19,8 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.eclipse.microprofile.openapi.annotations.tags.Tags; import org.eclipse.microprofile.rest.client.inject.RestClient; +import tech.ebp.oqm.core.baseStation.model.printouts.PageOrientation; +import tech.ebp.oqm.core.baseStation.model.printouts.PageSizeOption; import tech.ebp.oqm.core.baseStation.utils.Roles; import tech.ebp.oqm.lib.core.api.quarkus.runtime.restClient.OqmCoreApiClientService; import tech.ebp.oqm.lib.core.api.quarkus.runtime.restClient.searchObjects.ItemCategorySearch; @@ -53,7 +55,9 @@ public Uni storagePage(@BeanParam StorageBlockSearch search) { return this.getUni( this.setupPageTemplate() - .data("showSearch", false), + .data("showSearch", false) + .data("pageSizeOptions", PageSizeOption.values()) + .data("pageOrientationOptions", PageOrientation.values()), Map.of( "allCategorySearchResults", this.coreApiClient.itemCatSearch(this.getBearerHeaderStr(), this.getSelectedDb(), new ItemCategorySearch()), "searchResults", this.coreApiClient.storageBlockSearch(this.getBearerHeaderStr(), this.getSelectedDb(), search).call((ObjectNode results)->{ diff --git a/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/JacksonHelpersService.java b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/JacksonHelpersService.java new file mode 100644 index 000000000..fb341c008 --- /dev/null +++ b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/JacksonHelpersService.java @@ -0,0 +1,17 @@ +package tech.ebp.oqm.core.baseStation.service; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Named; + +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +@ApplicationScoped +@Named("JacksonHelpersService") +public class JacksonHelpersService { + + public Stream getStreamFromJsonArr(ArrayNode jsonArr){ + return StreamSupport.stream(jsonArr.spliterator(), false); + } +} diff --git a/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/PrintoutDataService.java b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/PrintoutDataService.java new file mode 100644 index 000000000..e63ce53fc --- /dev/null +++ b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/PrintoutDataService.java @@ -0,0 +1,16 @@ +package tech.ebp.oqm.core.baseStation.service.printout; + +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; + +import java.time.ZonedDateTime; + +public abstract class PrintoutDataService { + + protected TemplateInstance setupBasicPrintoutData( + Template template + ){ + return template.data("generateDatetime", ZonedDateTime.now()); + } + +} \ No newline at end of file diff --git a/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/StorageBlockInventorySheetService.java b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/StorageBlockInventorySheetService.java new file mode 100644 index 000000000..a69c053aa --- /dev/null +++ b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/StorageBlockInventorySheetService.java @@ -0,0 +1,189 @@ +package tech.ebp.oqm.core.baseStation.service.printout; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.itextpdf.html2pdf.ConverterProperties; +import com.itextpdf.html2pdf.HtmlConverter; +import com.itextpdf.kernel.geom.PageSize; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfWriter; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.quarkus.qute.Location; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateInstance; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import tech.ebp.oqm.core.baseStation.model.UserInfo; +import tech.ebp.oqm.core.baseStation.model.printouts.InventorySheetsOptions; +import tech.ebp.oqm.core.baseStation.model.printouts.PageOrientation; +import tech.ebp.oqm.lib.core.api.quarkus.runtime.restClient.OqmCoreApiClientService; +import tech.ebp.oqm.lib.core.api.quarkus.runtime.restClient.searchObjects.InventoryItemSearch; +import tech.ebp.oqm.lib.core.api.quarkus.runtime.restClient.searchObjects.StorageBlockSearch; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Predicate; +import java.util.stream.StreamSupport; + +@Slf4j +@ApplicationScoped +public class StorageBlockInventorySheetService extends PrintoutDataService { + + private static final DateTimeFormatter FILENAME_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("MM-dd-yyyy_kk-mm"); + + private static final String EXPORT_TEMP_DIR_PREFIX = "oqm-sheets"; + + private static final ConverterProperties CONVERTER_PROPERTIES; + + static { + CONVERTER_PROPERTIES = new ConverterProperties() + .setBaseUri(ConfigProvider.getConfig().getValue("runningInfo.baseUrl", String.class)); + } + + @RestClient + OqmCoreApiClientService coreApiClientService; + + @Inject + @Location("printouts/storageBlockInvSheet/storageBlockInventorySheet.html") + Template inventorySheetTemplate; + + private File getTempPdfFile(String name) throws IOException { + java.nio.file.Path tempDirPath = Files.createTempDirectory(EXPORT_TEMP_DIR_PREFIX); + File tempDir = tempDirPath.toFile(); + tempDir.deleteOnExit(); + String exportFileName = + "oqm_storage_sheet_" + name + "_" + ZonedDateTime.now().format(FILENAME_TIMESTAMP_FORMAT) + ".pdf"; + return new File(tempDir, exportFileName); + } + + private TemplateInstance getHtmlInventorySheet( + ObjectNode storageBlock, + ObjectNode childrenSr, + ObjectNode itemsInBlockSr, + InventorySheetsOptions options + ) { + Predicate simpleAmountFilter = new Predicate() { + @Override + public boolean test(ObjectNode inventoryItem) { + return "AMOUNT_SIMPLE".equals(inventoryItem.get("storageType").asText()); + } + }; + Predicate listAmountFilter = new Predicate() { + @Override + public boolean test(ObjectNode inventoryItem) { + return "AMOUNT_LIST".equals(inventoryItem.get("storageType").asText()); + } + }; + Predicate trackedFilter = new Predicate() { + @Override + public boolean test(ObjectNode inventoryItem) { + return "TRACKED".equals(inventoryItem.get("storageType").asText()); + } + }; + + + + return this.setupBasicPrintoutData(this.inventorySheetTemplate) + .data("simpleAmountFilter", simpleAmountFilter) + .data("listAmountFilter", listAmountFilter) + .data("trackedFilter", trackedFilter) + .data("options", options) + .data("storageBlock", storageBlock) + .data("storageBlockChildrenSearchResults", childrenSr) + .data("searchResult", itemsInBlockSr) + ; + } + + /** + * https://kb.itextpdf.com/home/it7kb/ebooks/itext-7-converting-html-to-pdf-with-pdfhtml https://www.baeldung.com/java-pdf-creation + * https://www.baeldung.com/java-html-to-pdf + * + * @param entity + * @param storageBlockId + * + * @return + * @throws IOException + */ + @WithSpan + public File getPdfInventorySheet( + UserInfo entity, + String oqmApiToken, + String oqmDbIdOrName, + String storageBlockId, + InventorySheetsOptions options + ) throws Throwable { + log.info("Getting inventory sheet for block {} with options: {}", storageBlockId, options); + + ObjectNode block; + ObjectNode storageBlockChildrenSr; + ObjectNode itemsInBlockSr; + { + CompletableFuture blockGetFut = this.coreApiClientService.storageBlockGet(oqmApiToken, oqmDbIdOrName, storageBlockId) + .subscribeAsCompletionStage(); + CompletableFuture blockChildrenGetFut = this.coreApiClientService.storageBlockSearch( + oqmApiToken, oqmDbIdOrName, + StorageBlockSearch.builder().isChildOf(storageBlockId).build() + ) + .subscribeAsCompletionStage(); + CompletableFuture itemsInBlockGetFut = this.coreApiClientService.invItemSearch( + oqmApiToken, + oqmDbIdOrName, + InventoryItemSearch.builder().inStorageBlocks(List.of(storageBlockId)).build() + ).subscribeAsCompletionStage(); + + try { + block = blockGetFut.join(); + storageBlockChildrenSr = blockChildrenGetFut.join(); + itemsInBlockSr = itemsInBlockGetFut.join(); + } catch(CompletionException e){ + throw e.getCause(); + } + } + + File outputFile = getTempPdfFile(storageBlockId); + + try ( + PdfWriter writer = new PdfWriter(outputFile); + ) { + PdfDocument doc = new PdfDocument(writer); + + { + PageSize size = new PageSize(options.getPageSize().size); + + if (PageOrientation.LANDSCAPE.equals(options.getPageOrientation())) { + size = size.rotate(); + } + doc.setDefaultPageSize(size); + } + + doc.getDocumentInfo().addCreationDate(); + doc.getDocumentInfo().setCreator("Open QuarterMaster Base Station"); + doc.getDocumentInfo().setProducer("Open QuarterMaster Base Station"); + doc.getDocumentInfo().setAuthor(entity.getName() + " via Open QuarterMaster Base Station"); + doc.getDocumentInfo().setSubject("Inventory sheet for " + block.get("label").asText()); + doc.getDocumentInfo().setTitle(block.get("labelText").asText() + " Inventory Sheet"); + doc.getDocumentInfo().setKeywords("inventory, sheet, " + storageBlockId); + + + String html = this.getHtmlInventorySheet( + block, + storageBlockChildrenSr, + itemsInBlockSr, + options + ).render(); + log.debug("Html generated: {}", html); + HtmlConverter.convertToPdf(html, doc, CONVERTER_PROPERTIES); + } + return outputFile; + } +} \ No newline at end of file diff --git a/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/reports/ReportGenerator.java b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/reports/ReportGenerator.java new file mode 100644 index 000000000..b1c11d09e --- /dev/null +++ b/software/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/service/printout/reports/ReportGenerator.java @@ -0,0 +1,5 @@ +package tech.ebp.oqm.core.baseStation.service.printout.reports; + +public abstract class ReportGenerator { + //TODO:: generic class to define report generating methods +} diff --git a/software/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/storedView.js b/software/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/storedView.js index c18f6f539..66a91e127 100644 --- a/software/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/storedView.js +++ b/software/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/item/storedView.js @@ -1,6 +1,6 @@ const StoredView = { getBlockViewCell(name, value) { - let output = $('

'); + let output = $('

'); output.find("h5").text(name); output.find("p").text(value); @@ -59,9 +59,9 @@ const StoredView = { output.html(Links.getStorageViewButton(storageBlockId, 'View in Storage')); if (small) { - output.addClass("col-1"); + output.addClass("col-sm-6 col-xs-6 col-md-4 col-lg-2"); } else { - output.addClass("col"); + output.addClass("col-sm-6 col-xs-6 col-md-4 col-lg-2"); } return output; @@ -81,9 +81,9 @@ const StoredView = { output.append(checkoutButton); if (small) { - output.addClass("col-1"); + output.addClass("col-sm-6 col-xs-6 col-md-4 col-lg-2"); } else { - output.addClass("col"); + output.addClass("col-sm-6 col-xs-6 col-md-4 col-lg-2"); } return output; diff --git a/software/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/media/fileAttachment/FileAttachmentView.js b/software/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/media/fileAttachment/FileAttachmentView.js index 0c512aa65..752995b9f 100644 --- a/software/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/media/fileAttachment/FileAttachmentView.js +++ b/software/oqm-core-base-station/src/main/resources/META-INF/resources/res/js/obj/media/fileAttachment/FileAttachmentView.js @@ -13,6 +13,16 @@ const FileAttachmentView = { numRevisions: $("#fileAttachmentViewRevisionsExpandNumRevisions"), revisionsAccord: $("#fileAttachmentViewRevisionsAccord"), + async getFileContent(dataUrl) { + return await Rest.call({ + url: dataUrl, + async: false, + returnType: "text", + done: function (data) { + return data; + } + }); + }, resetView() { this.previewContainer.text(""); this.fullViewContainer.text(""); @@ -27,13 +37,13 @@ const FileAttachmentView = { KeywordAttUtils.clearHideKeywordDisplay(this.keywords); KeywordAttUtils.clearHideAttDisplay(this.atts); }, - setupFileView(fileGetData, container, preview = true) { + async setupFileView(fileGetData, container, preview = true) { let latestMetadata = fileGetData.revisions[fileGetData.revisions.length - 1]; - let newContent; + let newContent = null; let dataUrl = Rest.passRoot + '/media/fileAttachment/' + fileGetData.id + '/revision/latest/data'; if (latestMetadata.mimeType.startsWith("audio/")) { newContent = $(' '); newContent.on("stalled", function (e) { let code = newContent[0].error.code @@ -42,7 +52,7 @@ const FileAttachmentView = { }); } else if (latestMetadata.mimeType.startsWith("video/")) { newContent = $(' '); newContent.on("stalled", function (e) { let code = newContent[0].error.code @@ -50,14 +60,28 @@ const FileAttachmentView = { console.log("Error code: ", code); }); } else if (latestMetadata.mimeType.startsWith("image/")) { - newContent = $(' \n'); + newContent = $(' \n'); } else if (!preview) {//only show these if we are not previewing - if (latestMetadata.mimeType === "application/pdf") { - //TODO:: neither of these work - newContent = $('

Failed to load pdf.

'); - // newContent = $(''); + switch (latestMetadata.mimeType) { + case "application/pdf": + newContent = $(''); + break; + case "text/plain": + case "text/x-yaml": + case "text/css": + case "text/x-java-properties": + case "application/x-sh": + case "application/json": + case "application/javascript": //TODO:: make this a complete list + let textContent = await this.getFileContent(dataUrl); + newContent = $('
' + textContent + '
'); + break; + case "text/x-web-markdown": + //TODO:: parse into markdown, using good lib + let mdContent = await this.getFileContent(dataUrl); + newContent = $('
' + mdContent + '
'); + break; } - //TODO:: show pdf, text, markdown? } container.append(newContent); }, diff --git a/software/oqm-core-base-station/src/main/resources/templates/printouts/mainPrintoutTemplate.html b/software/oqm-core-base-station/src/main/resources/templates/printouts/mainPrintoutTemplate.html index 225528076..c9913161e 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/printouts/mainPrintoutTemplate.html +++ b/software/oqm-core-base-station/src/main/resources/templates/printouts/mainPrintoutTemplate.html @@ -26,7 +26,7 @@ /*border: black 1px solid;*/ } @bottom-center { - content: "As of {generateDatetime.format(dateTimeFormatter)}" + content: "As of {cdi:DateTimeService.formatForUi(generateDatetime)}" } } diff --git a/software/oqm-core-base-station/src/main/resources/templates/printouts/storageBlockInvSheet/storageBlockInventorySheet.html b/software/oqm-core-base-station/src/main/resources/templates/printouts/storageBlockInvSheet/storageBlockInventorySheet.html index 678963d91..dc1aa0a85 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/printouts/storageBlockInvSheet/storageBlockInventorySheet.html +++ b/software/oqm-core-base-station/src/main/resources/templates/printouts/storageBlockInvSheet/storageBlockInventorySheet.html @@ -1,25 +1,25 @@ {#include printouts/mainPrintoutTemplate} - {#title}{storageBlock.getLabelText()} - Inventory sheet{/title} + {#title}{storageBlock.get("labelText").asText()} - Inventory sheet{/title} {#body}

- {storageBlock.getLabelText()} - Inventory sheet + {storageBlock.get("labelText").asText()} - Inventory sheet

- {#printouts/storageBlockTable}{/printouts/storageBlockTable} - -

Total Items: {searchResult.getNumResultsForEntireQuery()}

- {#if searchResult.isEmpty()} + {#printouts/storageBlockTable storageBlockSr=storageBlockChildrenSearchResults options=options}{/printouts/storageBlockTable} + +

Total Items: {searchResult.get("numResultsForEntireQuery").asText()}

+ {#if searchResult.get("numResults").asInt() == 0}

No Items Stored

- {#else} - {#let curItemList=searchResult.getResults().stream().filter(simpleAmountFilter).toArray()} - {#printouts/storageBlockInventorySheet/amountSimpleStoredTable}{/printouts/storageBlockInventorySheet/amountSimpleStoredTable} + {#else} + {#let curItemList=cdi:JacksonHelpersService.getStreamFromJsonArr(searchResult.get("results")).filter(simpleAmountFilter).toArray()} + {#printouts/storageBlockInventorySheet/amountSimpleStoredTable curItemList=curItemList storageBlock=storageBlock options=options}{/printouts/storageBlockInventorySheet/amountSimpleStoredTable} {/let} - {#let curItemList=searchResult.getResults().stream().filter(listAmountFilter).toArray()} - {#printouts/storageBlockInventorySheet/amountListStoredTable}{/printouts/storageBlockInventorySheet/amountListStoredTable} + {#let curItemList=cdi:JacksonHelpersService.getStreamFromJsonArr(searchResult.get("results")).filter(listAmountFilter).toArray()} + {#printouts/storageBlockInventorySheet/amountListStoredTable curItemList=curItemList storageBlock=storageBlock options=options}{/printouts/storageBlockInventorySheet/amountListStoredTable} {/let} - {#let curItemList=searchResult.getResults().stream().filter(trackedFilter).toArray()} - {#printouts/storageBlockInventorySheet/trackedStoredTable}{/printouts/storageBlockInventorySheet/trackedStoredTable} + {#let curItemList=cdi:JacksonHelpersService.getStreamFromJsonArr(searchResult.get("results")).filter(trackedFilter).toArray()} + {#printouts/storageBlockInventorySheet/trackedStoredTable curItemList=curItemList storageBlock=storageBlock options=options}{/printouts/storageBlockInventorySheet/trackedStoredTable} {/let} {/if} {/body} diff --git a/software/oqm-core-base-station/src/main/resources/templates/tags/fileAttachment/FileAttachmentViewModal.html b/software/oqm-core-base-station/src/main/resources/templates/tags/fileAttachment/FileAttachmentViewModal.html index 7c784311f..6917b7744 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/tags/fileAttachment/FileAttachmentViewModal.html +++ b/software/oqm-core-base-station/src/main/resources/templates/tags/fileAttachment/FileAttachmentViewModal.html @@ -66,7 +66,7 @@

{/footerButtons}
-
+
{/modal} \ No newline at end of file diff --git a/software/oqm-core-base-station/src/main/resources/templates/tags/icons/baseStation.html b/software/oqm-core-base-station/src/main/resources/templates/tags/icons/coreApi.html similarity index 100% rename from software/oqm-core-base-station/src/main/resources/templates/tags/icons/baseStation.html rename to software/oqm-core-base-station/src/main/resources/templates/tags/icons/coreApi.html diff --git a/software/oqm-core-base-station/src/main/resources/templates/tags/icons/interactingEntityType.html b/software/oqm-core-base-station/src/main/resources/templates/tags/icons/interactingEntityType.html index e64b79351..d83ac525d 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/tags/icons/interactingEntityType.html +++ b/software/oqm-core-base-station/src/main/resources/templates/tags/icons/interactingEntityType.html @@ -5,6 +5,6 @@ {#icons/extService}{/icons/extService} {#case "SERVICE_PLUGIN"} {#icons/extService}{/icons/extService} - {#case "BASE_STATION"} - {#icons/baseStation}{/icons/baseStation} + {#case "CORE_API"} + {#icons/coreApi}{/icons/coreApi} {/switch} \ No newline at end of file diff --git a/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/amountListStoredTable.html b/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/amountListStoredTable.html index f9e8b600e..7465f13f1 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/amountListStoredTable.html +++ b/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/amountListStoredTable.html @@ -38,43 +38,43 @@

List Amount Items ({curItemList.length})

{#for curItem in curItemList} - {#let curWrapper = curItem.getStoredWrapperForStorage(storageBlock.getId())} - {#for curAmountStored in curWrapper.getStored()} + {#let curWrapper = curItem.get("storageMap").get(storageBlock.get("id").asText())} + {#for curAmountStored in curWrapper.get("stored")} {#if curAmountStored_count == 1} {#if options.includeNumCol} - + {curItem_count} {/if} {#if options.includeImageCol} - {#if ! curItem.getImageIds().isEmpty()} - - {#let curImage = imageService.get(curItem.getImageIds().get(0))} - Image for item {curItem.getName()} + {#let curImage = imageService.get(curItem.get("imageIds").get(0).asText())} + Image for item {curItem.get( {/let} {#else} - + {/if} {/if} - - {curItem.getName()} + + {curItem.get("name").asText()} - - {curItem.getTotal().getValue()}{curItem.getTotal().getUnit().getSymbol()} + + {curItem.get("total").get("value").asDouble()}{curItem.get("total").get("unit").get("symbol").asText()} {/if} - {curAmountStored.getAmount().getValue()}{curAmountStored.getAmount().getUnit().getSymbol()} + {curAmountStored.get("amount").get("value").asDouble()}{curAmountStored.get("amount").get("unit").get("symbol").asText()} {#if options.includeConditionCol} - {curAmountStored.getCondition()} - {curAmountStored.getConditionNotes()} + {curAmountStored.get("condition").asDouble()} + {curAmountStored.get("conditionNotes").asText()} {/if} - {#if curAmountStored.getExpires() != null} - {curAmountStored.getNotificationStatus().isExpired()} + {#if curAmountStored.get("expires").asBoolean() != null} + {curAmountStored.get("notificationStatus").get("expired").asBoolean()} {#else} N/A {/if} diff --git a/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/amountSimpleStoredTable.html b/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/amountSimpleStoredTable.html index 095e81308..d70c6f11b 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/amountSimpleStoredTable.html +++ b/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/amountSimpleStoredTable.html @@ -35,7 +35,7 @@

Simple Amount Items ({curItemList.length})

{#for curItem in curItemList} - {#let curWrapper = curItem.getStoredWrapperForStorage(storageBlock.getId())} + {#let curWrapper = curItem.get("storageMap").get(storageBlock.get("id").asText())} {#if options.includeNumCol} @@ -43,7 +43,7 @@

Simple Amount Items ({curItemList.length})

{/if} {#if options.includeImageCol} - {#if ! curItem.getImageIds().isEmpty()} + {#if ! curItem.get("imageIds").isEmpty()} {#let curImage = imageService.get(curItem.getImageIds().get(0))} Image for item {curItem.getName()}Simple Amount Items ({curItemList.length})

{/if} {/if} - {curItem.getName()} + {curItem.get("name").asText()} - {curWrapper.getTotal().getValue()}{curWrapper.getTotal().getUnit().getSymbol()} + {curWrapper.get("total").get("value").asInt()}{curWrapper.get("total").get("unit").get("symbol").asText()} {#if options.includeConditionCol} - {curWrapper.getStored().getCondition()} - {curWrapper.getStored().getConditionNotes()} + {curWrapper.get("stored").get("condition").asInt()}% + {curWrapper.get("stored").get("conditionNotes").asText()} {/if} - {#if curWrapper.getStored().getExpires() != null} - {curWrapper.getStored().getNotificationStatus().isExpired()} + {#if curWrapper.get("stored").get("expires").asText() != null} + {curWrapper.get("stored").get("notificationStatus").get("expired").asBoolean()} {/if} diff --git a/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/trackedStoredTable.html b/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/trackedStoredTable.html index 470743ed6..ae8d716cb 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/trackedStoredTable.html +++ b/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockInventorySheet/trackedStoredTable.html @@ -44,47 +44,47 @@

Tracked Items ({curItemList.length})

{#for curItem in curItemList} - {#let curWrapper = curItem.getStoredWrapperForStorage(storageBlock.getId())} - {#for curTrackedStored in curWrapper.getStored().values()} + {#let curWrapper = curItem.get("storageMap").get(storageBlock.get("id").asText())} + {#for curTrackedStored in curWrapper.get("stored").elements()} {#if curTrackedStored_count == 1} {#if options.includeNumCol} - + {curItem_count} {/if} {#if options.includeImageCol} - {#if ! curItem.getImageIds().isEmpty()} - - {#let curImage = imageService.get(curItem.getImageIds().get(0))} - Image for item {curItem.getName()} + {#let curImage = imageService.get(curItem.get("imageIds").get(0))} + Image for item {curItem.get( {/let} {#else} - + {/if} {/if} - - {curItem.getName()} + + {curItem.get("name").asText()} - - {curItem.getTotal().getValue()}{curItem.getTotal().getUnit().getSymbol()} + + {curItem.get("total").get("value").asDouble()}{curItem.get("total").get("unit").get("symbol").asText()} - - {curItem.getTrackedItemIdentifierName()} + + {curItem.get("trackedItemIdentifierName").asText()} {/if} - {curTrackedStored.getIdentifier()} - {curTrackedStored.getIdentifyingDetails()} + {curTrackedStored.get("identifier").asText()} + {curTrackedStored.get("identifyingDetails").asText()} {#if options.includeConditionCol} - {curTrackedStored.getCondition()} - {curTrackedStored.getConditionNotes()} + {curTrackedStored.get("condition").asDouble()}% + {curTrackedStored.get("conditionNotes").asText()} {/if} - {#if curTrackedStored.getExpires() != null} - {curTrackedStored.getNotificationStatus().isExpired()} + {#if curTrackedStored.get("expires").asBoolean() != null} + {curTrackedStored.get("notificationStatus").get("expired").asBoolean()} {#else} N/A {/if} diff --git a/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockTable.html b/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockTable.html index 6e5e09be1..acb094a06 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockTable.html +++ b/software/oqm-core-base-station/src/main/resources/templates/tags/printouts/storageBlockTable.html @@ -1,6 +1,6 @@ -{#if storageBlockList.size() > 0} +{#if storageBlockSr.get("numResults").asInt() > 0}
-

Storage blocks ({storageBlockList.size()})

+

Storage blocks ({storageBlockSr.get("numResults").asInt()})

@@ -16,7 +16,7 @@

Storage blocks ({storageBlockList.size()})

- {#for curBlock in storageBlockList} + {#for curBlock in storageBlockSr.get("results")} {#if options.includeNumCol} {/if} {#if options.includeImageCol} - {#if ! curBlock.getImageIds().isEmpty()} + {#if ! curBlock.get("imageIds").isEmpty()} @@ -35,9 +35,9 @@

Storage blocks ({storageBlockList.size()})

{/if} {/if} - - - + + + {/for} diff --git a/software/oqm-core-base-station/src/main/resources/templates/webui/pages/storage.html b/software/oqm-core-base-station/src/main/resources/templates/webui/pages/storage.html index b4c7c84b7..ea1ce84ca 100644 --- a/software/oqm-core-base-station/src/main/resources/templates/webui/pages/storage.html +++ b/software/oqm-core-base-station/src/main/resources/templates/webui/pages/storage.html @@ -205,62 +205,62 @@
{/search/storage/searchSelectModal} {#search/image/imageSearchSelectModal otherModalId="addEditModal"} {/search/image/imageSearchSelectModal} -{!!} -{!{#modal id='storageBlockInventorySheetPrintout' title='Inventory Sheet Printout' otherModalId="storageBlockViewModal" submitForm="storageBlockInventorySheetPrintoutForm" submitDismiss=false}!} - {!{#titleIcon}{#icons/print}{/icons/print}{/titleIcon}!} - {!
!} - {!
!} - {!
!} - {!!} - {!
!} - {!!} - {!
!} - {!
!} - {!!} - {!
!} - {!!} - {!
!} - {!!} - {!
!} - {!
!} - {!
!} - {!!} - {!
!} - {!!} - {!
!} - {!
!} - {!!} - {!
!} - {!
!} - {!Options!} - {!
!} - {!
!} - {!
!} - {!!} - {!!} - {!
!} - {!
!} - {!!} - {!!} - {!
!} - {!
!} - {!!} - {!!} - {!
!} - {!
!} - {!
!} - {!!} - {!
!} -{!{/modal}!} -{!!} + +{#modal id='storageBlockInventorySheetPrintout' title='Inventory Sheet Printout' otherModalId="storageBlockViewModal" submitForm="storageBlockInventorySheetPrintoutForm" submitDismiss=false} + {#titleIcon}{#icons/print}{/icons/print}{/titleIcon} +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ Options +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+{/modal} + {#modal id='storageBlockView' size='xl' title='Storage Block View'} {#titleIcon}{#icons/storageBlock}{/icons/storageBlock}{/titleIcon}
@@ -806,7 +806,7 @@

{#icons/items}{/icons/items} Items Stored:

function setupStorageBlockInvSheetPrintoutForm(){ storageBlockInventorySheetPrintoutForm[0].reset(); - storageBlockInventorySheetPrintoutForm.attr("action", Rest.passRoot + "/inventory/storage-block/"+storageBlockViewId.text()+"/storageSheet"); + storageBlockInventorySheetPrintoutForm.attr("action", Rest.apiRoot + "/media/printouts/storage-block/"+storageBlockViewId.text()+"/storageSheet"); storageBlockInventorySheetPrintoutFormBlockNameInput.val(storageBlockViewModalLabel.text()); } diff --git a/software/plugins/open-qm-plugin-demo/build.gradle b/software/plugins/open-qm-plugin-demo/build.gradle index a38433952..176b14151 100644 --- a/software/plugins/open-qm-plugin-demo/build.gradle +++ b/software/plugins/open-qm-plugin-demo/build.gradle @@ -35,7 +35,7 @@ dependencies { testImplementation 'io.rest-assured:rest-assured' - testImplementation 'net.datafaker:datafaker:2.2.2' + testImplementation 'net.datafaker:datafaker:2.3.0' } group 'com.ebp.openQuarterMaster'
@@ -24,10 +24,10 @@

Storage blocks ({storageBlockList.size()})

{#let curImage = imageService.get(curBlock.getImageIds().get(0))} - Image for storage block {curBlock.getLabelText()} {/let} {curBlock.getLabelText()}{curBlock.getLocation()}{curBlock.getDescription()}{curBlock.get("labelText").asText()}{curBlock.get("location").asText()}{curBlock.get("description").asText()}