Suggest widget after executing Action (#5574)

* Add widget suggestion to query execution flow

* Change the logic for Chart widget suggestion

* Add tests for the all the suggested widgets

* Added enum class to store widget types
This commit is contained in:
Anagh Hegde 2021-07-06 18:15:48 +05:30 committed by GitHub
parent 28284b7207
commit 027603697a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 378 additions and 2 deletions

View File

@ -33,6 +33,8 @@ public class ActionExecutionResult {
List<ParsedDataType> dataTypes;
WidgetType suggestedWidget;
public void setErrorInfo(Throwable error) {
this.body = error.getMessage();

View File

@ -0,0 +1,5 @@
package com.appsmith.external.models;
public enum WidgetType {
TEXT_WIDGET, LIST_WIDGET, DROP_DOWN_WIDGET, CHART_WIDGET, TABLE_WIDGET
}

View File

@ -12,6 +12,7 @@ import com.appsmith.external.models.Param;
import com.appsmith.external.models.Policy;
import com.appsmith.external.models.Provider;
import com.appsmith.external.models.RequestParamDTO;
import com.appsmith.external.models.WidgetType;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.acl.PolicyGenerator;
@ -38,6 +39,8 @@ import com.appsmith.server.repositories.NewActionRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
@ -653,7 +656,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
return Mono.just(result);
})
.map(result -> addDataTypes(result));
.map(result -> addDataTypesAndSetSuggestedWidget(result));
}
/*
@ -680,11 +683,13 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
result.getRequest().setRequestParams(transformedParams);
}
private ActionExecutionResult addDataTypes(ActionExecutionResult result) {
private ActionExecutionResult addDataTypesAndSetSuggestedWidget(ActionExecutionResult result) {
/*
* - Do not process if data types are already present.
* - It means that data types have been added by specific plugin.
*/
result.setSuggestedWidget(getSuggestedWidget(result.getBody()));
if (!CollectionUtils.isEmpty(result.getDataTypes())) {
return result;
}
@ -693,6 +698,37 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
return result;
}
/**
* Suggest the best widget to the query response. We currently planning to support List, Select, Table and Chart widgets
* @return
*/
private WidgetType getSuggestedWidget(Object data) {
if(data instanceof String) {
return WidgetType.TEXT_WIDGET;
}
if(data instanceof ArrayNode && !((ArrayNode) data).isEmpty() && ((ArrayNode) data).isArray()) {
ArrayNode array = (ArrayNode) data;
Integer length = array.size();
ObjectNode objectNode = (ObjectNode)array.get(0);
Integer fieldsCount = array.get(0).size();
if( (objectNode.has("x") || (objectNode.has("X")) )&&
(objectNode.has("y") || (objectNode.has("Y"))) ) {
return WidgetType.CHART_WIDGET;
}
if(fieldsCount <= 2) {
return WidgetType.DROP_DOWN_WIDGET;
}
if(length <= 20 && fieldsCount <= 5) {
return WidgetType.LIST_WIDGET;
}
return WidgetType.TABLE_WIDGET;
}
return WidgetType.TEXT_WIDGET;
}
private Mono<ActionExecutionRequest> sendExecuteAnalyticsEvent(
NewAction action,
ActionDTO actionDTO,

View File

@ -15,6 +15,7 @@ import com.appsmith.external.models.PaginationType;
import com.appsmith.external.models.ParsedDataType;
import com.appsmith.external.models.Policy;
import com.appsmith.external.models.Property;
import com.appsmith.external.models.WidgetType;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
@ -36,7 +37,10 @@ import com.appsmith.server.helpers.MockPluginExecutor;
import com.appsmith.server.helpers.PluginExecutorHelper;
import com.appsmith.server.repositories.OrganizationRepository;
import com.appsmith.server.repositories.PluginRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONArray;
import net.minidev.json.JSONObject;
@ -439,6 +443,7 @@ public class ActionServiceTest {
mockResult.setBody("response-body");
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
@ -468,6 +473,7 @@ public class ActionServiceTest {
mockResult.setBody("response-body");
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
@ -494,6 +500,7 @@ public class ActionServiceTest {
ActionExecutionResult mockResult = new ActionExecutionResult();
mockResult.setIsExecutionSuccess(true);
mockResult.setBody("response-body");
mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
@ -785,6 +792,7 @@ public class ActionServiceTest {
private void executeAndAssertAction(ExecuteActionDTO executeActionDTO, ActionConfiguration actionConfiguration,
ActionExecutionResult mockResult, List<ParsedDataType> expectedReturnDataTypes) {
WidgetType expectedWidget = mockResult.getSuggestedWidget();
Mono<ActionExecutionResult> actionExecutionResultMono = executeAction(executeActionDTO, actionConfiguration, mockResult);
StepVerifier.create(actionExecutionResultMono)
@ -792,6 +800,7 @@ public class ActionServiceTest {
assertThat(result).isNotNull();
assertThat(result.getBody()).isEqualTo(mockResult.getBody());
assertThat(result.getDataTypes().toString()).isEqualTo(expectedReturnDataTypes.toString());
assertThat(result.getSuggestedWidget()).isEqualTo(expectedWidget);
})
.verifyComplete();
}
@ -1212,6 +1221,7 @@ public class ActionServiceTest {
"]");
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
@ -1251,6 +1261,7 @@ public class ActionServiceTest {
" }");
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
@ -1290,6 +1301,7 @@ public class ActionServiceTest {
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
@ -1320,6 +1332,7 @@ public class ActionServiceTest {
mockResult.setBody(null);
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setSuggestedWidget(WidgetType.TEXT_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
@ -1338,4 +1351,324 @@ public class ActionServiceTest {
executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult, new ArrayList<>());
}
@Test
@WithUserDetails(value = "api_user")
public void testWidgetSuggestionAfterExecutionWithChartWidgetData() throws JsonProcessingException {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor));
ActionExecutionResult mockResult = new ActionExecutionResult();
final String data = "{ \"data\": [\n" +
" {\n" +
" \"x\": \"Mon\",\n" +
" \"y\": 10000\n" +
" },\n" +
" {\n" +
" \"x\": \"Tue\",\n" +
" \"y\": 12000\n" +
" },\n" +
" {\n" +
" \"x\": \"Wed\",\n" +
" \"y\": 32000\n" +
" },\n" +
" {\n" +
" \"x\": \"Thu\",\n" +
" \"y\": 28000\n" +
" },\n" +
" {\n" +
" \"x\": \"Fri\",\n" +
" \"y\": 14000\n" +
" },\n" +
" {\n" +
" \"x\": \"Sat\",\n" +
" \"y\": 19000\n" +
" },\n" +
" {\n" +
" \"x\": \"Sun\",\n" +
" \"y\": 36000\n" +
" }\n" +
"]}";
final JsonNode arrNode = new ObjectMapper().readTree(data).get("data");;
mockResult.setIsExecutionSuccess(true);
mockResult.setBody(arrNode);
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
mockResult.setSuggestedWidget(WidgetType.CHART_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setHttpMethod(HttpMethod.POST);
actionConfiguration.setBody("random-request-body");
actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value")));
action.setActionConfiguration(actionConfiguration);
action.setPageId(testPage.getId());
action.setName("testActionExecute");
action.setDatasource(datasource);
ActionDTO createdAction = layoutActionService.createAction(action).block();
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
executeActionDTO.setActionId(createdAction.getId());
executeActionDTO.setViewMode(false);
executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult,
List.of(new ParsedDataType(DisplayDataType.RAW)));
}
@Test
@WithUserDetails(value = "api_user")
public void testWidgetSuggestionAfterExecutionWithTableWidgetData() throws JsonProcessingException {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor));
ActionExecutionResult mockResult = new ActionExecutionResult();
final String data = "{ \"data\": [\n" +
"\t{\n" +
"\t\t\"id\": \"0001\",\n" +
"\t\t\"type\": \"donut\",\n" +
"\t\t\"name\": \"Cake\",\n" +
"\t\t\"ppu\": 0.55,\n" +
"\t\t\"batters\":\n" +
"\t\t\t{\n" +
"\t\t\t\t\"batter\":\n" +
"\t\t\t\t\t[\n" +
"\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" },\n" +
"\t\t\t\t\t\t{ \"id\": \"1002\", \"type\": \"Chocolate\" },\n" +
"\t\t\t\t\t\t{ \"id\": \"1003\", \"type\": \"Blueberry\" },\n" +
"\t\t\t\t\t\t{ \"id\": \"1004\", \"type\": \"Devil's Food\" }\n" +
"\t\t\t\t\t]\n" +
"\t\t\t},\n" +
"\t\t\"topping\":\n" +
"\t\t\t[\n" +
"\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" +
"\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" +
"\t\t\t\t{ \"id\": \"5005\", \"type\": \"Sugar\" },\n" +
"\t\t\t\t{ \"id\": \"5007\", \"type\": \"Powdered Sugar\" },\n" +
"\t\t\t\t{ \"id\": \"5006\", \"type\": \"Chocolate with Sprinkles\" },\n" +
"\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" +
"\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" +
"\t\t\t]\n" +
"\t},\n" +
"\t{\n" +
"\t\t\"id\": \"0002\",\n" +
"\t\t\"type\": \"donut\",\n" +
"\t\t\"name\": \"Raised\",\n" +
"\t\t\"ppu\": 0.55,\n" +
"\t\t\"batters\":\n" +
"\t\t\t{\n" +
"\t\t\t\t\"batter\":\n" +
"\t\t\t\t\t[\n" +
"\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" }\n" +
"\t\t\t\t\t]\n" +
"\t\t\t},\n" +
"\t\t\"topping\":\n" +
"\t\t\t[\n" +
"\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" +
"\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" +
"\t\t\t\t{ \"id\": \"5005\", \"type\": \"Sugar\" },\n" +
"\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" +
"\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" +
"\t\t\t]\n" +
"\t},\n" +
"\t{\n" +
"\t\t\"id\": \"0003\",\n" +
"\t\t\"type\": \"donut\",\n" +
"\t\t\"name\": \"Old Fashioned\",\n" +
"\t\t\"ppu\": 0.55,\n" +
"\t\t\"batters\":\n" +
"\t\t\t{\n" +
"\t\t\t\t\"batter\":\n" +
"\t\t\t\t\t[\n" +
"\t\t\t\t\t\t{ \"id\": \"1001\", \"type\": \"Regular\" },\n" +
"\t\t\t\t\t\t{ \"id\": \"1002\", \"type\": \"Chocolate\" }\n" +
"\t\t\t\t\t]\n" +
"\t\t\t},\n" +
"\t\t\"topping\":\n" +
"\t\t\t[\n" +
"\t\t\t\t{ \"id\": \"5001\", \"type\": \"None\" },\n" +
"\t\t\t\t{ \"id\": \"5002\", \"type\": \"Glazed\" },\n" +
"\t\t\t\t{ \"id\": \"5003\", \"type\": \"Chocolate\" },\n" +
"\t\t\t\t{ \"id\": \"5004\", \"type\": \"Maple\" }\n" +
"\t\t\t]\n" +
"\t}\n" +
"]}";
final JsonNode arrNode = new ObjectMapper().readTree(data).get("data");;
mockResult.setIsExecutionSuccess(true);
mockResult.setBody(arrNode);
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
mockResult.setSuggestedWidget(WidgetType.TABLE_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setHttpMethod(HttpMethod.POST);
actionConfiguration.setBody("random-request-body");
actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value")));
action.setActionConfiguration(actionConfiguration);
action.setPageId(testPage.getId());
action.setName("testActionExecute");
action.setDatasource(datasource);
ActionDTO createdAction = layoutActionService.createAction(action).block();
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
executeActionDTO.setActionId(createdAction.getId());
executeActionDTO.setViewMode(false);
executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult,
List.of(new ParsedDataType(DisplayDataType.RAW)));
}
@Test
@WithUserDetails(value = "api_user")
public void testWidgetSuggestionAfterExecutionWithListWidgetData() throws JsonProcessingException {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor));
ActionExecutionResult mockResult = new ActionExecutionResult();
final String data = "{ \"data\": [\n" +
" {\n" +
" \"url\": \"images/thumbnails/0001.jpg\",\n" +
" \"width\": 32,\n" +
" \"height\": 32\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0001.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0002.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0002.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0003.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0004.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0005.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0006.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0007.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0008.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0009.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" },\n" +
" {\n" +
" \"url\": \"images/0010.jpg\",\n" +
" \"width\": 200,\n" +
" \"height\": 200\n" +
" }\n" +
"]}";
final JsonNode arrNode = new ObjectMapper().readTree(data).get("data");;
mockResult.setIsExecutionSuccess(true);
mockResult.setBody(arrNode);
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
mockResult.setSuggestedWidget(WidgetType.LIST_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setHttpMethod(HttpMethod.POST);
actionConfiguration.setBody("random-request-body");
actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value")));
action.setActionConfiguration(actionConfiguration);
action.setPageId(testPage.getId());
action.setName("testActionExecute");
action.setDatasource(datasource);
ActionDTO createdAction = layoutActionService.createAction(action).block();
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
executeActionDTO.setActionId(createdAction.getId());
executeActionDTO.setViewMode(false);
executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult,
List.of(new ParsedDataType(DisplayDataType.RAW)));
}
@Test
@WithUserDetails(value = "api_user")
public void testWidgetSuggestionAfterExecutionWithDropdownWidgetData() throws JsonProcessingException {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor));
ActionExecutionResult mockResult = new ActionExecutionResult();
final String data = "{ \"data\": [\n" +
" {\n" +
" \"CarType\": \"BMW\",\n" +
" \"carID\": \"bmw123\"\n" +
" },\n" +
" {\n" +
" \"CarType\": \"mercedes\",\n" +
" \"carID\": \"merc123\"\n" +
" },\n" +
" {\n" +
" \"CarType\": \"volvo\",\n" +
" \"carID\": \"vol123r\"\n" +
" },\n" +
" {\n" +
" \"CarType\": \"ford\",\n" +
" \"carID\": \"ford123\"\n" +
" }\n" +
" ]}";
final JsonNode arrNode = new ObjectMapper().readTree(data).get("data");;
mockResult.setIsExecutionSuccess(true);
mockResult.setBody(arrNode);
mockResult.setStatusCode("200");
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
mockResult.setSuggestedWidget(WidgetType.DROP_DOWN_WIDGET);
ActionDTO action = new ActionDTO();
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setHttpMethod(HttpMethod.POST);
actionConfiguration.setBody("random-request-body");
actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value")));
action.setActionConfiguration(actionConfiguration);
action.setPageId(testPage.getId());
action.setName("testActionExecute");
action.setDatasource(datasource);
ActionDTO createdAction = layoutActionService.createAction(action).block();
ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
executeActionDTO.setActionId(createdAction.getId());
executeActionDTO.setViewMode(false);
executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult,
List.of(new ParsedDataType(DisplayDataType.RAW)));
}
}