diff --git a/docs/site/content/en/docs/Tutorials/grafana/index.md b/docs/site/content/en/docs/Tutorials/grafana/index.md index 495cb92a5..481998204 100644 --- a/docs/site/content/en/docs/Tutorials/grafana/index.md +++ b/docs/site/content/en/docs/Tutorials/grafana/index.md @@ -103,3 +103,24 @@ We set the `filter` parameter by editing the Query for the grafana panel but it Define filter for query {{% /imgproc %}} +## Filtering labels + +Another common consideration is the amount of data in the `/labelValues` response. Tests with lots of labels, or labels that produce a lot of data, +can see the `/labelValues` transfer json size grow well beyond what they need for a particular integration. Horreum has the `include` and `exclude` +query parameter options on the `/labelValues` endpoint. + +### Include +Adding `include=foo` to the `/labelValues` endpoint query tells Horreum to only include the `foo` label and its value in the `values` part of the +`/labelValues` response. You can specify multiple labels with `incude=foo&include=bar` or `include=foo,bar` using url encoding or with curl: +```bash +curl --query-param "include=foo" --query-param "include=bar" ... +``` + +Note: any `include` that is also mentioned in `exclude` will not be part of the response `values` + +### Exclude +This functions similar to `include` except that it removes a label name from the response `values` field for the `/labelValues` endpoint. This filter +option leaves all other labels in the `values` field. +If a user specifies both `include` and `exclude` then the response will only contain the `include` label names that are not also in `exclude`. If all +`include` are also in `exclude` then the `exclude` takes priority and the response will contain all labels that are not mentioned in `exclude`. +Horreum uses this default behavior to avoid sending any data that is explicitly excluded. \ No newline at end of file diff --git a/docs/site/content/en/openapi/openapi.yaml b/docs/site/content/en/openapi/openapi.yaml index 92cd71135..fb7de0539 100644 --- a/docs/site/content/en/openapi/openapi.yaml +++ b/docs/site/content/en/openapi/openapi.yaml @@ -1012,6 +1012,22 @@ paths: default: 0 type: integer example: 2 + - name: include + in: query + description: name of a label to include in the result + schema: + type: array + items: + type: string + example: id + - name: exclude + in: query + description: name of a label to exclude from the result + schema: + type: array + items: + type: string + example: id responses: "200": description: label Values @@ -2008,7 +2024,7 @@ paths: tags: - Test description: List all Label Values for a Test - operationId: listLabelValues + operationId: labelValues parameters: - name: id in: path @@ -2090,6 +2106,22 @@ paths: default: 0 type: integer example: 2 + - name: include + in: query + description: name of a label to include in the result + schema: + type: array + items: + type: string + example: id + - name: exclude + in: query + description: name of a label to exclude from the result + schema: + type: array + items: + type: string + example: id responses: "200": description: OK 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 742bd4553..da17bc5c0 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 @@ -31,6 +31,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.reactive.RestForm; +import org.jboss.resteasy.reactive.Separator; import org.jboss.resteasy.reactive.multipart.FileUpload; @Path("/api/run") @@ -110,7 +111,17 @@ Object getData(@PathParam("id") int id, @Parameter(name = "sort", description = "label name for sorting"), @Parameter(name = "direction",description = "either Ascending or Descending",example="count"), @Parameter(name = "limit",description = "the maximum number of results to include",example="10"), - @Parameter(name = "page",description = "which page to skip to when using a limit",example="2") + @Parameter(name = "page",description = "which page to skip to when using a limit",example="2"), + @Parameter(name = "include", description = "label name(s) to include in the result as scalar or comma separated", + examples = { + @ExampleObject(name="single", value="id", description = "including a single label"), + @ExampleObject(name="multiple", value="id,count", description = "including multiple labels") + }), + @Parameter(name = "exclude", description = "label name(s) to exclude from the result as scalar or comma separated", + examples = { + @ExampleObject(name="single", value="id", description = "excluding a single label"), + @ExampleObject(name="multiple", value="id,count", description = "excluding multiple labels") + }) }) @APIResponses( value = { @@ -132,7 +143,9 @@ List labelValues( @QueryParam("sort") @DefaultValue("") String sort, @QueryParam("direction") @DefaultValue("Ascending") String direction, @QueryParam("limit") @DefaultValue(""+Integer.MAX_VALUE) int limit, - @QueryParam("page") @DefaultValue("0") int page); + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("include") @Separator(",") List include, + @QueryParam("exclude") @Separator(",") List exclude); @GET @Path("{id}/metadata") 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 3b91363c7..1ef5cec7b 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 @@ -35,6 +35,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponseSchema; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.jboss.resteasy.reactive.Separator; import java.util.Collection; import java.util.List; @@ -216,7 +217,17 @@ void updateNotifications(@PathParam("id") int id, @Parameter(name = "sort", description = "json path to sortable value or start or stop for sorting by time",example = "$.label or start or stop"), @Parameter(name = "direction",description = "either Ascending or Descending",example="count"), @Parameter(name = "limit",description = "the maximum number of results to include",example="10"), - @Parameter(name = "page",description = "which page to skip to when using a limit",example="2") + @Parameter(name = "page",description = "which page to skip to when using a limit",example="2"), + @Parameter(name = "include", description = "label name(s) to include in the result as scalar or comma separated", + examples = { + @ExampleObject(name="single", value="id", description = "including a single label"), + @ExampleObject(name="multiple", value="id,count", description = "including multiple labels") + }), + @Parameter(name = "exclude", description = "label name(s) to exclude from the result as scalar or comma separated", + examples = { + @ExampleObject(name="single", value="id", description = "excluding a single label"), + @ExampleObject(name="multiple", value="id,count", description = "excluding multiple labels") + }) }) @APIResponses( value = { @APIResponse( responseCode = "200", @@ -224,7 +235,7 @@ void updateNotifications(@PathParam("id") int id, @Content ( schema = @Schema(type = SchemaType.ARRAY, implementation = ExportedLabelValues.class)) } )} ) - List listLabelValues( + List labelValues( @PathParam("id") int testId, @QueryParam("filter") @DefaultValue("{}") String filter, @QueryParam("before") @DefaultValue("") String before, @@ -234,7 +245,9 @@ List listLabelValues( @QueryParam("sort") @DefaultValue("") String sort, @QueryParam("direction") @DefaultValue("Ascending") String direction, @QueryParam("limit") @DefaultValue(""+Integer.MAX_VALUE) int limit, - @QueryParam("page") @DefaultValue("0") int page); + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("include") @Separator(",") List include, + @QueryParam("exclude") @Separator(",") List exclude); @POST @Consumes(MediaType.APPLICATION_JSON) 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 05924f992..8e2921b6c 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 @@ -271,10 +271,10 @@ public Object getData(int id, String token, String schemaUri) { } } - //this is nearly identical to TestServiceImpl.listLabelValues (except the return object) + //this is nearly identical to TestServiceImpl.labelValues (except the return object) //this reads from the dataset table but provides data specific to the run... @Override - public List labelValues(int runId, String filter, String sort, String direction, int limit, int page){ + public List labelValues(int runId, String filter, String sort, String direction, int limit, int page, List include, List exclude){ List rtrn = new ArrayList<>(); Run run = getRun(runId,null); if(run == null){ @@ -314,10 +314,25 @@ public List labelValues(int runId, String filter, String so orderSql="order by combined.datasetId DESC"; } } + String includeExcludeSql = ""; + if (include!=null && !include.isEmpty()) { + if (exclude != null && !exclude.isEmpty()) { + include = new ArrayList<>(include); + include.removeAll(exclude); + } + if (!include.isEmpty()) { + includeExcludeSql = " AND label.name in :include"; + } + } + //includeExcludeSql is empty if include did not contain entries after exclude removal + if(includeExcludeSql.isEmpty() && exclude!=null && !exclude.isEmpty()){ + includeExcludeSql=" AND label.name NOT in :exclude"; + } + String sql = """ WITH combined as ( - SELECT DISTINCT COALESCE(jsonb_object_agg(label.name, lv.value) FILTER (WHERE label.name IS NOT NULL), '{}'::::jsonb) AS values, dataset.id AS datasetId, dataset.start AS start, dataset.stop AS stop + SELECT DISTINCT COALESCE(jsonb_object_agg(label.name, lv.value) FILTER (WHERE label.name IS NOT NULL INCLUDE_EXCLUDE_PLACEHOLDER), '{}'::::jsonb) AS values, dataset.id AS datasetId, dataset.start AS start, dataset.stop AS stop FROM dataset LEFT JOIN label_values lv ON dataset.id = lv.dataset_id LEFT JOIN label ON label.id = lv.label_id @@ -326,6 +341,7 @@ SELECT DISTINCT COALESCE(jsonb_object_agg(label.name, lv.value) FILTER (WHERE la ) select * from combined FILTER_PLACEHOLDER ORDER_PLACEHOLDER limit :limit offset :offset """ .replace("FILTER_PLACEHOLDER",filterSql) + .replace("INCLUDE_EXCLUDE_PLACEHOLDER",includeExcludeSql) .replace("ORDER_PLACEHOLDER",orderSql); NativeQuery query = ((NativeQuery) em.createNativeQuery(sql)) @@ -337,12 +353,17 @@ SELECT DISTINCT COALESCE(jsonb_object_agg(label.name, lv.value) FILTER (WHERE la query.setParameter("filter", filter); } } + if(includeExcludeSql.contains(":include")){ + query.setParameter("include",include); + }else if (includeExcludeSql.contains(":exclude")){ + query.setParameter("exclude",exclude); + } if(orderSql.contains(":orderBy")){ query.setParameter("orderBy",sort); } query .setParameter("limit",limit) - .setParameter("offset",limit * page) + .setParameter("offset",limit * Math.max(0,page)) .unwrap(NativeQuery.class) .addScalar("values", JsonBinaryType.INSTANCE) .addScalar("datasetId",Integer.class) 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 04dcb0a1e..f9c75e87b 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 @@ -78,7 +78,7 @@ public class TestServiceImpl implements TestService { protected static final String LABEL_VALUES_QUERY = """ WITH combined as ( - SELECT DISTINCT COALESCE(jsonb_object_agg(label.name, lv.value) FILTER (WHERE label.name IS NOT NULL), '{}'::::jsonb) AS values, runId, dataset.id AS datasetId, dataset.start AS start, dataset.stop AS stop + SELECT DISTINCT COALESCE(jsonb_object_agg(label.name, lv.value) FILTER (WHERE label.name IS NOT NULL INCLUDE_EXCLUDE_PLACEHOLDER), '{}'::::jsonb) AS values, runId, dataset.id AS datasetId, dataset.start AS start, dataset.stop AS stop FROM dataset LEFT JOIN label_values lv ON dataset.id = lv.dataset_id LEFT JOIN label ON label.id = lv.label_id @@ -550,9 +550,10 @@ private boolean isPredicate(String input){ return false; } + @Transactional @WithRoles @Override - public List listLabelValues(int testId, String filter, String before, String after, boolean filtering, boolean metrics, String sort, String direction, int limit, int page) { + public List labelValues(int testId, String filter, String before, String after, boolean filtering, boolean metrics, String sort, String direction, int limit, int page, List include, List exclude) { Test test = get(testId,null); if(test == null){ throw ServiceException.serverError("Cannot find test "+testId); @@ -600,6 +601,22 @@ public List listLabelValues(int testId, String filter, Stri filterSql+=FILTER_SEPARATOR+FILTER_AFTER; } } + + String includeExcludeSql = ""; + if (include!=null && !include.isEmpty()){ + if(exclude!=null && !exclude.isEmpty()){ + include = new ArrayList<>(include); + include.removeAll(exclude); + } + if(!include.isEmpty()) { + includeExcludeSql = " AND label.name in :include"; + } + } + //includeExcludeSql is empty if include did not contain entries after exclude removal + if(includeExcludeSql.isEmpty() && exclude!=null && !exclude.isEmpty()){ + includeExcludeSql=" AND label.name NOT in :exclude"; + } + if(filterSql.isBlank() && filter != null && !filter.isBlank()){ //TODO there was an error with the filter, do we return that info to the user? } @@ -625,7 +642,9 @@ public List listLabelValues(int testId, String filter, Stri String sql = LABEL_VALUES_QUERY .replace("FILTER_PLACEHOLDER",filterSql) + .replace("INCLUDE_EXCLUDE_PLACEHOLDER",includeExcludeSql) .replace("ORDER_PLACEHOLDER",orderSql); + NativeQuery query = ((NativeQuery) em.createNativeQuery(sql)) .setParameter("testId", test.id) .setParameter("filteringLabels", filtering) @@ -644,12 +663,17 @@ public List listLabelValues(int testId, String filter, Stri query.setParameter("after",afterInstant, StandardBasicTypes.INSTANT); } } + if(includeExcludeSql.contains(":include")){ + query.setParameter("include",include); + }else if (includeExcludeSql.contains(":exclude")){ + query.setParameter("exclude",exclude); + } if(orderSql.contains(LABEL_ORDER_JSONPATH)){ query.setParameter("orderBy", sort); } - query - .setParameter("limit",limit)//limit - .setParameter("offset",limit * page)//offset + query + .setParameter("limit",limit) + .setParameter("offset",limit * Math.max(0,page)) .unwrap(NativeQuery.class) .addScalar("values", JsonBinaryType.INSTANCE) .addScalar("runId",Integer.class) diff --git a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/RunServiceTest.java b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/RunServiceTest.java index 2771d233b..d7df3bbf6 100644 --- a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/RunServiceTest.java +++ b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/RunServiceTest.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import io.hyperfoil.tools.horreum.api.SortDirection; import io.hyperfoil.tools.horreum.api.alerting.ChangeDetection; import io.hyperfoil.tools.horreum.api.alerting.Variable; @@ -107,9 +108,130 @@ public void testTransformationWithoutSchema(TestInfo info) throws InterruptedExc assertFalse(run.trashed); assertNewDataset(dataSetQueue, runId); } + private int labelValuesSetup(Test t) throws JsonProcessingException { + Schema fooSchema = createSchema("foo","urn:foo"); + Extractor fooExtractor = new Extractor(); + fooExtractor.name="foo"; + fooExtractor.jsonpath="$.foo"; + Extractor barExtractor = new Extractor(); + barExtractor.name="bar"; + barExtractor.jsonpath="$.bar"; + addLabel(fooSchema,"labelFoo","",fooExtractor); + addLabel(fooSchema,"labelBar","",barExtractor); + + + ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);; + JsonNode data = mapper.readTree("{ \"foo\": \"uno\", \"bar\": \"dox\"}"); + String idString = uploadRun(data,t.name,fooSchema.uri); + int id = Integer.parseInt(idString); + return id; + } + + @org.junit.jupiter.api.Test + public void labelValuesIncludeExcluded() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + int id = labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/run/"+id+"/labelValues?include=labelFoo&exclude=labelFoo") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertFalse(objectNode.has("labelFoo"),objectNode.toString()); + assertTrue(objectNode.has("labelBar"),objectNode.toString()); + } + + @org.junit.jupiter.api.Test + public void labelValuesIncludeTwoParams() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + int id = labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/run/"+id+"/labelValues?include=labelFoo&include=labelBar") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertTrue(objectNode.has("labelFoo"),objectNode.toString()); + assertTrue(objectNode.has("labelBar"),objectNode.toString()); + } + @org.junit.jupiter.api.Test + public void labelValuesIncludeTwoSeparated() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + int id = labelValuesSetup(t); + JsonNode response = jsonRequest() + .get("/api/run/"+id+"/labelValues?include=labelFoo,labelBar") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertTrue(objectNode.has("labelFoo"),objectNode.toString()); + assertTrue(objectNode.has("labelBar"),objectNode.toString()); + } + + @org.junit.jupiter.api.Test + public void labelValuesInclude() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + int id = labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/run/"+id+"/labelValues?include=labelFoo") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertTrue(objectNode.has("labelFoo")); + assertFalse(objectNode.has("labelBar")); + } + @org.junit.jupiter.api.Test + public void labelValuesExclude() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + int id = labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/run/"+id+"/labelValues?exclude=labelFoo") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertFalse(objectNode.has("labelFoo"),objectNode.toPrettyString()); + assertTrue(objectNode.has("labelBar"),objectNode.toPrettyString()); + + } @org.junit.jupiter.api.Test - public void labelValues_publicTest_publicRun(TestInfo info) throws InterruptedException { + public void labelValuesPublicTestPublicRun(TestInfo info) throws InterruptedException { Test test = createExampleTest(getTestName(info)); test.access= Access.PUBLIC; Test persistedTest = createTest(test); @@ -538,7 +660,7 @@ private void validateScalarArray(Dataset ds, String expectedTarget) { } @org.junit.jupiter.api.Test - public void add_microseconds_in_timestamp() throws JsonProcessingException { + public void addMicrosecondsInTimestamp() throws JsonProcessingException { Test test = createExampleTest("foo"); test = createTest(test); JsonNode payload = new ObjectMapper().readTree("{\"start_time\": \"2024-03-13T21:18:10.878423-04:00\", \"stop_time\": \"2024-03-13T21:18:11.878423-04:00\"}"); 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 77a0615e0..5c3db9685 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 @@ -14,6 +14,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.hyperfoil.tools.horreum.api.SortDirection; import io.hyperfoil.tools.horreum.api.services.SchemaService; import io.hyperfoil.tools.horreum.bus.AsyncEventChannels; @@ -27,6 +30,7 @@ import io.hyperfoil.tools.horreum.entity.data.*; import io.restassured.common.mapper.TypeRef; import io.restassured.response.Response; +import jakarta.inject.Inject; import org.hibernate.query.NativeQuery; import org.junit.jupiter.api.TestInfo; @@ -52,6 +56,9 @@ @TestProfile(HorreumTestProfile.class) public class TestServiceTest extends BaseServiceTest { + @Inject + TestService testService; + @org.junit.jupiter.api.Test public void testCreateDelete(TestInfo info) throws InterruptedException { @@ -354,9 +361,131 @@ public void testListFingerprints() throws JsonProcessingException { assertEquals("RulesWithJoinsProvides", ((FingerprintValue) values.get(1).values.get(1).children.get(2)).value); } + private int labelValuesSetup(Test t) throws JsonProcessingException { + Schema fooSchema = createSchema("foo","urn:foo"); + Extractor fooExtractor = new Extractor(); + fooExtractor.name="foo"; + fooExtractor.jsonpath="$.foo"; + Extractor barExtractor = new Extractor(); + barExtractor.name="bar"; + barExtractor.jsonpath="$.bar"; + addLabel(fooSchema,"labelFoo","",fooExtractor); + addLabel(fooSchema,"labelBar","",barExtractor); + + + ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);; + JsonNode data = mapper.readTree("{ \"foo\": \"uno\", \"bar\": \"dox\"}"); + String idString = uploadRun(data,t.name,fooSchema.uri); + int id = Integer.parseInt(idString); + return id; + } + + @org.junit.jupiter.api.Test + public void labelValuesIncludeExcluded() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/test/"+t.id+"/labelValues?include=labelFoo&exclude=labelFoo") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertFalse(objectNode.has("labelFoo"),objectNode.toString()); + assertTrue(objectNode.has("labelBar"),objectNode.toString()); + } + + @org.junit.jupiter.api.Test + public void labelValuesIncludeTwoParams() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/test/"+t.id+"/labelValues?include=labelFoo&include=labelBar") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertTrue(objectNode.has("labelFoo"),objectNode.toString()); + assertTrue(objectNode.has("labelBar"),objectNode.toString()); + } + @org.junit.jupiter.api.Test + public void labelValuesIncludeTwoSeparated() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/test/"+t.id+"/labelValues?include=labelFoo,labelBar") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertTrue(objectNode.has("labelFoo"),objectNode.toString()); + assertTrue(objectNode.has("labelBar"),objectNode.toString()); + } + + @org.junit.jupiter.api.Test + public void labelValuesInclude() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/test/"+t.id+"/labelValues?include=labelFoo") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertTrue(objectNode.has("labelFoo")); + assertFalse(objectNode.has("labelBar")); + } + @org.junit.jupiter.api.Test + public void labelValuesExclude() throws JsonProcessingException { + Test t = createTest(createExampleTest("my-test")); + labelValuesSetup(t); + + JsonNode response = jsonRequest() + .get("/api/test/"+t.id+"/labelValues?exclude=labelFoo") + .then() + .statusCode(200) + .extract() + .body() + .as(JsonNode.class); + assertInstanceOf(ArrayNode.class,response); + ArrayNode arrayResponse = (ArrayNode)response; + assertEquals(1,arrayResponse.size()); + assertInstanceOf(ObjectNode.class,arrayResponse.get(0)); + ObjectNode objectNode = (ObjectNode)arrayResponse.get(0).get("values"); + assertFalse(objectNode.has("labelFoo"),objectNode.toPrettyString()); + assertTrue(objectNode.has("labelBar"),objectNode.toPrettyString()); + + } @org.junit.jupiter.api.Test - public void testListLabelValues() throws JsonProcessingException { + public void testLabelValues() throws JsonProcessingException { List toParse = new ArrayList<>(); toParse.add(new Object[]{mapper.readTree(""" { diff --git a/horreum-web/src/domain/runs/TestDatasets.tsx b/horreum-web/src/domain/runs/TestDatasets.tsx index 658d2fd28..9a28f510d 100644 --- a/horreum-web/src/domain/runs/TestDatasets.tsx +++ b/horreum-web/src/domain/runs/TestDatasets.tsx @@ -232,7 +232,7 @@ export default function TestDatasets() { } const labelsSource = useCallback(() => { - return testApi.listLabelValues(testIdInt) + return testApi.labelValues(testIdInt) .then((result: Array) => { return flattenLabelValues(result); })