From 51eb6dc352d7487d58796d98dd1716c0e9f80bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Pedersen?= Date: Wed, 20 Sep 2023 12:05:52 +0200 Subject: [PATCH] API / UI refactoring --- horreum-api/pom.xml | 2 +- .../tools/horreum/api/ConditionConfig.java | 3 + .../tools/horreum/api/data/DataSet.java | 4 + .../api/data/ExperimentComparison.java | 2 + .../tools/horreum/api/data/Extractor.java | 3 + .../tools/horreum/api/data/Label.java | 4 + .../hyperfoil/tools/horreum/api/data/Run.java | 9 +- .../tools/horreum/api/data/Schema.java | 9 ++ .../tools/horreum/api/data/Test.java | 8 +- .../tools/horreum/api/data/Transformer.java | 5 + .../horreum/api/data/ValidationError.java | 3 + .../horreum/api/services/DatasetService.java | 22 ++-- .../api/services/ExperimentService.java | 1 + .../horreum/api/services/QueryResult.java | 5 - .../horreum/api/services/RunService.java | 38 ++++-- .../horreum/api/services/SchemaService.java | 61 +++++++-- .../horreum/api/services/SqlService.java | 37 ------ .../horreum/api/services/TestService.java | 29 ++--- horreum-backend/pom.xml | 3 +- .../tools/horreum/mapper/TestMapper.java | 6 +- .../tools/horreum/svc/ActionServiceImpl.java | 30 ++++- .../tools/horreum/svc/DatasetServiceImpl.java | 37 +----- .../tools/horreum/svc/RunServiceImpl.java | 44 +++---- .../tools/horreum/svc/SchemaServiceImpl.java | 16 ++- .../tools/horreum/svc/SqlServiceImpl.java | 58 +++++++++ .../tools/horreum/svc/TestServiceImpl.java | 110 ++++------------ .../tools/horreum/svc/UIServiceImpl.java | 98 +++++++++++++++ .../tools/horreum/svc/BaseServiceTest.java | 45 ++++++- .../tools/horreum/svc/DatasetServiceTest.java | 10 +- .../tools/horreum/svc/TestServiceTest.java | 16 ++- horreum-client/pom.xml | 3 +- .../hyperfoil/tools/RunServiceExtension.java | 9 +- .../tools/horreum/api/client/RunService.java | 13 +- horreum-ui/pom.xml | 119 ++++++++++++++++++ .../tools/horreum/api/alerting/Change.java | 0 .../horreum/api/alerting/ChangeDetection.java | 1 + .../tools/horreum/api/alerting/DataPoint.java | 0 .../horreum/api/alerting/MissingDataRule.java | 2 + .../api/alerting/MissingDataRuleResult.java | 0 .../api/alerting/NotificationSettings.java | 0 .../horreum/api/alerting/RunExpectation.java | 0 .../api/alerting/TransformationLog.java | 0 .../tools/horreum/api/alerting/Variable.java | 0 .../tools/horreum/api/alerting/Watch.java | 0 .../tools/horreum/api/changes/Dashboard.java | 0 .../tools/horreum/api/changes/Target.java | 0 .../tools/horreum/api/changes/TimeRange.java | 0 .../tools/horreum/api/data/Action.java | 1 + .../tools/horreum/api/data/ActionLog.java | 0 .../tools/horreum/api/data/AllowedSite.java | 0 .../tools/horreum/api/data/Banner.java | 0 .../horreum/api/data/JsonpathValidation.java | 13 ++ .../tools/horreum/api/data/QueryResult.java | 5 + .../tools/horreum/api/data/View.java | 0 .../tools/horreum/api/data/ViewComponent.java | 1 + .../horreum/api/report/ReportComment.java | 0 .../horreum/api/report/ReportComponent.java | 0 .../tools/horreum/api/report/ReportLog.java | 0 .../tools/horreum/api/report/TableReport.java | 0 .../horreum/api/report/TableReportConfig.java | 0 .../horreum/api/services/ActionService.java | 4 + .../horreum/api/services/AlertingService.java | 0 .../horreum/api/services/BannerService.java | 0 .../horreum/api/services/ChangesService.java | 0 .../horreum/api/services/LogService.java | 0 .../api/services/NotificationService.java | 0 .../horreum/api/services/ReportService.java | 0 .../horreum/api/services/SqlService.java | 43 +++++++ .../api/services/SubscriptionService.java | 0 .../tools/horreum/api/services/UIService.java | 36 ++++++ .../horreum/api/services/UserService.java | 0 .../src/main/resources/META-INF/beans.xml | 4 + .../example-data/roadrunner_test.json | 18 --- .../example-data/roadrunner_view.json | 16 +++ pom.xml | 1 + webapp/src/components/ExportImport.tsx | 8 +- webapp/src/components/ImportButton.tsx | 4 +- .../domain/reports/TableReportConfigPage.tsx | 1 - webapp/src/domain/runs/DatasetComparison.tsx | 16 ++- webapp/src/domain/runs/DatasetData.tsx | 2 +- webapp/src/domain/runs/RunData.tsx | 2 +- webapp/src/domain/runs/SchemaList.tsx | 4 +- webapp/src/domain/runs/TestDatasets.tsx | 8 +- .../src/domain/runs/ValidationErrorTable.tsx | 13 +- webapp/src/domain/runs/actions.ts | 4 +- webapp/src/domain/runs/reducers.ts | 2 +- webapp/src/domain/runs/selectors.ts | 8 ++ webapp/src/domain/schemas/Schema.tsx | 3 +- .../src/domain/schemas/TryJsonPathModal.tsx | 4 +- webapp/src/domain/tests/Experiments.tsx | 6 +- webapp/src/domain/tests/General.tsx | 1 - webapp/src/domain/tests/Test.tsx | 3 +- webapp/src/domain/tests/actionTypes.ts | 2 + webapp/src/domain/tests/actions.ts | 29 ++++- webapp/src/domain/tests/reducers.ts | 40 +++++- 95 files changed, 827 insertions(+), 340 deletions(-) delete mode 100644 horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/QueryResult.java delete mode 100644 horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SqlService.java create mode 100644 horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/UIServiceImpl.java create mode 100644 horreum-ui/pom.xml rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Change.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java (92%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/DataPoint.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRule.java (91%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRuleResult.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/NotificationSettings.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/RunExpectation.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/TransformationLog.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Variable.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Watch.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/changes/Dashboard.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/changes/Target.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/changes/TimeRange.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/data/Action.java (96%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/data/ActionLog.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/data/AllowedSite.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/data/Banner.java (100%) create mode 100644 horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/JsonpathValidation.java create mode 100644 horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/QueryResult.java rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/data/View.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/data/ViewComponent.java (96%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComment.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComponent.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportLog.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReport.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReportConfig.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/ActionService.java (94%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/AlertingService.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/BannerService.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/ChangesService.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/LogService.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/NotificationService.java (100%) rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/ReportService.java (100%) create mode 100644 horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/SqlService.java rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/SubscriptionService.java (100%) create mode 100644 horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/UIService.java rename {horreum-api => horreum-ui}/src/main/java/io/hyperfoil/tools/horreum/api/services/UserService.java (100%) create mode 100644 horreum-ui/src/main/resources/META-INF/beans.xml create mode 100644 infra-legacy/example-data/roadrunner_view.json diff --git a/horreum-api/pom.xml b/horreum-api/pom.xml index 4d152ea8b..a5330ce6d 100644 --- a/horreum-api/pom.xml +++ b/horreum-api/pom.xml @@ -89,7 +89,7 @@ ${project.build.directory}/generated/openapi.yaml ${project.basedir}/../webapp/src/generated typescript-fetch - true + false Array=any false diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/ConditionConfig.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/ConditionConfig.java index 1a5cb1907..636290738 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/ConditionConfig.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/ConditionConfig.java @@ -7,11 +7,13 @@ import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; +@Schema(name = "ConditionConfig", type = SchemaType.OBJECT) public class ConditionConfig { @NotNull public String name; @@ -51,6 +53,7 @@ public class Component { @NotNull public String description; @NotNull + @Schema( type = SchemaType.OBJECT, implementation = ComponentType.class) public ComponentType type; @NotNull public Map properties = new HashMap<>(); diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/DataSet.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/DataSet.java index 81296d5b3..33f197d87 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/DataSet.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/DataSet.java @@ -4,12 +4,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.time.Instant; import java.util.Collection; import java.util.Objects; +@Schema(name = "DataSet", type = SchemaType.OBJECT) public class DataSet { public Integer id; @NotNull @@ -25,8 +27,10 @@ public class DataSet { public String owner; @NotNull @JsonProperty( required = true ) + @Schema( type = SchemaType.INTEGER, implementation = Access.class) public Access access; @NotNull + @Schema(implementation = String.class) public JsonNode data; @NotNull public int ordinal; diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ExperimentComparison.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ExperimentComparison.java index 0013357cd..40362f2a9 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ExperimentComparison.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ExperimentComparison.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; public class ExperimentComparison { @@ -12,6 +13,7 @@ public class ExperimentComparison { public String model; @NotNull @JsonProperty( required = true ) + @Schema(implementation = String.class) public JsonNode config; @NotNull diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Extractor.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Extractor.java index 1e88ffe1b..c5ec92fc9 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Extractor.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Extractor.java @@ -2,7 +2,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +@Schema(name = "Extractor", type = SchemaType.OBJECT) public class Extractor { @NotNull @JsonProperty( required = true ) diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Label.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Label.java index 89bdd09e3..2dfc07402 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Label.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Label.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.io.Serializable; import java.util.Collection; @@ -27,6 +29,7 @@ public class Label { @JsonProperty( required = true ) public String owner; @JsonProperty( required = true ) + @Schema( type = SchemaType.INTEGER, implementation = Access.class) public Access access = Access.PUBLIC; @NotNull @JsonProperty( value = "schemaId", required = true ) @@ -38,6 +41,7 @@ public Label() { public static class Value implements Serializable { public int datasetId; public int labelId; + @Schema(implementation = String.class) public JsonNode value; public Value() { diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Run.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Run.java index c06aa3606..918b40493 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Run.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Run.java @@ -10,22 +10,26 @@ import java.time.Instant; import java.util.Collection; +@Schema(name = "Run", type = SchemaType.OBJECT, + description = "Data object that represents a test run entry") public class Run { @JsonProperty(required = true) public Integer id; @NotNull - @Schema(type = SchemaType.NUMBER, required = true) + @Schema(type = SchemaType.NUMBER, implementation = Instant.class) public Instant start; @NotNull - @Schema(type = SchemaType.NUMBER, required = true) + @Schema(type = SchemaType.NUMBER, implementation = Instant.class) public Instant stop; public String description; @NotNull @JsonProperty(required = true) public Integer testid; @NotNull + @Schema(implementation = JsonNode.class, type = SchemaType.STRING) @JsonProperty(required = true) public JsonNode data; + @Schema(implementation = JsonNode.class, type = SchemaType.STRING) public JsonNode metadata; @NotNull @JsonProperty(required = true) @@ -38,6 +42,7 @@ public class Run { public String owner; @NotNull @JsonProperty(required = true) + @Schema( type = SchemaType.INTEGER, implementation = Access.class) public Access access; public Run() { diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Schema.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Schema.java index 271a84e95..95444c78e 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Schema.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Schema.java @@ -3,23 +3,32 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import java.util.Collection; +@org.eclipse.microprofile.openapi.annotations.media.Schema(name = "Schema", + description = "Data object that describes the schema definition for a test" ) public class Schema { + @org.eclipse.microprofile.openapi.annotations.media.Schema(required = true) @JsonProperty(required = true) public Integer id; @NotNull + @org.eclipse.microprofile.openapi.annotations.media.Schema(required = true) @JsonProperty(required = true) public String uri; @NotNull + @org.eclipse.microprofile.openapi.annotations.media.Schema(required = true) @JsonProperty(required = true) public String name; public String description; + @org.eclipse.microprofile.openapi.annotations.media.Schema(implementation = String.class) public JsonNode schema; + @org.eclipse.microprofile.openapi.annotations.media.Schema(required = true) @JsonProperty(required = true) public String owner; + @org.eclipse.microprofile.openapi.annotations.media.Schema( type = SchemaType.INTEGER, implementation = Access.class) public Access access; public String token; diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Test.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Test.java index 77d53d430..f880b410f 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Test.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Test.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.Collection; @@ -20,6 +21,7 @@ public class Test { public String owner; @NotNull @JsonProperty(required = true) + @Schema( type = SchemaType.INTEGER, implementation = Access.class) public Access access; public Collection tokens; @Schema(implementation = String[].class) @@ -28,9 +30,6 @@ public class Test { @Schema(implementation = String[].class) public JsonNode fingerprintLabels; public String fingerprintFilter; - @NotNull - @JsonProperty(required = true) - public Collection views; public String compareUrl; public Collection transformers; @NotNull @@ -55,7 +54,6 @@ public String toString() { ", timelineFunction='" + timelineFunction + '\'' + ", fingerprintLabels=" + fingerprintLabels + ", fingerprintFilter='" + fingerprintFilter + '\'' + - ", views=" + views + ", compareUrl='" + compareUrl + '\'' + ", transformers=" + transformers + ", notificationsEnabled=" + notificationsEnabled + @@ -66,7 +64,5 @@ public void clearIds() { id = null; if(tokens != null) tokens.stream().forEach( t -> t.clearId()); - if(views != null) - views.stream().forEach( v -> v.clearId()); } } diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Transformer.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Transformer.java index 0144dbd69..149a3e19c 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Transformer.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Transformer.java @@ -2,9 +2,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.Collection; +@Schema(name = "Transformer", type = SchemaType.OBJECT) public class Transformer { @JsonProperty(required = true) public Integer id; @@ -32,6 +35,8 @@ public class Transformer { public String owner; @NotNull + @JsonProperty(required = true) + @Schema( type = SchemaType.INTEGER, implementation = Access.class) public Access access = Access.PUBLIC; public Transformer() { diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ValidationError.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ValidationError.java index 7493f4309..230b209ab 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ValidationError.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ValidationError.java @@ -3,11 +3,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; public class ValidationError { public int schemaId; @NotNull @JsonProperty(required = true) + @Schema(implementation = String.class) + //TODO: stalep, does this really need to be a JsonNode? public JsonNode error; public ValidationError() { diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/DatasetService.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/DatasetService.java index bc17a647a..0b856851a 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/DatasetService.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/DatasetService.java @@ -17,6 +17,7 @@ import io.hyperfoil.tools.horreum.api.data.ValidationError; import io.hyperfoil.tools.horreum.api.data.DataSet; import io.hyperfoil.tools.horreum.api.data.Label; +import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; @@ -27,6 +28,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.hyperfoil.tools.horreum.api.data.Access; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema; @Path("/api/dataset") @Consumes({ MediaType.APPLICATION_JSON}) @@ -34,6 +37,14 @@ public interface DatasetService { @Path("{id}") @GET + @Produces(MediaType.APPLICATION_JSON) + @APIResponse( + responseCode = "404", + description = "No Dataset with the given id was found", + content = @Content(mediaType = "application/json")) + @APIResponseSchema(value = DataSet.class, + responseDescription = "JVM system properties of a particular host.", + responseCode = "200") DataSet getDataSet(@PathParam("id") int datasetId); @Path("list/{testId}") @@ -46,12 +57,6 @@ DatasetList listByTest(@PathParam("testId") int testId, @QueryParam("direction") SortDirection direction, @QueryParam("viewId") Integer viewId); - @Path("{id}/query") - @GET - QueryResult queryData(@PathParam("id") int datasetId, - @Parameter(required = true) @QueryParam("query") String jsonpath, - @QueryParam("array") @DefaultValue("false") boolean array, - @QueryParam("schemaUri") String schemaUri); @GET @Path("bySchema") @@ -59,7 +64,7 @@ DatasetList listBySchema(@Parameter(required = true) @QueryParam("uri") String u @QueryParam("limit") Integer limit, @QueryParam("page") Integer page, @QueryParam("sort") @DefaultValue("start") String sort, - @QueryParam("direction") @DefaultValue("Descending") SortDirection direction); + @QueryParam("direction") SortDirection direction); @GET @Path("{datasetId}/labelValues") @@ -93,6 +98,7 @@ class DatasetSummary { public String owner; @Schema(required = true, implementation = Access.class) public int access; + @Schema(implementation = String.class) public ObjectNode view; @JsonProperty(required = true) public List schemas; @@ -114,10 +120,12 @@ class LabelValue { public String name; @NotNull public SchemaService.SchemaDescriptor schema; + @Schema(implementation = String.class) public JsonNode value; } class LabelPreview { + @Schema(implementation = String.class) public JsonNode value; public String output; } diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ExperimentService.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ExperimentService.java index dfc56c599..a522e3557 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ExperimentService.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ExperimentService.java @@ -73,6 +73,7 @@ class ExperimentResult { @JsonDeserialize(keyUsing = ExperimentComparisonDeserializer.class) public Map results; + @Schema(implementation = String.class) public JsonNode extraLabels; public boolean notify; diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/QueryResult.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/QueryResult.java deleted file mode 100644 index 116ddb4a0..000000000 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/QueryResult.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.hyperfoil.tools.horreum.api.services; - -public class QueryResult extends SqlService.JsonpathValidation { - public String value; -} diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/RunService.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/RunService.java index 797aee41c..bb6eb8526 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/RunService.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/RunService.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map; +import io.hyperfoil.tools.horreum.api.data.DataSet; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DefaultValue; @@ -32,6 +33,7 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.multipart.FileUpload; @@ -40,14 +42,27 @@ @Consumes({ MediaType.APPLICATION_JSON}) @Produces(MediaType.APPLICATION_JSON) public interface RunService { - @APIResponse(content = @Content(schema = @Schema(implementation = RunExtended.class)), description = "Returns an instance of RunExtended") @GET @Path("{id}") + @APIResponse( + responseCode = "404", + description = "If no Run have been found with the given id", + content = @Content(mediaType = MediaType.APPLICATION_JSON)) + @APIResponseSchema( value = RunExtended.class, + responseDescription = "Run data with the referenced schemas and generated datasets", + responseCode = "200") RunExtended getRun(@PathParam("id") int id, @QueryParam("token") String token); @GET @Path("{id}/summary") + @APIResponse( + responseCode = "404", + description = "If no Run have been found with the given id", + content = @Content(mediaType = MediaType.APPLICATION_JSON)) + @APIResponseSchema( value = RunSummary.class, + responseDescription = "Run summary with the referenced schemas and generated datasets", + responseCode = "200") RunSummary getRunSummary(@PathParam("id") int id, @QueryParam("token") String token); @GET @@ -58,13 +73,6 @@ RunExtended getRun(@PathParam("id") int id, @Path("{id}/metadata") Object getMetadata(@PathParam("id") int id, @QueryParam("token") String token, @QueryParam("schemaUri") String schemaUri); - @GET - @Path("{id}/query") - QueryResult queryData(@PathParam("id") int id, - @Parameter(required = true) @QueryParam("query") String jsonpath, - @QueryParam("uri") String schemaUri, - @QueryParam("array") @DefaultValue("false") boolean array); - @POST @Path("{id}/resetToken") String resetToken(@PathParam("id") int id); @@ -91,7 +99,15 @@ Response add(@QueryParam("test") String testNameOrId, @POST @Path("data") - @Produces(MediaType.TEXT_PLAIN) // run ID as string + @RequestBody(content = @Content( mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( type = SchemaType.STRING, implementation = String.class)) ) + @APIResponse( + responseCode = "400", + description = "Some fields are missing or invalid", + content = @Content(mediaType = MediaType.APPLICATION_JSON)) + @APIResponseSchema(value = Integer.class, + responseDescription = "Returns the id of the newly generated run.", + responseCode = "200") Response addRunFromData(@Parameter(required = true) @QueryParam("start") String start, @Parameter(required = true) @QueryParam("stop") String stop, @Parameter(required = true) @QueryParam("test") String test, @@ -100,7 +116,7 @@ Response addRunFromData(@Parameter(required = true) @QueryParam("start") String @Parameter(description = "Horreum internal token. Incompatible with Keycloak") @QueryParam("token") String token, @QueryParam("schema") String schemaUri, @QueryParam("description") String description, - @RequestBody(required = true) JsonNode data); + @RequestBody(required = true) String data); @POST @Path("data") @@ -214,8 +230,10 @@ class RunSummary { @JsonIgnoreProperties({ "token", "old_start" }) //ignore properties that have not been mapped class RunExtended extends Run { @NotNull + @Schema(required = true) public List schemas; @NotNull + @Schema(required = true) public String testname; @Schema(required = true, implementation = int[].class) public Integer[] datasets; diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SchemaService.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SchemaService.java index ba76c242f..3a2180c3a 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SchemaService.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SchemaService.java @@ -7,7 +7,6 @@ import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -20,18 +19,29 @@ import io.hyperfoil.tools.horreum.api.data.Label; import io.hyperfoil.tools.horreum.api.data.Schema; import io.hyperfoil.tools.horreum.api.data.Transformer; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema; @Path("api/schema") +@Produces(MediaType.APPLICATION_JSON) +@Consumes({ MediaType.APPLICATION_JSON}) public interface SchemaService { @GET @Path("{id}") - @Produces(MediaType.APPLICATION_JSON) + @APIResponse( + responseCode = "404", + description = "No Schema with the given id was found", + content = @Content(mediaType = "application/json")) + @APIResponseSchema(value = Schema.class, + responseCode = "200", + responseDescription = "Returns Schema if a matching id is found") Schema getSchema(@PathParam("id") int id, @QueryParam("token") String token); @GET @@ -47,7 +57,7 @@ public interface SchemaService { SchemaQueryResult list(@QueryParam("limit") Integer limit, @QueryParam("page") Integer page, @QueryParam("sort") String sort, - @QueryParam("direction") @DefaultValue("Ascending") SortDirection direction); + @QueryParam("direction") SortDirection direction); @GET @@ -56,18 +66,15 @@ SchemaQueryResult list(@QueryParam("limit") Integer limit, List descriptors(@QueryParam("id") List ids); @POST - @Produces(MediaType.TEXT_PLAIN) @Path("{id}/resetToken") String resetToken(@PathParam("id") int id); @POST - @Produces(MediaType.TEXT_PLAIN) @Path("{id}/dropToken") String dropToken(@PathParam("id") int id); @POST @Path("{id}/updateAccess") - @Consumes(MediaType.TEXT_PLAIN) //is POST the correct verb for this method as we are not uploading a new artefact? // TODO: it would be nicer to use @FormParams but fetchival on client side doesn't support that void updateAccess(@PathParam("id") int id, @Parameter(required = true) @QueryParam("owner") String owner, @@ -125,12 +132,18 @@ int addOrUpdateTransformer(@PathParam("schemaId") int schemaId, @GET @Path("{id}/export") @Produces(MediaType.APPLICATION_JSON) - JsonNode exportSchema(@PathParam("id") int id); + @APIResponseSchema(value = String.class, + responseDescription = "A JSON representation of the Schema object", + responseCode = "200") + String exportSchema(@PathParam("id") int id); @POST @Path("import") @Consumes(MediaType.APPLICATION_JSON) - void importSchema(JsonNode config); + @RequestBody(content = @Content( mediaType = MediaType.APPLICATION_JSON, + schema = @org.eclipse.microprofile.openapi.annotations.media.Schema( + type = SchemaType.STRING, implementation = String.class)) ) + void importSchema(String config); class SchemaQueryResult { @NotNull @@ -144,9 +157,7 @@ public SchemaQueryResult(List schemas, long count) { } } - @org.eclipse.microprofile.openapi.annotations.media.Schema(anyOf = { - LabelInFingerprint.class, LabelInRule.class, LabelInReport.class, LabelInVariable.class, LabelInView.class - }) + @org.eclipse.microprofile.openapi.annotations.media.Schema(name = "LabelLocation", type = SchemaType.OBJECT) abstract class LabelLocation { public final String type; public int testId; @@ -159,12 +170,22 @@ public LabelLocation(String type, int testId, String testName) { } } + @org.eclipse.microprofile.openapi.annotations.media.Schema( + name = "LabelInFingerprint", + type = SchemaType.OBJECT, + allOf = {LabelLocation.class} + ) class LabelInFingerprint extends LabelLocation { public LabelInFingerprint(int testId, String testName) { super("FINGERPRINT", testId, testName); } } + @org.eclipse.microprofile.openapi.annotations.media.Schema( + name = "LabelInRule", + type = SchemaType.OBJECT, + allOf = {LabelLocation.class} + ) class LabelInRule extends LabelLocation { public int ruleId; public String ruleName; @@ -176,6 +197,11 @@ public LabelInRule(int testId, String testName, int ruleId, String ruleName) { } } + @org.eclipse.microprofile.openapi.annotations.media.Schema( + name = "LabelInVariable", + type = SchemaType.OBJECT, + allOf = {LabelLocation.class} + ) class LabelInVariable extends LabelLocation { public int variableId; public String variableName; @@ -187,6 +213,11 @@ public LabelInVariable(int testId, String testName, int variableId, String varia } } + @org.eclipse.microprofile.openapi.annotations.media.Schema( + name = "LabelInView", + type = SchemaType.OBJECT, + allOf = {LabelLocation.class} + ) class LabelInView extends LabelLocation { public int viewId; public String viewName; @@ -202,6 +233,11 @@ public LabelInView(int testId, String testName, int viewId, String viewName, int } } + @org.eclipse.microprofile.openapi.annotations.media.Schema( + name = "LabelInReport", + type = SchemaType.OBJECT, + allOf = {LabelLocation.class} + ) class LabelInReport extends LabelLocation { public int configId; public String title; @@ -248,6 +284,7 @@ public SchemaDescriptor(int id, String name, String uri) { } // this roughly matches run_schemas table + class SchemaUsage extends SchemaDescriptor { // 0 is data, 1 is metadata. DataSets always use 0 @JsonProperty(required = true) diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SqlService.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SqlService.java deleted file mode 100644 index 40954062a..000000000 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SqlService.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.hyperfoil.tools.horreum.api.services; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DefaultValue; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; - -import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; - -import com.fasterxml.jackson.annotation.JsonProperty; - -@Path("/api/sql") -@Consumes({ MediaType.APPLICATION_JSON}) -@Produces(MediaType.APPLICATION_JSON) -public interface SqlService { - @GET - @Path("testjsonpath") - JsonpathValidation testJsonPath(@Parameter(required = true) @QueryParam("query") String jsonpath); - - @Path("roles") - @GET - @Produces("text/plain") - String roles(@QueryParam("system") @DefaultValue("false") boolean system); - - class JsonpathValidation { - @JsonProperty(required = true) - public boolean valid; - public String jsonpath; - public int errorCode; - public String sqlState; - public String reason; - public String sql; - } -} diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/TestService.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/TestService.java index 9ebdf9557..7fa6eb987 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/TestService.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/TestService.java @@ -17,12 +17,16 @@ import io.hyperfoil.tools.horreum.api.SortDirection; import io.hyperfoil.tools.horreum.api.data.*; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema; @Path("/api/test") @Consumes({ MediaType.APPLICATION_JSON}) @@ -48,7 +52,7 @@ TestQueryResult list(@QueryParam("roles") String roles, @QueryParam("limit") Integer limit, @QueryParam("page") Integer page, @QueryParam("sort") @DefaultValue("name") String sort, - @QueryParam("direction") @DefaultValue("Ascending") SortDirection direction); + @QueryParam("direction") SortDirection direction); @Path("summary") @GET @@ -60,7 +64,6 @@ TestQueryResult list(@QueryParam("roles") String roles, @POST @Path("{id}/addToken") - @Produces(MediaType.TEXT_PLAIN) int addToken(@PathParam("id") int testId, TestToken token); @GET @@ -78,14 +81,6 @@ void updateAccess(@PathParam("id") int id, @Parameter(required = true) @QueryParam("owner") String owner, @Parameter(required = true) @QueryParam("access") Access access); - @POST - @Path("{testId}/view") - int updateView(@PathParam("testId") int testId, @RequestBody(required = true) View view); - - @DELETE - @Path("{testId}/view/{viewId}") - void deleteView(@PathParam("testId") int testId, @PathParam("viewId") int viewId); - @POST @Consumes // any @Path("{id}/notifications") @@ -95,10 +90,6 @@ void updateAccess(@PathParam("id") int id, @Path("{id}/move") void updateFolder(@PathParam("id") int id, @QueryParam("folder") String folder); - @POST - @Path("{testId}/action") - Action updateAction(@PathParam("testId") int testId, @RequestBody(required = true) Action action); - @GET @Path("{id}/fingerprint") List listFingerprints(@PathParam("id") int testId); @@ -124,11 +115,17 @@ List listLabelValues(@PathParam("id") int testId, @GET @Path("{id}/export") - JsonNode export(@PathParam("id") int testId); + @APIResponseSchema(value = String.class, + responseDescription = "A Run data object formatted as json", + responseCode = "200") + String export(@PathParam("id") int testId); @POST @Path("import") - void importTest(JsonNode testConfig); + @APIResponse(responseCode = "204", description = "Import a new test") + @RequestBody(content = @Content( mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( type = SchemaType.STRING, implementation = String.class)) ) + void importTest( String testConfig); class TestListing { public List tests; diff --git a/horreum-backend/pom.xml b/horreum-backend/pom.xml index d178482ee..621cd7274 100644 --- a/horreum-backend/pom.xml +++ b/horreum-backend/pom.xml @@ -14,7 +14,8 @@ io.hyperfoil.tools - horreum-api + horreum-ui + ${project.version} diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/mapper/TestMapper.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/mapper/TestMapper.java index 9850cdfc6..eb0043b07 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/mapper/TestMapper.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/mapper/TestMapper.java @@ -4,6 +4,7 @@ import io.hyperfoil.tools.horreum.api.data.Test; import io.hyperfoil.tools.horreum.entity.data.TestTokenDAO; import io.hyperfoil.tools.horreum.api.data.TestToken; +import io.hyperfoil.tools.horreum.entity.data.ViewDAO; import java.util.stream.Collectors; @@ -24,8 +25,6 @@ public static Test from(TestDAO t) { dto.notificationsEnabled = t.notificationsEnabled; if(t.tokens != null) dto.tokens = t.tokens.stream().map(TestMapper::fromTestToken).collect(Collectors.toList()); - if (t.views != null) - dto.views = t.views.stream().map(ViewMapper::from).collect(Collectors.toList()); if (t.transformers != null) dto.transformers = t.transformers.stream().map(TransformerMapper::from).collect(Collectors.toList()); @@ -60,8 +59,7 @@ public static TestDAO to(Test dto) { t.notificationsEnabled = dto.notificationsEnabled; if(dto.tokens != null) t.tokens = dto.tokens.stream().map(token -> TestMapper.toTestToken(token,t) ).collect(Collectors.toList()); - if (dto.views != null) - t.views = dto.views.stream().map(ViewMapper::to).collect(Collectors.toList()); + t.views = ViewDAO.find("test.id", dto.id).list(); if (dto.transformers != null) t.transformers = dto.transformers.stream().map(TransformerMapper::to).collect(Collectors.toList()); diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/ActionServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/ActionServiceImpl.java index 3f22d938b..8ab255bc3 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/ActionServiceImpl.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/ActionServiceImpl.java @@ -4,6 +4,7 @@ import io.hyperfoil.tools.horreum.api.data.AllowedSite; import io.hyperfoil.tools.horreum.api.data.Run; import io.hyperfoil.tools.horreum.api.data.Test; +import io.hyperfoil.tools.horreum.api.services.TestService; import io.hyperfoil.tools.horreum.bus.MessageBusChannels; import io.hyperfoil.tools.horreum.api.alerting.Change; import io.hyperfoil.tools.horreum.entity.data.*; @@ -71,6 +72,9 @@ public class ActionServiceImpl implements ActionService { @Inject EncryptionManager encryptionManager; + @Inject + TestServiceImpl testService; + @PostConstruct() public void postConstruct(){ plugins = actionPlugins.stream().collect(Collectors.toMap(ActionPlugin::type, Function.identity())); @@ -190,7 +194,6 @@ public Action add(Action action){ ActionDAO actionEntity = ActionMapper.to(action); merge(actionEntity); } - em.flush(); return action; } @@ -219,6 +222,31 @@ public Action get(int id){ return ActionMapper.from(action); } + @Override + @RolesAllowed(Roles.TESTER) + @WithRoles + @Transactional + public Action update(Action dto) { + if (dto.testId <= 0) { + throw ServiceException.badRequest("Missing test id"); + } + // just ensure the test exists + testService.getTestForUpdate(dto.testId); + validate(dto); + + ActionDAO action = ActionMapper.to(dto); + if (action.id == null) { + action.persist(); + } else { + if (!action.active) { + ActionDAO.deleteById(action.id); + return null; + } else { + merge(action); + } + } + return ActionMapper.from(action); + } @WithRoles @RolesAllowed(Roles.ADMIN) diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/DatasetServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/DatasetServiceImpl.java index 4b528aa6f..217a376de 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/DatasetServiceImpl.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/DatasetServiceImpl.java @@ -24,6 +24,7 @@ import io.hyperfoil.tools.horreum.api.data.Label; import io.hyperfoil.tools.horreum.entity.data.*; import io.hyperfoil.tools.horreum.mapper.DataSetMapper; +import jakarta.ws.rs.DefaultValue; import org.hibernate.Hibernate; import org.hibernate.query.NativeQuery; import org.hibernate.transform.AliasToBeanResultTransformer; @@ -36,7 +37,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import io.hyperfoil.tools.horreum.api.services.DatasetService; -import io.hyperfoil.tools.horreum.api.services.QueryResult; +import io.hyperfoil.tools.horreum.api.data.QueryResult; import io.hyperfoil.tools.horreum.api.services.SchemaService; import io.hyperfoil.tools.horreum.bus.MessageBus; import io.hyperfoil.tools.horreum.entity.PersistentLogDAO; @@ -250,38 +251,8 @@ private void addOrderAndPaging(Integer limit, Integer page, String sort, SortDir @WithRoles @Override - public QueryResult queryData(int datasetId, String jsonpath, boolean array, String schemaUri) { - if (schemaUri != null && schemaUri.isBlank()) { - schemaUri = null; - } - QueryResult result = new QueryResult(); - result.jsonpath = jsonpath; - try { - if (schemaUri == null) { - String func = array ? "jsonb_path_query_array" : "jsonb_path_query_first"; - String sqlQuery = "SELECT " + func + "(data, ?::::jsonpath)#>>'{}' FROM dataset WHERE id = ?"; - result.value = String.valueOf(Util.runQuery(em, sqlQuery, jsonpath, datasetId)); - } else { - // This schema-aware query already assumes that DataSet.data is an array of objects with defined schema - String schemaQuery = "jsonb_path_query(data, '$[*] ? (@.\"$schema\" == $schema)', ('{\"schema\":\"' || ? || '\"}')::::jsonb)"; - String sqlQuery; - if (!array) { - sqlQuery = "SELECT jsonb_path_query_first(" + schemaQuery + ", ?::::jsonpath)#>>'{}' FROM dataset WHERE id = ? LIMIT 1"; - } else { - sqlQuery = "SELECT jsonb_agg(v)#>>'{}' FROM (SELECT jsonb_path_query(" + schemaQuery + ", ?::::jsonpath) AS v FROM dataset WHERE id = ?) AS values"; - } - result.value = String.valueOf(Util.runQuery(em, sqlQuery, schemaUri, jsonpath, datasetId)); - } - result.valid = true; - } catch (PersistenceException pe) { - SqlServiceImpl.setFromException(pe, result); - } - return result; - } - - @WithRoles - @Override - public DatasetService.DatasetList listBySchema(String uri, Integer limit, Integer page, String sort, SortDirection direction) { + public DatasetService.DatasetList listBySchema(String uri, Integer limit, Integer page, String sort, + @DefaultValue("Descending") SortDirection direction) { StringBuilder sql = new StringBuilder(LIST_SCHEMA_DATASETS); // TODO: filtering by fingerprint addOrderAndPaging(limit, page, sort, direction, sql); diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/RunServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/RunServiceImpl.java index e43947148..5d92f47ad 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/RunServiceImpl.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/RunServiceImpl.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.hyperfoil.tools.horreum.api.alerting.DataPoint; import io.hyperfoil.tools.horreum.api.data.DataSet; +import io.hyperfoil.tools.horreum.api.data.JsonpathValidation; import io.hyperfoil.tools.horreum.api.data.Test; import io.hyperfoil.tools.horreum.bus.MessageBusChannels; import io.hyperfoil.tools.horreum.entity.alerting.DataPointDAO; @@ -61,7 +62,7 @@ import io.hyperfoil.tools.horreum.api.data.ValidationError; import io.hyperfoil.tools.horreum.entity.data.*; import io.hyperfoil.tools.horreum.mapper.RunMapper; -import io.hyperfoil.tools.horreum.api.services.QueryResult; +import io.hyperfoil.tools.horreum.api.data.QueryResult; import io.hyperfoil.tools.horreum.api.services.RunService; import io.hyperfoil.tools.horreum.api.services.SchemaService; import io.hyperfoil.tools.horreum.api.services.SqlService; @@ -290,30 +291,7 @@ public JsonNode getMetadata(int id, String token, String schemaUri) { } } - @PermitAll - @WithRoles - @WithToken - @Override - public QueryResult queryData(int id, String jsonpath, String schemaUri, boolean array) { - String func = array ? "jsonb_path_query_array" : "jsonb_path_query_first"; - QueryResult result = new QueryResult(); - result.jsonpath = jsonpath; - try { - if (schemaUri != null && !schemaUri.isEmpty()) { - String sqlQuery = "SELECT " + func + "((CASE " + - "WHEN rs.type = 0 THEN run.data WHEN rs.type = 1 THEN run.data->rs.key ELSE run.data->(rs.key::::integer) END)" + - ", (?1)::::jsonpath)#>>'{}' FROM run JOIN run_schemas rs ON rs.runid = run.id WHERE id = ?2 AND rs.uri = ?3"; - result.value = String.valueOf(Util.runQuery(em, sqlQuery, jsonpath, id, schemaUri)); - } else { - String sqlQuery = "SELECT " + func + "(data, (?1)::::jsonpath)#>>'{}' FROM run WHERE id = ?2"; - result.value = String.valueOf(Util.runQuery(em, sqlQuery, jsonpath, id)); - } - result.valid = true; - } catch (PersistenceException pe) { - SqlServiceImpl.setFromException(pe, result); - } - return result; - } + @RolesAllowed(Roles.TESTER) @WithRoles @@ -389,7 +367,7 @@ public Response add(String testNameOrId, String owner, Access access, String tok public Response addRunFromData(String start, String stop, String test, String owner, Access access, String token, String schemaUri, String description, - JsonNode data) { + String data) { return addRunFromData(start, stop, test, owner, access, token, schemaUri, description, data, null); } @@ -434,7 +412,7 @@ public Response addRunFromData(String start, String stop, String test, String ow log.error("Failed to read data/metadata from upload file", e); throw ServiceException.badRequest("Provided data/metadata can't be read (JSON encoding problem?)"); } - return addRunFromData(start, stop, test, owner, access, token, schemaUri, description, dataNode, metadataNode); + return addRunFromData(start, stop, test, owner, access, token, schemaUri, description, dataNode.toString(), metadataNode); } @Override @@ -467,11 +445,17 @@ public void waitForDatasets(int runId) { Response addRunFromData(String start, String stop, String test, String owner, Access access, String token, String schemaUri, String description, - JsonNode data, JsonNode metadata) { - if (data == null) { + String stringData, JsonNode metadata) { + if (stringData == null) { log.debugf("Failed to upload for test %s with description %s because of missing data.", test, description); throw ServiceException.badRequest("No data!"); } + JsonNode data = null; + try { + data = Util.OBJECT_MAPPER.readValue(stringData, JsonNode.class); + } catch (JsonProcessingException e) { + throw ServiceException.badRequest("Could not map incoming data to JsonNode: "+e.getMessage()); + } Object foundTest = findIfNotSet(test, data); Object foundStart = findIfNotSet(start, data); Object foundStop = findIfNotSet(stop, data); @@ -735,7 +719,7 @@ public RunsSummary listAllRuns(String query, boolean matchAll, String roles, boo Transaction old = tm.suspend(); try { for (String jsonpath : queryParts) { - SqlService.JsonpathValidation result = sqlService.testJsonPathInternal(jsonpath); + JsonpathValidation result = sqlService.testJsonPathInternal(jsonpath); if (!result.valid) { throw new WebApplicationException(Response.status(400).entity(result).build()); } diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SchemaServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SchemaServiceImpl.java index cc031c7b8..cfdacdb2c 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SchemaServiceImpl.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SchemaServiceImpl.java @@ -51,6 +51,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +import jakarta.ws.rs.DefaultValue; import org.hibernate.ScrollMode; import org.hibernate.ScrollableResults; import org.hibernate.query.NativeQuery; @@ -190,7 +191,8 @@ public Integer add(Schema schemaDTO){ @PermitAll @WithRoles @Override - public SchemaQueryResult list(Integer limit, Integer page, String sort, SortDirection direction) { + public SchemaQueryResult list(Integer limit, Integer page, String sort, + @DefaultValue("Ascending") SortDirection direction) { if (sort == null || sort.isEmpty()) { sort = "name"; } @@ -727,7 +729,7 @@ public List allTransformers() { @WithRoles @Transactional @Override - public JsonNode exportSchema(int id) { + public String exportSchema(int id) { SchemaDAO schema = SchemaDAO.findById(id); if (schema == null) { throw ServiceException.notFound("Schema not found"); @@ -735,14 +737,20 @@ public JsonNode exportSchema(int id) { ObjectNode exported = Util.OBJECT_MAPPER.valueToTree(schema); exported.set("labels", Util.OBJECT_MAPPER.valueToTree(LabelDAO.list("schema", schema))); exported.set("transformers", Util.OBJECT_MAPPER.valueToTree(TransformerDAO.list("schema", schema))); - return exported; + return exported.toString(); } @RolesAllowed({Roles.TESTER, Roles.ADMIN}) @WithRoles @Transactional @Override - public void importSchema(JsonNode config) { + public void importSchema(String newSchema) { + JsonNode config = null; + try { + config = Util.OBJECT_MAPPER.readValue(newSchema, JsonNode.class); + } catch (JsonProcessingException e) { + throw ServiceException.badRequest("Could not map Schema to JsonNode: "+e.getMessage()); + } if (!config.isObject()) { throw ServiceException.badRequest("Bad format of schema; expecting an object"); } diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SqlServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SqlServiceImpl.java index ec647234e..22f4ff4a1 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SqlServiceImpl.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/SqlServiceImpl.java @@ -1,8 +1,12 @@ package io.hyperfoil.tools.horreum.svc; +import io.hyperfoil.tools.horreum.api.data.JsonpathValidation; +import io.hyperfoil.tools.horreum.api.data.QueryResult; import io.hyperfoil.tools.horreum.api.services.SqlService; import io.hyperfoil.tools.horreum.server.ErrorReporter; import io.hyperfoil.tools.horreum.server.RoleManager; +import io.hyperfoil.tools.horreum.server.WithRoles; +import io.hyperfoil.tools.horreum.server.WithToken; import io.quarkus.security.identity.SecurityIdentity; import io.smallrye.mutiny.subscription.UniSubscriber; import io.smallrye.mutiny.subscription.UniSubscription; @@ -80,6 +84,60 @@ static void setFromException(PersistenceException pe, JsonpathValidation result) result.reason = pe.getMessage(); } } + @PermitAll + @WithRoles + @WithToken + @Override + public QueryResult queryRunData(int id, String jsonpath, String schemaUri, boolean array) { + String func = array ? "jsonb_path_query_array" : "jsonb_path_query_first"; + QueryResult result = new QueryResult(); + result.jsonpath = jsonpath; + try { + if (schemaUri != null && !schemaUri.isEmpty()) { + String sqlQuery = "SELECT " + func + "((CASE " + + "WHEN rs.type = 0 THEN run.data WHEN rs.type = 1 THEN run.data->rs.key ELSE run.data->(rs.key::::integer) END)" + + ", (?1)::::jsonpath)#>>'{}' FROM run JOIN run_schemas rs ON rs.runid = run.id WHERE id = ?2 AND rs.uri = ?3"; + result.value = String.valueOf(Util.runQuery(em, sqlQuery, jsonpath, id, schemaUri)); + } else { + String sqlQuery = "SELECT " + func + "(data, (?1)::::jsonpath)#>>'{}' FROM run WHERE id = ?2"; + result.value = String.valueOf(Util.runQuery(em, sqlQuery, jsonpath, id)); + } + result.valid = true; + } catch (PersistenceException pe) { + SqlServiceImpl.setFromException(pe, result); + } + return result; + } + @WithRoles + @Override + public QueryResult queryDatasetData(int datasetId, String jsonpath, boolean array, String schemaUri) { + if (schemaUri != null && schemaUri.isBlank()) { + schemaUri = null; + } + QueryResult result = new QueryResult(); + result.jsonpath = jsonpath; + try { + if (schemaUri == null) { + String func = array ? "jsonb_path_query_array" : "jsonb_path_query_first"; + String sqlQuery = "SELECT " + func + "(data, ?::::jsonpath)#>>'{}' FROM dataset WHERE id = ?"; + result.value = String.valueOf(Util.runQuery(em, sqlQuery, jsonpath, datasetId)); + } else { + // This schema-aware query already assumes that DataSet.data is an array of objects with defined schema + String schemaQuery = "jsonb_path_query(data, '$[*] ? (@.\"$schema\" == $schema)', ('{\"schema\":\"' || ? || '\"}')::::jsonb)"; + String sqlQuery; + if (!array) { + sqlQuery = "SELECT jsonb_path_query_first(" + schemaQuery + ", ?::::jsonpath)#>>'{}' FROM dataset WHERE id = ? LIMIT 1"; + } else { + sqlQuery = "SELECT jsonb_agg(v)#>>'{}' FROM (SELECT jsonb_path_query(" + schemaQuery + ", ?::::jsonpath) AS v FROM dataset WHERE id = ?) AS values"; + } + result.value = String.valueOf(Util.runQuery(em, sqlQuery, schemaUri, jsonpath, datasetId)); + } + result.valid = true; + } catch (PersistenceException pe) { + SqlServiceImpl.setFromException(pe, result); + } + return result; + } @Override @PermitAll diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/TestServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/TestServiceImpl.java index 3cf94a073..64ed3a97f 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/TestServiceImpl.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/TestServiceImpl.java @@ -1,5 +1,6 @@ package io.hyperfoil.tools.horreum.svc; +import com.fasterxml.jackson.databind.ObjectMapper; import io.hyperfoil.tools.horreum.api.SortDirection; import io.hyperfoil.tools.horreum.api.data.*; import io.hyperfoil.tools.horreum.bus.MessageBusChannels; @@ -26,6 +27,7 @@ import jakarta.persistence.PersistenceException; import jakarta.persistence.Query; import jakarta.transaction.Transactional; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -70,6 +72,9 @@ public class TestServiceImpl implements TestService { "GROUP BY dataset_id"; //@formatter:on + @Inject + ObjectMapper mapper; + @Inject EntityManager em; @@ -235,7 +240,8 @@ void addAuthenticated(TestDAO test) { @Override @PermitAll @WithRoles - public TestQueryResult list(String roles, Integer limit, Integer page, String sort, SortDirection direction){ + public TestQueryResult list(String roles, Integer limit, Integer page, String sort, + @DefaultValue("Ascending") SortDirection direction){ PanacheQuery query; Set actualRoles = null; if (Roles.hasRolesParam(roles)) { @@ -394,50 +400,6 @@ public void updateAccess(int id, } } - @Override - @RolesAllowed("tester") - @WithRoles - @Transactional - public int updateView(int testId, View dto) { - if (testId <= 0) { - throw ServiceException.badRequest("Missing test id"); - } - TestDAO test = getTestForUpdate(testId); - ViewDAO view = ViewMapper.to(dto); - view.ensureLinked(); - view.test = test; - if (view.id == null || view.id < 0) { - view.id = null; - view.persist(); - } else { - view = em.merge(view); - int viewId = view.id; - test.views.removeIf(v -> v.id == viewId); - } - test.views.add(view); - test.persist(); - em.flush(); - return view.id; - } - - @Override - @WithRoles - @Transactional - public void deleteView(int testId, int viewId) { - TestDAO test = getTestForUpdate(testId); - if (test.views == null) { - test.views = Collections.singleton(new ViewDAO("Default", test)); - } else if (test.views.stream().anyMatch(v -> v.id == viewId && "Default".equals(v.name))) { - throw ServiceException.badRequest("Cannot remove default view."); - } - if (!test.views.removeIf(v -> v.id == viewId)) { - throw ServiceException.badRequest("Test does not contain this view!"); - } - // the orphan removal doesn't work for some reason, we need to remove if manually - ViewDAO.deleteById(viewId); - test.persist(); - } - @Override @RolesAllowed("tester") @WithRoles @@ -465,34 +427,6 @@ public void updateFolder(int id, String folder) { test.persist(); } - @Override - @RolesAllowed("tester") - @WithRoles - @Transactional - public Action updateAction(int testId, Action dto) { - if (testId <= 0) { - throw ServiceException.badRequest("Missing test id"); - } - // just ensure the test exists - getTestForUpdate(testId); - dto.testId = testId; - actionService.validate(dto); - - ActionDAO action = ActionMapper.to(dto); - if (action.id == null) { - action.persist(); - } else { - if (!action.active) { - ActionDAO.deleteById(action.id); - return null; - } else { - actionService.merge(action); - } - } - em.flush(); - return ActionMapper.from(action); - } - @WithRoles @SuppressWarnings("unchecked") @Override @@ -599,7 +533,7 @@ public RecalculationStatus getRecalculationStatus(int testId) { @WithRoles @Transactional @Override - public JsonNode export(int testId) { + public String export(int testId) { TestDAO test = TestDAO.findById(testId); if (test == null) { throw ServiceException.notFound("Test " + testId + " was not found"); @@ -640,14 +574,21 @@ public JsonNode export(int testId) { export.set("actions", actionService.exportTest(testId)); export.set("experiments", experimentService.exportTest(testId)); export.set("subscriptions", subscriptionService.exportSubscriptions(testId)); - return export; + return export.toString(); } @RolesAllowed({Roles.ADMIN, Roles.TESTER}) @WithRoles @Transactional @Override - public void importTest(JsonNode testConfig) { + public void importTest(String newTest) { + JsonNode testConfig = null; + try { + testConfig = mapper.readValue(newTest, JsonNode.class); + } + catch (JsonProcessingException e) { + throw ServiceException.badRequest("Request object could not be mapped to JsonNode: "+ e.getMessage()); + } if (!testConfig.isObject()) { throw ServiceException.badRequest("Expected Test object as request body, got " + testConfig.getNodeType()); } @@ -662,11 +603,10 @@ public void importTest(JsonNode testConfig) { JsonNode actions = config.remove("actions"); JsonNode experiments = config.remove("experiments"); JsonNode subscriptions = config.remove("subscriptions"); - //Test test; Test dto; boolean forceUseTestId = false; try { - dto = Util.OBJECT_MAPPER.treeToValue(config, Test.class); + dto = mapper.treeToValue(config, Test.class); //test = TestMapper.to( dto); //test.ensureLinked(); if (dto.tokens != null && !dto.tokens.isEmpty()) { @@ -697,13 +637,17 @@ public void importTest(JsonNode testConfig) { } catch (JsonProcessingException e) { throw ServiceException.badRequest("Failed to deserialize test: " + e.getMessage()); } - alertingService.importTest(dto.id, alerting, forceUseTestId); - actionService.importTest(dto.id, actions, forceUseTestId); - experimentService.importTest(dto.id, experiments, forceUseTestId); - subscriptionService.importSubscriptions(dto.id, subscriptions); + if(alerting != null) + alertingService.importTest(dto.id, alerting, forceUseTestId); + if(actions != null) + actionService.importTest(dto.id, actions, forceUseTestId); + if(experiments != null) + experimentService.importTest(dto.id, experiments, forceUseTestId); + if(subscriptions != null) + subscriptionService.importSubscriptions(dto.id, subscriptions); } - TestDAO getTestForUpdate(int testId) { + protected TestDAO getTestForUpdate(int testId) { TestDAO test = TestDAO.findById(testId); if (test == null) { throw ServiceException.notFound("Test " + testId + " was not found"); diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/UIServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/UIServiceImpl.java new file mode 100644 index 000000000..69026c502 --- /dev/null +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/UIServiceImpl.java @@ -0,0 +1,98 @@ +package io.hyperfoil.tools.horreum.svc; + +import io.hyperfoil.tools.horreum.api.data.View; +import io.hyperfoil.tools.horreum.api.services.UIService; +import io.hyperfoil.tools.horreum.entity.data.TestDAO; +import io.hyperfoil.tools.horreum.entity.data.ViewDAO; +import io.hyperfoil.tools.horreum.mapper.ViewMapper; +import io.hyperfoil.tools.horreum.server.WithRoles; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class UIServiceImpl implements UIService { + + @Inject + EntityManager em; + + @Inject + TestServiceImpl testService; + + @Override + @RolesAllowed("tester") + @WithRoles + @Transactional + public int updateView(View dto) { + if (dto.testId <= 0) { + throw ServiceException.badRequest("Missing test id on view"); + } + return doUpdate(testService.getTestForUpdate(dto.testId), ViewMapper.to(dto)); + } + + @Override + @RolesAllowed({Roles.ADMIN, Roles.TESTER}) + @WithRoles + @Transactional + public void createViews(List views) { + if (views.isEmpty() || views.get(0).testId <= 0) { + throw ServiceException.badRequest("Missing test id on view"); + } + TestDAO test = testService.getTestForUpdate(views.get(0).testId); + for(View view : views) { + doUpdate(test, ViewMapper.to(view)); + } + } + + private int doUpdate(TestDAO test, ViewDAO view) { + view.ensureLinked(); + view.test = test; + if (view.id == null || view.id < 0) { + view.id = null; + view.persist(); + } else { + view = em.merge(view); + int viewId = view.id; + test.views.removeIf(v -> v.id == viewId); + } + test.views.add(view); + test.persist(); + em.flush(); + return view.id; + } + + @Override + @WithRoles + @Transactional + public void deleteView(int testId, int viewId) { + TestDAO test = testService.getTestForUpdate(testId); + if (test.views == null) { + test.views = Collections.singleton(new ViewDAO("Default", test)); + } else if (test.views.stream().anyMatch(v -> v.id == viewId && "Default".equals(v.name))) { + throw ServiceException.badRequest("Cannot remove default view."); + } + if (!test.views.removeIf(v -> v.id == viewId)) { + throw ServiceException.badRequest("Test does not contain this view!"); + } + // the orphan removal doesn't work for some reason, we need to remove if manually + ViewDAO.deleteById(viewId); + test.persist(); + } + + @Override + @RolesAllowed({Roles.ADMIN, Roles.TESTER}) + @WithRoles + @Transactional + public List getViews(int testId) { + if (testId <= 0) { + throw ServiceException.badRequest("Missing test id"); + } + return ViewDAO.find("test.id", testId) + .stream().map(ViewMapper::from).collect(Collectors.toList()); + } + +} diff --git a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BaseServiceTest.java b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BaseServiceTest.java index 13446bb02..54d20f72a 100644 --- a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BaseServiceTest.java +++ b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BaseServiceTest.java @@ -203,13 +203,18 @@ public static Test createExampleTest(String testName) { test.name = testName; test.description = "Bar"; test.owner = TESTER_ROLES[0]; + test.transformers = new ArrayList<>(); + return test; + } + + public static List createExampleViews(int testId) { View defaultView = new View(); defaultView.name = "Default"; + defaultView.testId = testId; defaultView.components = new ArrayList<>(); defaultView.components.add(new io.hyperfoil.tools.horreum.api.data.ViewComponent("Some column", null, "foo")); - test.views = Collections.singleton(defaultView); - test.transformers = new ArrayList<>(); - return test; + + return Collections.singletonList(defaultView); } public static String getAccessToken(String userName, String... groups) { @@ -344,12 +349,35 @@ protected List runExperiments(int datasetId) } protected Test createTest(Test test) { - return jsonRequest() + test = jsonRequest() .body(test) .post("/api/test") .then() .statusCode(200) .extract().body().as(Test.class); + + return test; + } + + protected void createViews(List views) { + jsonRequest() + .body(views) + .post("/api/ui/views") + .then() + .statusCode(204); + } + + protected int createView(View view) { + return jsonRequest() + .body(view) + .post("/api/ui/view") + .then() + .statusCode(200).extract().body().as(Integer.class); + } + + protected List getViews(int testId) { + return jsonRequest().get("/api/ui/"+testId+"/views") + .then().statusCode(200).extract().body().as(new ParameterizedTypeImpl(List.class, View.class)); } protected void deleteTest(Test test) { @@ -660,8 +688,9 @@ protected Response addTestHttpAction(Test test, MessageBusChannels event, String action.event = event.name(); action.type = HttpAction.TYPE_HTTP; action.active = true; + action.testId = test.id; action.config = JsonNodeFactory.instance.objectNode().put("url", url); - return jsonRequest().body(action).post("/api/test/" + test.id + "/action"); + return jsonRequest().auth().oauth2(getAdminToken()).body(action).post("/api/action"); } protected Response addTestGithubIssueCommentAction(Test test, MessageBusChannels event, String formatter, String owner, String repo, String issue, String secretToken) { @@ -805,6 +834,12 @@ protected void populateDataFromFiles() throws IOException { t.owner = "foo-team"; t = createTest(t); + View view = new ObjectMapper().readValue( + readFile(p.resolve("roadrunner_view.json").toFile()), View.class); + assertEquals("Default", view.name); + view.testId = t.id; + view.id = createView(view); + Schema s = new ObjectMapper().readValue( readFile(p.resolve("acme_benchmark_schema.json").toFile()), Schema.class); assertEquals("dev-team", s.owner); diff --git a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/DatasetServiceTest.java b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/DatasetServiceTest.java index 15a03ce8d..a62831cbd 100644 --- a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/DatasetServiceTest.java +++ b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/DatasetServiceTest.java @@ -16,6 +16,7 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import io.hyperfoil.tools.horreum.api.services.SqlService; import io.hyperfoil.tools.horreum.bus.MessageBusChannels; import jakarta.inject.Inject; @@ -31,7 +32,7 @@ import io.hyperfoil.tools.horreum.entity.data.ViewComponentDAO; import io.hyperfoil.tools.horreum.mapper.LabelMapper; import io.hyperfoil.tools.horreum.api.services.DatasetService; -import io.hyperfoil.tools.horreum.api.services.QueryResult; +import io.hyperfoil.tools.horreum.api.data.QueryResult; import io.hyperfoil.tools.horreum.server.CloseMe; import io.hyperfoil.tools.horreum.test.HorreumTestProfile; import io.hyperfoil.tools.horreum.test.PostgresResource; @@ -49,6 +50,9 @@ public class DatasetServiceTest extends BaseServiceTest { @Inject DatasetService datasetService; + @Inject + SqlService sqlService; + @org.junit.jupiter.api.Test public void testDataSetQueryNoSchema() { String value = testDataSetQuery("$.value", false, null); @@ -83,7 +87,7 @@ private String testDataSetQuery(String jsonPath, boolean array, String schemaUri AtomicReference result = new AtomicReference<>(); withExampleSchemas(schemas -> result.set(withExampleDataset(createTest(createExampleTest("dummy")), createABData(), ds -> { - QueryResult queryResult = datasetService.queryData(ds.id, jsonPath, array, schemaUri); + QueryResult queryResult = sqlService.queryDatasetData(ds.id, jsonPath, array, schemaUri); assertTrue(queryResult.valid); return queryResult.value; })), "urn:A", "urn:B"); @@ -279,7 +283,7 @@ public void testDatasetView() { Test test = createTest(createExampleTest("dummy")); Util.withTx(tm, () -> { try (CloseMe ignored = roleManager.withRoles(Arrays.asList(TESTER_ROLES))) { - ViewDAO view = ViewDAO.findById(test.views.iterator().next().id); + ViewDAO view = ViewDAO.find("test.id", test.id).firstResult(); view.components.clear(); ViewComponentDAO vc1 = new ViewComponentDAO(); vc1.view = view; diff --git a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/TestServiceTest.java b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/TestServiceTest.java index 772b16d81..23de88e0d 100644 --- a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/TestServiceTest.java +++ b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/TestServiceTest.java @@ -123,7 +123,8 @@ public void testAddTestAction(TestInfo info) { assertNotNull(action.id); assertTrue(action.active); action.active = false; - jsonRequest().body(action).post("/api/test/" + test.id + "/action").then().statusCode(204); + action.testId = test.id; + jsonRequest().body(action).post("/api/action/update").then().statusCode(204); deleteTest(test); } @@ -141,9 +142,11 @@ public void testUpdateView(TestInfo info) throws InterruptedException { ViewComponent vc = new ViewComponent(); vc.headerName = "Foobar"; vc.labels = JsonNodeFactory.instance.arrayNode().add("value"); - View defaultView = test.views.stream().filter(v -> "Default".equals(v.name)).findFirst().orElseThrow(); + List views = getViews(test.id); + View defaultView = views.stream().filter(v -> "Default".equals(v.name)).findFirst().orElseThrow(); defaultView.components.add(vc); - updateView(test.id, defaultView); + defaultView.testId = test.id; + updateView(defaultView); TestUtil.eventually(() -> { em.clear(); @@ -156,8 +159,8 @@ public void testUpdateView(TestInfo info) throws InterruptedException { }); } - private void updateView(int testId, View view) { - Integer viewId = jsonRequest().body(view).post("/api/test/" + testId + "/view") + private void updateView(View view) { + Integer viewId = jsonRequest().body(view).post("/api/ui/view") .then().statusCode(200).extract().body().as(Integer.class); if (view.id != null) { assertEquals(view.id, viewId); @@ -208,7 +211,8 @@ private void testImportExport(boolean wipe) { vc.labels = JsonNodeFactory.instance.arrayNode().add("foo"); vc.headerName = "Some foo"; view.components = Collections.singletonList(vc); - updateView(test.id, view); + view.testId = test.id; + updateView(view); addTestHttpAction(test, MessageBusChannels.RUN_NEW, "http://example.com"); addTestGithubIssueCommentAction(test, MessageBusChannels.EXPERIMENT_RESULT_NEW, diff --git a/horreum-client/pom.xml b/horreum-client/pom.xml index 9278d1aa5..172f40902 100644 --- a/horreum-client/pom.xml +++ b/horreum-client/pom.xml @@ -24,7 +24,8 @@ io.hyperfoil.tools - horreum-api + horreum-ui + ${project.version} diff --git a/horreum-client/src/main/java/io/hyperfoil/tools/RunServiceExtension.java b/horreum-client/src/main/java/io/hyperfoil/tools/RunServiceExtension.java index 3356a2198..30ba84688 100644 --- a/horreum-client/src/main/java/io/hyperfoil/tools/RunServiceExtension.java +++ b/horreum-client/src/main/java/io/hyperfoil/tools/RunServiceExtension.java @@ -16,7 +16,6 @@ import io.hyperfoil.tools.horreum.api.services.RunService.RunCount; import io.hyperfoil.tools.horreum.api.data.Run; -import io.hyperfoil.tools.horreum.api.services.QueryResult; import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; @@ -56,10 +55,10 @@ public Object getMetadata(int id, String token, String schemaUri) { return delegate.getMetadata(id, token, schemaUri); } - @Override - public QueryResult queryData(int id, String jsonpath, String schemaUri, boolean array) { - return delegate.queryData(id, jsonpath, schemaUri, array); - } +// @Override +// public QueryResult queryData(int id, String jsonpath, String schemaUri, boolean array) { +// return delegate.queryData(id, jsonpath, schemaUri, array); +// } @Override public String resetToken(int id) { diff --git a/horreum-client/src/main/java/io/hyperfoil/tools/horreum/api/client/RunService.java b/horreum-client/src/main/java/io/hyperfoil/tools/horreum/api/client/RunService.java index f8aeb352f..35bc48ced 100644 --- a/horreum-client/src/main/java/io/hyperfoil/tools/horreum/api/client/RunService.java +++ b/horreum-client/src/main/java/io/hyperfoil/tools/horreum/api/client/RunService.java @@ -4,7 +4,6 @@ import io.hyperfoil.tools.horreum.api.SortDirection; import io.hyperfoil.tools.horreum.api.data.Access; import io.hyperfoil.tools.horreum.api.data.Run; -import io.hyperfoil.tools.horreum.api.services.QueryResult; import io.hyperfoil.tools.horreum.api.services.RunService.RunsSummary; import io.hyperfoil.tools.horreum.api.services.RunService.RunSummary; @@ -52,12 +51,12 @@ RunExtended getRun(@PathParam("id") int id, @Path("{id}/metadata") Object getMetadata(@PathParam("id") int id, @QueryParam("token") String token, @QueryParam("schemaUri") String schemaUri); - @GET - @Path("{id}/query") - QueryResult queryData(@PathParam("id") int id, - @QueryParam("query") String jsonpath, - @QueryParam("uri") String schemaUri, - @QueryParam("array") @DefaultValue("false") boolean array); +// @GET +// @Path("{id}/query") +// QueryResult queryData(@PathParam("id") int id, +// @QueryParam("query") String jsonpath, +// @QueryParam("uri") String schemaUri, +// @QueryParam("array") @DefaultValue("false") boolean array); @POST @Path("{id}/resetToken") diff --git a/horreum-ui/pom.xml b/horreum-ui/pom.xml new file mode 100644 index 000000000..46ed57ecf --- /dev/null +++ b/horreum-ui/pom.xml @@ -0,0 +1,119 @@ + + + 4.0.0 + + io.hyperfoil.tools + horreum + 0.8-SNAPSHOT + + + horreum-ui + + + 8 + 8 + UTF-8 + + + + io.hyperfoil.tools + horreum-api + + + io.quarkus + quarkus-resteasy-reactive-jackson + true + + + io.quarkus + quarkus-rest-client + true + + + io.quarkus + quarkus-hibernate-validator + true + + + io.quarkus + quarkus-smallrye-openapi + true + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + javax.validation + validation-api + 2.0.1.Final + provided + + + + io.quarkus + quarkus-junit5 + test + + + + + + + smallrye-open-api-maven-plugin + io.smallrye + 2.1.22 + + + process-classes + + generate-schema + + + + io.hyperfoil.tools.horreum.api, + io.hyperfoil.tools.horreum.api.alerting, + io.hyperfoil.tools.horreum.api.data, + io.hyperfoil.tools.horreum.api.grafana, + io.hyperfoil.tools.horreum.api.report + + .* + CLASS_METHOD + + + + + + org.openapitools + openapi-generator-maven-plugin + 7.0.1 + + + package + + generate + + + ${project.build.directory}/generated/openapi.yaml + ${project.basedir}/../webapp/src/generated + typescript-fetch + true + Array=any + + false + + + true + false + true + + + + + + + + + diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Change.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Change.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Change.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Change.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java similarity index 92% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java index 6f42e2402..6daa212b7 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; public class ChangeDetection { @JsonProperty( required = true ) diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/DataPoint.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/DataPoint.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/DataPoint.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/DataPoint.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRule.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRule.java similarity index 91% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRule.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRule.java index 2a53849c7..1b2d68867 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRule.java +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRule.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ArrayNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.time.Instant; import java.util.Objects; @@ -11,6 +12,7 @@ public class MissingDataRule { @JsonProperty( required = true ) public Integer id; public String name; + @Schema(implementation = String[].class) public ArrayNode labels; public String condition; @NotNull diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRuleResult.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRuleResult.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRuleResult.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/MissingDataRuleResult.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/NotificationSettings.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/NotificationSettings.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/NotificationSettings.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/NotificationSettings.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/RunExpectation.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/RunExpectation.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/RunExpectation.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/RunExpectation.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/TransformationLog.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/TransformationLog.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/TransformationLog.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/TransformationLog.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Variable.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Variable.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Variable.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Variable.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Watch.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Watch.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Watch.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/alerting/Watch.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/changes/Dashboard.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/changes/Dashboard.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/changes/Dashboard.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/changes/Dashboard.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/changes/Target.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/changes/Target.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/changes/Target.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/changes/Target.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/changes/TimeRange.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/changes/TimeRange.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/changes/TimeRange.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/changes/TimeRange.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Action.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/Action.java similarity index 96% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Action.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/Action.java index 572054ff1..78a2bc631 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Action.java +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/Action.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; public class Action { @JsonProperty( required = true ) diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ActionLog.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/ActionLog.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ActionLog.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/ActionLog.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/AllowedSite.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/AllowedSite.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/AllowedSite.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/AllowedSite.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Banner.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/Banner.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/Banner.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/Banner.java diff --git a/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/JsonpathValidation.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/JsonpathValidation.java new file mode 100644 index 000000000..d6edcf50a --- /dev/null +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/JsonpathValidation.java @@ -0,0 +1,13 @@ +package io.hyperfoil.tools.horreum.api.data; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class JsonpathValidation { + @JsonProperty(required = true) + public boolean valid; + public String jsonpath; + public int errorCode; + public String sqlState; + public String reason; + public String sql; +} diff --git a/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/QueryResult.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/QueryResult.java new file mode 100644 index 000000000..c7a3a42f6 --- /dev/null +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/QueryResult.java @@ -0,0 +1,5 @@ +package io.hyperfoil.tools.horreum.api.data; + +public class QueryResult extends JsonpathValidation { + public String value; +} diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/View.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/View.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/View.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/View.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ViewComponent.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/ViewComponent.java similarity index 96% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ViewComponent.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/ViewComponent.java index 2bf11d47e..37b6b20f5 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ViewComponent.java +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/data/ViewComponent.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import jakarta.validation.constraints.NotNull; +import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.Objects; diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComment.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComment.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComment.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComment.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComponent.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComponent.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComponent.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportComponent.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportLog.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportLog.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportLog.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/ReportLog.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReport.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReport.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReport.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReport.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReportConfig.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReportConfig.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReportConfig.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/report/TableReportConfig.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ActionService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/ActionService.java similarity index 94% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ActionService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/ActionService.java index f7ba575a1..8fb23a6b7 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ActionService.java +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/ActionService.java @@ -33,6 +33,10 @@ public interface ActionService { @Path("{id}") void delete(@PathParam("id") int id); + @POST + @Path("update") + Action update(@RequestBody(required = true) Action action); + @GET @Path("list") List list(@QueryParam("limit") Integer limit, diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/AlertingService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/AlertingService.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/AlertingService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/AlertingService.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/BannerService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/BannerService.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/BannerService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/BannerService.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ChangesService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/ChangesService.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ChangesService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/ChangesService.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/LogService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/LogService.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/LogService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/LogService.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/NotificationService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/NotificationService.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/NotificationService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/NotificationService.java diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ReportService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/ReportService.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/ReportService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/ReportService.java diff --git a/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/SqlService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/SqlService.java new file mode 100644 index 000000000..6cfa7a8b4 --- /dev/null +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/SqlService.java @@ -0,0 +1,43 @@ +package io.hyperfoil.tools.horreum.api.services; + +import io.hyperfoil.tools.horreum.api.data.JsonpathValidation; +import io.hyperfoil.tools.horreum.api.data.QueryResult; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; + +@Path("/api/sql") +@Consumes({ MediaType.APPLICATION_JSON}) +@Produces(MediaType.APPLICATION_JSON) +public interface SqlService { + @GET + @Path("testjsonpath") + JsonpathValidation testJsonPath(@Parameter(required = true) @QueryParam("query") String jsonpath); + + @Path("roles") + @GET + @Produces("text/plain") + String roles(@QueryParam("system") @DefaultValue("false") boolean system); + + @GET + @Path("{id}/queryrun") + QueryResult queryRunData(@PathParam("id") int id, + @Parameter(required = true) @QueryParam("query") String jsonpath, + @QueryParam("uri") String schemaUri, + @QueryParam("array") @DefaultValue("false") boolean array); + + @Path("{id}/querydataset") + @GET + QueryResult queryDatasetData(@PathParam("id") int datasetId, + @Parameter(required = true) @QueryParam("query") String jsonpath, + @QueryParam("array") @DefaultValue("false") boolean array, + @QueryParam("schemaUri") String schemaUri); + +} diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SubscriptionService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/SubscriptionService.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/SubscriptionService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/SubscriptionService.java diff --git a/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/UIService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/UIService.java new file mode 100644 index 000000000..2f6949684 --- /dev/null +++ b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/UIService.java @@ -0,0 +1,36 @@ +package io.hyperfoil.tools.horreum.api.services; + +import io.hyperfoil.tools.horreum.api.data.View; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; + +import java.util.List; + +@Path("/api/ui") +@Consumes({ MediaType.APPLICATION_JSON}) +@Produces(MediaType.APPLICATION_JSON) +public interface UIService { + + @POST + @Path("view") + int updateView(@RequestBody(required = true) View view); + + @POST + @Path("views") + void createViews(@RequestBody(required = true) List views); + + @DELETE + @Path("{testId}/view/{viewId}") + void deleteView(@PathParam("testId") int testId, @PathParam("viewId") int viewId); + + @GET + @Path("{testId}/views") + List getViews(@PathParam("testId") int testId); +} diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/UserService.java b/horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/UserService.java similarity index 100% rename from horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/services/UserService.java rename to horreum-ui/src/main/java/io/hyperfoil/tools/horreum/api/services/UserService.java diff --git a/horreum-ui/src/main/resources/META-INF/beans.xml b/horreum-ui/src/main/resources/META-INF/beans.xml new file mode 100644 index 000000000..ba8f8ab7d --- /dev/null +++ b/horreum-ui/src/main/resources/META-INF/beans.xml @@ -0,0 +1,4 @@ + + + + diff --git a/infra-legacy/example-data/roadrunner_test.json b/infra-legacy/example-data/roadrunner_test.json index 0dc4ffd71..803284228 100644 --- a/infra-legacy/example-data/roadrunner_test.json +++ b/infra-legacy/example-data/roadrunner_test.json @@ -3,23 +3,5 @@ "description": "acme.com benchmark", "owner": "dev-team", "access": "PUBLIC", - "views": [ - { - "name": "Default", - "components": [ - { - "headerOrder": 1, - "headerName": "Test", - "labels": [ "benchmark_test" ] - }, - { - "headerOrder": 1, - "headerName": "Throughput", - "labels": [ "throughput" ], - "render": "value => value + \"req/s\"" - } - ] - } - ], "fingerprintLabels": [ "benchmark_test" ] } \ No newline at end of file diff --git a/infra-legacy/example-data/roadrunner_view.json b/infra-legacy/example-data/roadrunner_view.json new file mode 100644 index 000000000..a9c03d2f2 --- /dev/null +++ b/infra-legacy/example-data/roadrunner_view.json @@ -0,0 +1,16 @@ +{ + "name": "Default", + "components": [ + { + "headerOrder": 1, + "headerName": "Test", + "labels": [ "benchmark_test" ] + }, + { + "headerOrder": 1, + "headerName": "Throughput", + "labels": [ "throughput" ], + "render": "value => value + \"req/s\"" + } + ] +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6f477c5c7..eede365f1 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ infra/horreum-dev-services infra/horreum-infra-common horreum-api + horreum-ui horreum-backend horreum-client horreum-integration-tests diff --git a/webapp/src/components/ExportImport.tsx b/webapp/src/components/ExportImport.tsx index a2fba34e8..bfd8acb47 100644 --- a/webapp/src/components/ExportImport.tsx +++ b/webapp/src/components/ExportImport.tsx @@ -9,14 +9,14 @@ import ExportButton from "./ExportButton" type ExportImportProps = { name: string export(): Promise - import(cfg: Record): Promise - validate(cfg: Record): Promise + import(cfg: string): Promise + validate(cfg: string): Promise } export default function ExportImport(props: ExportImportProps) { const [uploadName, setUploadName] = useState() const [loading, setLoading] = useState(false) - const [uploadContent, setUploadContent] = useState>() + const [uploadContent, setUploadContent] = useState() const [uploading, setUploading] = useState(false) const [parseError, setParseError] = useState(undefined) const dispatch = useDispatch() @@ -46,7 +46,7 @@ export default function ExportImport(props: ExportImportProps) { .then( cfg => { try { - setUploadContent(JSON.parse(cfg) as Record) + setUploadContent(JSON.parse(cfg)) } catch (e) { setParseError(e) return diff --git a/webapp/src/components/ImportButton.tsx b/webapp/src/components/ImportButton.tsx index 575512b92..5947bd9a9 100644 --- a/webapp/src/components/ImportButton.tsx +++ b/webapp/src/components/ImportButton.tsx @@ -8,7 +8,7 @@ import { Bullseye, Button, FileUpload, Modal, Spinner } from "@patternfly/react- type ImportProps = { label?: string onLoad(config: Record): Promise | any - onImport(config: Record): Promise + onImport(config: string): Promise onImported(): void } @@ -17,7 +17,7 @@ export default function ImportButton({label, onLoad, onImport, onImported}: Impo const [filename, setFilename] = useState() const [loading, setLoading] = useState(false) const [checking, setChecking] = useState(false) - const [config, setConfig] = useState>() + const [config, setConfig] = useState() const [uploading, setUploading] = useState(false) const [parseError, setParseError] = useState() const [overridden, setOverridden] = useState() diff --git a/webapp/src/domain/reports/TableReportConfigPage.tsx b/webapp/src/domain/reports/TableReportConfigPage.tsx index ae0edcccc..359ad38b0 100644 --- a/webapp/src/domain/reports/TableReportConfigPage.tsx +++ b/webapp/src/domain/reports/TableReportConfigPage.tsx @@ -272,7 +272,6 @@ export default function TableReportConfigPage() { owner: "", access: 0, notificationsEnabled: false, - views: [], }, }) }} diff --git a/webapp/src/domain/runs/DatasetComparison.tsx b/webapp/src/domain/runs/DatasetComparison.tsx index 57e53dbc9..67f920b69 100644 --- a/webapp/src/domain/runs/DatasetComparison.tsx +++ b/webapp/src/domain/runs/DatasetComparison.tsx @@ -23,6 +23,8 @@ import PrintButton from "../../components/PrintButton" import FragmentTabs, { FragmentTab } from "../../components/FragmentTabs" import { renderValue } from "./components" +import { fetchViews } from "../tests/actions" +import { viewsSelector } from "./selectors" type Ds = { id: number @@ -35,11 +37,16 @@ export default function DatasetComparison() { const history = useHistory() const params = new URLSearchParams(history.location.search) const testId = parseInt(params.get("testId") || "-1") + const views = useSelector(viewsSelector(testId)) const dispatch = useDispatch() const [test, setTest] = useState() useEffect(() => { - Api.testServiceGet(testId).then(setTest, e => - dispatchError(dispatch, e, "FETCH_TEST", "Failed to fetch test " + testId) + Api.testServiceGet(testId).then( + test => { + setTest(test) + dispatch(fetchViews(testId)) + }, + e => dispatchError(dispatch, e, "FETCH_TEST", "Failed to fetch test " + testId) ) }, [testId]) const datasets = useMemo( @@ -71,7 +78,8 @@ export default function DatasetComparison() { [datasets] ) - const defaultView = test?.views.find(v => (v.name = "Default")) + const defaultView = views?.find(v => (v.name = "Default")) + return ( @@ -224,7 +232,7 @@ function ViewComparison(props: ViewComparisonProps) { vc.labels.length == 1 ? vc.labels[0] : undefined, token ) - return render(summary.view[vc.id], summary) + return render(summary.view?.[vc.id], summary) }), ], })) diff --git a/webapp/src/domain/runs/DatasetData.tsx b/webapp/src/domain/runs/DatasetData.tsx index f69497ad3..cb91f27ef 100644 --- a/webapp/src/domain/runs/DatasetData.tsx +++ b/webapp/src/domain/runs/DatasetData.tsx @@ -73,7 +73,7 @@ export default function DatasetData(props: DatasetDataProps) { Api.datasetServiceQueryData(props.datasetId, query, array)} + onRemoteQuery={(query, array) => Api.sqlServiceQueryDatasetData(props.datasetId, query, array)} onDataUpdate={setEditorData} /> diff --git a/webapp/src/domain/runs/RunData.tsx b/webapp/src/domain/runs/RunData.tsx index 6259b133b..3dd937196 100644 --- a/webapp/src/domain/runs/RunData.tsx +++ b/webapp/src/domain/runs/RunData.tsx @@ -106,7 +106,7 @@ export default function RunData(props: RunDataProps) { )} Api.runServiceQueryData(props.run.id, query, array)} + onRemoteQuery={(query, array) => Api.sqlServiceQueryRunData(props.run.id, query, array)} onDataUpdate={setEditorData} /> {memoizedEditor} diff --git a/webapp/src/domain/runs/SchemaList.tsx b/webapp/src/domain/runs/SchemaList.tsx index 148c28201..dc8d7e914 100644 --- a/webapp/src/domain/runs/SchemaList.tsx +++ b/webapp/src/domain/runs/SchemaList.tsx @@ -34,7 +34,7 @@ export default function SchemaList(props: SchemaListProps) {
    {validationErrors.map((e, i) => ( -
  • {e.error.message}
  • +
  • {JSON.parse(e.error).message}
  • ))}
Visit run/dataset for details. @@ -61,7 +61,7 @@ export default function SchemaList(props: SchemaListProps) {
    {noSchemaErrors.map((e, i) => ( -
  • {e.error.message}
  • +
  • {JSON.parse(e.error).message}
  • ))}
Visit run/dataset for details. diff --git a/webapp/src/domain/runs/TestDatasets.tsx b/webapp/src/domain/runs/TestDatasets.tsx index 994257025..6b0da2ce4 100644 --- a/webapp/src/domain/runs/TestDatasets.tsx +++ b/webapp/src/domain/runs/TestDatasets.tsx @@ -51,6 +51,7 @@ import { NoSchemaInDataset } from "./NoSchema" import ButtonLink from "../../components/ButtonLink" import LabelsSelect, { SelectedLabels } from "../../components/LabelsSelect" import ViewSelect from "../../components/ViewSelect" +import {viewsSelector} from "./selectors"; type C = CellProps & UseTableOptions & @@ -139,6 +140,7 @@ export default function TestDatasets() { const [loading, setLoading] = useState(false) const [datasets, setDatasets] = useState() const [comparedDatasets, setComparedDatasets] = useState() + const views = useSelector(viewsSelector(testId)) const teams = useSelector(teamsSelector) const token = useSelector(tokenSelector) useEffect(() => { @@ -194,7 +196,7 @@ export default function TestDatasets() { }, }) } - const view = test?.views.find(v => v.id === viewId) || test?.views.find(v => v.name === "Default") + const view = views?.find(v => v.id === viewId) || views?.find(v => v.name === "Default") const components = view?.components || [] components.forEach(vc => { allColumns.push({ @@ -232,8 +234,8 @@ export default function TestDatasets() { View: v.name === "Default")?.id || -1} + views={views || []} + viewId={viewId || views?.find(v => v.name === "Default")?.id || -1} onChange={setViewId} /> diff --git a/webapp/src/domain/runs/ValidationErrorTable.tsx b/webapp/src/domain/runs/ValidationErrorTable.tsx index 7078b89ac..a1864a3a2 100644 --- a/webapp/src/domain/runs/ValidationErrorTable.tsx +++ b/webapp/src/domain/runs/ValidationErrorTable.tsx @@ -3,6 +3,7 @@ import { NavLink } from "react-router-dom" import { SchemaDescriptor, ValidationError } from "../../api" import { Table, TableBody, TableHeader } from "@patternfly/react-table" +import { Json } from "../../generated" type ValidationErrorTableProps = { errors: ValidationError[] @@ -10,6 +11,7 @@ type ValidationErrorTableProps = { } export default function ValidationErrorTable(props: ValidationErrorTableProps) { + let err: Json; const rows = useMemo( () => props.errors && @@ -22,11 +24,12 @@ export default function ValidationErrorTable(props: ValidationErrorTableProps) { ) : ( "(none)" ), - e.error.type, - {e.error.path}, - {e.error.schemaPath}, - {e.error.arguments}, - e.error.message, + err = JSON.parse(e.error), + err.type, + {err.path}, + {err.schemaPath}, + {err.arguments}, + err.message, ], })), [props.errors, props.schemas] diff --git a/webapp/src/domain/runs/actions.ts b/webapp/src/domain/runs/actions.ts index 5d5e15238..4070eae90 100644 --- a/webapp/src/domain/runs/actions.ts +++ b/webapp/src/domain/runs/actions.ts @@ -53,9 +53,9 @@ export function getSummary(id: number, token?: string) { response => dispatch( loaded({ - data: undefined, + data: "", schemas: [], - metadata: response.hasMetadata ? {} : undefined, + metadata: response.hasMetadata ? "" : undefined, ...response, }) ), diff --git a/webapp/src/domain/runs/reducers.ts b/webapp/src/domain/runs/reducers.ts index c45013d57..1f3f886d6 100644 --- a/webapp/src/domain/runs/reducers.ts +++ b/webapp/src/domain/runs/reducers.ts @@ -149,7 +149,7 @@ export const reducer = (state = new RunsState(), action: RunsAction) => { if (run !== undefined) { testMap = testMap.set(run.id, { ...testMap.get(run.id), - data: undefined, + data: "", schemas: [], ...run, }) diff --git a/webapp/src/domain/runs/selectors.ts b/webapp/src/domain/runs/selectors.ts index 537eb32d5..c992051c4 100644 --- a/webapp/src/domain/runs/selectors.ts +++ b/webapp/src/domain/runs/selectors.ts @@ -52,6 +52,14 @@ export const get = (id: number) => (state: State) => { return state.runs.byId.get(id) } +export const viewsSelector = (testID : number) => (state: State) => { + if (!state.tests.byId) { + return undefined + } + return state.tests.byId.get(testID)?.views + +} + export const filter = (pagination: PaginationInfo) => (state: State) => { const byId = state.runs.byId if (!byId) { diff --git a/webapp/src/domain/schemas/Schema.tsx b/webapp/src/domain/schemas/Schema.tsx index 918ceddd9..b36c0901a 100644 --- a/webapp/src/domain/schemas/Schema.tsx +++ b/webapp/src/domain/schemas/Schema.tsx @@ -40,13 +40,14 @@ import Transformers from "./Transformers" import Labels from "./Labels" import { Access, Schema as SchemaDef } from "../../api" import SchemaExportImport from "./SchemaExportImport" +import { Json } from "../../generated" type SchemaParams = { schemaId: string } type GeneralProps = { - schema: SchemaDef | undefined + schema: SchemaDef | Json onChange(partialSchema: SchemaDef): void getUri?(): string } diff --git a/webapp/src/domain/schemas/TryJsonPathModal.tsx b/webapp/src/domain/schemas/TryJsonPathModal.tsx index eae214f6a..c86b8d42c 100644 --- a/webapp/src/domain/schemas/TryJsonPathModal.tsx +++ b/webapp/src/domain/schemas/TryJsonPathModal.tsx @@ -79,9 +79,9 @@ export default function TryJsonPathModal(props: TryJsonPathModalProps) { } let response: Promise if (props.target === "run") { - response = Api.runServiceQueryData(id, props.jsonpath, props.array, props.uri) + response = Api.sqlServiceQueryRunData(id, props.jsonpath, props.array, props.uri) } else { - response = Api.datasetServiceQueryData(id, props.jsonpath, props.array, props.uri) + response = Api.sqlServiceQueryDatasetData(id, props.jsonpath, props.array, props.uri) } return response.then( result => { diff --git a/webapp/src/domain/tests/Experiments.tsx b/webapp/src/domain/tests/Experiments.tsx index 7aca0001c..7585e3fee 100644 --- a/webapp/src/domain/tests/Experiments.tsx +++ b/webapp/src/domain/tests/Experiments.tsx @@ -353,9 +353,9 @@ export default function Experiments(props: ExperimentsProps) { { - c.config[comp.name] = value + JSON.parse(c.config)[comp.name] = value update({ comparisons: [...selected.comparisons] }) }} /> @@ -389,7 +389,7 @@ export default function Experiments(props: ExperimentsProps) { { model: modelToAdd, config: - models.find(m => m.name === modelToAdd)?.defaults || {}, + JSON.stringify(models.find(m => m.name === modelToAdd)?.defaults) || "", variableId: variables[0].id, }, ], diff --git a/webapp/src/domain/tests/General.tsx b/webapp/src/domain/tests/General.tsx index 45e7dcf57..364265fbd 100644 --- a/webapp/src/domain/tests/General.tsx +++ b/webapp/src/domain/tests/General.tsx @@ -51,7 +51,6 @@ export default function General({ test, onTestIdChange, onModified, funcsRef }: name, folder, description, - views: [], // automatically insert default view in backend compareUrl: compareUrl || undefined, // when empty set to undefined notificationsEnabled, fingerprintLabels: [], diff --git a/webapp/src/domain/tests/Test.tsx b/webapp/src/domain/tests/Test.tsx index f625a0a5a..60a1e0846 100644 --- a/webapp/src/domain/tests/Test.tsx +++ b/webapp/src/domain/tests/Test.tsx @@ -64,6 +64,7 @@ export default function Test() { if (testId !== 0) { setLoaded(false) dispatch(actions.fetchTest(testId)) + .then( () => dispatch(actions.fetchViews(testId)) ) .catch(noop) .finally(() => setLoaded(true)) } @@ -147,7 +148,7 @@ export default function Test() { > dispatch({ type: actionTypes.LOADED_SUMMARY, - tests: listing.tests?.map(t => ({ ...t, views: [], notificationsEnabled: false })) || [], + tests: listing.tests?.map(t => ({ ...t, notificationsEnabled: false })) || [], }), error => { dispatch(loading(false)) @@ -80,6 +81,24 @@ export function sendTest(test: Test) { } } +export function fetchViews(testId: number) { + return (dispatch: Dispatch) => { + dispatch(loading(true)) + return Api.uIServiceGetViews(testId).then( + views => dispatch({ type: actionTypes.LOADED_VIEWS, testId, views }), + error => { + dispatch(loading(false)) + return dispatchError( + dispatch, + error, + "FETCH_VIEWS", + "Failed to fetch test views; the views may not exist or you don't have sufficient permissions to access them." + ) + } + ) + } +} + export function updateView(testId: number, view: View) { return (dispatch: Dispatch): Promise => { for (const c of view.components) { @@ -94,7 +113,8 @@ export function updateView(testId: number, view: View) { return Promise.reject() } } - return Api.testServiceUpdateView(testId, view).then( + view.testId = testId + return Api.uIServiceUpdateView(view).then( viewId => { const id: number = ensureInteger(viewId) dispatch({ @@ -114,7 +134,7 @@ export function updateView(testId: number, view: View) { export function deleteView(testId: number, viewId: number) { return (dispatch: Dispatch) => { - return Api.testServiceDeleteView(testId, viewId).then( + return Api.uIServiceDeleteView(testId, viewId).then( _ => { dispatch({ type: actionTypes.DELETE_VIEW, @@ -147,7 +167,8 @@ export function updateActions(testId: number, actions: Action[]) { const promises: any[] = [] actions.forEach(action => { promises.push( - Api.testServiceUpdateAction(testId, action).then( + (action.testId = testId), + Api.actionServiceUpdate(action).then( response => { dispatch({ type: actionTypes.UPDATE_ACTION, diff --git a/webapp/src/domain/tests/reducers.ts b/webapp/src/domain/tests/reducers.ts index c53486073..b7179e07c 100644 --- a/webapp/src/domain/tests/reducers.ts +++ b/webapp/src/domain/tests/reducers.ts @@ -2,7 +2,15 @@ import * as actionTypes from "./actionTypes" import { Map } from "immutable" import { ThunkDispatch } from "redux-thunk" import { AddAlertAction } from "../../alerts" -import { Access, Action, Run, Test, TestToken, Transformer, View } from "../../api" +import { + Access, + Action, + Run, + Test, + TestToken, + Transformer, + View +} from "../../api" import { AnyAction } from "redux" export type CompareFunction = (runs: Run[]) => string @@ -11,6 +19,7 @@ export interface TestStorage extends Test { datasets?: number // dataset count in AllTests runs?: number // run count in AllTests watching?: string[] + views?: View[] } export class TestsState { @@ -54,6 +63,17 @@ export interface UpdateTestWatchAction { byId: Map } +export interface GetViewsAction { + type: typeof actionTypes.GET_VIEWS + testId: number +} + +export interface LoadedViewsAction { + type: typeof actionTypes.LOADED_VIEWS + testId: number + views: View[] +} + export interface UpdateViewAction { type: typeof actionTypes.UPDATE_VIEW testId: number @@ -125,6 +145,7 @@ export type TestAction = | DeleteAction | UpdateAccessAction | UpdateTestWatchAction + | LoadedViewsAction | UpdateViewAction | DeleteViewAction | UpdateActionAction @@ -160,6 +181,14 @@ export const reducer = (state = new TestsState(), action: TestAction) => { } state.byId = (state.byId as Map).set(action.test.id, action.test) break + case actionTypes.LOADED_VIEWS: + { + const test = state.byId?.get(action.testId) + if (test) { + state.byId = state.byId?.set(action.testId, {...test, views: action.views}) + } + } + break case actionTypes.UPDATE_ACCESS: { const test = state.byId?.get(action.id) @@ -182,11 +211,12 @@ export const reducer = (state = new TestsState(), action: TestAction) => { { const test = state.byId?.get(action.testId) if (test) { + const loadedViews : View[] = test.views || [] let views - if (test.views.some(v => v.id === action.view.id)) { - views = test.views.map(v => (v.id === action.view.id ? action.view : v)) + if (loadedViews.some(v => v.id === action.view.id)) { + views = loadedViews.map(v => (v.id === action.view.id ? action.view : v)) } else { - views = [...test.views, action.view] + views = [...loadedViews, action.view] } state.byId = state.byId?.set(action.testId, { ...test, views }) } @@ -199,7 +229,7 @@ export const reducer = (state = new TestsState(), action: TestAction) => { // just ignore deleting default view, that's not legal state.byId = state.byId?.set(action.testId, { ...test, - views: test.views.filter(v => v.id !== action.viewId), + views: test.views?.filter(v => v.id !== action.viewId), }) } }