From b511c3ada99d699ff1439d756f0e3fe7a447d6c3 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Mon, 10 Aug 2020 14:41:32 +0530 Subject: [PATCH 1/4] Change return type of plugin execution to be more specific (#247) --- .../external/plugins/PluginExecutor.java | 3 +- .../com/external/plugins/MongoPlugin.java | 6 ++-- .../com/external/plugins/MySqlPlugin.java | 22 ++++++-------- .../com/external/plugins/PostgresPlugin.java | 18 +++++------ .../com/external/plugins/RapidApiPlugin.java | 29 +++++++++++------- .../com/external/plugins/RestApiPlugin.java | 17 +++++------ .../external/plugins/RestApiPluginTest.java | 10 +++---- .../server/services/ActionServiceImpl.java | 30 +++++++++++-------- .../server/helpers/MockPluginExecutor.java | 3 +- .../server/services/PluginServiceTest.java | 2 +- 10 files changed, 72 insertions(+), 68 deletions(-) diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java index fb564f5ae5..fcefe3d151 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/PluginExecutor.java @@ -1,6 +1,7 @@ package com.appsmith.external.plugins; import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import org.pf4j.ExtensionPoint; @@ -20,7 +21,7 @@ public interface PluginExecutor extends ExtensionPoint { * @param actionConfiguration : These are the configurations which have been used to create an Action from a Datasource. * @return ActionExecutionResult : This object is returned to the user which contains the result values from the execution. */ - Mono execute(Object connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration); + Mono execute(Object connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration); /** * This function is responsible for creating the connection to the data source and returning the connection variable diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java index d90c0cbc7a..45230d6c5f 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -78,9 +78,9 @@ public class MongoPlugin extends BasePlugin { * @return Result data from executing the action's query. */ @Override - public Mono execute(Object connection, - DatasourceConfiguration datasourceConfiguration, - ActionConfiguration actionConfiguration) { + public Mono execute(Object connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { MongoClient mongoClient = (MongoClient) connection; if (mongoClient == null) { diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java index 8142d1e4ff..63f11277bf 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -51,14 +51,10 @@ public class MySqlPlugin extends BasePlugin { @Extension public static class MySqlPluginExecutor implements PluginExecutor { - private Mono pluginErrorMono(Object... args) { - return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, args)); - } - @Override - public Mono execute(Object connection, - DatasourceConfiguration datasourceConfiguration, - ActionConfiguration actionConfiguration) { + public Mono execute(Object connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { Connection conn = (Connection) connection; @@ -76,7 +72,7 @@ public class MySqlPlugin extends BasePlugin { String query = actionConfiguration.getBody(); if (query == null) { - return pluginErrorMono("Missing required parameter: Query."); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Missing required parameter: Query.")); } List> rowsList = new ArrayList<>(50); @@ -105,14 +101,14 @@ public class MySqlPlugin extends BasePlugin { } } catch (SQLException e) { - return pluginErrorMono(e.getMessage()); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage())); } finally { if (resultSet != null) { try { resultSet.close(); } catch (SQLException e) { - log.warn("Error closing MySql ResultSet", e); + log.warn("Error closing MySQL ResultSet", e); } } @@ -120,7 +116,7 @@ public class MySqlPlugin extends BasePlugin { try { statement.close(); } catch (SQLException e) { - log.warn("Error closing MySql Statement", e); + log.warn("Error closing MySQL Statement", e); } } @@ -138,7 +134,7 @@ public class MySqlPlugin extends BasePlugin { try { Class.forName(JDBC_DRIVER); } catch (ClassNotFoundException e) { - return pluginErrorMono("Error loading MySql JDBC Driver class."); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error loading MySQL JDBC Driver class.")); } String url; @@ -178,7 +174,7 @@ public class MySqlPlugin extends BasePlugin { configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode())); return Mono.just(connection); } catch (SQLException e) { - return pluginErrorMono("Error connecting to MySQL: " + e.getMessage(), e); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error connecting to MySQL: " + e.getMessage(), e)); } } diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index e96423e968..0d3994e1f0 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -63,9 +63,9 @@ public class PostgresPlugin extends BasePlugin { public static class PostgresPluginExecutor implements PluginExecutor { @Override - public Mono execute(Object connection, - DatasourceConfiguration datasourceConfiguration, - ActionConfiguration actionConfiguration) { + public Mono execute(Object connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { Connection conn = (Connection) connection; @@ -83,7 +83,7 @@ public class PostgresPlugin extends BasePlugin { String query = actionConfiguration.getBody(); if (query == null) { - return pluginErrorMono("Missing required parameter: Query."); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Missing required parameter: Query.")); } List> rowsList = new ArrayList<>(50); @@ -149,7 +149,7 @@ public class PostgresPlugin extends BasePlugin { } } catch (SQLException e) { - return pluginErrorMono(e.getMessage()); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage())); } finally { if (resultSet != null) { @@ -177,16 +177,12 @@ public class PostgresPlugin extends BasePlugin { return Mono.just(result); } - private Mono pluginErrorMono(Object... args) { - return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, args)); - } - @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { try { Class.forName(JDBC_DRIVER); } catch (ClassNotFoundException e) { - return pluginErrorMono("Error loading Postgres JDBC Driver class."); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error loading Postgres JDBC Driver class.")); } String url; @@ -233,7 +229,7 @@ public class PostgresPlugin extends BasePlugin { return Mono.just(connection); } catch (SQLException e) { - return pluginErrorMono("Error connecting to Postgres.", e); + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error connecting to Postgres.", e)); } } diff --git a/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java b/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java index 171fdee5aa..a8e6f3659d 100644 --- a/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java +++ b/app/server/appsmith-plugins/rapidApiPlugin/src/main/java/com/external/plugins/RapidApiPlugin.java @@ -24,6 +24,7 @@ import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.Exceptions; import reactor.core.publisher.Mono; import java.io.IOException; @@ -56,9 +57,9 @@ public class RapidApiPlugin extends BasePlugin { private static final String RAPID_API_KEY_VALUE = System.getenv("APPSMITH_RAPID_API_KEY_VALUE"); @Override - public Mono execute(Object connection, - DatasourceConfiguration datasourceConfiguration, - ActionConfiguration actionConfiguration) { + public Mono execute(Object connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { if (StringUtils.isEmpty(RAPID_API_KEY_VALUE)) { return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "RapidAPI Key value not set.")); @@ -100,10 +101,10 @@ public class RapidApiPlugin extends BasePlugin { } } - URI uri = null; + URI uri; try { uri = createFinalUriWithQueryParams(url, actionConfiguration.getQueryParameters()); - System.out.println("Final URL is : " + uri.toString()); + log.info("Final URL is : {}", uri); } catch (URISyntaxException e) { e.printStackTrace(); return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)); @@ -114,7 +115,7 @@ public class RapidApiPlugin extends BasePlugin { // First set the header to specify the content type webClientBuilder.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString()); - Map keyValueMap = new HashMap(); + Map keyValueMap = new HashMap<>(); List bodyFormData = actionConfiguration.getBodyFormData(); String jsonString = null; @@ -170,14 +171,14 @@ public class RapidApiPlugin extends BasePlugin { headerInJsonString = objectMapper.writeValueAsString(headers); } catch (JsonProcessingException e) { e.printStackTrace(); - return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)); } try { // Set headers in the result now result.setHeaders(objectMapper.readTree(headerInJsonString)); } catch (IOException e) { e.printStackTrace(); - return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)); } } @@ -194,7 +195,7 @@ public class RapidApiPlugin extends BasePlugin { result.setBody(objectMapper.readTree(jsonBody)); } catch (IOException e) { e.printStackTrace(); - return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)); } } else if (MediaType.IMAGE_GIF.equals(contentType) || MediaType.IMAGE_JPEG.equals(contentType) || @@ -207,9 +208,17 @@ public class RapidApiPlugin extends BasePlugin { result.setBody(bodyString.trim()); } } + return result; }) - .doOnError(e -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + .onErrorMap(throwable -> { + final Throwable actualException = Exceptions.unwrap(throwable); + if (actualException instanceof AppsmithPluginException) { + return actualException; + } else { + return new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, actualException); + } + }); } private Mono httpCall(WebClient webClient, HttpMethod httpMethod, URI uri, String requestBody, int iteration) { diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index 6b8bbb25ca..f27cc25b81 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -31,6 +31,7 @@ import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.Exceptions; import reactor.core.publisher.Mono; import java.io.IOException; @@ -65,15 +66,14 @@ public class RestApiPlugin extends BasePlugin { public static class RestApiPluginExecutor implements PluginExecutor { @Override - public Mono execute(Object connection, - DatasourceConfiguration datasourceConfiguration, - ActionConfiguration actionConfiguration) { + public Mono execute(Object connection, + DatasourceConfiguration datasourceConfiguration, + ActionConfiguration actionConfiguration) { ActionExecutionResult errorResult = new ActionExecutionResult(); errorResult.setStatusCode(AppsmithPluginError.PLUGIN_ERROR.getAppErrorCode().toString()); errorResult.setIsExecutionSuccess(false); - String path = (actionConfiguration.getPath() == null) ? "" : actionConfiguration.getPath(); String url = datasourceConfiguration.getUrl() + path; String reqContentType = ""; @@ -147,20 +147,19 @@ public class RestApiPlugin extends BasePlugin { result.setStatusCode(statusCode.toString()); result.setIsExecutionSuccess(statusCode.is2xxSuccessful()); - // Convert the headers into json tree to store in the results String headerInJsonString; try { headerInJsonString = objectMapper.writeValueAsString(headers); } catch (JsonProcessingException e) { - return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)); } // Set headers in the result now try { result.setHeaders(objectMapper.readTree(headerInJsonString)); } catch (IOException e) { - return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)); } if (body != null) { @@ -174,7 +173,7 @@ public class RestApiPlugin extends BasePlugin { String jsonBody = new String(body); result.setBody(objectMapper.readTree(jsonBody)); } catch (IOException e) { - return Mono.defer(() -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))); + throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e)); } } else if (MediaType.IMAGE_GIF.equals(contentType) || MediaType.IMAGE_JPEG.equals(contentType) || @@ -191,7 +190,7 @@ public class RestApiPlugin extends BasePlugin { return result; }) .onErrorResume(e -> { - errorResult.setBody(AppsmithPluginError.PLUGIN_ERROR.getMessage(e)); + errorResult.setBody(Exceptions.unwrap(e).getMessage()); errorResult.setRequest(actionExecutionRequest); return Mono.just(errorResult); }); diff --git a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java index b31b090d79..9b2ce98500 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java @@ -36,11 +36,10 @@ public class RestApiPluginTest { String requestBody = "{\"key\":\"value\"}"; actionConfig.setBody(requestBody); - Mono resultMono = pluginExecutor.execute(null, dsConfig, actionConfig); + Mono resultMono = pluginExecutor.execute(null, dsConfig, actionConfig); StepVerifier.create(resultMono) - .assertNext(r -> { - ActionExecutionResult result = (ActionExecutionResult) r; + .assertNext(result -> { assertTrue(result.getIsExecutionSuccess()); assertNotNull(result.getBody()); JsonNode data = ((ObjectNode) result.getBody()).get("data"); @@ -59,11 +58,10 @@ public class RestApiPluginTest { actionConfig.setHeaders(List.of(new Property("content-type", "application/x-www-form-urlencoded"))); actionConfig.setHttpMethod(HttpMethod.POST); actionConfig.setBodyFormData(List.of(new Property("key", "value"), new Property("key1", "value1"))); - Mono resultMono = pluginExecutor.execute(null, dsConfig, actionConfig); + Mono resultMono = pluginExecutor.execute(null, dsConfig, actionConfig); StepVerifier.create(resultMono) - .assertNext(r -> { - ActionExecutionResult result = (ActionExecutionResult) r; + .assertNext(result -> { assertTrue(result.getIsExecutionSuccess()); assertNotNull(result.getBody()); JsonNode data = ((ObjectNode) result.getBody()).get("form"); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java index b55cb4b93d..fc2feb828a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionServiceImpl.java @@ -355,8 +355,17 @@ public class ActionServiceImpl extends BaseService pluginExecutorMono = pluginExecutorHelper.getPluginExecutor(pluginMono); // 4. Execute the query - Mono actionExecutionResultMono = actionMono - .flatMap(action -> datasourceMono.zipWith(pluginExecutorMono, (datasource, pluginExecutor) -> { + Mono actionExecutionResultMono = Mono + .zip( + actionMono, + datasourceMono, + pluginExecutorMono + ) + .flatMap(tuple -> { + final Action action = tuple.getT1(); + final Datasource datasource = tuple.getT2(); + final PluginExecutor pluginExecutor = tuple.getT3(); + DatasourceConfiguration datasourceConfigurationTemp; ActionConfiguration actionConfigurationTemp; //Do variable substitution before invoking the plugin @@ -377,8 +386,8 @@ public class ActionServiceImpl extends BaseService oldValue) ); - datasourceConfigurationTemp = (DatasourceConfiguration) variableSubstitution(datasource.getDatasourceConfiguration(), replaceParamsMap); - actionConfigurationTemp = (ActionConfiguration) variableSubstitution(action.getActionConfiguration(), replaceParamsMap); + datasourceConfigurationTemp = variableSubstitution(datasource.getDatasourceConfiguration(), replaceParamsMap); + actionConfigurationTemp = variableSubstitution(action.getActionConfiguration(), replaceParamsMap); } else { datasourceConfigurationTemp = datasource.getDatasourceConfiguration(); actionConfigurationTemp = action.getActionConfiguration(); @@ -413,7 +422,7 @@ public class ActionServiceImpl extends BaseService executionMono = Mono.just(datasource) + Mono executionMono = Mono.just(datasource) .flatMap(datasourceContextService::getDatasourceContext) // Now that we have the context (connection details), execute the action. .flatMap( @@ -452,9 +461,7 @@ public class ActionServiceImpl extends BaseService obj) - .map(obj -> (ActionExecutionResult) obj); + }); // Populate the actionExecution result by setting the cached response and saving it to the DB return actionExecutionResultMono @@ -483,11 +490,8 @@ public class ActionServiceImpl extends BaseService { - ActionExecutionResult executionResult = tuple.getT2(); - return executionResult; - }); + + return actionFromDbMono.then(resultMono); }) .onErrorResume(AppsmithException.class, error -> { ActionExecutionResult result = new ActionExecutionResult(); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MockPluginExecutor.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MockPluginExecutor.java index 959be8219d..6cd9e9f871 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MockPluginExecutor.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MockPluginExecutor.java @@ -1,6 +1,7 @@ package com.appsmith.server.helpers; import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.plugins.PluginExecutor; @@ -12,7 +13,7 @@ import java.util.Set; public class MockPluginExecutor implements PluginExecutor { @Override - public Mono execute(Object connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) { + public Mono execute(Object connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) { System.out.println("In the execute"); return null; } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PluginServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PluginServiceTest.java index a66ef39653..ff345ebe57 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PluginServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PluginServiceTest.java @@ -35,7 +35,7 @@ public class PluginServiceTest { @Test public void checkPluginExecutor() { - Mono executeMono = pluginExecutor.execute(new Object(), new DatasourceConfiguration(), new ActionConfiguration()); + Mono executeMono = pluginExecutor.execute(new Object(), new DatasourceConfiguration(), new ActionConfiguration()); StepVerifier .create(executeMono) From 5e37a919ef3863ed45b63e579f82b815a9a0aff0 Mon Sep 17 00:00:00 2001 From: Abhinav Jha Date: Mon, 10 Aug 2020 14:59:06 +0530 Subject: [PATCH 2/4] Property pane render when navigating to a widget on a different page (#255) * Fix: When navigating to a widget, the widget reference isn't found, resulting in the property pane rendering without reference. Resolution: Show property pane only if reference is available --- app/client/src/pages/Editor/WidgetsEditor.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/client/src/pages/Editor/WidgetsEditor.tsx b/app/client/src/pages/Editor/WidgetsEditor.tsx index 2f519af90e..b0c27a46c6 100644 --- a/app/client/src/pages/Editor/WidgetsEditor.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor.tsx @@ -78,7 +78,8 @@ const WidgetsEditor = () => { if (!isFetchingPage && window.location.hash.length > 0) { const widgetIdFromURLHash = window.location.hash.substr(1); flashElementById(widgetIdFromURLHash); - selectWidget(widgetIdFromURLHash); + if (document.getElementById(widgetIdFromURLHash)) + selectWidget(widgetIdFromURLHash); } }, [isFetchingPage, selectWidget]); From 25b752843fd5962a41572044f7e45dc0e8d76b81 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Mon, 10 Aug 2020 14:59:56 +0530 Subject: [PATCH 3/4] Auto-login on signup (#201) * Auto-login on signup works! * Support form-encoded data body for signup requests * Remove debug log entry when getting session * Refactoring and add some docs to UserSignup solution * Move user object construction to UserSignup solution * Redirect with error message on signup errors --- .../handlers/CustomFormLoginServiceImpl.java | 9 +- .../server/controllers/ActionController.java | 4 +- .../controllers/ApplicationController.java | 4 +- .../server/controllers/BaseController.java | 4 +- .../controllers/CollectionController.java | 4 +- .../server/controllers/PageController.java | 4 +- .../server/controllers/UserController.java | 21 +++- .../services/SessionUserServiceImpl.java | 8 +- .../appsmith/server/solutions/UserSignup.java | 118 ++++++++++++++++++ 9 files changed, 160 insertions(+), 16 deletions(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/UserSignup.java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/CustomFormLoginServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/CustomFormLoginServiceImpl.java index 7fe2f26e81..4de22aaa26 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/CustomFormLoginServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/CustomFormLoginServiceImpl.java @@ -31,8 +31,11 @@ public class CustomFormLoginServiceImpl implements ReactiveUserDetailsService { public Mono findByUsername(String username) { return repository.findByEmail(username) .switchIfEmpty(Mono.error(new UsernameNotFoundException("Unable to find username: " + username))) - // This object cast is required to ensure that we send the right object type back to Spring framework. - // Doesn't work without this. - .map(user -> (UserDetails) user); + .onErrorMap(error -> { + log.error("Can't find user {}", username); + return error; + }) + // This seemingly useless call to `.map` is required to Java's type checker to compile. + .map(user -> user); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java index fe1edeb87f..79b6578b54 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ActionController.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.validation.Valid; @@ -50,7 +51,8 @@ public class ActionController extends BaseController> create(@Valid @RequestBody Action resource, - @RequestHeader(name = "Origin", required = false) String originHeader) { + @RequestHeader(name = "Origin", required = false) String originHeader, + ServerWebExchange exchange) { log.debug("Going to create resource {}", resource.getClass().getName()); return actionCollectionService.createAction(resource) .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java index 27e08f4660..bcf79ae3bf 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.validation.Valid; @@ -47,7 +48,8 @@ public class ApplicationController extends BaseController> create(@Valid @RequestBody Application resource, - @RequestParam String orgId) { + @RequestParam String orgId, + ServerWebExchange exchange) { if (orgId == null) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "organization id")); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java index 38f262ae68..441855a7f3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java @@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.validation.Valid; @@ -30,7 +31,8 @@ public abstract class BaseController> create(@Valid @RequestBody T resource, - @RequestHeader(name = "Origin", required = false) String originHeader) { + @RequestHeader(name = "Origin", required = false) String originHeader, + ServerWebExchange exchange) { log.debug("Going to create resource {}", resource.getClass().getName()); return service.create(resource) .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CollectionController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CollectionController.java index 3d061e996b..83cc52c1e0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CollectionController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CollectionController.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.validation.Valid; @@ -33,7 +34,8 @@ public class CollectionController extends BaseController> create(@Valid @RequestBody Collection resource, - @RequestHeader(name = "Origin", required = false) String originHeader) { + @RequestHeader(name = "Origin", required = false) String originHeader, + ServerWebExchange exchange) { log.debug("Going to create resource {}", resource.getClass().getName()); return actionCollectionService.createCollection(resource) .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java index c3d76e4bb8..3f8e3316d4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.validation.Valid; @@ -37,7 +38,8 @@ public class PageController extends BaseController { @PostMapping @ResponseStatus(HttpStatus.CREATED) public Mono> create(@Valid @RequestBody Page resource, - @RequestHeader(name = "Origin", required = false) String originHeader) { + @RequestHeader(name = "Origin", required = false) String originHeader, + ServerWebExchange exchange) { log.debug("Going to create resource {}", resource.getClass().getName()); return applicationPageService.createPage(resource) .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/UserController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/UserController.java index 7528dbde3e..54962e9822 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/UserController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/UserController.java @@ -8,9 +8,11 @@ import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.UserOrganizationService; import com.appsmith.server.services.UserService; +import com.appsmith.server.solutions.UserSignup; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.validation.Valid; @@ -32,24 +35,34 @@ public class UserController extends BaseController { private final SessionUserService sessionUserService; private final UserOrganizationService userOrganizationService; + private final UserSignup userSignup; @Autowired public UserController(UserService service, SessionUserService sessionUserService, - UserOrganizationService userOrganizationService) { + UserOrganizationService userOrganizationService, + UserSignup userSignup) { super(service); this.sessionUserService = sessionUserService; this.userOrganizationService = userOrganizationService; + this.userSignup = userSignup; } - @PostMapping + @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE}) @ResponseStatus(HttpStatus.CREATED) public Mono> create(@Valid @RequestBody User resource, - @RequestHeader(name = "Origin", required = false) String originHeader) { - return service.createUserAndSendEmail(resource, originHeader) + @RequestHeader(name = "Origin", required = false) String originHeader, + ServerWebExchange exchange) { + return userSignup.signupAndLogin(resource, exchange) .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); } + @PostMapping(consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE}) + @ResponseStatus(HttpStatus.CREATED) + public Mono createFormEncoded(ServerWebExchange exchange) { + return userSignup.signupAndLoginFromFormData(exchange); + } + @PutMapping("/switchOrganization/{orgId}") public Mono> setCurrentOrganization(@PathVariable String orgId) { return service.switchCurrentOrganization(orgId) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/SessionUserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/SessionUserServiceImpl.java index 06ad986272..cf3ad4af23 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/SessionUserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/SessionUserServiceImpl.java @@ -3,6 +3,7 @@ package com.appsmith.server.services; import com.appsmith.server.domains.User; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -12,10 +13,9 @@ public class SessionUserServiceImpl implements SessionUserService { @Override public Mono getCurrentUser() { - return ReactiveSecurityContextHolder.getContext() - .map(ctx -> ctx.getAuthentication()) - .map(auth -> auth.getPrincipal()) - .map(principal -> (User) principal); + .map(SecurityContext::getAuthentication) + .map(auth -> (User) auth.getPrincipal()); } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/UserSignup.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/UserSignup.java new file mode 100644 index 0000000000..80fefc9614 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/UserSignup.java @@ -0,0 +1,118 @@ +package com.appsmith.server.solutions; + +import com.appsmith.server.authentication.handlers.AuthenticationSuccessHandler; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.LoginSource; +import com.appsmith.server.domains.User; +import com.appsmith.server.domains.UserState; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.services.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.client.utils.URIBuilder; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.web.server.DefaultServerRedirectStrategy; +import org.springframework.security.web.server.ServerRedirectStrategy; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.WebSession; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME; + +@Component +@RequiredArgsConstructor +@Slf4j +public class UserSignup { + + private final UserService userService; + private final AuthenticationSuccessHandler authenticationSuccessHandler; + + private static final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy(); + + private static final WebFilterChain EMPTY_WEB_FILTER_CHAIN = serverWebExchange -> Mono.empty(); + + /** + * This function does the sign-up flow of the given user object as a new user, and then logs that user. After the + * login is successful, the authentication success handlers will be called directly. + * This needed to be pulled out into a separate solution class since it was creating a circular autowiring error if + * placed inside UserService. + * @param user User object representing the new user to be signed-up and then logged-in. + * @param exchange ServerWebExchange object with details of the current web request. + * @return Mono of User, published the saved user object with a non-null value for its `getId()`. + */ + public Mono signupAndLogin(User user, ServerWebExchange exchange) { + return Mono + .zip( + userService.createUserAndSendEmail(user, exchange.getRequest().getHeaders().getOrigin()), + exchange.getSession(), + ReactiveSecurityContextHolder.getContext() + ) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.INTERNAL_SERVER_ERROR))) + .flatMap(tuple -> { + final User savedUser = tuple.getT1(); + final WebSession session = tuple.getT2(); + final SecurityContext securityContext = tuple.getT3(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(savedUser, null, savedUser.getAuthorities()); + securityContext.setAuthentication(authentication); + session.getAttributes().put(DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME, securityContext); + + final WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, EMPTY_WEB_FILTER_CHAIN); + return authenticationSuccessHandler + .onAuthenticationSuccess(webFilterExchange, authentication) + .thenReturn(savedUser); + }); + } + + /** + * Creates a new user and logs them in, with the user details taken from the POST body, read as form-data. + * @param exchange The `ServerWebExchange` instance representing the request. + * @return Publisher of the created user object, with an `id` value. + */ + public Mono signupAndLoginFromFormData(ServerWebExchange exchange) { + return exchange.getFormData() + .map(formData -> { + final User user = new User(); + user.setEmail(formData.getFirst(FieldName.EMAIL)); + user.setPassword(formData.getFirst("password")); + if (formData.containsKey(FieldName.NAME)) { + user.setName(formData.getFirst(FieldName.NAME)); + } + if (formData.containsKey("source")) { + user.setSource(LoginSource.valueOf(formData.getFirst("source"))); + } + if (formData.containsKey("state")) { + user.setState(UserState.valueOf(formData.getFirst("state"))); + } + if (formData.containsKey("isEnabled")) { + user.setIsEnabled(Boolean.valueOf(formData.getFirst("isEnabled"))); + } + return user; + }) + .flatMap(user -> signupAndLogin(user, exchange)) + .then() + .onErrorResume(error -> { + final String referer = exchange.getRequest().getHeaders().getFirst("referer"); + final URIBuilder redirectUriBuilder = new URIBuilder(URI.create(referer)).setParameter("error", error.getMessage()); + URI redirectUri; + try { + redirectUri = redirectUriBuilder.build(); + } catch (URISyntaxException e) { + log.error("Error building redirect URI with error for signup, {}.", e.getMessage(), error); + redirectUri = URI.create(referer); + } + return redirectStrategy.sendRedirect(exchange, redirectUri); + }); + } + +} From 6a53e1e0ed2da5061fa21087c1d6cbcb5ca89b7d Mon Sep 17 00:00:00 2001 From: vicky-primathon <67091118+vicky-primathon@users.noreply.github.com> Date: Mon, 10 Aug 2020 15:31:36 +0530 Subject: [PATCH 4/4] Added compact modes to table widget UI (#222) New Feature: Table Widget compact rows Users can now use different sizes for rows in the table widget --- .../src/assets/icons/control/compact.svg | 9 ++ .../appsmith/ReactTableComponent.tsx | 5 + .../designSystems/appsmith/Table.tsx | 63 +++++--- .../appsmith/TableCompactMode.tsx | 135 ++++++++++++++++++ .../designSystems/appsmith/TableHeader.tsx | 9 ++ .../appsmith/TableStyledWrappers.tsx | 26 +++- .../designSystems/appsmith/TableUtilities.tsx | 8 +- .../src/pages/Editor/QueryEditor/Table.tsx | 8 +- .../src/pages/organization/settings.tsx | 8 +- app/client/src/widgets/TableWidget.tsx | 35 ++++- 10 files changed, 272 insertions(+), 34 deletions(-) create mode 100755 app/client/src/assets/icons/control/compact.svg create mode 100644 app/client/src/components/designSystems/appsmith/TableCompactMode.tsx diff --git a/app/client/src/assets/icons/control/compact.svg b/app/client/src/assets/icons/control/compact.svg new file mode 100755 index 0000000000..8cbefd7126 --- /dev/null +++ b/app/client/src/assets/icons/control/compact.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx index 3a72137400..461275b1f9 100644 --- a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -7,6 +7,7 @@ import { getMenuOptions, getAllTableColumnKeys, } from "components/designSystems/appsmith/TableUtilities"; +import { CompactMode } from "components/designSystems/appsmith/TableCompactMode"; export enum ColumnTypes { CURRENCY = "currency", @@ -95,6 +96,8 @@ interface ReactTableComponentProps { handleReorderColumn: Function; searchTableData: (searchKey: any) => void; columns: ReactTableColumnProps[]; + compactMode?: CompactMode; + updateCompactMode: (compactMode: CompactMode) => void; } const ReactTableComponent = (props: ReactTableComponentProps) => { @@ -322,6 +325,8 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { props.disableDrag(false); }} searchTableData={debounce(props.searchTableData, 500)} + compactMode={props.compactMode} + updateCompactMode={props.updateCompactMode} /> ); }; diff --git a/app/client/src/components/designSystems/appsmith/Table.tsx b/app/client/src/components/designSystems/appsmith/Table.tsx index 571c7fe530..ff4281c620 100644 --- a/app/client/src/components/designSystems/appsmith/Table.tsx +++ b/app/client/src/components/designSystems/appsmith/Table.tsx @@ -15,12 +15,29 @@ import { TableHeaderCell, renderEmptyRows } from "./TableUtilities"; import TableHeader from "./TableHeader"; import { Classes } from "@blueprintjs/core"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; +import { + CompactMode, + CompactModeTypes, +} from "components/designSystems/appsmith/TableCompactMode"; -export enum TABLE_SIZES { - COLUMN_HEADER_HEIGHT = 52, - TABLE_HEADER_HEIGHT = 61, - ROW_HEIGHT = 52, -} +export type TableSizes = { + COLUMN_HEADER_HEIGHT: number; + TABLE_HEADER_HEIGHT: number; + ROW_HEIGHT: number; +}; + +export const TABLE_SIZES: { [key: string]: TableSizes } = { + [CompactModeTypes.DEFAULT]: { + COLUMN_HEADER_HEIGHT: 52, + TABLE_HEADER_HEIGHT: 61, + ROW_HEIGHT: 52, + }, + [CompactModeTypes.SHORT]: { + COLUMN_HEADER_HEIGHT: 52, + TABLE_HEADER_HEIGHT: 61, + ROW_HEIGHT: 40, + }, +}; interface TableProps { width: number; @@ -54,24 +71,26 @@ interface TableProps { enableDrag: () => void; searchTableData: (searchKey: any) => void; columnActions?: ColumnAction[]; + compactMode?: CompactMode; + updateCompactMode: (compactMode: CompactMode) => void; } +const defaultColumn = { + minWidth: 30, + width: 150, + maxWidth: 400, +}; + export const Table = (props: TableProps) => { - const defaultColumn = React.useMemo( - () => ({ - minWidth: 30, - width: 150, - maxWidth: 400, - }), - [], - ); const pageCount = Math.ceil(props.data.length / props.pageSize); const currentPageIndex = props.pageNo < pageCount ? props.pageNo : 0; const data = React.useMemo(() => props.data, [JSON.stringify(props.data)]); - const columns = React.useMemo(() => props.columns, [ - JSON.stringify(props.columns), - JSON.stringify(props.columnActions), - ]); + const columnMemoKey = JSON.stringify({ + columns: props.columns, + columnActions: props.columnActions, + compactMode: props.compactMode, + }); + const columns = React.useMemo(() => props.columns, [columnMemoKey]); const { getTableProps, getTableBodyProps, @@ -104,11 +123,19 @@ export const Table = (props: TableProps) => { } const subPage = page.slice(startIndex, endIndex); const selectedRowIndex = props.selectedRowIndex; + const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT]; + /* Subtracting 9px to handling widget padding */ + const tableRowHeight = + (props.height - + (tableSizes.COLUMN_HEADER_HEIGHT + tableSizes.TABLE_HEADER_HEIGHT + 9)) / + props.pageSize; return ( { hiddenColumns={props.hiddenColumns} updateHiddenColumns={props.updateHiddenColumns} displayColumnActions={props.displayColumnActions} + compactMode={props.compactMode} + updateCompactMode={props.updateCompactMode} />
diff --git a/app/client/src/components/designSystems/appsmith/TableCompactMode.tsx b/app/client/src/components/designSystems/appsmith/TableCompactMode.tsx new file mode 100644 index 0000000000..1e34e60891 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TableCompactMode.tsx @@ -0,0 +1,135 @@ +import React from "react"; +import { + Popover, + Classes, + PopoverInteractionKind, + Position, + Icon, + Tooltip, +} from "@blueprintjs/core"; +import { IconWrapper } from "constants/IconConstants"; +import styled from "styled-components"; +import { Colors } from "constants/Colors"; +import { ReactComponent as CompactIcon } from "assets/icons/control/compact.svg"; +import { TableIconWrapper } from "components/designSystems/appsmith/TableStyledWrappers"; + +const DropDownWrapper = styled.div` + display: flex; + flex-direction: column; + background: white; + z-index: 1; + border-radius: 4px; + border: 1px solid ${Colors.ATHENS_GRAY}; + padding: 8px; +`; + +const OptionWrapper = styled.div<{ selected?: boolean }>` + display: flex; + width: calc(100% - 20px); + justify-content: space-between; + align-items: center; + height: 32px; + box-sizing: border-box; + padding: 8px; + color: ${Colors.OXFORD_BLUE}; + opacity: ${props => (props.selected ? 1 : 0.7)}; + min-width: 200px; + cursor: pointer; + margin-bottom: 4px; + background: ${props => (props.selected ? Colors.POLAR : Colors.WHITE)}; + border-left: ${props => (props.selected ? "4px solid #29CCA3" : "none")}; + border-radius: 4px; + .option-title { + font-weight: 500; + font-size: 14px; + line-height: 24px; + } + &:hover { + background: ${Colors.POLAR}; + } +`; + +export enum CompactModeTypes { + SHORT = "SHORT", + DEFAULT = "DEFAULT", +} + +export type CompactMode = keyof typeof CompactModeTypes; + +type CompactModeItem = { + title: string; + value: CompactMode; +}; + +const CompactModes: CompactModeItem[] = [ + { + title: "Short", + value: CompactModeTypes.SHORT, + }, + { + title: "Default", + value: CompactModeTypes.DEFAULT, + }, +]; + +interface TableCompactModeProps { + compactMode?: CompactMode; + updateCompactMode: (mode: CompactMode) => void; +} + +const TableCompactMode = (props: TableCompactModeProps) => { + const [selected, selectMenu] = React.useState(false); + return ( + { + selectMenu(false); + }} + > + { + selectMenu(!selected); + }} + > + + + + + + + + {CompactModes.map((item: CompactModeItem, index: number) => { + return ( + { + props.updateCompactMode(item.value); + }} + className={Classes.POPOVER_DISMISS} + > + {item.title} + + ); + })} + + + ); +}; + +export default TableCompactMode; diff --git a/app/client/src/components/designSystems/appsmith/TableHeader.tsx b/app/client/src/components/designSystems/appsmith/TableHeader.tsx index ca493d35ff..cdfa58473e 100644 --- a/app/client/src/components/designSystems/appsmith/TableHeader.tsx +++ b/app/client/src/components/designSystems/appsmith/TableHeader.tsx @@ -12,6 +12,9 @@ import SearchComponent from "components/designSystems/appsmith/SearchComponent"; import TableColumnsVisibility from "components/designSystems/appsmith/TableColumnsVisibility"; import { ReactTableColumnProps } from "components/designSystems/appsmith/ReactTableComponent"; import TableDataDownload from "components/designSystems/appsmith/TableDataDownload"; +import TableCompactMode, { + CompactMode, +} from "components/designSystems/appsmith/TableCompactMode"; import { Colors } from "constants/Colors"; const PageNumberInputWrapper = styled(NumericInput)` @@ -72,6 +75,8 @@ interface TableHeaderProps { searchTableData: (searchKey: any) => void; serverSidePaginationEnabled: boolean; displayColumnActions: boolean; + compactMode?: CompactMode; + updateCompactMode: (compactMode: CompactMode) => void; width: number; } @@ -99,6 +104,10 @@ const TableHeader = (props: TableHeaderProps) => { updateHiddenColumns={props.updateHiddenColumns} /> )} + {props.serverSidePaginationEnabled && ( diff --git a/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx b/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx index 0c611eef5d..c4ce903f25 100644 --- a/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx +++ b/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx @@ -1,8 +1,13 @@ import styled from "styled-components"; import { Colors } from "constants/Colors"; -import { TABLE_SIZES } from "components/designSystems/appsmith/Table"; +import { TableSizes } from "components/designSystems/appsmith/Table"; -export const TableWrapper = styled.div<{ width: number; height: number }>` +export const TableWrapper = styled.div<{ + width: number; + height: number; + tableSizes: TableSizes; + tableRowHeight?: number; +}>` width: 100%; height: 100%; background: white; @@ -24,7 +29,8 @@ export const TableWrapper = styled.div<{ width: number; height: number }>` position: relative; overflow-y: auto; /* Subtracting 9px to handling widget padding */ - height: ${props => props.height - TABLE_SIZES.TABLE_HEADER_HEIGHT - 9}px; + height: ${props => + props.height - props.tableSizes.TABLE_HEADER_HEIGHT - 9}px; .thead, .tbody { overflow: hidden; @@ -77,8 +83,14 @@ export const TableWrapper = styled.div<{ width: number; height: number }>` background: ${Colors.ATHENS_GRAY_DARKER}; } .td { - height: 52px; - line-height: 52px; + height: ${props => + props.tableRowHeight + ? props.tableRowHeight + : props.tableSizes.ROW_HEIGHT}px; + line-height: ${props => + props.tableRowHeight + ? props.tableRowHeight + : props.tableSizes.ROW_HEIGHT}px; padding: 0 10px; } } @@ -263,7 +275,7 @@ export const TableHeaderWrapper = styled.div<{ width: number; }>` display: flex; - align-items: center; + align-items: flex-end; width: 100%; border-bottom: 1px solid ${Colors.GEYSER_LIGHT}; min-width: ${props => @@ -276,7 +288,7 @@ export const TableHeaderWrapper = styled.div<{ export const CommonFunctionsMenuWrapper = styled.div` display: flex; align-items: center; - height: 100%; + height: 60px; `; export const RowWrapper = styled.div` diff --git a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx index 2853d5edf3..7db3a0e1c5 100644 --- a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx +++ b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx @@ -347,7 +347,9 @@ export const renderCell = ( ) => { switch (columnType) { case ColumnTypes.IMAGE: - if (!isString(value)) { + if (!value) { + return ; + } else if (!isString(value)) { return (
Invalid Image
@@ -384,7 +386,9 @@ export const renderCell = ( ); case ColumnTypes.VIDEO: const youtubeRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=|\?v=)([^#\&\?]*).*/; - if (isString(value) && youtubeRegex.test(value)) { + if (!value) { + return ; + } else if (isString(value) && youtubeRegex.test(value)) { return ( diff --git a/app/client/src/pages/Editor/QueryEditor/Table.tsx b/app/client/src/pages/Editor/QueryEditor/Table.tsx index 2d169b64ae..73fe171adc 100644 --- a/app/client/src/pages/Editor/QueryEditor/Table.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Table.tsx @@ -5,6 +5,8 @@ import { } from "components/designSystems/appsmith/TableStyledWrappers"; import { useTable, useFlexLayout } from "react-table"; import styled from "styled-components"; +import { CompactModeTypes } from "components/designSystems/appsmith/TableCompactMode"; +import { TABLE_SIZES } from "components/designSystems/appsmith/Table"; interface TableProps { data: Record[]; @@ -59,7 +61,11 @@ const Table = (props: TableProps) => { ); return ( - +
{headerGroups.map((headerGroup: any, index: number) => ( diff --git a/app/client/src/pages/organization/settings.tsx b/app/client/src/pages/organization/settings.tsx index d38fd8a85d..220077320b 100644 --- a/app/client/src/pages/organization/settings.tsx +++ b/app/client/src/pages/organization/settings.tsx @@ -2,6 +2,8 @@ import React, { useEffect } from "react"; import { connect } from "react-redux"; import { Icon } from "@blueprintjs/core"; import { TableWrapper } from "components/designSystems/appsmith/TableStyledWrappers"; +import { CompactModeTypes } from "components/designSystems/appsmith/TableCompactMode"; +import { TABLE_SIZES } from "components/designSystems/appsmith/Table"; import { AppState } from "reducers"; import { getAllUsers, @@ -265,7 +267,11 @@ export const OrgSettings = (props: PageProps) => { {props.isFetchAllUsers && props.isFetchAllRoles ? ( ) : ( - +
{headerGroups.map((headerGroup: any, index: number) => ( diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index 3d8ae2310f..237ff7fa8a 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -20,6 +20,10 @@ import { BASE_WIDGET_VALIDATION, } from "utils/ValidationFactory"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; +import { + CompactMode, + CompactModeTypes, +} from "components/designSystems/appsmith/TableCompactMode"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; import Skeleton from "components/utils/Skeleton"; import moment from "moment"; @@ -245,8 +249,10 @@ class TableWidget extends BaseWidget { } if (isValidDate) { tableRow[accessor] = moment(value).format(format); - } else { + } else if (value) { tableRow[accessor] = "Invalid Value"; + } else { + tableRow[accessor] = ""; } break; case ColumnTypes.TIME: @@ -259,8 +265,10 @@ class TableWidget extends BaseWidget { } if (isValidTime) { tableRow[accessor] = moment(value).format("HH:mm"); - } else { + } else if (value) { tableRow[accessor] = "Invalid Value"; + } else { + tableRow[accessor] = ""; } break; default: @@ -289,12 +297,22 @@ class TableWidget extends BaseWidget { super.updateWidgetMetaProperty("pageNo", pageNo); } const { componentWidth, componentHeight } = this.getComponentDimensions(); - const pageSize = Math.floor( + const tableSizes = + TABLE_SIZES[this.props.compactMode || CompactModeTypes.DEFAULT]; + let pageSize = Math.floor( (componentHeight - - TABLE_SIZES.TABLE_HEADER_HEIGHT - - TABLE_SIZES.COLUMN_HEADER_HEIGHT) / - TABLE_SIZES.ROW_HEIGHT, + tableSizes.TABLE_HEADER_HEIGHT - + tableSizes.COLUMN_HEADER_HEIGHT) / + tableSizes.ROW_HEIGHT, ); + if ( + componentHeight - + (tableSizes.TABLE_HEADER_HEIGHT + + tableSizes.COLUMN_HEADER_HEIGHT + + tableSizes.ROW_HEIGHT * pageSize) > + 10 + ) + pageSize += 1; if (pageSize !== this.props.pageSize) { super.updateWidgetMetaProperty("pageSize", pageSize); @@ -354,6 +372,10 @@ class TableWidget extends BaseWidget { this.disableDrag(disable); }} searchTableData={this.handleSearchTable} + compactMode={this.props.compactMode} + updateCompactMode={(compactMode: CompactMode) => { + super.updateWidgetMetaProperty("compactMode", compactMode); + }} sortTableColumn={(column: string, asc: boolean) => { this.resetSelectedRowIndex(); super.updateWidgetMetaProperty("sortedColumn", { @@ -467,6 +489,7 @@ export interface TableWidgetProps extends WidgetProps { columnNameMap?: { [key: string]: string }; columnTypeMap?: { [key: string]: { type: string; format: string } }; columnSizeMap?: { [key: string]: number }; + compactMode?: CompactMode; sortedColumn?: { column: string; asc: boolean;