From e59ec2a8a94c50b0afc3f00b3cf8eb292d6e0144 Mon Sep 17 00:00:00 2001 From: Trisha Anand Date: Thu, 27 Aug 2020 15:29:57 +0530 Subject: [PATCH 1/3] Use a new API to set (unset) execute on load for an action. (#443) --- .../com/appsmith/server/constants/FieldName.java | 1 + .../server/controllers/ActionController.java | 7 +++++++ .../java/com/appsmith/server/domains/Action.java | 4 ++++ .../appsmith/server/services/ActionService.java | 2 ++ .../server/services/ActionServiceImpl.java | 14 +++++++++++++- 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java index ea50cca00c..2cd1231f31 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java @@ -52,4 +52,5 @@ public class FieldName { "}"; public static String ANONYMOUS_USER = "anonymousUser"; public static String USERNAMES = "usernames"; + public static String ACTION = "action"; } 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 79b6578b54..d9e6b7ccaf 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 @@ -89,4 +89,11 @@ public class ActionController extends BaseController new ResponseDTO<>(HttpStatus.OK.value(), actions, null)); } + + @PutMapping("/executeOnLoad/{id}") + public Mono> setExecuteOnLoad(@PathVariable String id, @RequestParam Boolean flag) { + log.debug("Going to set execute on load for action id {} to {}", id, flag); + return service.setExecuteOnLoad(id, flag) + .map(action -> new ResponseDTO<>(HttpStatus.OK.value(), action, null)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java index 5f0aa950bd..788aa25f07 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Action.java @@ -3,6 +3,7 @@ package com.appsmith.server.domains; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.BaseDomain; import com.appsmith.external.models.Property; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.NoArgsConstructor; @@ -67,6 +68,9 @@ public class Action extends BaseDomain { @Transient String pluginId; + @JsonIgnore + Boolean userSetOnLoad = false; + Documentation documentation; /** diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java index 4300aad505..cb5b971833 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionService.java @@ -33,4 +33,6 @@ public interface ActionService extends CrudService { Flux getActionsForViewMode(String applicationId); + Mono setExecuteOnLoad(String id, Boolean isExecuteOnLoad); + } 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 a71ba20197..98e7aeda1c 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 @@ -60,6 +60,7 @@ import java.util.stream.Collectors; import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS; import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES; +import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; @@ -584,10 +585,21 @@ public class ActionServiceImpl extends BaseService setExecuteOnLoad(String id, Boolean isExecuteOnLoad) { + return repository.findById(id, MANAGE_ACTIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, id))) + .flatMap(action -> { + action.setUserSetOnLoad(true); + action.setExecuteOnLoad(isExecuteOnLoad); + return repository.save(action); + }); + } + @Override public Mono delete(String id) { Mono actionMono = repository.findById(id) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "action", id))); + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, id))); return actionMono .flatMap(toDelete -> repository.delete(toDelete).thenReturn(toDelete)) .flatMap(analyticsService::sendDeleteEvent); From 0de8ef6270b3c8f669d4a6402ec097fb86f65e1f Mon Sep 17 00:00:00 2001 From: areyabhishek Date: Mon, 31 Aug 2020 22:50:09 +0530 Subject: [PATCH 2/3] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e60ae6d869..e6e4fd01f0 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@

The quickest way to build dashboards, workflows, forms, or any kind of internal business tool.

+

The only open source alternative to MSFT Power Apps, Salesforce Lightning platform, Service Now platform, Quickbase, Retool, Forest Admin, and many more.

@@ -55,6 +56,7 @@ Appsmith is a quicker way of building internal tools by visualising them as modu ## Features +* **5 minute setup**: Install Appsmith in 5 minutes on your servers. * **Build custom UI**: Drag & drop, resize and style widgets **without HTML / CSS**. [Read more](https://docs.appsmith.com/core-concepts/building-the-ui) * **Query data**: Query & update your database directly from the UI. Connect to **postgres, mongo, MySQL, REST & GraphQL APIs**. [Read more](https://docs.appsmith.com/core-concepts/building-the-ui/displaying-api-data) * **JS Logic**: Write snippets of business logic using JS to transform data, manipuate UI or trigger workflows. Use popular libraries like lodash & moment anywhere in the app From 350ea64ab604d8c238ea74a735149b25344871fb Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Thu, 3 Sep 2020 19:36:12 +0530 Subject: [PATCH 3/3] Release (#492) * added deploy environment for sentry * added capture message * fixed sentry performance monitoring * Feature - Column type displayed in column options of table widget filters (#411) * Table widget filter type attribute displayed in column options * minor fix * removed initialised event * Updated sentry sdk (#461) Integrated react router for better performance reporting Added Sentry Profiler for editor, appviewer and widgets Co-authored-by: Nikhil Nandagopal * Members and org settings page (#424) * Members and org settings page. * Adding link back to applications * Making UI look better. * Fixing some more css * Table dropdown onBlur, UI transparency, Popover issue fixed. Added sort arrow (#413) * table dropdown ui issue and onblur click fixed * position props value fixed * tabs tripple click fixed * Position props in table dropdown made optional * tabs background color bug removed * General settings page is bug free. * Getting the manage users page to work. * Moving general settings into its own file. * Removing some unwanted URLs * User cant change their role or remove themselves * Changing onClick prop for Icon. * Fixing tests * Added loading states to text inputs. * Added validators for text input * Fixed border of skeleton for text input * Adding a loader for Members screen * Adding Noop to icon button * Removing console.log * Fixing imports. * Moving org reducer to immer. * Using utils email validator. * Removing placeholder from text input props. Co-authored-by: devrk96 * Feature-Icon enum added and size updated. (#463) * appIcon color, icon enums and size fixed * icon import updated * Show validation errors if any for keyvalue fields (#445) * Update host field with validation properties * Performance:entity explorer (#395) * Entity explorer performance enhancements * Feature: Text component props and enum values in stories updated (#468) * text component props updated * case prop updated * className added in text component and used enum values as array in stories * Add duplicate application option (#465) * Feature: Icon component icon names, props and className updated (#471) * className added in icon component and used icon names as an array in stories * button component semicolon fixed * fill prop added in button component * name prop and iconName import updated. * Remove `@appsmith` alias that causes build errors (#479) * Add API for uploading logo images for organizations (#376) * Start with new controller for assets * Progress on uploading logo image * Saving and loading organization logo assets now works * Remove existing logo asset before saving a new one * Fix 500 when uploading logo for the first time * Fix URL in response for uploading logo image * Add test for uploading logo for ogranization * Mild refactoring in tests * Add validation for host values in DB datasources (#444) * Add validation for host values in DB datasources * Mild refactoring, for a dummy commit * Add test for host name validation in datasources * Fix: Datasource name not getting updated from the entity explorer (#478) * Fix: Other page widget props are not shown on the entity explorer (#480) * Feature: Add widgets from explorer (#464) * Deprecate Navbar. Sidebar becomes a panelStack. It now has two panels, WidgetSidebar and Explorer Co-authored-by: Nandan * Feature: Dropdown component (#477) * dropdown with text and icon implemented * dropdown with lable implemented and story updated * Icon import and storyWrapper import updated * onclick cleaned * button height issue fixed in all sizes (#483) * Menu item disabled UI fixed (#484) * Add watchtower for auto updates to Appsmith containers (#456) * Trying to move horizontal rules into separate function * Delete install_dir if exists already, after confirmation * Add confirm function to take user conformations * Resolve warnings on read command without -r option * Use temporary directory for downloaded templates * Fix inverted calls to confirm * nginx template doesn't use global variables now * docker compose template doesn't depend on global vars now * Fix port checking for CentOS * Fix encryption.env to not use global variables * Fix mongo-init to not use global variables * Fix docker.env file to not use any global variables * Fix letsencrypt script to not use any global variables * Set mongo_database in the docker-compose file Spotted this after global variables were reduced. The `mongo_database` was not being used anywhere. * Move password generation into a function * Attempt to add watchtower * Fix port checking in install script for CentOS * Don't offer to delete install dir if it already exists * Remove vagrant source for templates (it's a dev-time thing only) * Add a node.js script to dump examples organization (#448) * Add a node.js script to dump examples organization This dump file is used to setup initial examples organization in a self-hosted setup. * Remove unused variable * Add npm run shortcut for dump script * Returning an error in case the user tries to get all users. (#472) * Fix column alias names in MySQL actions (#466) * Fix aliases not showing up for MySQL actions * Fix date column display for MySQL actions * Fix datetime columns for MySQL actions * Add support for timestamp and year data types for MySQL actions * Fix column label for MySQL plugin * Add tests for MySQL temporal data types * Add tests for alias columns in MySQL and Postgres * query and datasource related tests (#423) * query and datasource related tests * review comment incorporated * Adding multiple widgets test * Table widget related search/filter/compact mode tests * Added tests related to visiblity funvtionality in table widget * Validate action type in explorer * updated test * Test updated in accordance with new UI * removed sentrycli and release from webpack plugin * re-added sentry cli * removed sentry cli again increased redux depth * re-added release to webpack plugin * moved sentry plugin to the end so sourcemaps are generated correctly * hide edit icon issue fixed (#494) * Fixing Storybook deploy (#493) * fixed nginx template for mac added error boundary for sentry * icon size prop fixed (#485) * Fix Let's Encrypt certificate generation for self-hosted (#491) * Creating SSL certificates now works * Work with single domain * Move certificate generation into install script * Download templates from master branch, not release Co-authored-by: Nikhil Nandagopal Co-authored-by: vicky-primathon <67091118+vicky-primathon@users.noreply.github.com> Co-authored-by: satbir121 <39981226+satbir121@users.noreply.github.com> Co-authored-by: devrk96 Co-authored-by: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com> Co-authored-by: Abhinav Jha Co-authored-by: Hetu Nandu Co-authored-by: Shrikant Sharat Kandula Co-authored-by: Nandan Co-authored-by: Trisha Anand Co-authored-by: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> --- app/client/.sentryclirc | 7 - app/client/craco.build.config.js | 27 +- .../ApiPaneTests/API_Mustache_spec.js | 3 - .../Applications/DuplicateApplication_spec.js | 38 ++ .../Binding/Bind_DatePicker_Text_spec.js | 3 - .../Binding/Bind_TableTextPagination_spec.js | 15 +- .../Binding/Bind_tableApi_spec.js | 5 +- .../Smoke_TestSuite/Binding/ChartText.js | 2 - .../Smoke_TestSuite/Binding/TextTable.js | 4 - .../DisplayWidgets/Chart_spec.js | 1 - .../DisplayWidgets/Image_spec.js | 3 - .../DisplayWidgets/Table_spec.js | 174 +++++-- .../DisplayWidgets/Text_spec.js | 1 - .../DynamicInput/autocomplete_spec.js | 4 - .../Entity_Explorer_API_Pane_spec.js | 3 + ...xplorer_CopyQuery_RenameDatasource_spec.js | 101 ++++ .../Entity_Explorer_Multiple_Widgets_spec.js | 66 +++ .../Entity_Explorer_Widgets_spec.js | 1 - .../FormWidgets/Button_spec.js | 1 - .../FormWidgets/CheckBox_spec.js | 5 - .../FormWidgets/DatePicker_spec.js | 1 - .../FormWidgets/Dropdown_spec.js | 3 - .../FormWidgets/FilePicker_spec.js | 1 - .../FormWidgets/FormWidget_spec.js | 2 - .../Smoke_TestSuite/FormWidgets/Input_spec.js | 5 - .../Smoke_TestSuite/FormWidgets/Radio_spec.js | 3 - .../FormWidgets/RichTextEditor_spec.js | 1 - .../Smoke_TestSuite/LayoutWidgets/Tab_spec.js | 3 - .../OrganisationTests/CreateOrgTests_spec.js | 21 +- app/client/cypress/locators/HomePage.json | 24 +- .../cypress/locators/apiWidgetslocator.json | 3 +- .../cypress/locators/explorerlocators.json | 4 +- .../cypress/locators/publishWidgetspage.json | 17 +- app/client/cypress/support/commands.js | 81 ++- .../docker/templates/nginx-mac.conf.template | 2 +- app/client/package.json | 4 +- app/client/src/actions/applicationActions.ts | 9 + app/client/src/actions/datasourceActions.ts | 7 +- app/client/src/actions/orgActions.ts | 68 +++ app/client/src/api/ApplicationApi.tsx | 10 + app/client/src/api/OrgApi.ts | 5 +- .../src/assets/icons/ads/create-new.svg | 4 + .../src/assets/icons/ads/invite-users.svg | 3 + app/client/src/assets/icons/ads/launch.svg | 3 + app/client/src/assets/icons/ads/share.svg | 3 + .../src/assets/icons/ads/upper_arrow.svg | 3 + app/client/src/assets/icons/ads/view-all.svg | 3 + app/client/src/assets/icons/ads/workspace.svg | 5 + app/client/src/components/ads/AppIcon.tsx | 116 +++-- app/client/src/components/ads/Button.tsx | 80 +-- app/client/src/components/ads/Dropdown.tsx | 179 ++++++- .../src/components/ads/EditableText.tsx | 68 +-- app/client/src/components/ads/Icon.tsx | 127 ++++- .../src/components/ads/IconSelector.tsx | 43 +- app/client/src/components/ads/Menu.tsx | 4 +- app/client/src/components/ads/MenuItem.tsx | 49 +- app/client/src/components/ads/SearchInput.tsx | 34 +- app/client/src/components/ads/Spinner.tsx | 25 +- app/client/src/components/ads/Table.tsx | 23 +- .../src/components/ads/TableDropdown.tsx | 99 ++-- app/client/src/components/ads/Tabs.tsx | 57 +-- app/client/src/components/ads/Text.tsx | 31 +- app/client/src/components/ads/TextInput.tsx | 69 ++- app/client/src/components/ads/common.tsx | 12 + .../designSystems/appsmith/CascadeFields.tsx | 47 +- .../appsmith/TableStyledWrappers.tsx | 22 + .../components/editorComponents/Sidebar.tsx | 26 +- .../components/formControls/BaseControl.tsx | 1 + .../formControls/KeyValueArrayControl.tsx | 37 +- .../src/components/stories/Button.stories.tsx | 20 +- .../stories/ColorSelector.stories.tsx | 2 +- .../components/stories/Dropdown.stories.tsx | 112 +++++ .../stories/EditableText.stories.tsx | 4 +- .../src/components/stories/Icon.stories.tsx | 77 +-- .../stories/IconSelector.stories.tsx | 25 +- .../src/components/stories/Menu.stories.tsx | 37 +- .../stories/SearchInput.stories.tsx | 4 +- .../src/components/stories/Table.stories.tsx | 54 +- .../stories/TableDropdown.stories.tsx | 26 +- .../src/components/stories/Tabs.stories.tsx | 53 +- .../src/components/stories/Text.stories.tsx | 25 +- .../components/stories/TextInput.stories.tsx | 4 +- app/client/src/configs/index.ts | 17 +- app/client/src/configs/types.ts | 3 + app/client/src/constants/DefaultTheme.tsx | 20 +- app/client/src/constants/Explorer.ts | 2 +- .../src/constants/ReduxActionConstants.tsx | 9 +- app/client/src/constants/messages.ts | 6 + app/client/src/constants/orgConstants.ts | 1 + app/client/src/constants/routes.ts | 13 - app/client/src/index.tsx | 31 +- .../src/mockResponses/MongoConfigResponse.tsx | 2 + .../mockResponses/PostgresConfigResponse.tsx | 2 + app/client/src/pages/AppViewer/index.tsx | 3 +- app/client/src/pages/Applications/index.tsx | 19 +- .../pages/Editor/DataSourceEditor/DBForm.tsx | 2 +- .../Editor/Explorer/Actions/ActionEntity.tsx | 57 +-- .../Editor/Explorer/Actions/ActionsGroup.tsx | 16 +- .../pages/Editor/Explorer/Actions/helpers.tsx | 9 +- .../Datasources/DataSourceContextMenu.tsx | 10 + .../Explorer/Datasources/DatasourceEntity.tsx | 11 +- .../Explorer/Datasources/DatasourcesGroup.tsx | 26 +- .../Explorer/Entity/EntityProperties.tsx | 90 ++++ .../src/pages/Editor/Explorer/Entity/Name.tsx | 247 +++++----- .../pages/Editor/Explorer/Entity/index.tsx | 162 +++--- .../pages/Editor/Explorer/ExplorerSearch.tsx | 8 +- .../Editor/Explorer/Pages/PageEntity.tsx | 57 ++- .../pages/Editor/Explorer/Pages/PageGroup.tsx | 81 ++- .../Editor/Explorer/Widgets/WidgetEntity.tsx | 28 +- .../Editor/Explorer/Widgets/WidgetGroup.tsx | 26 +- app/client/src/pages/Editor/Explorer/hooks.ts | 213 +++----- .../src/pages/Editor/Explorer/index.tsx | 110 ++--- app/client/src/pages/Editor/MainContainer.tsx | 4 +- app/client/src/pages/Editor/Navbar.tsx | 85 ---- app/client/src/pages/Editor/WidgetSidebar.tsx | 126 ++++- app/client/src/pages/Editor/index.tsx | 30 +- app/client/src/pages/Editor/routes.tsx | 7 +- app/client/src/pages/UserAuth/SignUp.tsx | 2 +- app/client/src/pages/common/AppRoute.tsx | 5 +- app/client/src/pages/common/PageWrapper.tsx | 34 +- app/client/src/pages/organization/General.tsx | 123 +++++ app/client/src/pages/organization/Members.tsx | 171 +++++++ .../pages/organization/OrgInviteUsersForm.tsx | 2 +- app/client/src/pages/organization/index.tsx | 3 +- .../src/pages/organization/settings.tsx | 462 ++++-------------- app/client/src/reducers/index.tsx | 2 +- .../uiReducers/applicationsReducer.tsx | 22 + .../src/reducers/uiReducers/editorReducer.tsx | 12 + .../src/reducers/uiReducers/orgReducer.ts | 200 +++----- .../src/reducers/uiReducers/pageDSLReducer.ts | 4 +- app/client/src/sagas/ApplicationSagas.tsx | 51 ++ app/client/src/sagas/DatasourcesSagas.ts | 12 +- app/client/src/sagas/OrgSagas.ts | 17 +- app/client/src/sagas/PageSagas.tsx | 17 + .../src/selectors/applicationSelectors.tsx | 2 + .../src/selectors/organizationSelectors.tsx | 8 + app/client/src/utils/AppsmithUtils.tsx | 14 + app/client/src/utils/WidgetRegistry.tsx | 107 ++-- app/client/src/utils/hooks/useClick.tsx | 36 ++ app/client/src/widgets/ButtonWidget.tsx | 2 + app/client/src/widgets/CanvasWidget.tsx | 2 + app/client/src/widgets/ChartWidget.tsx | 2 + app/client/src/widgets/CheckboxWidget.tsx | 2 + app/client/src/widgets/ContainerWidget.tsx | 2 + app/client/src/widgets/DatePickerWidget.tsx | 2 + app/client/src/widgets/DropdownWidget.tsx | 2 + app/client/src/widgets/FilepickerWidget.tsx | 2 + app/client/src/widgets/FormButtonWidget.tsx | 2 + app/client/src/widgets/FormWidget.tsx | 2 + app/client/src/widgets/IconWidget.tsx | 2 + app/client/src/widgets/ImageWidget.tsx | 2 + app/client/src/widgets/InputWidget.tsx | 2 + app/client/src/widgets/MapWidget.tsx | 2 + app/client/src/widgets/ModalWidget.tsx | 8 +- app/client/src/widgets/RadioGroupWidget.tsx | 2 + .../src/widgets/RichTextEditorWidget.tsx | 4 + app/client/src/widgets/TableWidget.tsx | 2 + app/client/src/widgets/TabsWidget.tsx | 2 + app/client/src/widgets/TextWidget.tsx | 2 + app/client/tsconfig.json | 1 - app/client/yarn.lock | 104 ++-- .../mongoPlugin/src/main/resources/form.json | 4 +- .../com/external/plugins/MySqlPlugin.java | 41 +- .../mysqlPlugin/src/main/resources/form.json | 4 +- .../com/external/plugins/MySqlPluginTest.java | 153 +++++- .../com/external/plugins/PostgresPlugin.java | 3 +- .../src/main/resources/form.json | 4 +- .../external/plugins/PostgresPluginTest.java | 41 +- .../com/appsmith/server/constants/Url.java | 1 + .../server/controllers/AssetController.java | 46 ++ .../controllers/OrganizationController.java | 12 + .../com/appsmith/server/domains/Asset.java | 22 + .../appsmith/server/domains/Organization.java | 9 + .../server/helpers/PluginExecutorHelper.java | 2 +- .../server/repositories/AssetRepository.java | 8 + .../server/services/AssetService.java | 10 + .../server/services/AssetServiceImpl.java | 22 + .../services/DatasourceServiceImpl.java | 25 +- .../server/services/OrganizationService.java | 3 + .../services/OrganizationServiceImpl.java | 45 +- .../server/services/UserServiceImpl.java | 7 + .../main/resources/examples-organization.json | 349 ++++++------- .../server/configurations/SeedMongoData.java | 64 ++- .../services/DatasourceServiceTest.java | 48 ++ .../services/OrganizationServiceTest.java | 88 +++- .../server/services/UserServiceTest.java | 15 + .../my_organization_logo.png | Bin 0 -> 1811 bytes .../{acl-migration => node}/.editorconfig | 0 .../scripts/{acl-migration => node}/README.md | 2 +- .../main.js => node/acl-migration.js} | 0 app/server/scripts/node/dump-examples-org.js | 206 ++++++++ .../{acl-migration => node}/package-lock.json | 6 + .../{acl-migration => node}/package.json | 5 +- deploy/install.sh | 392 +++++++++------ deploy/template/docker-compose.yml.sh | 29 +- deploy/template/docker.env.sh | 12 +- deploy/template/encryption.env.sh | 12 +- deploy/template/init-letsencrypt.sh.sh | 93 ---- deploy/template/mongo-init.js.sh | 14 +- deploy/template/nginx_app.conf.sh | 105 ++-- 200 files changed, 4566 insertions(+), 2682 deletions(-) delete mode 100644 app/client/.sentryclirc create mode 100644 app/client/cypress/integration/Smoke_TestSuite/Applications/DuplicateApplication_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Multiple_Widgets_spec.js create mode 100644 app/client/src/actions/orgActions.ts create mode 100644 app/client/src/assets/icons/ads/create-new.svg create mode 100644 app/client/src/assets/icons/ads/invite-users.svg create mode 100644 app/client/src/assets/icons/ads/launch.svg create mode 100644 app/client/src/assets/icons/ads/share.svg create mode 100644 app/client/src/assets/icons/ads/upper_arrow.svg create mode 100644 app/client/src/assets/icons/ads/view-all.svg create mode 100644 app/client/src/assets/icons/ads/workspace.svg create mode 100644 app/client/src/components/stories/Dropdown.stories.tsx create mode 100644 app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx delete mode 100644 app/client/src/pages/Editor/Navbar.tsx create mode 100644 app/client/src/pages/organization/General.tsx create mode 100644 app/client/src/pages/organization/Members.tsx create mode 100644 app/client/src/utils/hooks/useClick.tsx create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/AssetController.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Asset.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/AssetRepository.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/services/AssetService.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/services/AssetServiceImpl.java create mode 100644 app/server/appsmith-server/src/test/resources/test_assets/OrganizationServiceTest/my_organization_logo.png rename app/server/scripts/{acl-migration => node}/.editorconfig (100%) rename app/server/scripts/{acl-migration => node}/README.md (94%) rename app/server/scripts/{acl-migration/main.js => node/acl-migration.js} (100%) create mode 100644 app/server/scripts/node/dump-examples-org.js rename app/server/scripts/{acl-migration => node}/package-lock.json (97%) rename app/server/scripts/{acl-migration => node}/package.json (70%) delete mode 100755 deploy/template/init-letsencrypt.sh.sh diff --git a/app/client/.sentryclirc b/app/client/.sentryclirc deleted file mode 100644 index 738f048db5..0000000000 --- a/app/client/.sentryclirc +++ /dev/null @@ -1,7 +0,0 @@ -[auth] -token=a86809ea5ede4755936cb72aa7c23f9555be3c21fb2449e6b1f47be6b00a9098 -[defaults] -project=appsmith -org=appsmith -dsn=https://a63362b692d64edeb175741f1f80b091@sentry.io/1546547 -log-level=debug \ No newline at end of file diff --git a/app/client/craco.build.config.js b/app/client/craco.build.config.js index 3737d31b06..f7f720ff5f 100644 --- a/app/client/craco.build.config.js +++ b/app/client/craco.build.config.js @@ -8,18 +8,6 @@ const env = process.env.REACT_APP_ENVIRONMENT; const plugins = []; -if (env === "PRODUCTION" || env === "STAGING") { - plugins.push( - new SentryWebpackPlugin({ - include: "build", - ignore: ["node_modules", "webpack.config.js"], - release: process.env.REACT_APP_SENTRY_RELEASE, - setCommits: { - auto: true - } - }), - ); -} plugins.push( new WorkboxPlugin.InjectManifest({ swSrc: "./src/serviceWorker.js", @@ -29,6 +17,21 @@ plugins.push( }), ); +if (env === "PRODUCTION" || env === "STAGING") { + plugins.push( + new SentryWebpackPlugin({ + include: "build", + ignore: ["node_modules", "webpack.config.js"], + setCommits: { + auto: true + }, + deploy: { + env: process.env.REACT_APP_SENTRY_ENVIRONMENT + } + }), + ); +} + module.exports = merge(common, { webpack: { plugins: plugins, diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js index be79b8feac..806c3e89f4 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Mustache_spec.js @@ -9,12 +9,9 @@ describe("Moustache test Functionality", function() { cy.addDsl(dsl); }); it("Moustache test Functionality", function() { - //cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.widgetText("Api", widgetsPage.textWidget, widgetsPage.textInputval); cy.testCodeMirror("/api/users/2"); - cy.NavigateToEntityExplorer(); - cy.wait(10000); cy.NavigateToAPI_Panel(); cy.log("Navigation to API Panel screen successful"); cy.CreateAPI("TestAPINew"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Applications/DuplicateApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Applications/DuplicateApplication_spec.js new file mode 100644 index 0000000000..75e4756c8e --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Applications/DuplicateApplication_spec.js @@ -0,0 +1,38 @@ +const dsl = require("../../../fixtures/displayWidgetDsl.json"); +const homePage = require("../../../locators/HomePage.json"); +const commonlocators = require("../../../locators/commonlocators.json"); +const explorerlocators = require("../../../locators/explorerlocators.json"); +let duplicateApplicationDsl; + +describe("Duplicate application", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Check whether the duplicate application has the same dsl as the original", function() { + cy.get(commonlocators.homeIcon).click({ force: true }); + const appname = localStorage.getItem("AppName"); + cy.get(homePage.searchInput).type(appname); + cy.wait(2000); + + cy.get(homePage.applicationCard).trigger("mouseover"); + cy.get(homePage.appMoreIcon) + .first() + .click({ force: true }); + cy.get(homePage.deleteButton) + .contains("Duplicate") + .click({ force: true }); + + cy.wait("@getPage").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get("@getPage").then(httpResponse => { + const data = httpResponse.response.body.data; + duplicateApplicationDsl = data.layouts[0].dsl; + + expect(duplicateApplicationDsl).to.deep.equal(dsl.dsl); + }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_DatePicker_Text_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_DatePicker_Text_spec.js index 8a895531ab..2b763f76f5 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_DatePicker_Text_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_DatePicker_Text_spec.js @@ -16,7 +16,6 @@ describe("Binding the Datepicker and Text Widget", function() { /** * Bind DatePicker1 to Text for "selectedDate" */ - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", "{{DatePicker1.selectedDate}}"); cy.get(commonlocators.editPropCrossButton).click(); @@ -50,7 +49,6 @@ describe("Binding the Datepicker and Text Widget", function() { }); it("DatePicker1-text: Change the date in DatePicker1 and Validate the same in text widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); /** @@ -108,7 +106,6 @@ describe("Binding the Datepicker and Text Widget", function() { /** * Bind the DatePicker1 and DatePicker2 along with hard coded text to Text widget */ - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext( "text", diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js index 62fab2131c..9089c61aac 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js @@ -16,15 +16,14 @@ describe("Test Create Api and Bind to Table widget", function() { }); it("Table-Text, Validate Server Side Pagination of Paginate with Table Page No", function() { - cy.get(pages.widgetsEditor).click(); - cy.openPropertyPane("tablewidget"); + cy.SearchEntityandOpen("Table1"); /**Bind Api1 with Table widget */ cy.testJsontext("tabledata", "{{Api1.data.users}}"); cy.CheckWidgetProperties(commonlocators.serverSidePaginationCheckbox); /**Bind Table with Textwidget with selected row */ - cy.get(pages.widgetsEditor).click(); - cy.openPropertyPane("textwidget"); + cy.SearchEntityandOpen("Text1"); cy.testJsontext("text", "{{Table1.selectedRow.url}}"); + cy.SearchEntityandOpen("Table1"); cy.readTabledata("0", "0").then(tabData => { const tableData = tabData; localStorage.setItem("tableDataPage1", tableData); @@ -43,7 +42,6 @@ describe("Test Create Api and Bind to Table widget", function() { cy.get(commonlocators.rightArrowBtn).click({ force: true }); cy.validateToastMessage("done"); cy.ValidatePublishTableData("11"); - cy.get(publishPage.backToEditor).click({ force: true }); }); @@ -59,13 +57,12 @@ describe("Test Create Api and Bind to Table widget", function() { parseSpecialCharSequences: false, }); cy.WaitAutoSave(); - cy.get(pages.widgetsEditor).click(); - cy.openPropertyPane("textwidget"); + cy.SearchEntityandOpen("Text1"); + //cy.openPropertyPane("textwidget"); /** Bind the Table widget with Text widget*/ cy.testJsontext("text", "{{Table1.selectedRow.url}}"); cy.get(commonlocators.editPropCrossButton).click(); - cy.get(pages.widgetsEditor).click(); - cy.openPropertyPane("tablewidget"); + cy.SearchEntityandOpen("Table1"); cy.testJsontext("tabledata", "{{Api2.data.users}}"); cy.callApi("Api2"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js index f499396bb4..d833b0b01f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_tableApi_spec.js @@ -27,9 +27,8 @@ describe("Test Create Api and Bind to Table widget", function() { }); it("Test_Validate the Api data is updated on Table widget", function() { - //cy.get(pages.pagesIcon).click({ force: true }); - cy.get(pages.widgetsEditor).click(); - cy.openPropertyPane("tablewidget"); + cy.SearchEntityandOpen("Table1"); + //cy.openPropertyPane("tablewidget"); cy.testJsontext("tabledata", "{{Api1.data}}"); cy.get(commonlocators.editPropCrossButton).click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js index aa519de87f..5ebdc10a05 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js @@ -9,7 +9,6 @@ describe("Text-Chart Binding Functionality", function() { cy.addDsl(dsl); }); it("Text-Chart Binding Functionality View", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", JSON.stringify(this.data.chartInputValidate)); cy.get(commonlocators.TextInside).should( @@ -17,7 +16,6 @@ describe("Text-Chart Binding Functionality", function() { JSON.stringify(this.data.chartInputValidate), ); cy.closePropertyPane(); - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("chartwidget"); cy.get(viewWidgetsPage.chartType) .find(commonlocators.dropdownbuttonclick) diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js index 5eff6c04da..96272e2cd9 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js @@ -14,7 +14,6 @@ describe("Text-Table Binding Functionality", function() { cy.addDsl(dsl); }); it("Text-Table Binding Functionality For Id", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tablewidget"); /** * @param(Index) Provide index value to select the row. @@ -43,7 +42,6 @@ describe("Text-Table Binding Functionality", function() { cy.get(publish.backToEditor) .first() .click(); - cy.get(pages.widgetsEditor).click(); cy.isSelectRow(2); cy.openPropertyPane("textwidget"); cy.testJsontext("text", "{{Table1.selectedRow.email}}"); @@ -68,7 +66,6 @@ describe("Text-Table Binding Functionality", function() { cy.get(publish.backToEditor) .first() .click(); - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", "{{Table1.pageSize}}"); cy.get(commonlocators.TableRow) @@ -95,7 +92,6 @@ describe("Text-Table Binding Functionality", function() { * @param(Index) Provide index value to select the row. */ cy.isSelectRow(1); - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); cy.testJsontext("text", JSON.stringify(this.data.textfun)); /** diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js index 0328351635..8f038e5490 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Chart_spec.js @@ -10,7 +10,6 @@ describe("Chart Widget Functionality", function() { }); beforeEach(() => { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("chartwidget"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js index c35bc2f779..432f9b098b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Image_spec.js @@ -10,7 +10,6 @@ describe("Image Widget Functionality", function() { }); it("Image Widget Functionality", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("imagewidget"); /** * @param{Text} Random Text @@ -43,7 +42,6 @@ describe("Image Widget Functionality", function() { }); it("Image Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("imagewidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -51,7 +49,6 @@ describe("Image Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Image Widget Functionality To Check Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("imagewidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Table_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Table_spec.js index 216eedd243..41989a93e7 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Table_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Table_spec.js @@ -10,7 +10,6 @@ describe("Table Widget Functionality", function() { }); it("Table Widget Functionality", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tablewidget"); /** @@ -21,18 +20,7 @@ describe("Table Widget Functionality", function() { cy.widgetText("Table1", widgetsPage.tableWidget, commonlocators.tableInner); cy.testJsontext("tabledata", JSON.stringify(this.data.TableInput)); cy.wait("@updateLayout"); - // cy.ExportVerify(commonlocators.pdfSupport, "PDF Export"); - // cy.ExportVerify(commonlocators.ExcelSupport, "Excel Export"); - // cy.ExportVerify(commonlocators.csvSupport, "CSV Export"); cy.get(widgetsPage.ColumnAction).click({ force: true }); - // cy.readTabledata("1", "5").then(tabData => { - // const tabValue = tabData; - // expect(tabValue).to.be.equal("Action"); - // cy.log("the value is" + tabValue); - // }); - /* - cy.openPropertyPane("tablewidget"); - */ cy.get(widgetsPage.tableOnRowSelected) .get(commonlocators.dropdownSelectButton) .first() @@ -58,51 +46,129 @@ describe("Table Widget Functionality", function() { cy.log("the value is" + tabValue); }); }); - it("Table Widget Functionality To Verify The PageNo", function() { + + it("Table Widget Functionality To Search The Data", function() { + cy.isSelectRow(1); + cy.readTabledataPublish("1", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Lindsay Ferguson"); + cy.log("the value is" + tabValue); + cy.get(publish.searchInput) + .first() + .type(tabData); + cy.wait(500); + cy.readTabledataPublish("0", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Lindsay Ferguson"); + }); + cy.get(publish.downloadBtn).click(); + cy.wait(5000); + cy.get(publish.searchInput) + .first() + .clear() + .type("7434532"); + cy.wait(1000); + cy.readTabledataPublish("0", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Byron Fields"); + }); + }); + }); + + it("Table Widget Functionality To Filter The Data", function() { + cy.get(publish.searchInput) + .first() + .clear(); + cy.wait(1000); + cy.isSelectRow(1); + cy.readTabledataPublish("1", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Lindsay Ferguson"); + cy.log("the value is" + tabValue); + cy.get(publish.filterBtn).click(); + cy.get(publish.attributeDropdown).click(); + cy.get(publish.attributeValue) + .contains("userName") + .click(); + cy.get(publish.conditionDropdown).click(); + cy.get(publish.attributeValue) + .contains("is exactly") + .click(); + cy.get(publish.inputValue).type(tabValue); + cy.wait(500); + cy.get(publish.canvas) + .first() + .click(); + cy.readTabledataPublish("0", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Lindsay Ferguson"); + }); + cy.get(publish.filterBtn).click(); + cy.get(publish.removeFilter).click(); + cy.wait(500); + cy.readTabledataPublish("0", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Michael Lawson"); + }); + }); + }); + + it("Table Widget Functionality To Check Compact Mode", function() { + cy.isSelectRow(1); + cy.readTabledataPublish("1", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Lindsay Ferguson"); + cy.log("the value is" + tabValue); + cy.get(publish.compactMode).click(); + cy.get(publish.compactOpt) + .contains("Tall") + .click(); + cy.scrollTabledataPublish("3", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Byron Fields"); + }); + cy.get(publish.compactMode).click(); + cy.get(publish.compactOpt) + .contains("Short") + .click(); + cy.readTabledataPublish("4", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Ryan Holmes"); + }); + }); + }); + + it("Table Widget Functionality To Verify The Visiblity mode functionality", function() { cy.get(publish.backToEditor) .first() .click(); + cy.isSelectRow(1); + cy.readTabledataPublish("1", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Lindsay Ferguson"); + cy.log("the value is" + tabValue); + cy.get(publish.visibilityMode).click(); + cy.get(publish.visibilityOpt) + .contains("userName") + .click(); + cy.get(publish.containerWidget).click(); + cy.readTabledataPublish("1", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.not.equal("Lindsay Ferguson"); + }); + cy.get(publish.visibilityMode).click(); + cy.get(publish.visibilityOpt) + .contains("userName") + .click(); + cy.get(publish.containerWidget).click(); + cy.readTabledataPublish("1", "2").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("Lindsay Ferguson"); + }); + }); }); - // it("Table Widget Functionality To Verify The Extension Support", function() { - // cy.openPropertyPane("tablewidget"); - // cy.togglebar(commonlocators.pdfSupport); - // cy.PublishtheApp(); - // cy.get(publish.tableWidget + " " + "button").should( - // "contain", - // "PDF Export", - // ); - // cy.get(publish.backToEditor).click(); - // cy.openPropertyPane("tablewidget"); - // cy.togglebarDisable(commonlocators.pdfSupport); - // cy.togglebar(commonlocators.ExcelSupport); - // cy.PublishtheApp(); - // cy.get(publish.tableWidget + " " + "button").should( - // "not.contain", - // "PDF Export", - // ); - // cy.get(publish.tableWidget + " " + "button").should( - // "contain", - // "Excel Export", - // ); - // }); -}); -Cypress.on("test:after:run", attributes => { - /* eslint-disable no-console */ - console.log( - 'Test "%s" has finished in %dms', - attributes.title, - attributes.duration, - ); -}); -afterEach(() => { - // put your clean up code if any -}); -Cypress.on("test:after:run", attributes => { - /* eslint-disable no-console */ - console.log( - 'Test "%s" has finished in %dms', - attributes.title, - attributes.duration, - ); + afterEach(() => { + // put your clean up code if any + }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Text_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Text_spec.js index f933a44a04..8abbd02993 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Text_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Text_spec.js @@ -10,7 +10,6 @@ describe("Text Widget Functionality", function() { }); beforeEach(() => { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("textwidget"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js index 3b6a72834f..aa590e0618 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js @@ -10,7 +10,6 @@ describe("Dynamic input autocomplete", () => { cy.addDsl(dsl); }); it("opens autocomplete for bindings", () => { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("buttonwidget"); cy.get(dynamicInputLocators.input) .first() @@ -57,14 +56,11 @@ describe("Dynamic input autocomplete", () => { }); it("opens current value popup", () => { // Test on widgets pane - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("buttonwidget"); cy.get(dynamicInputLocators.input) .first() .focus(); cy.assertEvaluatedValuePopup("string"); - - cy.NavigateToEntityExplorer(); // Test on api pane cy.NavigateToAPI_Panel(); cy.get(apiwidget.createapi).click({ force: true }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_API_Pane_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_API_Pane_spec.js index 3a17de7855..8e545ee661 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_API_Pane_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_API_Pane_spec.js @@ -35,6 +35,9 @@ describe("Entity explorer API pane related testcases", function() { expect($lis.eq(1)).to.contain("{{FirstAPI.data}}"); expect($lis.eq(2)).to.contain("{{FirstAPI.run()}}"); }); + cy.get(apiwidget.actionlist) + .contains(testdata.Get) + .should("be.visible"); cy.Createpage(pageid); cy.GlobalSearchEntity("FirstAPI"); cy.EditApiNameFromExplorer("SecondAPI"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js new file mode 100644 index 0000000000..1e020b7499 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_CopyQuery_RenameDatasource_spec.js @@ -0,0 +1,101 @@ +const queryLocators = require("../../../locators/QueryEditor.json"); +const datasource = require("../../../locators/DatasourcesEditor.json"); +const apiwidget = require("../../../locators/apiWidgetslocator.json"); +const commonlocators = require("../../../locators/commonlocators.json"); + +const pageid = "MyPage"; +let updatedName; +let datasourceName; + +describe("Entity explorer tests related to copy query", function() { + it("Create a query with dataSource in explorer", function() { + cy.NavigateToDatasourceEditor(); + cy.get(datasource.PostgreSQL).click(); + + cy.getPluginFormsAndCreateDatasource(); + + cy.fillPostgresDatasourceForm(); + + cy.testSaveDatasource(); + + cy.NavigateToQueryEditor(); + + cy.get("@createDatasource").then(httpResponse => { + datasourceName = httpResponse.response.body.data.name; + + cy.get(".t--datasource-name") + .contains(datasourceName) + .click(); + }); + + cy.get("@getPluginForm").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + + cy.get(queryLocators.templateMenu).click(); + cy.get(".CodeMirror textarea") + .first() + .focus() + .type("select * from users"); + + cy.EvaluateCurrentValue("select * from users"); + + cy.get("@createDatasource").then(httpResponse => { + datasourceName = httpResponse.response.body.data.name; + + cy.get(apiwidget.propertyList).then(function($lis) { + expect($lis).to.have.length(3); + expect($lis.eq(0)).to.contain("{{Query1.isLoading}}"); + expect($lis.eq(1)).to.contain("{{Query1.data}}"); + expect($lis.eq(2)).to.contain("{{Query1.run()}}"); + }); + }); + }); + + it("Create a page and copy query in explorer", function() { + cy.Createpage(pageid); + cy.GlobalSearchEntity("Query1"); + cy.xpath(apiwidget.popover) + .last() + .should("be.hidden") + .invoke("show") + .click({ force: true }); + cy.copyEntityToPage(pageid); + cy.SearchEntityandOpen("Query1Copy"); + cy.runQuery(); + cy.get(apiwidget.propertyList).then(function($lis) { + expect($lis).to.have.length(3); + expect($lis.eq(0)).to.contain("{{Query1Copy.isLoading}}"); + expect($lis.eq(1)).to.contain("{{Query1Copy.data}}"); + expect($lis.eq(2)).to.contain("{{Query1Copy.run()}}"); + }); + }); + + it("Delete query and rename datasource in explorer", function() { + cy.deleteQuery(); + cy.get(commonlocators.entityExplorersearch).clear(); + cy.NavigateToDatasourceEditor(); + cy.get(datasource.PostgresEntity).click(); + cy.GlobalSearchEntity(`${datasourceName}`); + cy.get(`.t--entity-name:contains(${datasourceName})`).click(); + cy.generateUUID().then(uid => { + updatedName = uid; + cy.log("complete uid :" + updatedName); + updatedName = uid.replace(/-/g, "_").slice(1, 15); + cy.log("sliced id :" + updatedName); + cy.EditEntityNameByDoubleClick(datasourceName, updatedName); + cy.SearchEntityandOpen(updatedName); + cy.testSaveDatasource(); + cy.hoverAndClick(); + cy.get(apiwidget.delete).click({ force: true }); + //This is check to make sure if a datasource is active 409 + cy.wait("@deleteDatasource").should( + "have.nested.property", + "response.body.responseMeta.status", + 409, + ); + }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Multiple_Widgets_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Multiple_Widgets_spec.js new file mode 100644 index 0000000000..1f06908698 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Multiple_Widgets_spec.js @@ -0,0 +1,66 @@ +const tdsl = require("../../../fixtures/tableWidgetDsl.json"); +const commonlocators = require("../../../locators/commonlocators.json"); +const dsl = require("../../../fixtures/displayWidgetDsl.json"); +const widgetsPage = require("../../../locators/Widgets.json"); +const testdata = require("../../../fixtures/testdata.json"); +const pages = require("../../../locators/Pages.json"); +const apiwidget = require("../../../locators/apiWidgetslocator.json"); +const explorer = require("../../../locators/explorerlocators.json"); +const pageid = "MyPage"; + +describe("Entity explorer tests related to widgets and validation", function() { + it("Add a widget to default page and verify the properties", function() { + cy.addDsl(dsl); + cy.SearchEntityandOpen("Text1"); + cy.get(explorer.collapse) + .last() + .click({ force: true }); + cy.get(explorer.property) + .last() + .click({ force: true }); + cy.get(apiwidget.propertyList).then(function($lis) { + expect($lis).to.have.length(2); + expect($lis.eq(0)).to.contain("{{Text1.isVisible}}"); + expect($lis.eq(1)).to.contain("{{Text1.text}}"); + }); + }); + + it("Create another page and add another widget and verify properties", function() { + cy.Createpage(pageid); + cy.addDsl(tdsl); + cy.openPropertyPane("tablewidget"); + cy.widgetText("Table1", widgetsPage.tableWidget, commonlocators.tableInner); + cy.GlobalSearchEntity("Table1"); + cy.get(explorer.collapse) + .last() + .click({ force: true }); + cy.get(explorer.property) + .last() + .click({ force: true }); + cy.get(apiwidget.propertyList).then(function($lis) { + expect($lis).to.have.length(7); + expect($lis.eq(0)).to.contain("{{Table1.selectedRow}}"); + expect($lis.eq(1)).to.contain("{{Table1.selectedRowIndex}}"); + expect($lis.eq(2)).to.contain("{{Table1.tableData}}"); + expect($lis.eq(3)).to.contain("{{Table1.pageNo}}"); + expect($lis.eq(4)).to.contain("{{Table1.pageSize}}"); + expect($lis.eq(5)).to.contain("{{Table1.isVisible}}"); + expect($lis.eq(6)).to.contain("{{Table1.searchText}}"); + }); + }); + + it("Toggle between widgets in different pages using search functionality", function() { + cy.SearchEntityandOpen("Text1"); + cy.get(explorer.collapse) + .last() + .click({ force: true }); + cy.get(explorer.property) + .last() + .click({ force: true }); + cy.get(apiwidget.propertyList).then(function($lis) { + expect($lis).to.have.length(2); + expect($lis.eq(0)).to.contain("{{Text1.isVisible}}"); + expect($lis.eq(1)).to.contain("{{Text1.text}}"); + }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_spec.js index 2dd1ba83e5..71dd4c178e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Widgets_spec.js @@ -8,7 +8,6 @@ describe("Entity explorer tests related to widgets and validation", function() { }); it("Widget edit/delete/copy to clipboard validation", function() { - cy.NavigateToEntityExplorer(); cy.SearchEntityandOpen("Text1"); cy.get(explorer.collapse) .last() diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js index 341bbd6cde..0586b1ec96 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Button_spec.js @@ -12,7 +12,6 @@ describe("Button Widget Functionality", function() { }); beforeEach(() => { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("buttonwidget"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js index 1b90e5a8ca..62ac7139cf 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js @@ -10,7 +10,6 @@ describe("Checkbox Widget Functionality", function() { cy.addDsl(dsl); }); it("Checkbox Widget Functionality", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); /** * @param{Text} Random Text @@ -45,7 +44,6 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Check Disabled Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); cy.togglebar(commonlocators.Disablejs + " " + "input"); cy.PublishtheApp(); @@ -53,7 +51,6 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Check Enabled Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); cy.togglebarDisable(commonlocators.Disablejs + " " + "input"); cy.PublishtheApp(); @@ -61,7 +58,6 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Unchecked Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -69,7 +65,6 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Checkbox Functionality To Check Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("checkboxwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js index 8226d22581..620f79dfbc 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js @@ -10,7 +10,6 @@ describe("DatePicker Widget Functionality", function() { }); beforeEach(() => { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("datepickerwidget"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js index 8ce22e97e3..279bed8d5c 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js @@ -9,7 +9,6 @@ describe("Dropdown Widget Functionality", function() { cy.addDsl(dsl); }); it("Dropdown Widget Functionality", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("dropdownwidget"); /** * @param{Text} Random Text @@ -48,7 +47,6 @@ describe("Dropdown Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Dropdown Functionality To Unchecked Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("dropdownwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -56,7 +54,6 @@ describe("Dropdown Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Dropdown Functionality To Check Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("dropdownwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js index a4c836c789..d52e69e302 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js @@ -7,7 +7,6 @@ describe("FilePicker Widget Functionality", function() { cy.addDsl(dsl); }); it("FilePicker Widget Functionality", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("filepickerwidget"); //Checking the edit props for FilePicker and also the properties of FilePicker widget diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js index 404e09e62b..92d6ef913e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js @@ -9,7 +9,6 @@ describe("Form Widget Functionality", function() { cy.addDsl(dsl); }); it("Form Widget Functionality", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("formwidget"); /** * @param{Text} Random Text @@ -45,7 +44,6 @@ describe("Form Widget Functionality", function() { }); it("Form Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("formwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js index c5bb1ca99f..9efd1ddfeb 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Input_spec.js @@ -9,7 +9,6 @@ describe("Input Widget Functionality", function() { cy.addDsl(dsl); }); it("Input Widget Functionality", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); /** * @param{Text} Random Text @@ -65,7 +64,6 @@ describe("Input Widget Functionality", function() { cy.get(publish.backToEditor).click({ force: true }); }); it("Input Widget Functionality To Check Disabled Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); cy.togglebar(commonlocators.Disablejs + " " + "input"); cy.PublishtheApp(); @@ -73,7 +71,6 @@ describe("Input Widget Functionality", function() { cy.get(publish.backToEditor).click({ force: true }); }); it("Input Widget Functionality To Check Enabled Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); cy.togglebarDisable(commonlocators.Disablejs + " " + "input"); cy.PublishtheApp(); @@ -81,7 +78,6 @@ describe("Input Widget Functionality", function() { cy.get(publish.backToEditor).click({ force: true }); }); it("Input Functionality To Unchecked Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -89,7 +85,6 @@ describe("Input Widget Functionality", function() { cy.get(publish.backToEditor).click({ force: true }); }); it("Input Functionality To Check Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("inputwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js index f6b0cde75a..5d5d0a4d31 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js @@ -9,7 +9,6 @@ describe("Radio Widget Functionality", function() { cy.addDsl(dsl); }); it("Radio Widget Functionality", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("radiogroupwidget"); /** * @param{Text} Random Text @@ -59,7 +58,6 @@ describe("Radio Widget Functionality", function() { }); it("Radio Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("radiogroupwidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -67,7 +65,6 @@ describe("Radio Widget Functionality", function() { cy.get(publish.backToEditor).click(); }); it("Radio Functionality To Check Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("radiogroupwidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js index 8dc4c98a98..46aa8617b8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js @@ -10,7 +10,6 @@ describe("RichTextEditor Widget Functionality", function() { }); beforeEach(() => { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("richtexteditorwidget"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js index 7ee578507e..38e9147674 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Tab_spec.js @@ -10,7 +10,6 @@ describe("Tab widget test", function() { cy.addDsl(dsl); }); it("Tab Widget Functionality Test", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tabswidget"); /** * @param{Text} Random Text @@ -60,7 +59,6 @@ describe("Tab widget test", function() { }); it("Tab Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tabswidget"); cy.togglebarDisable(commonlocators.visibleCheckbox); cy.PublishtheApp(); @@ -68,7 +66,6 @@ describe("Tab widget test", function() { cy.get(publish.backToEditor).click(); }); it("Tab Widget Functionality To Check Visible Widget", function() { - cy.get(pages.widgetsEditor).click(); cy.openPropertyPane("tabswidget"); cy.togglebar(commonlocators.visibleCheckbox); cy.PublishtheApp(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js index dc90234757..1f8c678876 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js @@ -98,11 +98,11 @@ describe("Create new org and share with a user", function() { homePage.viewerRole, ); cy.navigateToOrgSettings(orgid); - cy.get(homePage.emailList).then(function($lis) { - expect($lis).to.have.length(3); - expect($lis.eq(0)).to.contain(Cypress.env("USERNAME")); - expect($lis.eq(1)).to.contain(Cypress.env("TESTUSERNAME1")); - expect($lis.eq(2)).to.contain(Cypress.env("TESTUSERNAME2")); + cy.get(homePage.emailList).then(function($list) { + expect($list).to.have.length(3); + expect($list.eq(0)).to.contain(Cypress.env("USERNAME")); + expect($list.eq(1)).to.contain(Cypress.env("TESTUSERNAME1")); + expect($list.eq(2)).to.contain(Cypress.env("TESTUSERNAME2")); }); }); @@ -117,14 +117,15 @@ describe("Create new org and share with a user", function() { cy.get(homePage.searchInput).type(appid); cy.wait(2000); cy.navigateToOrgSettings(orgid); - cy.get(homePage.emailList).then(function($lis) { - expect($lis).to.have.length(3); - expect($lis.eq(0)).to.contain(Cypress.env("USERNAME")); - expect($lis.eq(1)).to.contain(Cypress.env("TESTUSERNAME1")); - expect($lis.eq(2)).to.contain(Cypress.env("TESTUSERNAME2")); + cy.get(homePage.emailList).then(function($list) { + expect($list).to.have.length(3); + expect($list.eq(0)).to.contain(Cypress.env("USERNAME")); + expect($list.eq(1)).to.contain(Cypress.env("TESTUSERNAME1")); + expect($list.eq(2)).to.contain(Cypress.env("TESTUSERNAME2")); }); cy.xpath(homePage.appHome) .should("be.visible") + .first() .click(); cy.wait("@applications").should( "have.nested.property", diff --git a/app/client/cypress/locators/HomePage.json b/app/client/cypress/locators/HomePage.json index 2df73d42c9..d32dcdd55a 100644 --- a/app/client/cypress/locators/HomePage.json +++ b/app/client/cypress/locators/HomePage.json @@ -1,15 +1,15 @@ { - "CreateApp":"span[class='bp3-button-text']", + "CreateApp": "span[class='bp3-button-text']", "searchInput": "input[type='text']", "appEditIcon": ".t--application-edit-link", - "publishButton":".t--application-publish-btn", - "shareButton":".t--application-share-btn", - "publishCrossButton":"span[icon='small-cross']", - "homePageID":"//div[@id='root']", - "appMoreIcon":".bp3-popover-wrapper.more .bp3-popover-target", - "deleteButton":".bp3-menu-item.bp3-popover-dismiss", - "selectAction":"#Base", - "deleteApp":".bp3-menu-item", + "publishButton": ".t--application-publish-btn", + "shareButton": ".t--application-share-btn", + "publishCrossButton": "span[icon='small-cross']", + "homePageID": "//div[@id='root']", + "appMoreIcon": ".bp3-popover-wrapper.more .bp3-popover-target", + "deleteButton": ".bp3-menu-item.bp3-popover-dismiss", + "selectAction": "#Base", + "deleteApp": ".bp3-menu-item", "homeIcon": ".t--appsmith-logo", "inputAppName": "input[name=applicationName]", "createNew": ".createnew", @@ -20,7 +20,9 @@ "members": "//div[contains(text(),'Members')]", "share": "//div[contains(text(),'Share')]", "OrgSettings": "//div[contains(text(),'Organization Settings')]", + "MemberSettings": "//div[contains(text(),'Members')]", "inviteUser": "//span[text()='Invite Users']/parent::button", + "inviteUserMembersPage": "[data-cy=t--invite-users]", "email": "//input[@type='email']", "selectRole": "//span[text()='Select a role']", "adminRole": "//div[@class='bp3-overlay bp3-overlay-open']//div[contains(text(),'Administrator')]", @@ -28,7 +30,7 @@ "developerRole": "//div[@class='bp3-overlay bp3-overlay-open']//div[contains(text(),'Developer')]", "inviteBtn": "//span[text()='Invite']/parent::button", "manageUsers": ".manageUsers", - "DeleteBtn": "//div[@data-colindex='3']//*[local-name()='svg']", + "DeleteBtn": "[data-cy=t--deleteUser]", "ShareBtn": "//span[text()='Share']/parent::button", "launchBtn": "//span[text()='Launch']/parent::button", "appView": ".t--application-view-link", @@ -40,4 +42,4 @@ "shareOrg": ") .bp3-button-text:contains('Share')", "orgSection": ".bp3-button-text:contains(", "createAppFrOrg": ") .t--create-app-popup" -} +} \ No newline at end of file diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index 501bbce0f7..8db7aec1f7 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -46,5 +46,6 @@ "deleteAPI": ".t--apiFormDeleteBtn", "editName": ".single-select >div:contains('Edit Name')", "page": ".single-select >div", - "propertyList": ".t--entity-property" + "propertyList": ".t--entity-property", + "actionlist": ".action div div" } diff --git a/app/client/cypress/locators/explorerlocators.json b/app/client/cypress/locators/explorerlocators.json index d082b10063..3a74403c37 100644 --- a/app/client/cypress/locators/explorerlocators.json +++ b/app/client/cypress/locators/explorerlocators.json @@ -14,5 +14,7 @@ "entityQuery": ".t--entity-name:contains('Queries')", "collapse": ".t--entity-collapse-toggle", "property": ".language-appsmith-binding", - "editNameField": ".editing input" + "editNameField": ".editing input", + "editEntityField": ".bp3-editable-text-input", + "entity":".t--entity-name" } \ No newline at end of file diff --git a/app/client/cypress/locators/publishWidgetspage.json b/app/client/cypress/locators/publishWidgetspage.json index a5e6f071c4..2383588e7e 100644 --- a/app/client/cypress/locators/publishWidgetspage.json +++ b/app/client/cypress/locators/publishWidgetspage.json @@ -20,6 +20,19 @@ "mapSearch": ".t--widget-mapwidget input", "pickMyLocation": ".t--widget-mapwidget div[title='Pick My Location']", "rectChart":".t--widget-chartwidget g rect", - "chartLab":".t--widget-chartwidget g:nth-child(5) text" - + "chartLab":".t--widget-chartwidget g:nth-child(5) text", + "searchInput": "input", + "downloadBtn": ".t--table-download-btn", + "filterBtn": ".t--table-filter-toggle-btn", + "attributeDropdown": ".t--table-filter-columns-dropdown", + "attributeValue": ".t--dropdown-option", + "conditionDropdown": ".t--table-filter-conditions-dropdown", + "inputValue": ".t--table-filter-value-input", + "canvas": ".canvas", + "removeFilter": ".t--table-filter-remove-btn", + "compactMode": ".t--table-compact-mode-toggle-btn", + "compactOpt": ".t--table-compact-mode-option", + "visibilityMode": ".t--table-column-visibility-toggle-btn", + "visibilityOpt": ".option-title", + "containerWidget": ".t--widget-containerwidget" } \ No newline at end of file diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 9b4a491d2c..81199bcc96 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -39,13 +39,14 @@ Cypress.Commands.add("navigateToOrgSettings", orgName => { .scrollIntoView() .should("be.visible"); cy.get(".t--org-name").click({ force: true }); - cy.xpath(homePage.OrgSettings).click({ force: true }); + cy.xpath(homePage.MemberSettings).click({ force: true }); + cy.wait("@getOrganisation"); cy.wait("@getRoles").should( "have.nested.property", "response.body.responseMeta.status", 200, ); - cy.xpath(homePage.inviteUser).should("be.visible"); + cy.get(homePage.inviteUserMembersPage).should("be.visible"); }); Cypress.Commands.add("inviteUserForOrg", (orgName, email, role) => { @@ -70,6 +71,7 @@ Cypress.Commands.add("inviteUserForOrg", (orgName, email, role) => { cy.contains(email); cy.get(homePage.manageUsers).click({ force: true }); cy.xpath(homePage.appHome) + .first() .should("be.visible") .click(); }); @@ -79,14 +81,16 @@ Cypress.Commands.add("deleteUserFromOrg", (orgName, email) => { .scrollIntoView() .should("be.visible"); cy.get(".t--org-name").click({ force: true }); - cy.xpath(homePage.OrgSettings).click({ force: true }); + cy.xpath(homePage.MemberSettings).click({ force: true }); + cy.wait("@getOrganisation"); cy.wait("@getRoles").should( "have.nested.property", "response.body.responseMeta.status", 200, ); - cy.xpath(homePage.DeleteBtn).click({ force: true }); + cy.get(homePage.DeleteBtn).click({ force: true }); cy.xpath(homePage.appHome) + .first() .should("be.visible") .click(); cy.wait("@applications").should( @@ -101,13 +105,13 @@ Cypress.Commands.add("updateUserRoleForOrg", (orgName, email, role) => { .scrollIntoView() .should("be.visible"); cy.get(".t--org-name").click({ force: true }); - cy.xpath(homePage.OrgSettings).click({ force: true }); + cy.xpath(homePage.MemberSettings).click({ force: true }); cy.wait("@getRoles").should( "have.nested.property", "response.body.responseMeta.status", 200, ); - cy.xpath(homePage.inviteUser).click({ force: true }); + cy.get(homePage.inviteUserMembersPage).click({ force: true }); cy.xpath(homePage.email) .click({ force: true }) .type(email); @@ -122,6 +126,7 @@ Cypress.Commands.add("updateUserRoleForOrg", (orgName, email, role) => { cy.contains(email); cy.get(".bp3-icon-small-cross").click({ force: true }); cy.xpath(homePage.appHome) + .first() .should("be.visible") .click(); cy.wait("@applications").should( @@ -237,7 +242,9 @@ Cypress.Commands.add("DeleteApp", appName => { cy.get(homePage.appMoreIcon) .first() .click({ force: true }); - cy.get(homePage.deleteButton).click({ force: true }); + cy.get(homePage.deleteButton) + .contains("Delete") + .click({ force: true }); }); Cypress.Commands.add("DeletepageFromSideBar", () => { @@ -381,10 +388,29 @@ Cypress.Commands.add("EditApiNameFromExplorer", apiname => { cy.get(explorer.editNameField) .clear() .type(apiname, { force: true }) - .should("have.value", apiname); + .should("have.value", apiname) + .blur(); cy.wait(3000); }); +Cypress.Commands.add( + "EditEntityNameByDoubleClick", + (entityName, updatedName) => { + cy.get(explorer.entity) + .contains(entityName) + .dblclick({ force: true }); + cy.log(updatedName); + cy.get(explorer.editEntityField) + .clear() + .type(updatedName + "{enter}", { force: true }); + cy.wait("@saveDatasource").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + }, +); + Cypress.Commands.add("WaitAutoSave", () => { // wait for save query to trigger cy.wait(2000); @@ -635,6 +661,21 @@ Cypress.Commands.add("MoveAPIToPage", pageName => { ); }); +Cypress.Commands.add("copyEntityToPage", pageName => { + cy.xpath(apiwidget.popover) + .last() + .click({ force: true }); + cy.get(apiwidget.copyTo).click({ force: true }); + cy.get(apiwidget.page) + .contains(pageName) + .click(); + cy.wait("@createNewApi").should( + "have.nested.property", + "response.body.responseMeta.status", + 201, + ); +}); + Cypress.Commands.add("CopyAPIToHome", () => { cy.xpath(apiwidget.popover) .last() @@ -1200,14 +1241,16 @@ Cypress.Commands.add("NavigateToQueryEditor", () => { cy.xpath(queryEditor.addQueryEntity).click({ force: true }); }); -Cypress.Commands.add("testSaveDatasource", () => { +Cypress.Commands.add("testDatasource", () => { cy.get(".t--test-datasource").click(); cy.wait("@testDatasource").should( "have.nested.property", "response.body.data.success", true, ); +}); +Cypress.Commands.add("saveDatasource", () => { cy.get(".t--save-datasource").click(); cy.wait("@saveDatasource").should( "have.nested.property", @@ -1216,6 +1259,11 @@ Cypress.Commands.add("testSaveDatasource", () => { ); }); +Cypress.Commands.add("testSaveDatasource", () => { + cy.testDatasource(); + cy.saveDatasource(); +}); + Cypress.Commands.add("fillMongoDatasourceForm", () => { cy.get(datasourceEditor["host"]).type(datasourceFormData["mongo-host"]); cy.get(datasourceEditor["port"]).type(datasourceFormData["mongo-port"]); @@ -1444,6 +1492,8 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("DELETE", "/api/v1/datasources/*").as("deleteDatasource"); cy.route("GET", "/api/v1/organizations").as("organizations"); + cy.route("GET", "/api/v1/organizations/*").as("getOrganisation"); + cy.route("POST", "/api/v1/actions/execute").as("executeAction"); cy.route("POST", "/api/v1/applications/publish/*").as("publishApp"); cy.route("PUT", "/api/v1/layouts/*/pages/*").as("updateLayout"); @@ -1502,6 +1552,15 @@ Cypress.Commands.add("readTabledataPublish", (rowNum, colNum) => { return tabVal; }); +Cypress.Commands.add("scrollTabledataPublish", (rowNum, colNum) => { + const selector = `.t--widget-tablewidget .tbody .td[data-rowindex=${rowNum}][data-colindex=${colNum}] div`; + const tabVal = cy + .get(selector) + .scrollIntoView() + .invoke("text"); + return tabVal; +}); + Cypress.Commands.add("assertEvaluatedValuePopup", expectedType => { cy.get(dynamicInputLocators.evaluatedValue) .should("be.visible") @@ -1544,8 +1603,6 @@ Cypress.Commands.add("ValidatePublishTableData", value => { }); Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { - cy.NavigateToEntityExplorer(); - cy.NavigateToApiEditor(); cy.SearchEntityandOpen("Api2"); cy.NavigateToPaginationTab(); cy.RunAPI(); @@ -1563,7 +1620,7 @@ Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { const respBody = tabData.match(/"(.*)"/)[0]; localStorage.setItem("respBody", respBody); cy.log(respBody); - cy.get(pages.widgetsEditor).click({ force: true }); + cy.SearchEntityandOpen("Table1"); // cy.openPropertyPane("tablewidget"); // cy.testJsontext("tabledata", "{{Api2.data.results}}"); cy.isSelectRow(0); diff --git a/app/client/docker/templates/nginx-mac.conf.template b/app/client/docker/templates/nginx-mac.conf.template index aa348e5cbd..5ebbe8481e 100644 --- a/app/client/docker/templates/nginx-mac.conf.template +++ b/app/client/docker/templates/nginx-mac.conf.template @@ -86,7 +86,7 @@ server { location / { proxy_pass http://host.docker.internal:3000; sub_filter __APPSMITH_SENTRY_DSN__ '${APPSMITH_SENTRY_DSN}'; - sub_filter __APPSMITH_SMART_LOOK_ID__ '${APPSMITH_SMART_LOOK_ID}; + sub_filter __APPSMITH_SMART_LOOK_ID__ '${APPSMITH_SMART_LOOK_ID}'; sub_filter __APPSMITH_OAUTH2_GOOGLE_CLIENT_ID__ '${APPSMITH_OAUTH2_GOOGLE_CLIENT_ID}'; sub_filter __APPSMITH_OAUTH2_GITHUB_CLIENT_ID__ '${APPSMITH_OAUTH2_GITHUB_CLIENT_ID}'; sub_filter __APPSMITH_MARKETPLACE_ENABLED__ '${APPSMITH_MARKETPLACE_ENABLED}'; diff --git a/app/client/package.json b/app/client/package.json index 856a8d8823..54319e0891 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -16,8 +16,8 @@ "@craco/craco": "^5.6.1", "@manaflair/redux-batch": "^1.0.0", "@optimizely/optimizely-sdk": "^4.0.0", - "@sentry/react": "^5.22.0", - "@sentry/tracing": "^5.22.0", + "@sentry/react": "^5.22.2", + "@sentry/tracing": "^5.22.2", "@sentry/webpack-plugin": "^1.12.1", "@types/chance": "^1.0.7", "@types/lodash": "^4.14.120", diff --git a/app/client/src/actions/applicationActions.ts b/app/client/src/actions/applicationActions.ts index 3ab9eb39ec..41ecde5b57 100644 --- a/app/client/src/actions/applicationActions.ts +++ b/app/client/src/actions/applicationActions.ts @@ -36,3 +36,12 @@ export const publishApplication = (applicationId: string) => { }, }; }; + +export const duplicateApplication = (applicationId: string) => { + return { + type: ReduxActionTypes.DUPLICATE_APPLICATION_INIT, + payload: { + applicationId, + }, + }; +}; diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index 0ea4e64793..50ab33bdb4 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -15,13 +15,10 @@ export const createDatasourceFromForm = (payload: CreateDatasourceConfig) => { }; }; -export const updateDatasource = ( - payload: Datasource, - reinitializeForm?: boolean, -) => { +export const updateDatasource = (payload: Datasource) => { return { type: ReduxActionTypes.UPDATE_DATASOURCE_INIT, - payload: { datasource: payload, reinitializeForm: !!reinitializeForm }, + payload, }; }; diff --git a/app/client/src/actions/orgActions.ts b/app/client/src/actions/orgActions.ts new file mode 100644 index 0000000000..e18d723cbe --- /dev/null +++ b/app/client/src/actions/orgActions.ts @@ -0,0 +1,68 @@ +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { SaveOrgRequest } from "api/OrgApi"; + +export const fetchOrg = (orgId: string) => { + return { + type: ReduxActionTypes.FETCH_CURRENT_ORG, + payload: { + orgId, + }, + }; +}; + +export const changeOrgName = (name: string) => { + return { + type: ReduxActionTypes.UPDATE_ORG_NAME_INIT, + payload: { + name, + }, + }; +}; + +export const changeOrgUserRole = ( + orgId: string, + role: string, + username: string, +) => { + return { + type: ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT, + payload: { + orgId, + role, + username, + }, + }; +}; + +export const deleteOrgUser = (orgId: string, username: string) => { + return { + type: ReduxActionTypes.DELETE_ORG_USER_INIT, + payload: { + orgId, + username, + }, + }; +}; +export const fetchUsersForOrg = (orgId: string) => { + return { + type: ReduxActionTypes.FETCH_ALL_USERS_INIT, + payload: { + orgId, + }, + }; +}; +export const fetchRolesForOrg = (orgId: string) => { + return { + type: ReduxActionTypes.FETCH_ALL_ROLES_INIT, + payload: { + orgId, + }, + }; +}; + +export const saveOrg = (orgSettings: SaveOrgRequest) => { + return { + type: ReduxActionTypes.SAVE_ORG_INIT, + payload: orgSettings, + }; +}; diff --git a/app/client/src/api/ApplicationApi.tsx b/app/client/src/api/ApplicationApi.tsx index 205fe34e3c..86bfea6bcf 100644 --- a/app/client/src/api/ApplicationApi.tsx +++ b/app/client/src/api/ApplicationApi.tsx @@ -55,6 +55,10 @@ export interface DeleteApplicationRequest { applicationId: string; } +export interface DuplicateApplicationRequest { + applicationId: string; +} + export interface GetAllApplicationResponse extends ApiResponse { data: Array; } @@ -144,6 +148,12 @@ class ApplicationApi extends Api { ): AxiosPromise { return Api.delete(ApplicationApi.baseURL + request.applicationId); } + + static duplicateApplication( + request: DuplicateApplicationRequest, + ): AxiosPromise { + return Api.post(ApplicationApi.baseURL + "clone/" + request.applicationId); + } } export default ApplicationApi; diff --git a/app/client/src/api/OrgApi.ts b/app/client/src/api/OrgApi.ts index 8c3f4791c4..8ea68790e5 100644 --- a/app/client/src/api/OrgApi.ts +++ b/app/client/src/api/OrgApi.ts @@ -47,8 +47,9 @@ export interface FetchAllRolesRequest { export interface SaveOrgRequest { id: string; - name: string; - website: string; + name?: string; + website?: string; + email?: string; } export interface CreateOrgRequest { diff --git a/app/client/src/assets/icons/ads/create-new.svg b/app/client/src/assets/icons/ads/create-new.svg new file mode 100644 index 0000000000..5954b566af --- /dev/null +++ b/app/client/src/assets/icons/ads/create-new.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/ads/invite-users.svg b/app/client/src/assets/icons/ads/invite-users.svg new file mode 100644 index 0000000000..4b0b6cd9a0 --- /dev/null +++ b/app/client/src/assets/icons/ads/invite-users.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/launch.svg b/app/client/src/assets/icons/ads/launch.svg new file mode 100644 index 0000000000..83d1bd28fb --- /dev/null +++ b/app/client/src/assets/icons/ads/launch.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/share.svg b/app/client/src/assets/icons/ads/share.svg new file mode 100644 index 0000000000..8db07e7a08 --- /dev/null +++ b/app/client/src/assets/icons/ads/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/upper_arrow.svg b/app/client/src/assets/icons/ads/upper_arrow.svg new file mode 100644 index 0000000000..022072bf4a --- /dev/null +++ b/app/client/src/assets/icons/ads/upper_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/view-all.svg b/app/client/src/assets/icons/ads/view-all.svg new file mode 100644 index 0000000000..0689c29ced --- /dev/null +++ b/app/client/src/assets/icons/ads/view-all.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/ads/workspace.svg b/app/client/src/assets/icons/ads/workspace.svg new file mode 100644 index 0000000000..0af4818d88 --- /dev/null +++ b/app/client/src/assets/icons/ads/workspace.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/client/src/components/ads/AppIcon.tsx b/app/client/src/components/ads/AppIcon.tsx index fe8fd23ce5..d3bee95975 100644 --- a/app/client/src/components/ads/AppIcon.tsx +++ b/app/client/src/components/ads/AppIcon.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { ReactComponent as BagIcon } from "assets/icons/ads/bag.svg"; import { ReactComponent as ProductIcon } from "assets/icons/ads/product.svg"; import { ReactComponent as BookIcon } from "assets/icons/ads/book.svg"; @@ -15,101 +15,127 @@ import { ReactComponent as FlightIcon } from "assets/icons/ads/flight.svg"; import styled from "styled-components"; import { Size } from "./Button"; -export enum AppIconName { - BAG = "bag", - PRODUCT = "product", - BOOK = "book", - CAMERA = "camera", - FILE = "file", - CHAT = "chat", - CALENDER = "calender", - FLIGHT = "flight", - FRAME = "frame", - GLOBE = "globe", - SHOPPER = "shopper", - HEART = "heart", -} +export const AppIconCollection = [ + "bag", + "product", + "book", + "camera", + "file", + "chat", + "calender", + "flight", + "frame", + "globe", + "shopper", + "heart", +] as const; -export const sizeHandler = (size: Size) => { - let iconSize = 0; - switch (size) { - case Size.small: - iconSize = 20; - break; - case Size.medium: - iconSize = 30; - break; - case Size.large: - iconSize = 54; - break; - } - return iconSize; +export type AppIconName = typeof AppIconCollection[number]; + +type cssAttributes = { + width: number; + height: number; + padding: number; }; -const IconWrapper = styled.div` +const appSizeHandler = (size: Size): cssAttributes => { + let width, height, padding; + switch (size) { + case Size.small: + width = 20; + height = 20; + padding = 5; + break; + case Size.medium: + width = 32; + height = 32; + padding = 20; + break; + case Size.large: + width = 50; + height = 50; + padding = 50; + break; + } + return { width, height, padding }; +}; + +const IconWrapper = styled.div` cursor: pointer; &:focus { outline: none; } display: flex; + width: ${props => props.styledProps.width + 2 * props.styledProps.padding}px; + height: ${props => + props.styledProps.height + 2 * props.styledProps.padding}px; svg { - width: ${props => sizeHandler(props.size)}px; - height: ${props => sizeHandler(props.size)}px; + width: ${props => props.styledProps.width}px; + height: ${props => props.styledProps.height}px; path { fill: ${props => props.theme.colors.blackShades[9]}; } } + padding: ${props => props.styledProps.padding}px; + background-color: ${props => props.color}; `; export type AppIconProps = { size: Size; + color: string; name: AppIconName; }; const AppIcon = (props: AppIconProps) => { + const styledProps = useMemo(() => appSizeHandler(props.size), [props]); + let returnIcon; switch (props.name) { - case AppIconName.BAG: + case "bag": returnIcon = ; break; - case AppIconName.PRODUCT: + case "product": returnIcon = ; break; - case AppIconName.BOOK: + case "book": returnIcon = ; break; - case AppIconName.CAMERA: + case "camera": returnIcon = ; break; - case AppIconName.FILE: + case "file": returnIcon = ; break; - case AppIconName.CHAT: + case "chat": returnIcon = ; break; - case AppIconName.CALENDER: + case "calender": returnIcon = ; break; - case AppIconName.FRAME: + case "frame": returnIcon = ; break; - case AppIconName.GLOBE: + case "globe": returnIcon = ; break; - case AppIconName.SHOPPER: + case "shopper": returnIcon = ; break; - case AppIconName.HEART: + case "heart": returnIcon = ; break; - case AppIconName.FLIGHT: + case "flight": returnIcon = ; break; default: returnIcon = null; break; } - return returnIcon ? {returnIcon} : null; + return returnIcon ? ( + + {returnIcon} + + ) : null; }; export default AppIcon; diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx index 715ff763b2..2de8b9402b 100644 --- a/app/client/src/components/ads/Button.tsx +++ b/app/client/src/components/ads/Button.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { CommonComponentProps, hexToRgba, ThemeProp } from "./common"; +import { CommonComponentProps, hexToRgba, ThemeProp, Classes } from "./common"; import styled from "styled-components"; -import Icon, { IconName } from "./Icon"; +import Icon, { IconName, IconSize } from "./Icon"; import Spinner from "./Spinner"; import { mediumButton, smallButton, largeButton } from "constants/DefaultTheme"; @@ -45,6 +45,7 @@ type BtnColorType = { type BtnFontType = { buttonFont: any; padding: string; + height: number; }; type ButtonProps = CommonComponentProps & { @@ -54,6 +55,7 @@ type ButtonProps = CommonComponentProps & { variant?: Variant; icon?: IconName; size?: Size; + fill?: boolean; }; const stateStyles = ( @@ -208,35 +210,40 @@ const btnColorStyles = ( const btnFontStyles = (props: ThemeProp & ButtonProps): BtnFontType => { let buttonFont, - padding = ""; + padding = "", + height = 0; switch (props.size) { case Size.small: buttonFont = smallButton; + height = 20; padding = !props.text && props.icon - ? `${props.theme.spaces[1]}px ${props.theme.spaces[1]}px` - : `${props.theme.spaces[1]}px ${props.theme.spaces[6]}px ${props.theme - .spaces[1] - 1}px`; + ? `0px ${props.theme.spaces[1]}px` + : `0px ${props.theme.spaces[6]}px`; break; case Size.medium: buttonFont = mediumButton; + height = 30; padding = !props.text && props.icon - ? `${props.theme.spaces[2]}px ${props.theme.spaces[2]}px` - : `${props.theme.spaces[3] - 1}px ${props.theme.spaces[7]}px`; + ? `0px ${props.theme.spaces[2]}px` + : `0px ${props.theme.spaces[7]}px`; break; case Size.large: buttonFont = largeButton; + height = 38; padding = !props.text && props.icon - ? `${props.theme.spaces[3]}px` - : `${props.theme.spaces[5] - 1}px ${props.theme.spaces[12] - 4}px`; + ? `0px ${props.theme.spaces[3]}px` + : `0px ${props.theme.spaces[12] - 4}px`; break; } - return { buttonFont, padding }; + return { buttonFont, padding, height }; }; const StyledButton = styled("button")` + width: ${props => (props.fill ? "100%" : "auto")}; + height: ${props => btnFontStyles(props).height}px; border: none; outline: none; text-transform: uppercase; @@ -246,10 +253,10 @@ const StyledButton = styled("button")` border-radius: ${props => props.theme.radii[0]}; ${props => btnFontStyles(props).buttonFont}; padding: ${props => btnFontStyles(props).padding}; - .ads-icon { + .${Classes.ICON} { margin-right: ${props => - props.text && props.icon ? `${props.theme.spaces[4]}px` : `0`} - path { + props.text && props.icon ? `${props.theme.spaces[4]}px` : `0`}; + path { fill: ${props => btnColorStyles(props, "main").txtColor}; } } @@ -259,10 +266,10 @@ const StyledButton = styled("button")` border: ${props => btnColorStyles(props, "hover").border}; cursor: ${props => props.isLoading || props.disabled ? `not-allowed` : `pointer`}; - .ads-icon { + .${Classes.ICON} { margin-right: ${props => - props.text && props.icon ? `${props.theme.spaces[4]}px` : `0`} - path { + props.text && props.icon ? `${props.theme.spaces[4]}px` : `0`}; + path { fill: ${props => btnColorStyles(props, "hover").txtColor}; } } @@ -274,13 +281,15 @@ const StyledButton = styled("button")` border: ${props => btnColorStyles(props, "active").border}; cursor: ${props => props.isLoading || props.disabled ? `not-allowed` : `pointer`}; - .ads-icon { + .${Classes.ICON} { path { fill: ${props => btnColorStyles(props, "active").txtColor}; } } } display: flex; + align-items: center; + justify-content: center; position: relative; .new-spinner { position: absolute; @@ -291,21 +300,34 @@ const StyledButton = styled("button")` } `; -Button.defaultProps = { - category: Category.primary, - variant: Variant.success, - size: Size.small, - isLoading: false, - disabled: false, -}; - export const VisibilityWrapper = styled.div` visibility: hidden; `; +const IconSizeProp = (size?: Size) => { + if (size === Size.small) { + return IconSize.SMALL; + } else if (size === Size.medium) { + return IconSize.MEDIUM; + } else if (size === Size.large) { + return IconSize.LARGE; + } else { + return IconSize.SMALL; + } +}; + +Button.defaultProps = { + category: Category.primary, + variant: Variant.info, + size: Size.small, + isLoading: false, + disabled: false, + fill: false, +}; + function Button(props: ButtonProps) { const IconLoadingState = ( - + ); const TextLoadingState = {props.text}; @@ -322,13 +344,13 @@ function Button(props: ButtonProps) { props.isLoading ? ( IconLoadingState ) : ( - + ) ) : null} {props.text ? (props.isLoading ? TextLoadingState : props.text) : null} - {props.isLoading ? : null} + {props.isLoading ? : null} ); } diff --git a/app/client/src/components/ads/Dropdown.tsx b/app/client/src/components/ads/Dropdown.tsx index bcb0e6bac0..d7e91c022f 100644 --- a/app/client/src/components/ads/Dropdown.tsx +++ b/app/client/src/components/ads/Dropdown.tsx @@ -1,36 +1,169 @@ -import { ReactNode } from "react"; -import { IconName } from "./Icon"; -import { CommonComponentProps } from "./common"; +import React, { useState, useEffect, useCallback } from "react"; +import Icon, { IconName, IconSize } from "./Icon"; +import { CommonComponentProps, Classes } from "./common"; +import styled from "styled-components"; +import Text, { TextType } from "./Text"; type DropdownOption = { - label: string; + label?: string; value: string; id?: string; - icon: IconName; // Create an icon library + icon?: IconName; onSelect?: (option: DropdownOption) => void; children?: DropdownOption[]; }; -export enum DropdownDisplayType { - TAGS = "TAGS", - CHECKBOXES = "CHECKBOXES", -} - type DropdownProps = CommonComponentProps & { options: DropdownOption[]; - selectHandler: (selectedValue: string) => void; - selected?: DropdownOption; - multiselectDisplayType?: DropdownDisplayType; - checked?: boolean; - multi?: boolean; - autocomplete?: boolean; - addItem?: { - displayText: string; - addItemHandler: (name: string) => void; - }; - toggle?: ReactNode; + selected: DropdownOption; }; -export default function Button(props: DropdownProps) { - return null; +const DropdownContainer = styled.div` + width: 260px; +`; + +const Selected = styled.div<{ isOpen: boolean; disabled?: boolean }>` + padding: ${props => props.theme.spaces[4]}px + ${props => props.theme.spaces[6]}px; + background: ${props => + props.disabled + ? props.theme.colors.blackShades[2] + : props.theme.colors.blackShades[0]}; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + cursor: pointer; + ${props => + props.isOpen && !props.disabled + ? `border: 1.2px solid ${props.theme.colors.info.main}` + : null}; + ${props => + props.isOpen && !props.disabled ? "box-sizing: border-box" : null}; + ${props => + props.isOpen && !props.disabled + ? "box-shadow: 0px 0px 4px 4px rgba(203, 72, 16, 0.18)" + : null}; + .${Classes.TEXT} { + ${props => + props.disabled + ? `color: ${props.theme.colors.blackShades[6]}` + : `color: ${props.theme.colors.blackShades[7]}`}; + } +`; + +const DropdownWrapper = styled.div` + margin-top: ${props => props.theme.spaces[2] - 1}px; + background: ${props => props.theme.colors.blackShades[3]}; + box-shadow: 0px 12px 28px rgba(0, 0, 0, 0.6); + width: 100%; +`; + +const OptionWrapper = styled.div<{ selected: boolean }>` + padding: ${props => props.theme.spaces[4]}px + ${props => props.theme.spaces[6]}px; + cursor: pointer; + display: flex; + align-items: center; + ${props => + props.selected ? `background: ${props.theme.colors.blackShades[4]}` : null}; + .${Classes.TEXT} { + ${props => + props.selected ? `color: ${props.theme.colors.blackShades[9]}` : null}; + } + .${Classes.ICON} { + margin-right: ${props => props.theme.spaces[5]}px; + svg { + path { + ${props => + props.selected + ? `fill: ${props.theme.colors.blackShades[8]}` + : `fill: ${props.theme.colors.blackShades[6]}`}; + } + } + } + + &:hover { + .${Classes.TEXT} { + color: ${props => props.theme.colors.blackShades[9]}; + } + .${Classes.ICON} { + svg { + path { + fill: ${props => props.theme.colors.blackShades[8]}; + } + } + } + } +`; + +const LabelWrapper = styled.div<{ label?: string }>` + display: flex; + flex-direction: column; + align-item: flex-start; + + ${props => + props.label + ? ` + .${Classes.TEXT}:last-child { + margin-top: ${props.theme.spaces[2] - 1}px; + } + ` + : null} +`; + +export default function Dropdown(props: DropdownProps) { + const [isOpen, setIsOpen] = useState(false); + const [selected, setSelected] = useState(props.selected); + + useEffect(() => { + setSelected(props.selected); + }, [props.selected]); + + const optionClickHandler = useCallback((option: DropdownOption) => { + setSelected(option); + setIsOpen(false); + option.onSelect && option.onSelect(option); + }, []); + + return ( + setIsOpen(false)}> + setIsOpen(!isOpen)} + > + {selected.value} + + + + {isOpen && !props.disabled ? ( + + {props.options.map((option: DropdownOption, index: number) => { + return ( + optionClickHandler(option)} + > + {option.icon ? ( + + ) : null} + + {option.label ? ( + {option.value} + ) : ( + {option.value} + )} + {option.label ? ( + {option.label} + ) : null} + + + ); + })} + + ) : null} + + ); } diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx index cc6343fbe7..b64ef37ce0 100644 --- a/app/client/src/components/ads/EditableText.tsx +++ b/app/client/src/components/ads/EditableText.tsx @@ -1,13 +1,12 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { EditableText as BlueprintEditableText } from "@blueprintjs/core"; import styled from "styled-components"; -import { Size } from "./Button"; import Text, { TextType } from "./Text"; import Spinner from "./Spinner"; -import { hexToRgba } from "./common"; +import { hexToRgba, Classes } from "./common"; import { theme } from "constants/DefaultTheme"; import { noop } from "lodash"; -import Icon from "./Icon"; +import Icon, { IconSize } from "./Icon"; export enum EditInteractionKind { SINGLE = "SINGLE", @@ -48,7 +47,7 @@ const EditableTextWrapper = styled.div<{ fill?: boolean; }>` width: ${props => (!props.fill ? "234px" : "100%")}; - .error-message { + .${Classes.TEXT} { margin-left: ${props => props.theme.spaces[5]}px; color: ${props => props.theme.colors.danger.main}; } @@ -160,13 +159,17 @@ export const EditableText = (props: EditableTextProps) => { [isInvalid, isEditing, savingState], ); - const editMode = useCallback((e: React.MouseEvent) => { - setIsEditing(true); - const errorMessage = props.isInvalid && props.isInvalid(props.defaultValue); - setIsInvalid(errorMessage ? errorMessage : false); - e.preventDefault(); - e.stopPropagation(); - }, []); + const editMode = useCallback( + (e: React.MouseEvent) => { + setIsEditing(true); + const errorMessage = + props.isInvalid && props.isInvalid(props.defaultValue); + setIsInvalid(errorMessage ? errorMessage : false); + e.preventDefault(); + e.stopPropagation(); + }, + [props], + ); const onConfirm = (_value: string) => { if ( @@ -183,21 +186,24 @@ export const EditableText = (props: EditableTextProps) => { setChangeStarted(false); }; - const onInputchange = useCallback((_value: string) => { - let finalVal: string = _value; - if (props.valueTransform) { - finalVal = props.valueTransform(_value); - } - setValue(finalVal); + const onInputchange = useCallback( + (_value: string) => { + let finalVal: string = _value; + if (props.valueTransform) { + finalVal = props.valueTransform(_value); + } + setValue(finalVal); - const errorMessage = props.isInvalid && props.isInvalid(finalVal); - const error = errorMessage ? errorMessage : false; - if (!error) { - setLastValidValue(finalVal); - } - setIsInvalid(error); - setChangeStarted(true); - }, []); + const errorMessage = props.isInvalid && props.isInvalid(finalVal); + const error = errorMessage ? errorMessage : false; + if (!error) { + setLastValidValue(finalVal); + } + setIsInvalid(error); + setChangeStarted(true); + }, + [props], + ); const SavingStateHandler = (isSaving: boolean, state?: SavingState) => { setIsEditing(false); @@ -217,7 +223,9 @@ export const EditableText = (props: EditableTextProps) => { }; const iconName = - !isEditing && savingState.name === SavingState.NOT_STARTED + !isEditing && + savingState.name === SavingState.NOT_STARTED && + !props.hideEditIcon ? "edit" : !isEditing && savingState.name === SavingState.SUCCESS ? "success" @@ -270,16 +278,14 @@ export const EditableText = (props: EditableTextProps) => { {savingState.isSaving ? ( - + ) : ( - + )} {isEditing && !!isInvalid ? ( - - {isInvalid} - + {isInvalid} ) : null} ); diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index f1585607d2..014576198d 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -8,35 +8,86 @@ import { ReactComponent as ErrorIcon } from "assets/icons/ads/error.svg"; import { ReactComponent as SuccessIcon } from "assets/icons/ads/success.svg"; import { ReactComponent as SearchIcon } from "assets/icons/ads/search.svg"; import { ReactComponent as CloseIcon } from "assets/icons/ads/close.svg"; +import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg"; +import { ReactComponent as ShareIcon } from "assets/icons/ads/share.svg"; +import { ReactComponent as RocketIcon } from "assets/icons/ads/launch.svg"; +import { ReactComponent as WorkspaceIcon } from "assets/icons/ads/workspace.svg"; +import { ReactComponent as CreateNewIcon } from "assets/icons/ads/create-new.svg"; +import { ReactComponent as InviteUserIcon } from "assets/icons/ads/invite-users.svg"; +import { ReactComponent as ViewAllIcon } from "assets/icons/ads/view-all.svg"; import styled from "styled-components"; -import { Size } from "./Button"; -import { sizeHandler } from "./Spinner"; +import { CommonComponentProps, Classes } from "./common"; +import { noop } from "lodash"; +import { theme } from "constants/DefaultTheme"; -export type IconName = - | "Select icon" - | "delete" - | "user" - | "general" - | "billing" - | "edit" - | "error" - | "success" - | "search" - | "close" - | undefined; +export enum IconSize { + SMALL = "small", + MEDIUM = "medium", + LARGE = "large", + XL = "extraLarge", + XXL = "extraExtraLarge", + XXXL = "extraExtraExtraLarge", +} -const IconWrapper = styled.div` +export const sizeHandler = (size?: IconSize) => { + let iconSize = 0; + switch (size) { + case IconSize.SMALL: + iconSize = theme.iconSizes.SMALL; + break; + case IconSize.MEDIUM: + iconSize = theme.iconSizes.MEDIUM; + break; + case IconSize.LARGE: + iconSize = theme.iconSizes.LARGE; + break; + case IconSize.XL: + iconSize = theme.iconSizes.XL; + break; + case IconSize.XXL: + iconSize = theme.iconSizes.XXL; + break; + case IconSize.XXXL: + iconSize = theme.iconSizes.XXXL; + break; + default: + iconSize = theme.iconSizes.SMALL; + break; + } + return iconSize; +}; + +export const IconCollection = [ + "delete", + "user", + "general", + "billing", + "edit", + "error", + "success", + "search", + "close", + "share", + "rocket", + "workspace", + "plus", + "invite-user", + "view-all", + "downArrow", +] as const; + +export type IconName = typeof IconCollection[number]; + +const IconWrapper = styled.span` &:focus { outline: none; } display: flex; svg { - width: ${props => - props.size ? sizeHandler(props) : props.theme.spaces[9]}px; - height: ${props => - props.size ? sizeHandler(props) : props.theme.spaces[9]}px; + width: ${props => sizeHandler(props.size)}px; + height: ${props => sizeHandler(props.size)}px; path { - fill: ${props => props.theme.colors.blackShades[5]}; + fill: ${props => props.theme.colors.blackShades[6]}; } } visibility: ${props => (props.invisible ? "hidden" : "visible")}; @@ -44,27 +95,27 @@ const IconWrapper = styled.div` &:hover { cursor: pointer; path { - fill: ${props => props.theme.colors.blackShades[6]}; + fill: ${props => props.theme.colors.blackShades[8]}; } } &:active { cursor: pointer; path { - fill: ${props => props.theme.colors.blackShades[7]}; + fill: ${props => props.theme.colors.blackShades[9]}; } } `; export type IconProps = { - size?: Size; + size?: IconSize; name?: IconName; invisible?: boolean; className?: string; - click?: () => void; + onClick?: () => void; }; -const Icon = (props: IconProps) => { +const Icon = (props: IconProps & CommonComponentProps) => { let returnIcon; switch (props.name) { case "delete": @@ -94,15 +145,37 @@ const Icon = (props: IconProps) => { case "close": returnIcon = ; break; + case "downArrow": + returnIcon = ; + break; + case "share": + returnIcon = ; + break; + case "rocket": + returnIcon = ; + break; + case "workspace": + returnIcon = ; + break; + case "plus": + returnIcon = ; + break; + case "invite-user": + returnIcon = ; + break; + case "view-all": + returnIcon = ; + break; default: returnIcon = null; break; } return returnIcon ? ( props.click && props.click()} + onClick={props.onClick || noop} > {returnIcon} diff --git a/app/client/src/components/ads/IconSelector.tsx b/app/client/src/components/ads/IconSelector.tsx index c722ec33ad..3614e2f536 100644 --- a/app/client/src/components/ads/IconSelector.tsx +++ b/app/client/src/components/ads/IconSelector.tsx @@ -1,24 +1,9 @@ import React, { useState, useEffect } from "react"; import styled from "styled-components"; -import AppIcon, { AppIconName } from "./AppIcon"; +import AppIcon, { AppIconName, AppIconCollection } from "./AppIcon"; import { Size } from "./Button"; import { CommonComponentProps } from "./common"; -export const appIconPalette = [ - AppIconName.BAG, - AppIconName.PRODUCT, - AppIconName.BOOK, - AppIconName.CAMERA, - AppIconName.FILE, - AppIconName.CHAT, - AppIconName.CALENDER, - AppIconName.FLIGHT, - AppIconName.FRAME, - AppIconName.GLOBE, - AppIconName.SHOPPER, - AppIconName.HEART, -]; - type IconSelectorProps = CommonComponentProps & { onSelect?: (icon: AppIconName) => void; selectedColor: string; @@ -36,20 +21,9 @@ const IconPalette = styled.div<{ fill?: boolean }>` width: ${props => (props.fill ? "100%" : "234px")}; `; -const IconBox = styled.div<{ - iconName: AppIconName; - selected: AppIconName; - bgColor: string; -}>` - padding: ${props => props.theme.spaces[2]}px - ${props => props.theme.spaces[2] - 1}px; +const IconBox = styled.div` margin: 0 ${props => props.theme.spaces[2]}px ${props => props.theme.spaces[2]}px 0; - background-color: ${props => - props.selected === props.iconName - ? props.bgColor - : props.theme.colors.blackShades[2]}; - cursor: pointer; position: relative; &:nth-child(6n) { @@ -62,7 +36,7 @@ const IconSelector = (props: IconSelectorProps) => { if (props.iconPalette && props.iconPalette[0]) { return props.iconPalette[0]; } - return appIconPalette[0]; + return AppIconCollection[0]; } const [selected, setSelected] = useState(firstSelectedIcon()); @@ -80,15 +54,16 @@ const IconSelector = (props: IconSelectorProps) => { return ( { setSelected(iconName); props.onSelect && props.onSelect(iconName); }} > - + ); })} @@ -98,7 +73,7 @@ const IconSelector = (props: IconSelectorProps) => { IconSelector.defaultProps = { fill: false, - iconPalette: appIconPalette, + iconPalette: AppIconCollection, }; export default IconSelector; diff --git a/app/client/src/components/ads/Menu.tsx b/app/client/src/components/ads/Menu.tsx index 574ec81e1c..8f89cd01d3 100644 --- a/app/client/src/components/ads/Menu.tsx +++ b/app/client/src/components/ads/Menu.tsx @@ -1,5 +1,5 @@ import React, { ReactNode } from "react"; -import { CommonComponentProps } from "./common"; +import { CommonComponentProps, Classes } from "./common"; import styled from "styled-components"; import { Popover } from "@blueprintjs/core/lib/esm/components/popover/popover"; import { Position } from "@blueprintjs/core/lib/esm/common/position"; @@ -19,7 +19,7 @@ const MenuWrapper = styled.div` const MenuOption = styled.div` color: ${props => props.theme.colors.blackShades[6]}; font-family: ${props => props.theme.fonts[3]}; - .ads-icon { + .${Classes.ICON} { path { fill: ${props => props.theme.colors.blackShades[6]}; } diff --git a/app/client/src/components/ads/MenuItem.tsx b/app/client/src/components/ads/MenuItem.tsx index 427831e010..7107f2f04b 100644 --- a/app/client/src/components/ads/MenuItem.tsx +++ b/app/client/src/components/ads/MenuItem.tsx @@ -1,9 +1,8 @@ import React, { ReactNode } from "react"; -import { CommonComponentProps } from "./common"; +import { CommonComponentProps, Classes } from "./common"; import styled from "styled-components"; -import Icon, { IconName } from "./Icon"; -import Text, { TextType } from "./Text"; -import { Size } from "./Button"; +import Icon, { IconName, IconSize } from "./Icon"; +import Text, { TextType, FontWeight } from "./Text"; type MenuItemProps = CommonComponentProps & { icon?: IconName; @@ -12,42 +11,54 @@ type MenuItemProps = CommonComponentProps & { onSelect?: () => void; }; -const ItemRow = styled.div` +const ItemRow = styled.div<{ disabled?: boolean }>` display: flex; align-items: center; justify-content: space-between; padding: ${props => props.theme.spaces[4]}px ${props => props.theme.spaces[6]}px; - &:hover { - cursor: pointer; - background-color: ${props => props.theme.colors.blackShades[4]}; - span { - color: ${props => props.theme.colors.blackShades[9]}; - } - .ads-icon { - path { - fill: ${props => props.theme.colors.blackShades[9]}; + ${props => + !props.disabled + ? ` + &:hover { + cursor: pointer; + background-color: ${props.theme.colors.blackShades[4]}; + .${Classes.TEXT} { + color: ${props.theme.colors.blackShades[9]}; } + .${Classes.ICON} { + path { + fill: ${props.theme.colors.blackShades[9]}; + } + } + }` + : ` + &:hover { + cursor: not-allowed; } - } + `} `; const IconContainer = styled.div` display: flex; align-items: center; - .ads-icon { + .${Classes.ICON} { margin-right: ${props => props.theme.spaces[5]}px; } `; function MenuItem(props: MenuItemProps) { return ( - + - {props.icon ? : null} - {props.text ? {props.text} : null} + {props.icon ? : null} + {props.text ? ( + + {props.text} + + ) : null} {props.label ? {props.label} : null} diff --git a/app/client/src/components/ads/SearchInput.tsx b/app/client/src/components/ads/SearchInput.tsx index 2500dda235..eb1e04c775 100644 --- a/app/client/src/components/ads/SearchInput.tsx +++ b/app/client/src/components/ads/SearchInput.tsx @@ -2,14 +2,12 @@ import React, { forwardRef, Ref, useCallback, - useMemo, useState, useEffect, } from "react"; -import { CommonComponentProps } from "./common"; +import { CommonComponentProps, Classes } from "./common"; import styled from "styled-components"; -import { Size } from "./Button"; -import Icon from "./Icon"; +import Icon, { IconSize } from "./Icon"; export enum SearchVariant { BACKGROUND = "BACKGROUND", @@ -70,8 +68,13 @@ const InputWrapper = styled.div<{ ? `box-shadow: 0px 1px 0px ${props.theme.colors.info.main}` : `box-shadow: 0px 1px 0px ${props.theme.colors.blackShades[4]}` : null} +`; - .search-icon { +const SearchIcon = styled.div<{ + value?: string; + isFocused: boolean; +}>` + .${Classes.ICON} { margin-right: ${props => props.theme.spaces[5]}px; svg { @@ -84,8 +87,10 @@ const InputWrapper = styled.div<{ } } } +`; - .close-icon { +const CloseIcon = styled.div` + .${Classes.ICON} { margin-right: ${props => props.theme.spaces[4]}px; margin-left: ${props => props.theme.spaces[4]}px; } @@ -115,7 +120,9 @@ const SearchInput = forwardRef( variant={props.variant} fill={props.fill} > - + + + {searchValue && props.variant === SearchVariant.BACKGROUND ? ( - setSearchValue("")} - /> + + setSearchValue("")} + /> + ) : null} ); diff --git a/app/client/src/components/ads/Spinner.tsx b/app/client/src/components/ads/Spinner.tsx index 137382a5c5..9adc4101ad 100644 --- a/app/client/src/components/ads/Spinner.tsx +++ b/app/client/src/components/ads/Spinner.tsx @@ -1,23 +1,6 @@ import React from "react"; import styled, { keyframes } from "styled-components"; -import { ThemeProp } from "./common"; -import { Size } from "./Button"; - -export const sizeHandler = (props: ThemeProp & SpinnerProp) => { - let iconSize = 0; - switch (props.size) { - case Size.small: - iconSize = props.theme.iconSizes.small; - break; - case Size.medium: - iconSize = props.theme.iconSizes.medium; - break; - case Size.large: - iconSize = props.theme.iconSizes.large; - break; - } - return iconSize; -}; +import { sizeHandler, IconSize } from "./Icon"; const rotate = keyframes` 100% { @@ -42,8 +25,8 @@ const dash = keyframes` const SvgContainer = styled("svg")` animation: ${rotate} 2s linear infinite; - width: ${props => sizeHandler(props)}px; - height: ${props => sizeHandler(props)}px; + width: ${props => sizeHandler(props.size)}px; + height: ${props => sizeHandler(props.size)}px; `; const SvgCircle = styled("circle")` @@ -54,7 +37,7 @@ const SvgCircle = styled("circle")` `; export type SpinnerProp = { - size?: Size; + size?: IconSize; }; Spinner.defaultProp = { diff --git a/app/client/src/components/ads/Table.tsx b/app/client/src/components/ads/Table.tsx index ff31671d50..aadb9a3306 100644 --- a/app/client/src/components/ads/Table.tsx +++ b/app/client/src/components/ads/Table.tsx @@ -1,7 +1,9 @@ import { useTable, useSortBy } from "react-table"; import React from "react"; import styled from "styled-components"; -import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg"; +import { ReactComponent as DownArrow } from "../../assets/icons/ads/down_arrow.svg"; +import { ReactComponent as UpperArrow } from "../../assets/icons/ads/upper_arrow.svg"; +import { Classes } from "./common"; const Styles = styled.div` table { @@ -63,7 +65,7 @@ const Styles = styled.div` &:hover { background-color: ${props => props.theme.colors.blackShades[4]}; - .ads-icon { + .${Classes.ICON} { path { fill: ${props => props.theme.colors.blackShades[9]}; } @@ -80,10 +82,13 @@ const Styles = styled.div` } `; -function Table(props: any) { - const data = React.useMemo(() => props.data, []); +interface TableProps { + data: any[]; + columns: any[]; +} - const columns = React.useMemo(() => props.columns, []); +function Table(props: TableProps) { + const { data, columns } = props; const { getTableProps, @@ -107,7 +112,7 @@ function Table(props: any) { {column.render("Header")} {column.isSorted ? ( column.isSortedDesc ? ( - " 🔼" + ) : ( ) @@ -126,7 +131,11 @@ function Table(props: any) { {row.cells.map((cell, index) => { return ( - + {cell.render("Cell")} ); diff --git a/app/client/src/components/ads/TableDropdown.tsx b/app/client/src/components/ads/TableDropdown.tsx index aa1ca5c975..3ba911b5d8 100644 --- a/app/client/src/components/ads/TableDropdown.tsx +++ b/app/client/src/components/ads/TableDropdown.tsx @@ -1,40 +1,38 @@ -import React, { useCallback, useState } from "react"; -import { CommonComponentProps, hexToRgba } from "./common"; +import React, { useState } from "react"; +import { CommonComponentProps, hexToRgba, Classes } from "./common"; import { ReactComponent as DownArrow } from "assets/icons/ads/down_arrow.svg"; import Text, { TextType } from "./Text"; import styled from "styled-components"; +import { + Popover, + PopoverInteractionKind, +} from "@blueprintjs/core/lib/esm/components/popover/popover"; +import { Position } from "@blueprintjs/core/lib/esm/common/position"; type DropdownOption = { - label: string; - value: string; + name: string; + desc: string; }; type DropdownProps = CommonComponentProps & { options: DropdownOption[]; - onSelect: (selectedValue: string) => void; - selectedOption: DropdownOption; + onSelect: (selectedValue: DropdownOption) => void; + selectedIndex: number; + position?: Position; }; -const DropdownWrapper = styled.div` - width: 100%; - position: relative; -`; - const SelectedItem = styled.div` display: flex; align-items: center; cursor: pointer; user-select: none; - span { + .${Classes.TEXT} { margin-right: ${props => props.theme.spaces[1] + 1}px; } `; const OptionsWrapper = styled.div` - position: absolute; - margin-top: ${props => props.theme.spaces[8]}px; - left: -60px; width: 200px; display: flex; flex-direction: column; @@ -45,64 +43,67 @@ const OptionsWrapper = styled.div` `; const DropdownOption = styled.div<{ - selected: DropdownOption; - option: DropdownOption; + isSelected: boolean; }>` display: flex; flex-direction: column; padding: 10px 12px; cursor: pointer; - background-color: ${props => - props.option.label === props.selected.label - ? props.theme.colors.blackShades[4] - : "transparent"}; + ${props => + props.isSelected + ? `background-color: ${props.theme.colors.blackShades[4]}` + : null}; - span:last-child { + .${Classes.TEXT}:last-child { margin-top: ${props => props.theme.spaces[1] + 1}px; } &:hover { - span { + .${Classes.TEXT} { color: ${props => props.theme.colors.blackShades[9]}; } } `; const TableDropdown = (props: DropdownProps) => { - const [selected, setSelected] = useState(props.selectedOption); + const [selectedIndex, setSelectedIndex] = useState(props.selectedIndex); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [selectedOption, setSelectedOption] = useState( + props.options[props.selectedIndex] || {}, + ); - const dropdownHandler = () => { - setIsDropdownOpen(!isDropdownOpen); - }; - - const optionSelector = (option: DropdownOption) => { - setSelected(option); + const optionSelector = (index: number) => { + setSelectedIndex(index); + setSelectedOption(props.options[index]); + props.onSelect && props.onSelect(props.options[index]); setIsDropdownOpen(false); }; return ( - - dropdownHandler()}> - {selected.label} + setIsDropdownOpen(state)} + interactionKind={PopoverInteractionKind.CLICK} + > + + {selectedOption.name} - {isDropdownOpen ? ( - - {props.options.map((el: DropdownOption, index: number) => ( - optionSelector(el)} - > - {el.label} - {el.value} - - ))} - - ) : null} - + + {props.options.map((el: DropdownOption, index: number) => ( + optionSelector(index)} + > + {el.name} + {el.desc} + + ))} + + ); }; diff --git a/app/client/src/components/ads/Tabs.tsx b/app/client/src/components/ads/Tabs.tsx index 8bf7f809e3..9170f78e68 100644 --- a/app/client/src/components/ads/Tabs.tsx +++ b/app/client/src/components/ads/Tabs.tsx @@ -2,19 +2,22 @@ import React from "react"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import "react-tabs/style/react-tabs.css"; import styled from "styled-components"; -import Icon, { IconName } from "./Icon"; -import { Size } from "./Button"; +import Icon, { IconName, IconSize } from "./Icon"; +import { Classes } from "./common"; + +export type TabProp = { + key: string; + title: string; + panelComponent: JSX.Element; + icon: IconName; +}; const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` user-select: none; border-radius: 0px; height: 100%; - .ads-icon { + .${Classes.ICON} { margin-right: ${props => props.theme.spaces[3]}px; - svg { - width: ${props => props.theme.spaces[9]}px; - height: ${props => props.theme.spaces[9]}px; - } } .react-tabs { height: 100%; @@ -59,14 +62,6 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` fill: ${props => props.theme.colors.blackShades[9]}; } } - .react-tabs__tab:focus { - box-shadow: none; - border-bottom: ${props => props.theme.colors.info.main} - ${props => props.theme.spaces[1] - 2}px solid; - path { - fill: ${props => props.theme.colors.blackShades[9]}; - } - } .react-tabs__tab--selected { color: ${props => props.theme.colors.blackShades[9]}; background-color: transparent; @@ -85,10 +80,21 @@ const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` background-color: ${props => props.theme.colors.info.main}; } } - .react-tabs__tab:focus:after { - content: none; - height: ${props => props.theme.spaces[1] - 2}px; - background: ${props => props.theme.colors.info.main}; + .react-tabs__tab:focus { + &::after { + content: ""; + position: absolute; + width: 100%; + bottom: ${props => props.theme.spaces[0] - 1}px; + left: ${props => props.theme.spaces[0]}px; + height: ${props => props.theme.spaces[1] - 2}px; + background-color: ${props => props.theme.colors.info.main}; + } + box-shadow: none; + border-color: transparent; + path { + fill: ${props => props.theme.colors.blackShades[9]}; + } } `; @@ -100,14 +106,9 @@ const TabTitle = styled.span` `; type TabbedViewComponentType = { - tabs: Array<{ - key: string; - title: string; - panelComponent: JSX.Element; - icon?: IconName; - }>; + tabs: Array; selectedIndex?: number; - setSelectedIndex?: Function; + onSelect?: Function; overflow?: boolean; }; @@ -117,13 +118,13 @@ export const TabComponent = (props: TabbedViewComponentType) => { { - props.setSelectedIndex && props.setSelectedIndex(index); + props.onSelect && props.onSelect(index); }} > {props.tabs.map(tab => ( - {tab.icon ? : null} + {tab.icon ? : null} {tab.title} ))} diff --git a/app/client/src/components/ads/Text.tsx b/app/client/src/components/ads/Text.tsx index f7b75c2abd..cd3cd83ed2 100644 --- a/app/client/src/components/ads/Text.tsx +++ b/app/client/src/components/ads/Text.tsx @@ -1,5 +1,5 @@ import styled from "styled-components"; -import { ThemeProp } from "./common"; +import { ThemeProp, Classes } from "./common"; export enum TextType { P1 = "p1", @@ -13,10 +13,24 @@ export enum TextType { H6 = "h6", } +export enum Case { + UPPERCASE = "uppercase", + LOWERCASE = "lowercase", + CAPITALIZE = "capitalize", +} + +export enum FontWeight { + BOLD = "bold", + NORMAL = "normal", +} + export type TextProps = { type: TextType; underline?: boolean; italic?: boolean; + case?: Case; + weight?: FontWeight; + highlight?: boolean; }; const typeSelector = (props: TextProps & ThemeProp): string => { @@ -38,15 +52,24 @@ const typeSelector = (props: TextProps & ThemeProp): string => { return color; }; -const Text = styled.span` +const Text = styled.span.attrs(() => ({ + className: Classes.TEXT, +}))` text-decoration: ${props => (props.underline ? "underline" : "unset")}; font-style: ${props => (props.italic ? "italic" : "normal")}; - font-weight: ${props => props.theme.typography[props.type].fontWeight}; + font-weight: ${props => + props.weight + ? props.weight === FontWeight.BOLD + ? props.theme.fontWeights[2] + : "normal" + : props.theme.typography[props.type].fontWeight}; font-size: ${props => props.theme.typography[props.type].fontSize}px; line-height: ${props => props.theme.typography[props.type].lineHeight}px; letter-spacing: ${props => props.theme.typography[props.type].letterSpacing}px; - color: ${props => typeSelector(props)}; + color: ${props => + props.highlight ? props.theme.colors.blackShades[9] : typeSelector(props)}; + text-transform: ${props => (props.case ? props.case : "none")}; `; export default Text; diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx index 05a531336a..e4bb8b9cd4 100644 --- a/app/client/src/components/ads/TextInput.tsx +++ b/app/client/src/components/ads/TextInput.tsx @@ -1,8 +1,36 @@ import React, { forwardRef, Ref, useCallback, useMemo, useState } from "react"; -import { CommonComponentProps, hexToRgba } from "./common"; +import { CommonComponentProps, hexToRgba, Classes } from "./common"; import styled from "styled-components"; import Text, { TextType } from "./Text"; import { theme } from "constants/DefaultTheme"; +import { + FORM_VALIDATION_INVALID_EMAIL, + ERROR_MESSAGE_NAME_EMPTY, +} from "constants/messages"; +import { isEmail } from "utils/formhelpers"; + +export type Validator = ( + value: string, +) => { + isValid: boolean; + message: string; +}; + +export function emailValidator(email: string) { + const isValid = isEmail(email); + return { + isValid: isValid, + message: !isValid ? FORM_VALIDATION_INVALID_EMAIL : "", + }; +} + +export function notEmptyValidator(value: string) { + const isValid = !!value; + return { + isValid: isValid, + message: !isValid ? ERROR_MESSAGE_NAME_EMPTY : "", + }; +} export type TextInputProps = CommonComponentProps & { placeholder?: string; @@ -43,7 +71,6 @@ const StyledInput = styled.input< border-radius: 0; outline: 0; box-shadow: none; - margin-bottom: ${props => props.theme.spaces[1]}px; border: 1px solid ${props => props.inputStyle.borderColor}; padding: ${props => props.theme.spaces[4]}px ${props => props.theme.spaces[6]}px; @@ -73,12 +100,17 @@ const InputWrapper = styled.div` display: flex; flex-direction: column; align-items: flex-start; + position: relative; - span { + .${Classes.TEXT} { color: ${props => props.theme.colors.danger.main}; } `; +const ErrorWrapper = styled.div` + position absolute; + bottom: -17px; +`; const TextInput = forwardRef( (props: TextInputProps, ref: Ref) => { const initialValidation = () => { @@ -95,19 +127,32 @@ const TextInput = forwardRef( }>(initialValidation()); const inputStyle = useMemo(() => boxStyles(props, validation.isValid), [ - props.disabled, - validation, + props, + validation.isValid, ]); const memoizedChangeHandler = useCallback( el => { - props.validator && setValidation(props.validator(el.target.value)); - return props.onChange && props.onChange(el.target.value); + const validation = props.validator && props.validator(el.target.value); + if (validation) { + props.validator && setValidation(validation); + return ( + validation.isValid && + props.onChange && + props.onChange(el.target.value) + ); + } else { + return props.onChange && props.onChange(el.target.value); + } }, [props], ); - const ErrorMessage = {validation.message}; + const ErrorMessage = ( + + {validation.message} + + ); return ( @@ -118,19 +163,15 @@ const TextInput = forwardRef( isValid={validation.isValid} defaultValue={props.defaultValue} {...props} - placeholder={props.placeholder ? props.placeholder : ""} + placeholder={props.placeholder} onChange={memoizedChangeHandler} /> - {validation.isValid ? null : ErrorMessage} + {ErrorMessage} ); }, ); -TextInput.defaultProps = { - fill: false, -}; - TextInput.displayName = "TextInput"; export default TextInput; diff --git a/app/client/src/components/ads/common.tsx b/app/client/src/components/ads/common.tsx index 2e70303af3..4f779bebb0 100644 --- a/app/client/src/components/ads/common.tsx +++ b/app/client/src/components/ads/common.tsx @@ -1,4 +1,5 @@ import { Theme } from "constants/DefaultTheme"; +import styled from "styled-components"; export interface CommonComponentProps { isLoading?: boolean; //default false @@ -10,6 +11,11 @@ export type ThemeProp = { theme: Theme; }; +export enum Classes { + ICON = "cs-icon", + TEXT = "cs-text", +} + export const hexToRgb = ( hex: string, ): { @@ -35,3 +41,9 @@ export const hexToRgba = (color: string, alpha: number) => { const value = hexToRgb(color); return `rgba(${value.r}, ${value.g}, ${value.b}, ${alpha});`; }; + +export const StoryWrapper = styled.div` + background: #1a191c; + height: 700px; + padding: 50px 100px; +`; diff --git a/app/client/src/components/designSystems/appsmith/CascadeFields.tsx b/app/client/src/components/designSystems/appsmith/CascadeFields.tsx index 1445ab5478..c5ca2a9bc0 100644 --- a/app/client/src/components/designSystems/appsmith/CascadeFields.tsx +++ b/app/client/src/components/designSystems/appsmith/CascadeFields.tsx @@ -19,6 +19,7 @@ import { DropdownOption, ReactTableFilter, } from "components/designSystems/appsmith/TableFilters"; +import { RenderOptionWrapper } from "components/designSystems/appsmith/TableStyledWrappers"; import { debounce } from "lodash"; const StyledRemoveIcon = styled( @@ -169,11 +170,35 @@ const operatorOptions: DropdownOption[] = [ { label: "AND", value: OperatorTypes.AND, type: "" }, ]; +const columnTypeNameMap: Record = { + [ColumnTypes.TEXT]: "Text", + [ColumnTypes.VIDEO]: "Video", + [ColumnTypes.IMAGE]: "Image", + [ColumnTypes.NUMBER]: "Num", + [ColumnTypes.DATE]: "Date", + [ColumnTypes.CURRENCY]: "Curr", + [ColumnTypes.TIME]: "Time", +}; + +const RenderOption = (props: { + type: string; + title: string; + active: boolean; +}) => { + return ( + +
{props.title}
+
{columnTypeNameMap[props.type as ColumnTypes]}
+
+ ); +}; + const RenderOptions = (props: { columns: DropdownOption[]; selectItem: (column: DropdownOption) => void; placeholder: string; value?: string | Condition; + showType?: boolean; className?: string; }) => { const [selectedValue, selectValue] = useState(props.placeholder); @@ -181,10 +206,19 @@ const RenderOptions = (props: { sections: [ { options: props.columns.map((column: DropdownOption) => { + const isActive = column.value === props.value; return { - content: column.label, + content: props.showType ? ( + + ) : ( + column.label + ), value: column.value, - active: column.value === props.value, + active: isActive, onSelect: () => { selectValue(column.label); props.selectItem(column); @@ -207,19 +241,19 @@ const RenderOptions = (props: { skin: Skin.LIGHT, }; useEffect(() => { - if (props.value && configs.sections[0].options) { - const selectedOptions = configs.sections[0].options.filter( + if (props.value && props.columns) { + const selectedOptions = props.columns.filter( i => i.value === props.value, ); if (selectedOptions && selectedOptions.length) { - selectValue(selectedOptions[0].content); + selectValue(selectedOptions[0].value); } else { selectValue(props.placeholder); } } else { selectValue(props.placeholder); } - }, [props.value, props.placeholder, configs.sections]); + }, [props.value, props.placeholder, props.columns]); return ; }; @@ -503,6 +537,7 @@ const Fields = (props: CascadeFieldProps & { state: CascadeFieldState }) => { columns={props.columns} selectItem={selectColumn} value={column} + showType placeholder="Attribute" className="t--table-filter-columns-dropdown" /> diff --git a/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx b/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx index 81a06d3955..6048971997 100644 --- a/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx +++ b/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx @@ -340,3 +340,25 @@ export const TableIconWrapper = styled.div<{ export const SortIconWrapper = styled.div<{ rotate: string }>` transform: ${props => (props.rotate === "true" ? "rotate(180deg)" : "none")}; `; + +export const RenderOptionWrapper = styled.div<{ selected: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + width: 150px; + background: ${props => props.selected && Colors.GREEN}; + position: relative; + .title { + color: ${props => (props.selected ? Colors.WHITE : Colors.OXFORD_BLUE)}; + width: 120px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .type { + position: absolute; + left: 135px; + font-size: 12px !important; + color: ${props => (props.selected ? Colors.WHITE : Colors.BLUE_BAYOUX)}; + } +`; diff --git a/app/client/src/components/editorComponents/Sidebar.tsx b/app/client/src/components/editorComponents/Sidebar.tsx index ca199e45ba..c88446f159 100644 --- a/app/client/src/components/editorComponents/Sidebar.tsx +++ b/app/client/src/components/editorComponents/Sidebar.tsx @@ -1,28 +1,30 @@ import React, { memo } from "react"; -import { Switch, Route } from "react-router"; import styled from "styled-components"; -import { WIDGETS_URL } from "constants/routes"; -import WidgetSidebar from "pages/Editor/WidgetSidebar"; import ExplorerSidebar from "pages/Editor/Explorer"; +import { PanelStack, Classes } from "@blueprintjs/core"; +import { Colors } from "constants/Colors"; const SidebarWrapper = styled.div` + background-color: ${Colors.MINE_SHAFT}; padding: 0px 0 0 6px; + width: ${props => props.theme.sidebarWidth}; + color: ${props => props.theme.colors.textOnDarkBG}; overflow-y: auto; + & .${Classes.PANEL_STACK} { + height: 100%; + .${Classes.PANEL_STACK_VIEW} { + background: none; + } + } `; +const initialPanel = { component: ExplorerSidebar }; + export const Sidebar = memo(() => { return ( - - - - + ); }); diff --git a/app/client/src/components/formControls/BaseControl.tsx b/app/client/src/components/formControls/BaseControl.tsx index e61274ba20..d4fae33346 100644 --- a/app/client/src/components/formControls/BaseControl.tsx +++ b/app/client/src/components/formControls/BaseControl.tsx @@ -28,6 +28,7 @@ export interface ControlData { propertyValue?: any; isValid: boolean; validationMessage?: string; + validationRegex?: string; dataType?: InputType; isRequired?: boolean; } diff --git a/app/client/src/components/formControls/KeyValueArrayControl.tsx b/app/client/src/components/formControls/KeyValueArrayControl.tsx index 715db3633d..e456649456 100644 --- a/app/client/src/components/formControls/KeyValueArrayControl.tsx +++ b/app/client/src/components/formControls/KeyValueArrayControl.tsx @@ -1,9 +1,9 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useCallback } from "react"; import { FieldArray, WrappedFieldArrayProps } from "redux-form"; import styled from "styled-components"; import { Icon } from "@blueprintjs/core"; import { FormIcons } from "icons/FormIcons"; -import BaseControl, { ControlProps, ControlData } from "./BaseControl"; +import BaseControl, { ControlProps } from "./BaseControl"; import TextField from "components/editorComponents/form/fields/TextField"; import { ControlType } from "constants/PropertyControlConstants"; import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField"; @@ -29,11 +29,13 @@ const StyledTextField = styled(TextField)` } `; -const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { +const KeyValueRow = (props: KeyValueArrayProps & WrappedFieldArrayProps) => { const { extraData = [] } = props; const keyName = getFieldName(extraData[0].configProperty); const valueName = getFieldName(extraData[1].configProperty); const valueDataType = getType(extraData[1].dataType); + const keyFieldProps = extraData[0]; + let isRequired: boolean | undefined; useEffect(() => { @@ -55,6 +57,19 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { } }, [props.fields]); + const keyFieldValidate = useCallback( + (value: string) => { + if (value && keyFieldProps.validationRegex) { + const regex = new RegExp(keyFieldProps.validationRegex); + + return regex.test(value) ? undefined : keyFieldProps.validationMessage; + } + + return undefined; + }, + [keyFieldProps.validationRegex, keyFieldProps.validationMessage], + ); + if (extraData) { isRequired = extraData[0].isRequired || extraData[1].isRequired; } @@ -85,7 +100,11 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { {extraData && extraData[0].label} {isRequired && "*"} - + {!props.actionConfig && (
@@ -143,16 +162,6 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { ); }; -type Props = { - name: string; - label: string; - rightIcon?: Function; - description?: string; - actionConfig?: any; - extraData?: ControlData[]; - isRequired?: boolean; -}; - class KeyValueFieldArray extends BaseControl { render() { const name = getFieldName(this.props.configProperty); diff --git a/app/client/src/components/stories/Button.stories.tsx b/app/client/src/components/stories/Button.stories.tsx index 658c0ca203..22bcb3b19a 100644 --- a/app/client/src/components/stories/Button.stories.tsx +++ b/app/client/src/components/stories/Button.stories.tsx @@ -2,7 +2,8 @@ import React from "react"; import Button, { Size, Category, Variant } from "components/ads/Button"; import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; -import { StoryWrapper } from "./Tabs.stories"; +import { StoryWrapper } from "components/ads/common"; +import { IconCollection } from "components/ads/Icon"; export default { title: "Button", @@ -13,21 +14,14 @@ export default { export const withDynamicProps = () => ( ); diff --git a/app/client/src/components/stories/ColorSelector.stories.tsx b/app/client/src/components/stories/ColorSelector.stories.tsx index ec30c74238..34e8bdc6ad 100644 --- a/app/client/src/components/stories/ColorSelector.stories.tsx +++ b/app/client/src/components/stories/ColorSelector.stories.tsx @@ -3,7 +3,7 @@ import { action } from "@storybook/addon-actions"; import ColorSelector, { appColorPalette } from "components/ads/ColorSelector"; import { withKnobs, array, boolean } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; -import { StoryWrapper } from "./Tabs.stories"; +import { StoryWrapper } from "components/ads/common"; export default { title: "ColorSelector", diff --git a/app/client/src/components/stories/Dropdown.stories.tsx b/app/client/src/components/stories/Dropdown.stories.tsx new file mode 100644 index 0000000000..74233c6a6a --- /dev/null +++ b/app/client/src/components/stories/Dropdown.stories.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; +import { withDesign } from "storybook-addon-designs"; +import Dropdown from "components/ads/Dropdown"; +import { action } from "@storybook/addon-actions"; +import { IconCollection } from "components/ads/Icon"; +import { StoryWrapper } from "components/ads/common"; + +export default { + title: "Dropdown", + component: Dropdown, + decorators: [withKnobs, withDesign], +}; + +export const Text = () => ( + + + +); + +export const IconAndText = () => ( + + + +); + +export const LabelAndText = () => ( + + + +); diff --git a/app/client/src/components/stories/EditableText.stories.tsx b/app/client/src/components/stories/EditableText.stories.tsx index 7cc28a8130..fc69fa1c91 100644 --- a/app/client/src/components/stories/EditableText.stories.tsx +++ b/app/client/src/components/stories/EditableText.stories.tsx @@ -7,7 +7,7 @@ import EditableText, { SavingState, } from "components/ads/EditableText"; import { action } from "@storybook/addon-actions"; -import { StoryWrapper } from "./Tabs.stories"; +import { StoryWrapper } from "components/ads/common"; export default { title: "EditableText", @@ -43,7 +43,7 @@ export const EditableTextStory = () => ( defaultValue={text("defaultValue", "Product design app")} editInteractionKind={select( "editInteractionKind", - [EditInteractionKind.SINGLE, EditInteractionKind.DOUBLE], + Object.values(EditInteractionKind), EditInteractionKind.SINGLE, )} onTextChanged={action("text-changed")} diff --git a/app/client/src/components/stories/Icon.stories.tsx b/app/client/src/components/stories/Icon.stories.tsx index be68e7b7cd..90b1b5be7e 100644 --- a/app/client/src/components/stories/Icon.stories.tsx +++ b/app/client/src/components/stories/Icon.stories.tsx @@ -1,10 +1,10 @@ import React from "react"; +import Icon, { IconSize, IconCollection } from "components/ads/Icon"; import Button, { Size, Category, Variant } from "components/ads/Button"; import { withKnobs, select, boolean } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; -import Icon from "components/ads/Icon"; -import AppIcon, { AppIconName } from "components/ads/AppIcon"; -import { StoryWrapper } from "./Tabs.stories"; +import AppIcon, { AppIconCollection } from "components/ads/AppIcon"; +import { StoryWrapper } from "components/ads/common"; export default { title: "Icon", @@ -15,18 +15,10 @@ export default { export const ButtonIcon = () => ( @@ -36,34 +28,47 @@ export const ButtonIcon = () => ( export const BordelessIcon = () => ( ); -export const BorderlessAppIcon = () => ( +export const AppIconVariant = () => ( ); diff --git a/app/client/src/components/stories/IconSelector.stories.tsx b/app/client/src/components/stories/IconSelector.stories.tsx index 9908ae8529..dce1dabf0f 100644 --- a/app/client/src/components/stories/IconSelector.stories.tsx +++ b/app/client/src/components/stories/IconSelector.stories.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; +import { withKnobs, select, boolean } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; import IconSelector from "components/ads/IconSelector"; import { action } from "@storybook/addon-actions"; -import { AppIconName } from "components/ads/AppIcon"; -import { StoryWrapper } from "./Tabs.stories"; +import { AppIconCollection } from "components/ads/AppIcon"; +import { StoryWrapper } from "components/ads/common"; export default { title: "IconSelector", @@ -17,24 +17,7 @@ export const IconPicker = () => ( { }} > { W} + disabled={boolean("First option disabled", false)} /> {boolean("First menu item divider", false) ? : null} W} /> diff --git a/app/client/src/components/stories/SearchInput.stories.tsx b/app/client/src/components/stories/SearchInput.stories.tsx index 1d902f2b34..e53ef0b5e8 100644 --- a/app/client/src/components/stories/SearchInput.stories.tsx +++ b/app/client/src/components/stories/SearchInput.stories.tsx @@ -2,7 +2,7 @@ import React from "react"; import { withKnobs, boolean, text, select } from "@storybook/addon-knobs"; import { action } from "@storybook/addon-actions"; import SearchInput, { SearchVariant } from "components/ads/SearchInput"; -import { StoryWrapper } from "./Tabs.stories"; +import { StoryWrapper } from "components/ads/common"; export default { title: "Search Input", @@ -16,7 +16,7 @@ export const SearchInputStory = () => ( placeholder={text("placeholder", "Search for apps...")} variant={select( "variant", - [SearchVariant.BACKGROUND, SearchVariant.SEAMLESS], + Object.values(SearchVariant), SearchVariant.SEAMLESS, )} fill={boolean("fill", false)} diff --git a/app/client/src/components/stories/Table.stories.tsx b/app/client/src/components/stories/Table.stories.tsx index f1b64772f1..7b21131027 100644 --- a/app/client/src/components/stories/Table.stories.tsx +++ b/app/client/src/components/stories/Table.stories.tsx @@ -1,8 +1,10 @@ import React from "react"; import Table from "components/ads/Table"; import Button, { Category, Variant, Size } from "components/ads/Button"; -import Icon from "components/ads/Icon"; -import { StoryWrapper } from "./Tabs.stories"; +import Icon, { IconSize } from "components/ads/Icon"; +import TableDropdown from "components/ads/TableDropdown"; +import { Position } from "@blueprintjs/core/lib/esm/common/position"; +import { StoryWrapper } from "components/ads/common"; export default { title: "Table", @@ -36,11 +38,33 @@ const columns = [ }, ]; +const options = [ + { + name: "Admin", + desc: "Can edit, view and invite other user to an app", + }, + { + name: "Developer", + desc: "Can view and invite other user to an app", + }, + { + name: "User", + desc: "Can view and invite other user to an app and...", + }, +]; + const data = [ { col1: "Dustin Howard", col2: "dustin_01@jlegue.com", - col3: "Developer", + col3: ( + console.log(selectedValue)} + selectedIndex={0} + > + ), col4: "App Access", col5: ( ), - col6: , + col6: , }, { col1: "Austin Howard", col2: "dustin_02@jlegue.com", - col3: "User", + col3: ( + console.log(selectedValue)} + selectedIndex={1} + > + ), col4: "Map Access", col5: ( ), - col6: , + col6: , }, { col1: "Justing Howard", col2: "dustin_03@jlegue.com", - col3: "Admin", + col3: ( + console.log(selectedValue)} + selectedIndex={2} + > + ), col4: "Dm Access", col5: ( ), - col6: , + col6: , }, ]; diff --git a/app/client/src/components/stories/TableDropdown.stories.tsx b/app/client/src/components/stories/TableDropdown.stories.tsx index f4b2bc480b..2a9bbdd1d5 100644 --- a/app/client/src/components/stories/TableDropdown.stories.tsx +++ b/app/client/src/components/stories/TableDropdown.stories.tsx @@ -1,8 +1,9 @@ import React from "react"; -import { withKnobs, select, boolean, text } from "@storybook/addon-knobs"; +import { withKnobs, select } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; import TableDropdown from "components/ads/TableDropdown"; -import { StoryWrapper } from "./Tabs.stories"; +import { Position } from "@blueprintjs/core/lib/esm/common/position"; +import { StoryWrapper } from "components/ads/common"; export default { title: "Dropdown", @@ -12,25 +13,28 @@ export default { const options = [ { - label: "Admin", - value: "Can edit, view and invite other user to an app", + name: "Admin", + desc: "Can edit, view and invite other user to an app", }, { - label: "Developer", - value: "Can view and invite other user to an app", + name: "Developer", + desc: "Can view and invite other user to an app", }, { - label: "User", - value: "Can view and invite other user to an app and...", + name: "User", + desc: "Can view and invite other user to an app and...", }, ]; export const TableDropdownStory = () => ( - + console.log(selectedValue)} - selectedOption={options[0]} + onSelect={selectedValue => console.log(selectedValue)} + selectedIndex={0} > ); diff --git a/app/client/src/components/stories/Tabs.stories.tsx b/app/client/src/components/stories/Tabs.stories.tsx index ed6e1d02da..6b7e97a9c4 100644 --- a/app/client/src/components/stories/Tabs.stories.tsx +++ b/app/client/src/components/stories/Tabs.stories.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { TabComponent } from "components/ads/Tabs"; +import { TabComponent, TabProp } from "components/ads/Tabs"; import { select, text, withKnobs } from "@storybook/addon-knobs"; import { withDesign } from "storybook-addon-designs"; -import { IconName } from "components/ads/Icon"; -import styled from "styled-components"; +import { IconCollection } from "components/ads/Icon"; +import { StoryWrapper } from "components/ads/common"; export default { title: "Tabs", @@ -11,15 +11,8 @@ export default { decorators: [withKnobs, withDesign], }; -type tabSingle = { - key: string; - title: string; - panelComponent: JSX.Element; - icon: IconName; -}; - const TabStory = (props: any) => { - const tabArr: tabSingle[] = [ + const tabArr: TabProp[] = [ { key: "1", title: props.title1, @@ -94,35 +87,13 @@ const TabStory = (props: any) => { export const Tabs = () => ( ); - -export const StoryWrapper = styled.div` - background: #1a191c; - height: 700px; - padding: 50px 100px; -`; diff --git a/app/client/src/components/stories/Text.stories.tsx b/app/client/src/components/stories/Text.stories.tsx index 67e4c5ce73..ca0e495c32 100644 --- a/app/client/src/components/stories/Text.stories.tsx +++ b/app/client/src/components/stories/Text.stories.tsx @@ -1,8 +1,8 @@ import React from "react"; import { boolean, select, text, withKnobs } from "@storybook/addon-knobs"; -import Text, { TextType } from "components/ads/Text"; +import Text, { TextType, Case, FontWeight } from "components/ads/Text"; import styled from "styled-components"; -import { StoryWrapper } from "./Tabs.stories"; +import { StoryWrapper } from "components/ads/common"; export default { title: "Text", @@ -48,29 +48,18 @@ const ValueWrapper = (props: { type: TextType; value: string }) => ( type={props.type} underline={boolean("underline", false)} italic={boolean("italic", false)} + highlight={boolean("highlight", false)} + case={select("Case", Object.values(Case), undefined)} + weight={select("Weight", Object.values(FontWeight), undefined)} > {props.value} ); -export const SingleText = () => ( +export const CustomizeText = () => ( diff --git a/app/client/src/components/stories/TextInput.stories.tsx b/app/client/src/components/stories/TextInput.stories.tsx index 3947150e52..00baa3c35f 100644 --- a/app/client/src/components/stories/TextInput.stories.tsx +++ b/app/client/src/components/stories/TextInput.stories.tsx @@ -2,7 +2,7 @@ import React from "react"; import { withKnobs, boolean, text } from "@storybook/addon-knobs"; import TextInput from "components/ads/TextInput"; import { action } from "@storybook/addon-actions"; -import { StoryWrapper } from "./Tabs.stories"; +import { StoryWrapper } from "components/ads/common"; export default { title: "Text Input", @@ -13,7 +13,7 @@ export default { const callValidator1 = () => { return { isValid: true, - message: "This is a warning text for the above field.", + message: "", }; }; diff --git a/app/client/src/configs/index.ts b/app/client/src/configs/index.ts index 3acccf9b02..3a77498a8f 100644 --- a/app/client/src/configs/index.ts +++ b/app/client/src/configs/index.ts @@ -1,13 +1,14 @@ import { AppsmithUIConfigs, FeatureFlagConfig } from "./types"; import { Integrations } from "@sentry/tracing"; +import * as Sentry from "@sentry/react"; +import { createBrowserHistory } from "history"; +const history = createBrowserHistory(); type INJECTED_CONFIGS = { sentry: { dsn: string; release: string; environment: string; - integrations: any[]; - tracesSampleRate: number; }; smartLook: { id: string; @@ -55,8 +56,6 @@ const getConfigsFromEnvVars = (): INJECTED_CONFIGS => { environment: process.env.REACT_APP_SENTRY_ENVIRONMENT || capitalizeText(process.env.NODE_ENV), - integrations: [new Integrations.BrowserTracing()], - tracesSampleRate: 1.0, }, smartLook: { id: process.env.REACT_APP_SMART_LOOK_ID || "", @@ -137,8 +136,8 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => { APPSMITH_FEATURE_CONFIGS.sentry.release, ); const sentryENV = getConfig( - APPSMITH_FEATURE_CONFIGS.sentry.environment, ENV_CONFIG.sentry.environment, + APPSMITH_FEATURE_CONFIGS.sentry.environment, ); const segment = getConfig( ENV_CONFIG.segment, @@ -172,6 +171,14 @@ export const getAppsmithConfigs = (): AppsmithUIConfigs => { dsn: sentryDSN.value, release: sentryRelease.value, environment: sentryENV.value, + normalizeDepth: 7, + integrations: [ + new Integrations.BrowserTracing({ + // Can also use reactRouterV4Instrumentation + routingInstrumentation: Sentry.reactRouterV5Instrumentation(history), + }), + ], + tracesSampleRate: 1.0, }, smartLook: { enabled: smartLook.enabled, diff --git a/app/client/src/configs/types.ts b/app/client/src/configs/types.ts index e218d3ad99..34a16b9cfd 100644 --- a/app/client/src/configs/types.ts +++ b/app/client/src/configs/types.ts @@ -24,6 +24,9 @@ export type AppsmithUIConfigs = { dsn: string; release: string; environment: string; + integrations: any[]; + normalizeDepth: number; + tracesSampleRate: number; }; smartLook: { enabled: boolean; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index b8494427c6..2d110ce332 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -371,9 +371,12 @@ export type Theme = { }; type iconSizeType = { - small: number; - medium: number; - large: number; + SMALL: number; + MEDIUM: number; + LARGE: number; + XL: number; + XXL: number; + XXXL: number; }; export const getColorWithOpacity = (color: Color, opacity: number) => { @@ -505,9 +508,12 @@ export const theme: Theme = { }, }, iconSizes: { - small: 12, - medium: 14, - large: 15, + SMALL: 12, + MEDIUM: 14, + LARGE: 15, + XL: 16, + XXL: 18, + XXXL: 20, }, propertyPane: { width: 270, @@ -639,7 +645,7 @@ export const theme: Theme = { }, ], sidebarWidth: "320px", - headerHeight: "50px", + headerHeight: "48px", canvasPadding: "20px 0 200px 0", sideNav: { maxWidth: 220, diff --git a/app/client/src/constants/Explorer.ts b/app/client/src/constants/Explorer.ts index 2ad4fb114b..94f482e1a5 100644 --- a/app/client/src/constants/Explorer.ts +++ b/app/client/src/constants/Explorer.ts @@ -1,2 +1,2 @@ export const ENTITY_EXPLORER_SEARCH_ID = "entity-explorer-search"; -export const ENTITY_EXPLORER_SEARCH_LOCATION_HASH = "#search"; +export const WIDGETS_SEARCH_ID = "#widgets-search"; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index d251925f76..5b128dd79e 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -137,6 +137,8 @@ export const ReduxActionTypes: { [key: string]: string } = { FETCH_ORGS_SUCCESS: "FETCH_ORGS_SUCCES", FETCH_ORGS_INIT: "FETCH_ORGS_INIT", SAVE_ORG_INIT: "SAVE_ORG_INIT", + SAVE_ORG_SUCCESS: "SAVE_ORG_SUCCESS", + SET_CURRENT_ORG: "SET_CURRENT_ORG", SET_CURRENT_ORG_ID: "SET_CURRENT_ORG_ID", FETCH_CURRENT_ORG: "FETCH_CURRENT_ORG", STORE_DATASOURCE_REFS: "STORE_DATASOURCE_REFS", @@ -165,6 +167,8 @@ export const ReduxActionTypes: { [key: string]: string } = { COPY_ACTION_SUCCESS: "COPY_ACTION_SUCCESS", DELETE_APPLICATION_INIT: "DELETE_APPLICATION_INIT", DELETE_APPLICATION_SUCCESS: "DELETE_APPLICATION_SUCCESS", + DUPLICATE_APPLICATION_INIT: "DUPLICATE_APPLICATION_INIT", + DUPLICATE_APPLICATION_SUCCESS: "DUPLICATE_APPLICATION_SUCCESS", DELETE_PAGE_INIT: "DELETE_PAGE_INIT", DELETE_PAGE_SUCCESS: "DELETE_PAGE_SUCCESS", CLONE_PAGE_INIT: "CLONE_PAGE_INIT", @@ -233,10 +237,8 @@ export const ReduxActionTypes: { [key: string]: string } = { FETCH_ALL_ROLES_INIT: "FETCH_ALL_ROLES_INIT", DELETE_ORG_USER_INIT: "DELETE_ORG_USER_INIT", DELETE_ORG_USER_SUCCESS: "DELETE_ORG_USER_SUCCESS", - DELETE_ORG_USER_ERROR: "DELETE_ORG_USER_ERROR", CHANGE_ORG_USER_ROLE_INIT: "CHANGE_ORG_USER_ROLE_INIT", CHANGE_ORG_USER_ROLE_SUCCESS: "CHANGE_ORG_USER_ROLE_SUCCESS", - CHANGE_ORG_USER_ROLE_ERROR: "CHANGE_ORG_USER_ROLE_ERROR", SET_DEFAULT_REFINEMENT: "SET_DEFAULT_REFINEMENT", SET_HELP_MODAL_OPEN: "SET_HELP_MODAL_OPEN", SAVE_ACTION_NAME_INIT: "SAVE_ACTION_NAME_INIT", @@ -276,6 +278,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { WIDGET_ADD_CHILD_ERROR: "WIDGET_ADD_CHILD_ERROR", FETCH_PAGE_ERROR: "FETCH_PAGE_ERROR", SAVE_PAGE_ERROR: "SAVE_PAGE_ERROR", + DELETE_ORG_USER_ERROR: "DELETE_ORG_USER_ERROR", FETCH_WIDGET_CARDS_ERROR: "FETCH_WIDGET_CARDS_ERROR", WIDGET_OPERATION_ERROR: "WIDGET_OPERATION_ERROR", FETCH_PROPERTY_PANE_CONFIGS_ERROR: "FETCH_PROPERTY_PANE_CONFIGS_ERROR", @@ -305,6 +308,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { LOGIN_USER_ERROR: "LOGIN_USER_ERROR", CREATE_USER_ERROR: "CREATE_USER_ERROR", RESET_USER_PASSWORD_ERROR: "RESET_USER_PASSWORD_ERROR", + CHANGE_ORG_USER_ROLE_ERROR: "CHANGE_ORG_USER_ROLE_ERROR", SAVE_JS_EXECUTION_RECORD: "SAVE_JS_EXECUTION_RECORD", FETCH_PLUGINS_ERROR: "FETCH_PLUGINS_ERROR", UPDATE_ORG_NAME_ERROR: "UPDATE_ORG_NAME_ERROR", @@ -328,6 +332,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { DELETE_PAGE_ERROR: "DELETE_PAGE_ERROR", CLONE_PAGE_ERROR: "CLONE_PAGE_ERROR", DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR", + DUPLICATE_APPLICATION_ERROR: "DUPLICATE_APPLICATION_ERROR", SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR", CREATE_ORGANIZATION_ERROR: "CREATE_ORGANIZATION_ERROR", ADD_USER_TO_ORG_ERROR: "ADD_USER_TO_ORG_ERROR", diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index 7a7330ed9d..616a46fc5f 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -1,5 +1,6 @@ export const ERROR_MESSAGE_SELECT_ACTION = "Please select an action"; export const ERROR_MESSAGE_SELECT_ACTION_TYPE = "Please select an action type"; +export const ERROR_MESSAGE_NAME_EMPTY = "Please select a name"; export const ERROR_MESSAGE_CREATE_APPLICATION = "We could not create the Application"; export const API_PATH_START_WITH_SLASH_ERROR = "Path cannot start with /"; @@ -123,6 +124,7 @@ export const CREATE_PASSWORD_INVALID_TOKEN = "The invite link is invalid. Please try request a new invite"; export const DELETING_APPLICATION = "Deleting application..."; +export const DUPLICATING_APPLICATION = "Duplicating application..."; export const CURL_IMPORT_SUCCESS = "Curl Import Successfull"; export const FORGOT_PASSWORD_PAGE_LOGIN_LINK = "Back to Login"; @@ -155,3 +157,7 @@ export const SHOW_REQUEST = "Show Request"; export const TABLE_FILTER_COLUMN_TYPE_CALLOUT = "Change column datatype to see filter operators"; + +export const WIDGET_SIDEBAR_TITLE = "Widgets"; +export const WIDGET_SIDEBAR_CAPTION = + "To add a widget, please drag and drop a widget on the canvas to the right"; diff --git a/app/client/src/constants/orgConstants.ts b/app/client/src/constants/orgConstants.ts index 7d76b5857f..05a5a57c77 100644 --- a/app/client/src/constants/orgConstants.ts +++ b/app/client/src/constants/orgConstants.ts @@ -11,6 +11,7 @@ export type Org = { id: string; name: string; website?: string; + email?: string; }; export type OrgUser = { diff --git a/app/client/src/constants/routes.ts b/app/client/src/constants/routes.ts index b0b8394c4a..af0f81ca0f 100644 --- a/app/client/src/constants/routes.ts +++ b/app/client/src/constants/routes.ts @@ -50,19 +50,6 @@ export const BUILDER_PAGE_URL = ( ); }; -export const WIDGETS_URL = ( - applicationId = ":applicationId", - pageId = ":pageId", - params?: Record, -): string => { - if (!pageId) return APPLICATIONS_URL; - const queryParams = convertToQueryParams(params); - return ( - `${BUILDER_BASE_URL(applicationId)}/pages/${pageId}/edit/widgets` + - queryParams - ); -}; - export const API_EDITOR_URL = ( applicationId = ":applicationId", pageId = ":pageId", diff --git a/app/client/src/index.tsx b/app/client/src/index.tsx index 0985723d23..88b4a41dd7 100755 --- a/app/client/src/index.tsx +++ b/app/client/src/index.tsx @@ -9,25 +9,28 @@ import { Slide, ToastContainer } from "react-toastify"; import store from "./store"; import { LayersContext, Layers } from "constants/Layers"; import AppRouter from "./AppRouter"; +import * as Sentry from "@sentry/react"; appInitializer(); const App = () => { return ( - - - - - - - - + + + + + + + + + + ); }; diff --git a/app/client/src/mockResponses/MongoConfigResponse.tsx b/app/client/src/mockResponses/MongoConfigResponse.tsx index 72caacda15..64fc144578 100644 --- a/app/client/src/mockResponses/MongoConfigResponse.tsx +++ b/app/client/src/mockResponses/MongoConfigResponse.tsx @@ -46,6 +46,8 @@ const MongoConfigResponse = [ label: "Host Address", configProperty: "datasourceConfiguration.endpoints[*].host", controlType: "KEYVALUE_ARRAY", + validationMessage: "Please enter a valid host", + validationRegex: "^((?![/:]).)*$", }, { label: "Port", diff --git a/app/client/src/mockResponses/PostgresConfigResponse.tsx b/app/client/src/mockResponses/PostgresConfigResponse.tsx index 0d7e80289d..1c531a85d8 100644 --- a/app/client/src/mockResponses/PostgresConfigResponse.tsx +++ b/app/client/src/mockResponses/PostgresConfigResponse.tsx @@ -33,6 +33,8 @@ const PostgresConfigResponse = [ label: "Host Address", configProperty: "datasourceConfiguration.endpoints[*].host", controlType: "KEYVALUE_ARRAY", + validationMessage: "Please enter a valid host", + validationRegex: "^((?![/:]).)*$", }, { label: "Port", diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index 50ef282c58..5134c091bd 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -23,6 +23,7 @@ import { } from "actions/metaActions"; import AppRoute from "pages/common/AppRoute"; import { editorInitializer } from "utils/EditorUtils"; +import * as Sentry from "@sentry/react"; const AppViewerBody = styled.section` display: flex; @@ -134,5 +135,5 @@ const mapDispatchToProps = (dispatch: any) => ({ }); export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(AppViewer), + connect(mapStateToProps, mapDispatchToProps)(Sentry.withProfiler(AppViewer)), ); diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx index d75e7bf674..9858a0fb03 100644 --- a/app/client/src/pages/Applications/index.tsx +++ b/app/client/src/pages/Applications/index.tsx @@ -11,6 +11,7 @@ import { getCreateApplicationError, getIsDeletingApplication, getUserApplicationsOrgsList, + getIsDuplicatingApplication, } from "selectors/applicationSelectors"; import { ReduxActionTypes, @@ -23,8 +24,6 @@ import ApplicationCard from "./ApplicationCard"; import CreateApplicationForm from "./CreateApplicationForm"; import OrgInviteUsersForm from "pages/organization/OrgInviteUsersForm"; import { PERMISSION_TYPE, isPermitted } from "./permissionHelpers"; -import { DELETING_APPLICATION } from "constants/messages"; -import { AppToaster } from "components/editorComponents/ToastComponent"; import FormDialogComponent from "components/editorComponents/form/FormDialogComponent"; import { User } from "constants/userConstants"; import CustomizedDropdown, { @@ -39,6 +38,7 @@ import { } from "pages/common/CustomizedDropdown/dropdownHelpers"; import { Directions } from "utils/helpers"; import { HeaderIcons } from "icons/HeaderIcons"; +import { duplicateApplication } from "actions/applicationActions"; const OrgDropDown = styled.div` display: flex; @@ -132,7 +132,9 @@ type ApplicationProps = { createApplicationError?: string; searchApplications: (keyword: string) => void; deleteApplication: (id: string) => void; + duplicateApplication: (id: string) => void; deletingApplication: boolean; + duplicatingApplication: boolean; getAllApplication: () => void; userOrgs: any; currentUser?: User; @@ -173,7 +175,7 @@ class Applications extends Component< content: "Organization Settings", onSelect: () => getOnSelectAction(DropdownOnSelectActions.REDIRECT, { - path: `/org/${orgId}/settings`, + path: `/org/${orgId}/settings/general`, }), }, { @@ -187,7 +189,7 @@ class Applications extends Component< content: "Members", onSelect: () => getOnSelectAction(DropdownOnSelectActions.REDIRECT, { - path: `/org/${orgId}/settings`, + path: `/org/${orgId}/settings/members`, }), }, ], @@ -207,9 +209,6 @@ class Applications extends Component< return ( - {this.props.deletingApplication - ? AppToaster.show({ message: DELETING_APPLICATION }) - : AppToaster.clear()} ) ); @@ -341,12 +341,14 @@ class Applications extends Component< ); } } + const mapStateToProps = (state: AppState) => ({ applicationList: getApplicationList(state), isFetchingApplications: getIsFetchingApplications(state), isCreatingApplication: getIsCreatingApplication(state), createApplicationError: getCreateApplicationError(state), deletingApplication: getIsDeletingApplication(state), + duplicatingApplication: getIsDuplicatingApplication(state), userOrgs: getUserApplicationsOrgsList(state), currentUser: getCurrentUser(state), }); @@ -380,6 +382,9 @@ const mapDispatchToProps = (dispatch: any) => ({ }); } }, + duplicateApplication: (applicationId: string) => { + dispatch(duplicateApplication(applicationId)); + }, }); export default connect(mapStateToProps, mapDispatchToProps)(Applications); diff --git a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx index 0d867162b1..04a3846345 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx @@ -184,7 +184,7 @@ class DatasourceDBEditor extends React.Component< } }); - return !_.isEmpty(errors); + return !_.isEmpty(errors) || this.props.invalid; }; render() { diff --git a/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx b/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx index cf43ea3b13..4bab144064 100644 --- a/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Actions/ActionEntity.tsx @@ -3,45 +3,17 @@ import Entity, { EntityClassNames } from "../Entity"; import ActionEntityContextMenu from "./ActionEntityContextMenu"; import history from "utils/history"; import { saveActionName } from "actions/actionActions"; -import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; -import EntityProperty from "../Entity/EntityProperty"; -import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; +import EntityProperties from "../Entity/EntityProperties"; +import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import { ExplorerURLParams } from "../helpers"; +import { useParams } from "react-router"; const getUpdateActionNameReduxAction = (id: string, name: string) => { return saveActionName({ id, name }); }; -const getActionProperties = (action: any, step: number) => { - const config = entityDefinitions.ACTION(action); - - return ( - config && - Object.keys(config) - .filter(k => k.indexOf("!") === -1) - .map((actionProperty: string) => { - let value = action[actionProperty]; - if (actionProperty === "run") { - value = "Function"; - actionProperty = actionProperty + "()"; - } - if (actionProperty === "data") { - value = action.data; - } - return ( - - ); - }) - ); -}; - type ExplorerActionEntityProps = { - action: DataTreeAction; + action: any; url: string; icon: ReactNode; active: boolean; @@ -51,34 +23,41 @@ type ExplorerActionEntityProps = { }; export const ExplorerActionEntity = memo((props: ExplorerActionEntityProps) => { + const { pageId } = useParams(); const switchToAction = useCallback(() => { props.url && history.push(props.url); }, [props.url]); const contextMenu = ( ); return ( - {getActionProperties(props.action, props.step + 1)} + ); }); diff --git a/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx b/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx index c23e357225..dab4b62ce3 100644 --- a/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx @@ -4,14 +4,12 @@ import { Page } from "constants/ReduxActionConstants"; import { ExplorerURLParams, getActionIdFromURL } from "../helpers"; import { ActionGroupConfig } from "./helpers"; import { useParams } from "react-router"; -import { ApiActionConfig } from "entities/Action"; import EntityPlaceholder from "../Entity/Placeholder"; import Entity from "../Entity"; import history from "utils/history"; -import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; type ExplorerActionsGroupProps = { - actions: DataTreeAction[]; + actions: any[]; step: number; searchKeyword?: string; config: ActionGroupConfig; @@ -19,21 +17,21 @@ type ExplorerActionsGroupProps = { }; export const ExplorerActionsGroup = memo((props: ExplorerActionsGroupProps) => { const params = useParams(); - let childNode: ReactNode = props.actions.map((action: DataTreeAction) => { + let childNode: ReactNode = props.actions.map((action: any) => { const url = props.config?.getURL( params.applicationId, props.page.pageId, - action.actionId, + action.config.id, ); const actionId = getActionIdFromURL(); - const active = actionId === action.actionId; + const active = actionId === action.config.id; let method = undefined; - method = (action.config as ApiActionConfig).httpMethod; + method = action.config.actionConfiguration.httpMethod; const icon = props.config?.getIcon(method); return ( { entityId={props.page.pageId + "_" + props.config?.type} step={props.step} disabled={!!props.searchKeyword && (!childNode || !props.actions.length)} - createFn={switchToCreateActionPage} + onCreate={switchToCreateActionPage} isDefaultExpanded={ props.config?.isGroupExpanded(params, props.page.pageId) || !!props.searchKeyword diff --git a/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx b/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx index 86e7322b95..a4be0e4ec5 100644 --- a/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx +++ b/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx @@ -13,7 +13,6 @@ import { import { Page } from "constants/ReduxActionConstants"; import ExplorerActionsGroup from "./ActionsGroup"; import { ExplorerURLParams } from "../helpers"; -import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; export type ActionGroupConfig = { groupName: string; @@ -90,16 +89,16 @@ export const ACTION_PLUGIN_MAP: Array< // APIs, Queries, etc. export const getActionGroups = ( page: Page, - actions: DataTreeAction[], step: number, + actions?: any[], searchKeyword?: string, ) => { return ACTION_PLUGIN_MAP?.map((config?: ActionGroupConfig) => { if (!config) return null; - const entries = actions.filter( - (entry: DataTreeAction) => entry.pluginType === config?.type, + const entries = actions?.filter( + (entry: any) => entry.config.pluginType === config?.type, ); - if (entries.length === 0 && !!searchKeyword) return null; + if (!entries || (entries.length === 0 && !!searchKeyword)) return null; return ( { dispatch(deleteDatasource({ id: props.datasourceId })); }, [dispatch, props.datasourceId]); + const editDatasourceName = useCallback( + () => dispatch(initExplorerEntityNameEdit(props.datasourceId)), + [dispatch, props.datasourceId], + ); return ( { - return updateDatasource({ ...props.datasource, name: name }, active); - }, - [props.datasource, active], - ); + const updateDatasourceName = (id: string, name: string) => + saveDatasourceName({ id, name }); + return ( { const params = useParams(); - const disableDatasourceGroup = - !props.dataSources || !props.dataSources.length; + const plugins = useSelector((state: AppState) => { + return state.entities.plugins.list; + }); + const { datasources } = props; + const disableDatasourceGroup = !datasources || !datasources.length; - const pluginGroups = useMemo(() => groupBy(props.dataSources, "pluginId"), [ - props.dataSources, + const pluginGroups = useMemo(() => groupBy(datasources, "pluginId"), [ + datasources, ]); const pluginGroupNodes: ReactNode[] = []; for (const [pluginId, datasources] of Object.entries(pluginGroups)) { - const plugin = props.plugins.find( - (plugin: Plugin) => plugin.id === pluginId, - ); + const plugin = plugins.find((plugin: Plugin) => plugin.id === pluginId); pluginGroupNodes.push( , ); } + + if (pluginGroupNodes.length === 0 && props.searchKeyword) return null; return ( -1 || !!props.searchKeyword } disabled={disableDatasourceGroup} - createFn={() => { + onCreate={() => { history.push( DATA_SOURCES_EDITOR_URL(params.applicationId, params.pageId), ); diff --git a/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx b/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx new file mode 100644 index 0000000000..ff367c25ba --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Entity/EntityProperties.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import EntityProperty, { EntityPropertyProps } from "./EntityProperty"; +import { isFunction } from "lodash"; +import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; +import { WidgetType } from "constants/WidgetConstants"; +import { + ENTITY_TYPE, + DataTreeAction, + DataTree, +} from "entities/DataTree/dataTreeFactory"; +import { useSelector } from "react-redux"; +import { evaluateDataTreeWithoutFunctions } from "selectors/dataTreeSelectors"; + +export const EntityProperties = (props: { + entityType: ENTITY_TYPE; + entityName: string; + isCurrentPage: boolean; + step: number; + entity?: any; +}) => { + let entity: any; + const dataTree: DataTree = useSelector(evaluateDataTreeWithoutFunctions); + if (props.isCurrentPage && dataTree[props.entityName]) { + entity = dataTree[props.entityName]; + } else if (props.entity) { + entity = props.entity; + } else { + return null; + } + + let config: any; + let entityProperties: Array = []; + switch (props.entityType) { + case ENTITY_TYPE.ACTION: + config = entityDefinitions.ACTION(entity as DataTreeAction); + if (config) { + entityProperties = Object.keys(config) + .filter(k => k.indexOf("!") === -1) + .map((actionProperty: string) => { + let value = entity[actionProperty]; + if (actionProperty === "isLoading") { + value = entity.isLoading; + } + if (actionProperty === "run") { + value = "Function"; + actionProperty = actionProperty + "()"; + } + if (actionProperty === "data") { + value = entity.data; + } + return { + propertyName: actionProperty, + entityName: props.entityName, + value, + step: props.step, + }; + }); + } + break; + case ENTITY_TYPE.WIDGET: + const type: Exclude< + Partial, + "CANVAS_WIDGET" | "ICON_WIDGET" + > = entity.type; + config = entityDefinitions[type]; + + if (isFunction(config)) config = config(entity); + + entityProperties = Object.keys(config) + .filter(k => k.indexOf("!") === -1) + .map(widgetProperty => { + return { + propertyName: widgetProperty, + entityName: entity.widgetName, + value: entity[widgetProperty], + step: props.step, + }; + }); + break; + } + return ( + <> + {entityProperties.map(entityProperty => ( + + ))} + + ); +}; + +export default EntityProperties; diff --git a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx index 9fa513523b..cc0148282a 100644 --- a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx +++ b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useMemo, useState, useEffect } from "react"; +import React, { + useCallback, + useMemo, + useState, + useEffect, + forwardRef, +} from "react"; import { useSelector, useDispatch } from "react-redux"; import styled from "styled-components"; import EditableText, { @@ -17,6 +23,7 @@ const Wrapper = styled.div` text-overflow: ellipsis; white-space: nowrap; margin: 0 4px; + line-height: 13px; & span.token { color: ${Colors.OCEAN_GREEN}; } @@ -60,127 +67,135 @@ export interface EntityNameProps { nameTransformFn?: (input: string, limit?: number) => string; } -export const EntityName = (props: EntityNameProps) => { - const { name, updateEntityName, searchKeyword } = props; - - const nameUpdateError = useSelector((state: AppState) => { - return state.ui.explorer.updateEntityError === props.entityId; - }); - - const [updatedName, setUpdatedName] = useState(name); - - useEffect(() => { - setUpdatedName(name); - }, [name, nameUpdateError]); - - const dispatch = useDispatch(); - - const existingPageNames: string[] = useSelector((state: AppState) => - state.entities.pageList.pages.map((page: Page) => page.pageName), - ); - - const existingWidgetNames: string[] = useSelector((state: AppState) => - Object.values(state.entities.canvasWidgets).map( - widget => widget.widgetName, - ), - ); - - const existingActionNames: string[] = useSelector((state: AppState) => - state.entities.actions.map( - (action: { config: { name: string } }) => action.config.name, - ), - ); - - const hasNameConflict = useCallback( - (newName: string) => - !( - existingPageNames.indexOf(newName) === -1 && - existingActionNames.indexOf(newName) === -1 && - existingWidgetNames.indexOf(newName) === -1 - ), - [existingPageNames, existingActionNames, existingWidgetNames], - ); - - const isInvalidName = useCallback( - (newName: string): string | boolean => { - if (!newName || newName.trim().length === 0) { - return "Please enter a name"; - } else if (newName !== name && hasNameConflict(newName)) { - return `${newName} is already being used.`; - } - return false; - }, - [name, hasNameConflict], - ); - - const handleAPINameChange = useCallback( - (newName: string) => { - if (name && newName !== name && !isInvalidName(newName)) { - setUpdatedName(newName); - dispatch(updateEntityName(newName)); - } - }, - [dispatch, isInvalidName, name, updateEntityName], - ); - - const searchHighlightedName = useMemo(() => { - if (searchKeyword) { - const regex = new RegExp(searchKeyword, "gi"); - const delimited = updatedName.replace(regex, function(str) { - return searchTokenizationDelimiter + str + searchTokenizationDelimiter; - }); - - const final = replace( - delimited, - searchTokenizationDelimiter, - searchHighlightSpanClassName, - ); - return final; - } - return updatedName; - }, [searchKeyword, updatedName]); - - const exitEditMode = useCallback(() => { - dispatch({ - type: ReduxActionTypes.END_EXPLORER_ENTITY_NAME_EDIT, +export const EntityName = forwardRef( + (props: EntityNameProps, ref: React.Ref) => { + const { name, updateEntityName, searchKeyword } = props; + const nameUpdateError = useSelector((state: AppState) => { + return state.ui.explorer.updateEntityError === props.entityId; }); - }, [dispatch]); - const enterEditMode = useCallback( - () => - props.updateEntityName && + const [updatedName, setUpdatedName] = useState(name); + + useEffect(() => { + setUpdatedName(name); + }, [name, nameUpdateError]); + + const existingPageNames: string[] = useSelector((state: AppState) => + state.entities.pageList.pages.map((page: Page) => page.pageName), + ); + + const existingWidgetNames: string[] = useSelector((state: AppState) => + Object.values(state.entities.canvasWidgets).map( + widget => widget.widgetName, + ), + ); + const dispatch = useDispatch(); + + const existingActionNames: string[] = useSelector((state: AppState) => + state.entities.actions.map( + (action: { config: { name: string } }) => action.config.name, + ), + ); + + const hasNameConflict = useCallback( + (newName: string) => + !( + existingPageNames.indexOf(newName) === -1 && + existingActionNames.indexOf(newName) === -1 && + existingWidgetNames.indexOf(newName) === -1 + ), + [existingPageNames, existingActionNames, existingWidgetNames], + ); + + const isInvalidName = useCallback( + (newName: string): string | boolean => { + if (!newName || newName.trim().length === 0) { + return "Please enter a name"; + } else if (newName !== name && hasNameConflict(newName)) { + return `${newName} is already being used.`; + } + return false; + }, + [name, hasNameConflict], + ); + + const handleAPINameChange = useCallback( + (newName: string) => { + if (name && newName !== name && !isInvalidName(newName)) { + setUpdatedName(newName); + dispatch(updateEntityName(newName)); + } + }, + [dispatch, isInvalidName, name, updateEntityName], + ); + + const searchHighlightedName = useMemo(() => { + if (searchKeyword) { + const regex = new RegExp(searchKeyword, "gi"); + const delimited = updatedName.replace(regex, function(str) { + return ( + searchTokenizationDelimiter + str + searchTokenizationDelimiter + ); + }); + + const final = replace( + delimited, + searchTokenizationDelimiter, + searchHighlightSpanClassName, + ); + return final; + } + return updatedName; + }, [searchKeyword, updatedName]); + + const exitEditMode = useCallback(() => { dispatch({ - type: ReduxActionTypes.INIT_EXPLORER_ENTITY_NAME_EDIT, - payload: { - id: props.entityId, - }, - }), - [dispatch, props.entityId, props.updateEntityName], - ); + type: ReduxActionTypes.END_EXPLORER_ENTITY_NAME_EDIT, + }); + }, [dispatch]); - if (!props.isEditing) + const enterEditMode = useCallback( + () => + props.updateEntityName && + dispatch({ + type: ReduxActionTypes.INIT_EXPLORER_ENTITY_NAME_EDIT, + payload: { + id: props.entityId, + }, + }), + [dispatch, props.entityId, props.updateEntityName], + ); + + if (!props.isEditing) + return ( + + {searchHighlightedName} + + ); return ( - - {searchHighlightedName} + + ); - return ( - - - - ); -}; + }, +); + +EntityName.displayName = "EntityName"; export default EntityName; diff --git a/app/client/src/pages/Editor/Explorer/Entity/index.tsx b/app/client/src/pages/Editor/Explorer/Entity/index.tsx index 4eb4eec986..7d79b8dc32 100644 --- a/app/client/src/pages/Editor/Explorer/Entity/index.tsx +++ b/app/client/src/pages/Editor/Explorer/Entity/index.tsx @@ -1,4 +1,10 @@ -import React, { ReactNode, useState, useEffect } from "react"; +import React, { + ReactNode, + useState, + useEffect, + useRef, + forwardRef, +} from "react"; import styled from "styled-components"; import { Colors } from "constants/Colors"; import CollapseToggle from "./CollapseToggle"; @@ -8,6 +14,8 @@ import Collapse from "./Collapse"; import { useEntityUpdateState, useEntityEditState } from "../hooks"; import Loader from "./Loader"; import { Classes } from "@blueprintjs/core"; +import { noop } from "lodash"; +import useClick from "utils/hooks/useClick"; export enum EntityClassNames { CONTEXT_MENU = "entity-context-menu", @@ -74,90 +82,98 @@ export type EntityProps = { action?: () => void; active?: boolean; isDefaultExpanded?: boolean; - createFn?: () => void; + onCreate?: () => void; contextMenu?: ReactNode; searchKeyword?: string; step: number; updateEntityName?: (id: string, name: string) => any; runActionOnExpand?: boolean; - nameTransformFn?: (input: string, limit?: number) => string; + onNameEdit?: (input: string, limit?: number) => string; }; -export const Entity = (props: EntityProps) => { - const [isOpen, open] = useState(!props.disabled && !!props.isDefaultExpanded); - const isUpdating = useEntityUpdateState(props.entityId); - const isEditing = useEntityEditState(props.entityId); - - useEffect(() => { - // If the default state must be expanded, expand to show children - if (props.isDefaultExpanded) { - open(true); - } - if (!props.searchKeyword && !props.isDefaultExpanded) { - open(false); - } - }, [props.isDefaultExpanded, open, props.searchKeyword]); - - const toggleChildren = () => { - // Make sure this entity is enabled before toggling the collpse of children. - !props.disabled && open(!isOpen); - if (props.runActionOnExpand && !isOpen) { - props.action && props.action(); - } - }; - - const updateNameCallback = (name: string) => { - return ( - props.updateEntityName && props.updateEntityName(props.entityId, name) +export const Entity = forwardRef( + (props: EntityProps, ref: React.Ref) => { + const [isOpen, open] = useState( + !props.disabled && !!props.isDefaultExpanded, ); - }; + const isUpdating = useEntityUpdateState(props.entityId); + const isEditing = useEntityEditState(props.entityId); - const handleClick = () => { - if (props.action) props.action(); - else toggleChildren(); - }; + useEffect(() => { + // If the default state must be expanded, expand to show children + if (props.isDefaultExpanded) { + open(true); + } + if (!props.searchKeyword && !props.isDefaultExpanded) { + open(false); + } + }, [props.isDefaultExpanded, open, props.searchKeyword]); - return ( - - { + // Make sure this entity is enabled before toggling the collpse of children. + !props.disabled && open(!isOpen); + if (props.runActionOnExpand && !isOpen) { + props.action && props.action(); + } + }; + + const updateNameCallback = (name: string) => { + return ( + props.updateEntityName && props.updateEntityName(props.entityId, name) + ); + }; + + const handleClick = () => { + if (props.action) props.action(); + else toggleChildren(); + }; + + const itemRef = useRef(null); + useClick(itemRef, handleClick, noop); + + return ( + - - {props.icon} - - - {props.contextMenu} - - - - {props.children} - - - ); -}; + + + {props.icon} + + + {props.contextMenu} + + + + {props.children} + + + ); + }, +); Entity.displayName = "Entity"; diff --git a/app/client/src/pages/Editor/Explorer/ExplorerSearch.tsx b/app/client/src/pages/Editor/Explorer/ExplorerSearch.tsx index 91d12b394e..c8dab06ef1 100644 --- a/app/client/src/pages/Editor/Explorer/ExplorerSearch.tsx +++ b/app/client/src/pages/Editor/Explorer/ExplorerSearch.tsx @@ -63,15 +63,19 @@ const Underline = styled.div` `; /*eslint-disable react/display-name */ export const ExplorerSearch = forwardRef( - (props: { clear: () => void }, ref: Ref) => { + ( + props: { clear: () => void; placeholder?: string }, + ref: Ref, + ) => { return ( diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx index 1da6b8afbd..8e7aa65992 100644 --- a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx @@ -1,29 +1,37 @@ -import React, { useCallback, memo } from "react"; +import React, { useCallback } from "react"; import { Page } from "constants/ReduxActionConstants"; -import { WidgetTree } from "../Widgets/WidgetEntity"; import Entity, { EntityClassNames } from "../Entity"; -import { pageIcon, homePageIcon } from "../ExplorerIcons"; import { useParams } from "react-router"; import { ExplorerURLParams } from "../helpers"; -import { getActionGroups } from "../Actions/helpers"; import { BUILDER_PAGE_URL } from "constants/routes"; import history from "utils/history"; import { updatePage } from "actions/pageActions"; import PageContextMenu from "./PageContextMenu"; -import ExplorerWidgetGroup from "../Widgets/WidgetGroup"; +import { useSelector } from "react-redux"; +import { AppState } from "reducers"; +import { WidgetProps } from "widgets/BaseWidget"; import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; +import { homePageIcon, pageIcon } from "../ExplorerIcons"; +import { getActionGroups } from "../Actions/helpers"; +import ExplorerWidgetGroup from "../Widgets/WidgetGroup"; import { resolveAsSpaceChar } from "utils/helpers"; type ExplorerPageEntityProps = { page: Page; - isCurrentPage: boolean; - widgets?: WidgetTree; - actions: DataTreeAction[]; + widgets?: WidgetProps; + actions: any[]; step: number; searchKeyword?: string; + showWidgetsSidebar: () => void; }; -export const ExplorerPageEntity = memo((props: ExplorerPageEntityProps) => { +export const ExplorerPageEntity = (props: ExplorerPageEntityProps) => { const params = useParams(); + + const currentPageId = useSelector((state: AppState) => { + return state.entities.pageList.currentPageId; + }); + const isCurrentPage = currentPageId === props.page.pageId; + const switchPage = useCallback(() => { if (!!params.applicationId) { history.push(BUILDER_PAGE_URL(params.applicationId, props.page.pageId)); @@ -40,7 +48,12 @@ export const ExplorerPageEntity = memo((props: ExplorerPageEntityProps) => { isDefaultPage={props.page.isDefault} /> ); + const icon = props.page.isDefault ? homePageIcon : pageIcon; + + let addWidgetsFn; + if (isCurrentPage) addWidgetsFn = props.showWidgetsSidebar; + return ( { step={props.step} action={switchPage} entityId={props.page.pageId} - active={props.isCurrentPage} - isDefaultExpanded={props.isCurrentPage || !!props.searchKeyword} + active={isCurrentPage} + isDefaultExpanded={isCurrentPage || !!props.searchKeyword} updateEntityName={updatePage} contextMenu={contextMenu} - nameTransformFn={resolveAsSpaceChar} + onNameEdit={resolveAsSpaceChar} > - {!(!props.widgets && props.searchKeyword) && ( - - )} + + {getActionGroups( props.page, - props.actions, props.step + 1, + props.actions as DataTreeAction[], props.searchKeyword, )} ); -}); +}; ExplorerPageEntity.displayName = "ExplorerPageEntity"; diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx index 641f872bb7..d9be1dfc07 100644 --- a/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx @@ -1,61 +1,58 @@ -import React, { useCallback, useMemo } from "react"; +import React, { useCallback } from "react"; import Entity from "../Entity"; import { pageGroupIcon } from "../ExplorerIcons"; -import { noop, compact } from "lodash"; -import { useDispatch } from "react-redux"; +import { noop } from "lodash"; +import { useDispatch, useSelector } from "react-redux"; import { getNextEntityName } from "utils/AppsmithUtils"; import { createPage } from "actions/pageActions"; import { useParams } from "react-router"; import { ExplorerURLParams } from "../helpers"; import { Page } from "constants/ReduxActionConstants"; -import { WidgetTree } from "../Widgets/WidgetEntity"; import ExplorerPageEntity from "./PageEntity"; -import { DataTreeAction } from "entities/DataTree/dataTreeFactory"; +import { AppState } from "reducers"; +import { WidgetProps } from "widgets/BaseWidget"; type ExplorerPageGroupProps = { - pages: Page[]; - widgets?: (WidgetTree | undefined)[]; - actions: DataTreeAction[]; - currentPageId?: string; searchKeyword?: string; step: number; + widgets?: Record; + actions: Record; + showWidgetsSidebar: () => void; }; export const ExplorerPageGroup = (props: ExplorerPageGroupProps) => { const dispatch = useDispatch(); const params = useParams(); + const pages = useSelector((state: AppState) => { + return state.entities.pageList.pages; + }); const createPageCallback = useCallback(() => { const name = getNextEntityName( "Page", - props.pages.map((page: Page) => page.pageName), + pages.map((page: Page) => page.pageName), ); dispatch(createPage(params.applicationId, name)); - }, [dispatch, props.pages, params.applicationId]); + }, [dispatch, pages, params.applicationId]); - const pageEntityList = useMemo( - () => - compact( - props.pages.map((page: Page) => { - const widgets = props.widgets?.find( - (tree?: WidgetTree) => tree && tree.pageId === page.pageId, - ); - const actions = props.actions.filter( - (action: DataTreeAction & { pageId?: string }) => - action.pageId === page.pageId, - ); - if ( - (!widgets || widgets.length === 0) && - actions.length === 0 && - props.searchKeyword - ) { - return null; - } - return { page, widgets, actions }; - }), - ), - [props.widgets, props.actions, props.pages, props.searchKeyword], - ); + const pageEntities = pages.map(page => { + const pageWidgets = props.widgets && props.widgets[page.pageId]; + const pageActions = props.actions[page.pageId] || []; + if (!pageWidgets && pageActions.length === 0) return null; + return ( + + ); + }); + + if (pageEntities.filter(Boolean).length === 0) return null; return ( { action={noop} entityId="Pages" step={props.step} - createFn={createPageCallback} + onCreate={createPageCallback} > - {pageEntityList.map(({ page, widgets, actions }) => { - return ( - - ); - })} + {pageEntities} ); }; diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx index c4e6bc9c35..cf4b2af0bb 100644 --- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx @@ -16,11 +16,13 @@ import { import { useWidgetSelection } from "utils/hooks/dragResizeHooks"; import { AppState } from "reducers"; import { getWidgetIcon } from "../ExplorerIcons"; -import EntityProperty, { EntityPropertyProps } from "../Entity/EntityProperty"; +import { EntityPropertyProps } from "../Entity/EntityProperty"; import { entityDefinitions } from "utils/autocomplete/EntityDefinitions"; import { isFunction, noop } from "lodash"; import WidgetContextMenu from "./WidgetContextMenu"; import { updateWidgetName } from "actions/propertyPaneActions"; +import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import EntityProperties from "../Entity/EntityProperties"; export type WidgetTree = WidgetProps & { children?: WidgetTree[] }; @@ -120,7 +122,8 @@ export type WidgetEntityProps = { }; export const WidgetEntity = memo((props: WidgetEntityProps) => { - const params = useParams(); + const { pageId } = useParams(); + const { navigateToWidget, isWidgetSelected } = useWidget( props.widgetProps.widgetId, props.widgetProps.type, @@ -133,12 +136,15 @@ export const WidgetEntity = memo((props: WidgetEntityProps) => { let children: ReactNode = props.children; if (!props.children) { - children = getWidgetProperies( - props.widgetProps, - props.step + 1, - ).map((widgetProperty: EntityPropertyProps) => ( - - )); + children = ( + + ); } const contextMenu = ( @@ -159,15 +165,13 @@ export const WidgetEntity = memo((props: WidgetEntityProps) => { active={isWidgetSelected} entityId={props.widgetProps.widgetId} step={props.step} - updateEntityName={ - props.pageId === params?.pageId ? updateWidgetName : noop - } + updateEntityName={props.pageId === pageId ? updateWidgetName : noop} searchKeyword={props.searchKeyword} isDefaultExpanded={ (!!props.searchKeyword && !!props.widgetProps.children) || !!props.isDefaultExpanded } - contextMenu={props.pageId === params?.pageId && contextMenu} + contextMenu={props.pageId === pageId && contextMenu} > {children} diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx index e5ee5029c5..660b1ae607 100644 --- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx @@ -10,7 +10,7 @@ import { } from "constants/WidgetConstants"; import { useParams } from "react-router"; import { ExplorerURLParams } from "../helpers"; -import { BUILDER_PAGE_URL, WIDGETS_URL } from "constants/routes"; +import { BUILDER_PAGE_URL } from "constants/routes"; import { Link } from "react-router-dom"; import styled from "styled-components"; import { AppState } from "reducers"; @@ -24,11 +24,7 @@ const getWidgetEntity = ( widgetIdsToExpand?: string[], ) => { if (!entity) { - if (searchKeyword) { - return ; - } else { - return; - } + return null; } if (entity.type === WidgetTypes.CANVAS_WIDGET) { if (!entity.children || entity.children.length === 0) return; @@ -110,8 +106,9 @@ const useWidgetExpandList = ( type ExplorerWidgetGroupProps = { pageId: string; step: number; - widgets: WidgetTree | null; + widgets?: WidgetTree; searchKeyword?: string; + addWidgetsFn?: () => void; }; const StyledLink = styled(Link)` @@ -159,21 +156,17 @@ export const ExplorerWidgetGroup = memo((props: ExplorerWidgetGroupProps) => { ) : ( " " )} - click the{" "} - - - Widgets - - {" "} - navigation menu icon on the left to drag and drop widgets + click the + icon on the Widgets group + to drag and drop widgets ); - } + } else if (!childNode && props.searchKeyword) return null; + return ( { !!props.searchKeyword || (params.pageId === props.pageId && !!selectedWidget) } + onCreate={props.addWidgetsFn} > {childNode} diff --git a/app/client/src/pages/Editor/Explorer/hooks.ts b/app/client/src/pages/Editor/Explorer/hooks.ts index b0e62e7807..be94a343de 100644 --- a/app/client/src/pages/Editor/Explorer/hooks.ts +++ b/app/client/src/pages/Editor/Explorer/hooks.ts @@ -7,40 +7,25 @@ import { } from "react"; import { useSelector } from "react-redux"; import { AppState } from "reducers"; -import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; -import { - ENTITY_TYPE, - DataTreeEntity, - DataTree, - DataTreeAction, -} from "entities/DataTree/dataTreeFactory"; -import { compact } from "lodash"; +import { compact, groupBy } from "lodash"; import { Datasource } from "api/DatasourcesApi"; import { debounce } from "lodash"; import { WidgetProps } from "widgets/BaseWidget"; -import { evaluateDataTreeWithoutFunctions } from "selectors/dataTreeSelectors"; -import { ActionData } from "reducers/entityReducers/actionsReducer"; import log from "loglevel"; +import produce from "immer"; const findWidgets = (widgets: WidgetProps, keyword: string) => { + if (!widgets || !widgets.widgetName) return widgets; + const widgetNameMached = + widgets.widgetName.toLowerCase().indexOf(keyword) > -1; if (widgets.children) { widgets.children = compact( widgets.children.map((widget: WidgetProps) => findWidgets(widget, keyword), ), ); - return widgets.children.length > 0 || - widgets.widgetName.toLowerCase().indexOf(keyword) > -1 - ? widgets - : undefined; } - if (widgets.widgetName.toLowerCase().indexOf(keyword) > -1) return widgets; -}; - -const findActions = (actions: Array, keyword: string) => { - return actions.filter( - (action: DataTreeAction) => action.name.toLowerCase().indexOf(keyword) > -1, - ); + if (widgetNameMached || widgets.children?.length > 0) return widgets; }; const findDataSources = (dataSources: Datasource[], keyword: string) => { @@ -50,118 +35,82 @@ const findDataSources = (dataSources: Datasource[], keyword: string) => { ); }; +export const useFilteredDatasources = (searchKeyword?: string) => { + const dataSources = useSelector((state: AppState) => { + return state.entities.datasources.list; + }); + + return useMemo( + () => + searchKeyword + ? findDataSources(dataSources, searchKeyword.toLowerCase()) + : dataSources, + [searchKeyword, dataSources], + ); +}; + +export const useActions = (searchKeyword?: string) => { + const reducerActions = useSelector( + (state: AppState) => state.entities.actions, + ); + + const actions = useMemo(() => { + return groupBy(reducerActions, "config.pageId"); + }, [reducerActions]); + + return useMemo(() => { + if (searchKeyword) { + const start = performance.now(); + const filteredActions = produce(actions, draft => { + for (const [key, value] of Object.entries(draft)) { + value.forEach((action, index) => { + const searchMatches = + action.config.name + .toLowerCase() + .indexOf(searchKeyword.toLowerCase()) > -1; + if (searchMatches) { + draft[key][index] = action; + } else { + delete draft[key][index]; + } + }); + draft[key] = draft[key].filter(Boolean); + } + }); + log.debug("Filtered actions in:", performance.now() - start, "ms"); + return filteredActions; + } + return actions; + }, [searchKeyword, actions]); +}; + +export const useWidgets = (searchKeyword?: string) => { + const pageDSLs = useSelector((state: AppState) => state.ui.pageDSLs); + return useMemo(() => { + if (searchKeyword && pageDSLs) { + const start = performance.now(); + const filteredDSLs = produce(pageDSLs, draft => { + for (const [key, value] of Object.entries(draft)) { + const filteredWidgets = findWidgets( + value, + searchKeyword.toLowerCase(), + ) as WidgetProps; + draft[key] = filteredWidgets; + } + }); + log.debug("Filtered widgets in: ", performance.now() - start, "ms"); + return filteredDSLs; + } + return pageDSLs; + }, [searchKeyword, pageDSLs]); +}; + export const useFilteredEntities = ( ref: MutableRefObject, ) => { const start = performance.now(); const [searchKeyword, setSearchKeyword] = useState(null); - const dataTree: DataTree = useSelector(evaluateDataTreeWithoutFunctions); - const pages = useSelector((state: AppState) => { - return state.entities.pageList.pages; - }); - - const currentPageId = useSelector((state: AppState) => { - return state.entities.pageList.currentPageId; - }); - - const dataSources = useSelector((state: AppState) => { - return state.entities.datasources.list; - }); - const plugins = useSelector((state: AppState) => { - return state.entities.plugins.list; - }); - - const currentPageWidgetEntities = useMemo(() => { - const canvasWidgets: { [id: string]: any } = {}; - Object.values(dataTree).forEach( - ( - entity: DataTreeEntity & { - ENTITY_TYPE?: ENTITY_TYPE; - widgetId?: string; - }, - ) => { - if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET && entity.widgetId) { - canvasWidgets[entity.widgetId] = entity; - } - }, - ); - - const widgetTree = CanvasWidgetsNormalizer.denormalize("0", { - canvasWidgets, - }); - widgetTree.pageId = currentPageId; - - return searchKeyword !== null - ? findWidgets(widgetTree, searchKeyword.toLowerCase()) - : widgetTree; - }, [searchKeyword, dataTree, currentPageId]); - - const allPageDSLs = useSelector((state: AppState) => state.ui.pageDSLs); - const otherPagesWidgetEntities = useMemo(() => { - return Object.keys(allPageDSLs) - .filter((pageId: string) => pageId !== currentPageId) - .map((pageId: string) => { - const tree = allPageDSLs[pageId]; - tree.pageId = pageId; - - return searchKeyword !== null - ? findWidgets(tree, searchKeyword.toLowerCase()) - : tree; - }); - }, [searchKeyword, allPageDSLs, currentPageId]); - - const actions = useMemo( - () => - Object.values(dataTree).filter( - (entity: DataTreeEntity & { ENTITY_TYPE?: ENTITY_TYPE }) => - entity.ENTITY_TYPE === ENTITY_TYPE.ACTION, - ), - [dataTree], - ); - - const allAppActions = useSelector( - (state: AppState) => state.entities.actions, - ); - - const actionEntities = useMemo(() => { - const otherPageDataTreeActions: DataTreeAction[] = allAppActions - .filter((action: ActionData) => action.config.pageId !== currentPageId) - .map((action: ActionData) => ({ - isLoading: action.isLoading, - actionId: action.config.id, - pluginType: action.config.pluginType, - name: action.config.name, - pageId: action.config.pageId, - run: {}, - dynamicBindingPathList: action.config.dynamicBindingPathList, - ENTITY_TYPE: ENTITY_TYPE.ACTION, - data: action.data || {}, - config: { - paginationType: action.config.actionConfiguration.paginationType, - timeoutInMillisecond: - action.config.actionConfiguration.timeoutInMillisecond, - httpMethod: action.config.actionConfiguration.httpMethod, - }, - })); - const currentPageActions = actions.map(action => ({ - ...action, - pageId: currentPageId, - })); - const allActions = [...currentPageActions, ...otherPageDataTreeActions]; - return searchKeyword !== null - ? findActions(allActions as DataTreeAction[], searchKeyword.toLowerCase()) - : allActions; - }, [searchKeyword, actions, allAppActions, currentPageId]); - - const datasourceEntities = useMemo( - () => - searchKeyword !== null - ? findDataSources(dataSources, searchKeyword.toLowerCase()) - : dataSources, - [searchKeyword, dataSources], - ); - const search = debounce((e: any) => { const keyword = e.target.value; if (keyword.trim().length > 0) { @@ -191,20 +140,10 @@ export const useFilteredEntities = ( el?.dispatchEvent(event); } }, [ref, event]); - const allWidgetEntities = useMemo( - () => compact([currentPageWidgetEntities, ...otherPagesWidgetEntities]), - [currentPageWidgetEntities, otherPagesWidgetEntities], - ); const stop = performance.now(); log.debug("Explorer hook props calculations took", stop - start, "ms"); return { - widgets: allWidgetEntities, - actions: actionEntities as DataTreeAction[], - dataSources: datasourceEntities, - currentPageId, - plugins, - pages, searchKeyword: searchKeyword ?? undefined, clearSearch, }; diff --git a/app/client/src/pages/Editor/Explorer/index.tsx b/app/client/src/pages/Editor/Explorer/index.tsx index f78e2116ce..9ca0a51f7f 100644 --- a/app/client/src/pages/Editor/Explorer/index.tsx +++ b/app/client/src/pages/Editor/Explorer/index.tsx @@ -1,13 +1,22 @@ -import React, { useRef, MutableRefObject } from "react"; +import React, { useRef, MutableRefObject, useCallback } from "react"; import styled from "styled-components"; import Divider from "components/editorComponents/Divider"; -import { useFilteredEntities } from "./hooks"; +import { + useFilteredEntities, + useWidgets, + useActions, + useFilteredDatasources, +} from "./hooks"; import Search from "./ExplorerSearch"; import ExplorerPageGroup from "./Pages/PageGroup"; import ExplorerDatasourcesGroup from "./Datasources/DatasourcesGroup"; import { scrollbarDark } from "constants/DefaultTheme"; -import { NonIdealState, Classes } from "@blueprintjs/core"; - +import { NonIdealState, Classes, IPanelProps } from "@blueprintjs/core"; +import WidgetSidebar from "../WidgetSidebar"; +import { BUILDER_PAGE_URL } from "constants/routes"; +import history from "utils/history"; +import { useParams } from "react-router"; +import { ExplorerURLParams } from "./helpers"; const Wrapper = styled.div` height: 100%; overflow-y: scroll; @@ -24,68 +33,59 @@ const StyledDivider = styled(Divider)` border-bottom-color: rgba(255, 255, 255, 0.1); `; -const EntityExplorer = () => { +const EntityExplorer = (props: IPanelProps) => { + const { applicationId, pageId } = useParams(); const searchInputRef: MutableRefObject = useRef( null, ); - const { - widgets, - actions, - dataSources, - currentPageId, - pages, - plugins, - searchKeyword, - clearSearch, - } = useFilteredEntities(searchInputRef); - const explorerPageGroup = ( - - ); + const explorerRef = useRef(null); + const { searchKeyword, clearSearch } = useFilteredEntities(searchInputRef); + const datasources = useFilteredDatasources(searchKeyword); - const datasourcesGroup = ( - - ); + const widgets = useWidgets(searchKeyword); + const actions = useActions(searchKeyword); - const noResults = - widgets.length === 0 && - actions.length === 0 && - dataSources.length === 0 && - !!searchKeyword; + let noResults = false; + if (searchKeyword) { + const noWidgets = Object.values(widgets).filter(Boolean).length === 0; + const noActions = + Object.values(actions).filter(actions => actions && actions.length > 0) + .length === 0; - const noPageEntities = - widgets.length === 0 && actions.length === 0 && !!searchKeyword; - - const noDatsourceEntities = dataSources.length === 0 && !!searchKeyword; - - const noResultMessage = ( - - ); + const noDatasource = !datasources || datasources.length === 0; + noResults = noWidgets && noActions && noDatasource; + } + const { openPanel } = props; + const showWidgetsSidebar = useCallback(() => { + history.push(BUILDER_PAGE_URL(applicationId, pageId)); + openPanel({ component: WidgetSidebar }); + }, [openPanel, applicationId, pageId]); return ( - + - {!noPageEntities && explorerPageGroup} - {noResults && noResultMessage} + + {noResults && ( + + )} - {!noDatsourceEntities && datasourcesGroup} + ); }; diff --git a/app/client/src/pages/Editor/MainContainer.tsx b/app/client/src/pages/Editor/MainContainer.tsx index fcf2889710..2c2edb848e 100644 --- a/app/client/src/pages/Editor/MainContainer.tsx +++ b/app/client/src/pages/Editor/MainContainer.tsx @@ -1,8 +1,8 @@ import React from "react"; import EditorsRouter from "./routes"; -import Navbar from "./Navbar"; import WidgetsEditor from "./WidgetsEditor"; import styled from "styled-components"; +import Sidebar from "components/editorComponents/Sidebar"; const Container = styled.div` display: flex; @@ -17,7 +17,7 @@ const EditorContainer = styled.div` const MainContainer = () => { return ( - + diff --git a/app/client/src/pages/Editor/Navbar.tsx b/app/client/src/pages/Editor/Navbar.tsx deleted file mode 100644 index b1ed90beb8..0000000000 --- a/app/client/src/pages/Editor/Navbar.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from "react"; -import { useParams } from "react-router-dom"; -import styled from "styled-components"; -import SidebarComponent from "components/editorComponents/Sidebar"; -import NavBarItem from "components/editorComponents/NavBarItem"; -import { - BuilderRouteParams, - BUILDER_PAGE_URL, - WIDGETS_URL, -} from "constants/routes"; -import { Colors } from "constants/Colors"; -import { MenuIcons } from "icons/MenuIcons"; - -const Wrapper = styled.div` - display: grid; - grid-template-columns: 1fr 5fr; - width: ${props => props.theme.sidebarWidth}; - box-shadow: 0px 1px 3px ${Colors.MINE_SHAFT}; - background-color: ${Colors.MINE_SHAFT}; - z-index: 3; -`; - -const NavBar = styled.div` - background-color: ${Colors.MINE_SHAFT}; - color: ${props => props.theme.colors.textOnDarkBG}; - display: flex; - flex-direction: column; - box-shadow: 0px 0px 4px ${Colors.CODE_GRAY}; -`; - -const Navbar = () => { - const params = useParams(); - - return ( - - - { - // Currently, the explorer shows on all paths except for the - // WIDGETS_URL path - - // get the applicationId and pageId from the current location pathname - const found = current.match( - /^\/applications\/(?\w+)\/pages\/(?\w+)\//, - ); - // In this case: expected = BUILDER_PAGE_URL(applicationId, pageId) - // If current url begins with expected url AND - // If the current url isn't the WIDGETS_URL THEN - // this is an explorer sidebar path - return ( - current.indexOf(expected) === 0 && - current !== - WIDGETS_URL(found?.groups?.applicationId, found?.groups?.pageId) - ); - }} - /> - expected === current} - /> - - - - ); -}; -Navbar.displayName = "Navbar"; - -Navbar.whyDidYouRender = { - logOnDifferentValues: false, - customName: "MainSidebar", -}; - -export default Navbar; diff --git a/app/client/src/pages/Editor/WidgetSidebar.tsx b/app/client/src/pages/Editor/WidgetSidebar.tsx index e3b5445942..5e94818a78 100644 --- a/app/client/src/pages/Editor/WidgetSidebar.tsx +++ b/app/client/src/pages/Editor/WidgetSidebar.tsx @@ -1,14 +1,16 @@ -import React from "react"; +import React, { useRef, useEffect, useState } from "react"; import { useSelector } from "react-redux"; import WidgetCard from "./WidgetCard"; import styled from "styled-components"; import { WidgetCardProps } from "widgets/BaseWidget"; import { getWidgetCards } from "selectors/editorSelectors"; import { getColorWithOpacity } from "constants/DefaultTheme"; - -type WidgetSidebarProps = { - cards: { [id: string]: WidgetCardProps[] }; -}; +import { IPanelProps, Icon, Classes } from "@blueprintjs/core"; +import { Colors } from "constants/Colors"; +import ExplorerSearch from "./Explorer/ExplorerSearch"; +import { debounce } from "lodash"; +import produce from "immer"; +import { WIDGET_SIDEBAR_CAPTION } from "constants/messages"; const MainWrapper = styled.div` text-transform: capitalize; @@ -43,22 +45,108 @@ const CardsWrapper = styled.div` align-items: stretch; `; -const WidgetSidebar = () => { +const CloseIcon = styled(Icon)` + &&.${Classes.ICON} { + cursor: pointer; + display: flex; + justify-content: center; + opacity: 0.6; + &:hover { + opacity: 1; + } + } +`; + +const Header = styled.div` + display: grid; + grid-template-columns: 7fr 1fr; +`; + +const Info = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: space-around; + text-transform: none; + h4 { + margin-top: 0px; + } + p { + opacity: 0.6; + } +`; + +const WidgetSidebar = (props: IPanelProps) => { const cards = useSelector(getWidgetCards); - const groups = Object.keys(cards); + const [filteredCards, setFilteredCards] = useState(cards); + const searchInputRef = useRef(null); + const clearSearchInput = () => { + if (searchInputRef.current) searchInputRef.current.value = ""; + }; + const search = debounce((e: any) => { + const keyword = e.target.value.toLowerCase(); + let filteredCards = cards; + if (keyword.trim().length > 0) { + filteredCards = produce(cards, draft => { + for (const [key, value] of Object.entries(cards)) { + value.forEach((card, index) => { + if (card.widgetCardName.toLowerCase().indexOf(keyword) === -1) { + delete draft[key][index]; + } + }); + draft[key] = draft[key].filter(Boolean); + if (draft[key].length === 0) { + delete draft[key]; + } + } + }); + } + setFilteredCards(filteredCards); + }, 300); + useEffect(() => { + const el: HTMLInputElement | null = searchInputRef.current; + + el?.addEventListener("keydown", search); + el?.addEventListener("cleared", search); + return () => { + el?.removeEventListener("keydown", search); + el?.removeEventListener("cleared", search); + }; + }, [searchInputRef, search]); + const groups = Object.keys(filteredCards); return ( - - {groups.map((group: string) => ( - -
{group}
- - {cards[group].map((card: WidgetCardProps) => ( - - ))} - -
- ))} -
+ <> + + + +
+ +

{WIDGET_SIDEBAR_CAPTION}

+
+ +
+ {groups.map((group: string) => ( + +
{group}
+ + {filteredCards[group].map((card: WidgetCardProps) => ( + + ))} + +
+ ))} +
+ ); }; diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index 6f46282c0c..44a9ac159b 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -5,7 +5,6 @@ import { withRouter, RouteComponentProps } from "react-router-dom"; import { BuilderRouteParams, getApplicationViewerPageURL, - BUILDER_PAGE_URL, } from "constants/routes"; import { AppState } from "reducers"; import MainContainer from "./MainContainer"; @@ -32,14 +31,14 @@ import { initEditor } from "actions/initActions"; import { editorInitializer } from "utils/EditorUtils"; import { ENTITY_EXPLORER_SEARCH_ID, - ENTITY_EXPLORER_SEARCH_LOCATION_HASH, + WIDGETS_SEARCH_ID, } from "constants/Explorer"; -import history from "utils/history"; import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; import { getAppsmithConfigs } from "configs"; import { getCurrentUser } from "selectors/usersSelectors"; import { User } from "constants/userConstants"; import ConfirmRunModal from "pages/Editor/ConfirmRunModal"; +import * as Sentry from "@sentry/react"; const { cloudHosting, intercomAppID } = getAppsmithConfigs(); @@ -65,19 +64,14 @@ class Editor extends Component { combo="meta + f" label="Search entities" onKeyDown={(e: any) => { - //TODO(abhinav): make this id into a constant. - const el = document.getElementById(ENTITY_EXPLORER_SEARCH_ID); - if (!el) { - history.push( - `${BUILDER_PAGE_URL( - this.props.currentApplicationId, - this.props.currentPageId, - )}${ENTITY_EXPLORER_SEARCH_LOCATION_HASH}`, - ); - } else { - el?.focus(); - } - + const entitySearchInput = document.getElementById( + ENTITY_EXPLORER_SEARCH_ID, + ); + const widgetSearchInput = document.getElementById( + WIDGETS_SEARCH_ID, + ); + if (entitySearchInput) entitySearchInput.focus(); + if (widgetSearchInput) widgetSearchInput.focus(); e.preventDefault(); e.stopPropagation(); }} @@ -216,4 +210,6 @@ const mapDispatchToProps = (dispatch: any) => { }; }; -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Editor)); +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(Sentry.withProfiler(Editor)), +); diff --git a/app/client/src/pages/Editor/routes.tsx b/app/client/src/pages/Editor/routes.tsx index c632b60056..de253e8c90 100644 --- a/app/client/src/pages/Editor/routes.tsx +++ b/app/client/src/pages/Editor/routes.tsx @@ -19,7 +19,6 @@ import { getCurlImportPageURL, API_EDITOR_URL_WITH_SELECTED_PAGE_ID, getProviderTemplatesURL, - WIDGETS_URL, } from "constants/routes"; import styled from "styled-components"; import AppRoute from "pages/common/AppRoute"; @@ -63,8 +62,7 @@ class EditorsRouter extends React.Component< this.state = { isVisible: this.props.location.pathname !== - BUILDER_PAGE_URL(applicationId, pageId) && - this.props.location.pathname !== WIDGETS_URL(applicationId, pageId), + BUILDER_PAGE_URL(applicationId, pageId), }; } @@ -74,8 +72,7 @@ class EditorsRouter extends React.Component< this.setState({ isVisible: this.props.location.pathname !== - BUILDER_PAGE_URL(applicationId, pageId) && - this.props.location.pathname !== WIDGETS_URL(applicationId, pageId), + BUILDER_PAGE_URL(applicationId, pageId), }); } } diff --git a/app/client/src/pages/UserAuth/SignUp.tsx b/app/client/src/pages/UserAuth/SignUp.tsx index 653940f63a..ac31e60146 100644 --- a/app/client/src/pages/UserAuth/SignUp.tsx +++ b/app/client/src/pages/UserAuth/SignUp.tsx @@ -49,7 +49,7 @@ import AnalyticsUtil from "utils/AnalyticsUtil"; import { getAppsmithConfigs } from "configs"; import { SIGNUP_SUBMIT_PATH } from "constants/ApiConstants"; import { connect } from "react-redux"; -import { AppState } from "@appsmith/reducers"; +import { AppState } from "reducers"; const { enableGithubOAuth, enableGoogleOAuth, diff --git a/app/client/src/pages/common/AppRoute.tsx b/app/client/src/pages/common/AppRoute.tsx index 0b446f84dd..ae0b920c97 100644 --- a/app/client/src/pages/common/AppRoute.tsx +++ b/app/client/src/pages/common/AppRoute.tsx @@ -1,6 +1,9 @@ import React, { useEffect } from "react"; import { Route } from "react-router-dom"; import AnalyticsUtil from "utils/AnalyticsUtil"; +import * as Sentry from "@sentry/react"; + +const SentryRoute = Sentry.withSentryRouting(Route); const AppRoute = ({ component: Component, @@ -22,7 +25,7 @@ const AppRoute = ({ } }, [rest.name, rest.logDisable, rest.location.pathname]); return ( - { return ; diff --git a/app/client/src/pages/common/PageWrapper.tsx b/app/client/src/pages/common/PageWrapper.tsx index d474cdd9e4..9e7b3da994 100644 --- a/app/client/src/pages/common/PageWrapper.tsx +++ b/app/client/src/pages/common/PageWrapper.tsx @@ -1,8 +1,12 @@ import React, { ReactNode } from "react"; import { Helmet } from "react-helmet"; import styled from "styled-components"; +import { useLocation } from "react-router"; -const Wrapper = styled.section` +const Wrapper = styled.section<{ + background: string; +}>` + background: ${props => props.background}; && .fade { position: relative; } @@ -34,7 +38,8 @@ const PageBody = styled.div` flex-direction: column; justify-content: flex-start; align-items: flex-start; - margin: ${props => props.theme.spaces[12]}px auto; + padding-top: ${props => props.theme.spaces[12]}px; + margin: 0 auto; & > * { width: 100%; } @@ -45,15 +50,20 @@ type PageWrapperProps = { displayName?: string; }; -export const PageWrapper = (props: PageWrapperProps) => ( - - - {`${ - props.displayName ? `${props.displayName} | ` : "" - }Appsmith`} - - {props.children} - -); +export const PageWrapper = (props: PageWrapperProps) => { + const location = useLocation(); + const isSettingsPage = location.pathname.indexOf("settings") !== -1; + + return ( + + + {`${ + props.displayName ? `${props.displayName} | ` : "" + }Appsmith`} + + {props.children} + + ); +}; export default PageWrapper; diff --git a/app/client/src/pages/organization/General.tsx b/app/client/src/pages/organization/General.tsx new file mode 100644 index 0000000000..8c6131bf42 --- /dev/null +++ b/app/client/src/pages/organization/General.tsx @@ -0,0 +1,123 @@ +import React from "react"; + +import { saveOrg } from "actions/orgActions"; +import { SaveOrgRequest } from "api/OrgApi"; +import { throttle } from "lodash"; +import TextInput, { + emailValidator, + notEmptyValidator, +} from "components/ads/TextInput"; +import { useSelector, useDispatch } from "react-redux"; +import { getCurrentOrg } from "selectors/organizationSelectors"; +import { useParams } from "react-router-dom"; +import styled from "styled-components"; +import Text, { TextType } from "components/ads/Text"; +import { Classes } from "@blueprintjs/core"; +import { getOrgLoadingStates } from "selectors/organizationSelectors"; +const InputLabelWrapper = styled.div` + width: 200px; + display: flex; + align-items: center; +`; + +const SettingWrapper = styled.div` + width: 520px; + display: flex; + margin-bottom: 25px; +`; + +export const SettingsHeading = styled(Text)` + color: white; + display: inline-block; + margin-top: 25px; + margin-bottom: 32px; +`; + +const Loader = styled.div` + height: 38px; + width: 260px; + border-radius: 0; +`; + +export function GeneralSettings() { + const { orgId } = useParams(); + const dispatch = useDispatch(); + const currentOrg = useSelector(getCurrentOrg); + function saveChanges(settings: SaveOrgRequest) { + dispatch(saveOrg(settings)); + } + + const throttleTimeout = 1000; + + const onWorkspaceNameChange = throttle((newName: string) => { + saveChanges({ + id: orgId as string, + name: newName, + }); + }, throttleTimeout); + + const onWebsiteChange = throttle((newWebsite: string) => { + saveChanges({ + id: orgId as string, + website: newWebsite, + }); + }, throttleTimeout); + + const onEmailChange = throttle((newEmail: string) => { + saveChanges({ + id: orgId as string, + email: newEmail, + }); + }, throttleTimeout); + + const { isFetchingOrg } = useSelector(getOrgLoadingStates); + + return ( + <> + General + + + Workspace + + {isFetchingOrg && } + {!isFetchingOrg && ( + + )} + + + + + Website + + {isFetchingOrg && } + {!isFetchingOrg && ( + + )} + + + + + Email + + {isFetchingOrg && } + {!isFetchingOrg && ( + + )} + + + ); +} diff --git a/app/client/src/pages/organization/Members.tsx b/app/client/src/pages/organization/Members.tsx new file mode 100644 index 0000000000..7518c6643b --- /dev/null +++ b/app/client/src/pages/organization/Members.tsx @@ -0,0 +1,171 @@ +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +import { + getAllUsers, + getAllRoles, + getCurrentOrg, + getOrgLoadingStates, +} from "selectors/organizationSelectors"; +import PageSectionHeader from "pages/common/PageSectionHeader"; +import OrgInviteUsersForm from "pages/organization/OrgInviteUsersForm"; +import { RouteComponentProps } from "react-router"; +// import Spinner from "components/editorComponents/Spinner"; +import FormDialogComponent from "components/editorComponents/form/FormDialogComponent"; +import { getCurrentUser } from "selectors/usersSelectors"; +import Table from "components/ads/Table"; +import Icon, { IconSize } from "components/ads/Icon"; +import { + fetchUsersForOrg, + fetchRolesForOrg, + fetchOrg, + changeOrgUserRole, + deleteOrgUser, +} from "actions/orgActions"; +import Button, { Size, Variant } from "components/ads/Button"; +import TableDropdown from "components/ads/TableDropdown"; +import { TextType } from "components/ads/Text"; +import { SettingsHeading } from "./General"; +import styled from "styled-components"; +import { Classes } from "@blueprintjs/core"; + +export type PageProps = RouteComponentProps<{ + orgId: string; +}>; + +const Loader = styled.div` + height: 120px; + width: 100%; +`; +export default function MemberSettings(props: PageProps) { + const { + match: { + params: { orgId }, + }, + // deleteOrgUser, + // changeOrgUserRole, + } = props; + + const dispatch = useDispatch(); + useEffect(() => { + dispatch(fetchUsersForOrg(orgId)); + dispatch(fetchRolesForOrg(orgId)); + dispatch(fetchOrg(orgId)); + }, [orgId]); + + const { isFetchingAllUsers, isFetchingAllRoles } = useSelector( + getOrgLoadingStates, + ); + const allUsers = useSelector(getAllUsers); + const currentUser = useSelector(getCurrentUser); + const currentOrg = useSelector(getCurrentOrg); + + const userTableData = allUsers.map(user => ({ + ...user, + isCurrentUser: user.username === currentUser?.username, + })); + + const columns = [ + { + Header: "Email", + accessor: "username", + }, + { + Header: "Name", + accessor: "name", + }, + { + Header: "Role", + accessor: "roleName", + Cell: function DropdownCell(cellProps: any) { + const allRoles = useSelector(getAllRoles); + const roles = allRoles + ? Object.keys(allRoles).map(role => { + return { + name: role, + desc: allRoles[role], + }; + }) + : []; + const index = roles.findIndex( + (role: { name: string; desc: string }) => + role.name === cellProps.cell.value, + ); + if ( + cellProps.cell.row.values.username === + useSelector(getCurrentUser)?.username + ) { + return cellProps.cell.value; + } + return ( + { + dispatch( + changeOrgUserRole( + orgId, + option.name, + cellProps.cell.row.values.username, + ), + ); + }} + > + ); + }, + }, + { + Header: "Delete", + accessor: "delete", + Cell: function DeleteCell(cellProps: any) { + if ( + cellProps.cell.row.values.username === + useSelector(getCurrentUser)?.username + ) { + return null; + } + return ( + { + dispatch( + deleteOrgUser(orgId, cellProps.cell.row.values.username), + ); + }} + /> + ); + }, + }, + ]; + + const currentOrgName = currentOrg?.name ?? ""; + + return ( + + + Manage Users + + } + canOutsideClickClose={true} + Form={OrgInviteUsersForm} + orgId={orgId} + title={`Invite Users to ${currentOrgName}`} + /> + + {isFetchingAllUsers && isFetchingAllRoles ? ( + + ) : ( +
+ )} +
+ ); +} diff --git a/app/client/src/pages/organization/OrgInviteUsersForm.tsx b/app/client/src/pages/organization/OrgInviteUsersForm.tsx index 3abda4d23d..2b0ccbddb3 100644 --- a/app/client/src/pages/organization/OrgInviteUsersForm.tsx +++ b/app/client/src/pages/organization/OrgInviteUsersForm.tsx @@ -302,7 +302,7 @@ const OrgInviteUsersForm = (props: any) => { filled intent="primary" onClick={() => { - history.push(`/org/${props.orgId}/settings`); + history.push(`/org/${props.orgId}/settings/members`); }} /> )} diff --git a/app/client/src/pages/organization/index.tsx b/app/client/src/pages/organization/index.tsx index 14bd8171fa..282a054dea 100644 --- a/app/client/src/pages/organization/index.tsx +++ b/app/client/src/pages/organization/index.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Switch, useRouteMatch, useLocation } from "react-router-dom"; import PageWrapper from "pages/common/PageWrapper"; -import Settings from "./settings"; import DefaultOrgPage from "./defaultOrgPage"; import AppRoute from "pages/common/AppRoute"; +import Settings from "./settings"; export const Organization = () => { const { path } = useRouteMatch(); const location = useLocation(); @@ -11,7 +11,6 @@ export const Organization = () => { void; - fetchCurrentOrg: (orgId: string) => void; - fetchUser: (orgId: string) => void; - fetchAllRoles: (orgId: string) => void; - deleteOrgUser: (orgId: string, username: string) => void; - changeOrgUserRole: (orgId: string, role: string, username: string) => void; - allUsers: OrgUser[]; - allRole: object; - currentUser: User | undefined; - isFetchAllUsers: boolean; - isFetchAllRoles: boolean; -}; +import MemberSettings from "./Members"; +import IconComponent from "components/designSystems/appsmith/IconComponent"; +import { fetchOrg } from "actions/orgActions"; +import { GeneralSettings } from "./General"; -export type PageProps = OrgProps & - RouteComponentProps<{ - orgId: string; - }>; - -export type MenuItemProps = { - rolename: string; -}; - -type DropdownProps = { - activeItem: string; - userRoles: object; - username: string; - changeOrgUserRole: (orgId: string, role: string, username: string) => void; - orgId: string; -}; - -const StyledDropDown = styled.div` - cursor: pointer; -`; - -const StyledTableWrapped = styled(TableWrapper)` - height: ${props => props.height}px; - overflow: visible; - .tableWrap { - height: ${props => props.height}px; +const LinkToApplications = styled(Link)` + margin-bottom: 35px; + width: auto; + &:hover { + text-decoration: none; } - .table { - .tbody { - height: ${props => props.height}px; - } - } -`; - -const StyledMenu = styled(Menu)` - &&&&.bp3-menu { - max-width: 250px; + svg { cursor: pointer; } `; -const RoleNameCell = (props: any) => { - const { - roleName, - roles, - username, - isCurrentUser, - isChangingRole, - } = props.cellProps.row.original; - - if (isCurrentUser) { - return
{roleName}
; - } - - return ( - - } - position={Position.BOTTOM_LEFT} - > - - {roleName} - - {isChangingRole ? : undefined} - - - ); -}; - -const DeleteActionCell = (props: any) => { - const { username, isCurrentUser, isDeleting } = props.cellProps.row.original; - - return ( - !isCurrentUser && - (isDeleting ? ( - - ) : ( - props.deleteOrgUser(props.orgId, username)} - style={{ alignSelf: "center", cursor: "pointer" }} - /> - )) - ); -}; - -const Dropdown = (props: DropdownProps) => { - return ( - - {Object.entries(props.userRoles).map((role, index) => { - const MenuContent = ( -
- - {role[0]} - -
{role[1]}
-
- ); - - return ( - - props.changeOrgUserRole(props.orgId, role[0], props.username) - } - active={props.activeItem === role[0]} - text={MenuContent} - /> - ); - })} -
- ); -}; - -export const OrgSettings = (props: PageProps) => { - const { - match: { - params: { orgId }, - }, - deleteOrgUser, - changeOrgUserRole, - fetchCurrentOrg, - fetchUser, - fetchAllRoles, - currentOrg, - } = props; - - const userTableData = props.allUsers.map(user => ({ - ...user, - roles: props.allRole, - isCurrentUser: user.username === props.currentUser?.username, - })); - const data = React.useMemo(() => userTableData, [userTableData]); - - const tableHeight = React.useMemo(() => { - const tableDataLength = - userTableData.length * TABLE_SIZES[CompactModeTypes.DEFAULT].ROW_HEIGHT + - TABLE_SIZES[CompactModeTypes.DEFAULT].COLUMN_HEADER_HEIGHT; - return tableDataLength; - }, [userTableData]); - - const columns = React.useMemo(() => { - return [ - { - Header: "Email", - accessor: "username", - }, - { - Header: "Name", - accessor: "name", - }, - { - Header: "Role", - accessor: "roleName", - Cell: (cellProps: any) => { - return RoleNameCell({ cellProps, changeOrgUserRole, orgId }); - }, - }, - { - Header: "Delete", - accessor: "delete", - Cell: (cellProps: any) => { - return DeleteActionCell({ cellProps, deleteOrgUser, orgId }); - }, - }, - ]; - }, [orgId, deleteOrgUser, changeOrgUserRole]); - - const currentOrgName = currentOrg?.name ?? ""; - const { - getTableProps, - getTableBodyProps, - headerGroups, - rows, - prepareRow, - } = useTable( - { - columns, - data, - manualPagination: true, - }, - useFlexLayout, - ); - +export default function Settings() { + const { orgId } = useParams(); + const currentOrg = useSelector(getCurrentOrg); + const { path } = useRouteMatch(); + const location = useLocation(); + const dispatch = useDispatch(); useEffect(() => { - fetchUser(orgId); - fetchAllRoles(orgId); - fetchCurrentOrg(orgId); - }, [orgId, fetchUser, fetchAllRoles, fetchCurrentOrg]); - return ( - - -

{currentOrgName}

-
- - -

Users

- - } - canOutsideClickClose={true} - Form={OrgInviteUsersForm} - orgId={orgId} - title={`Invite Users to ${currentOrgName}`} - /> -
- {props.isFetchAllUsers && props.isFetchAllRoles ? ( - - ) : ( - -
-
- {headerGroups.map((headerGroup: any, index: number) => ( -
- {headerGroup.headers.map( - (column: any, columnIndex: number) => ( -
-
- {column.render("Header")} -
-
- ), - )} -
- ))} -
- {rows.map((row: any, index: number) => { - prepareRow(row); - return ( -
- {row.cells.map((cell: any, cellIndex: number) => { - return ( -
- {cell.render("Cell")} -
- ); - })} -
- ); - })} -
-
-
-
- )} -
+ dispatch(fetchOrg(orgId as string)); + }, []); + + const SettingsRenderer = ( +
+ + +
); -}; -const mapStateToProps = (state: AppState) => ({ - allUsers: getAllUsers(state), - allRole: getAllRoles(state), - isFetchAllUsers: state.ui.orgs.loadingStates.isFetchAllUsers, - isFetchAllRoles: state.ui.orgs.loadingStates.isFetchAllRoles, - currentOrg: getCurrentOrg(state), - currentUser: getCurrentUser(state), -}); + const tabArr: TabProp[] = [ + { + key: "general", + title: "General", + panelComponent: SettingsRenderer, + icon: "general", + }, + { + key: "members", + title: "Members", + panelComponent: SettingsRenderer, + icon: "user", + }, + ]; + const isMembersPage = location.pathname.indexOf("members") !== -1; -const mapDispatchToProps = (dispatch: any) => ({ - fetchCurrentOrg: (orgId: string) => - dispatch({ - type: ReduxActionTypes.FETCH_CURRENT_ORG, - payload: { - orgId, - }, - }), - changeOrgName: (name: string) => - dispatch({ - type: ReduxActionTypes.UPDATE_ORG_NAME_INIT, - payload: { - name, - }, - }), - changeOrgUserRole: (orgId: string, role: string, username: string) => - dispatch({ - type: ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT, - payload: { - orgId, - role, - username, - }, - }), - deleteOrgUser: (orgId: string, username: string) => - dispatch({ - type: ReduxActionTypes.DELETE_ORG_USER_INIT, - payload: { - orgId, - username, - }, - }), - fetchUser: (orgId: string) => - dispatch({ - type: ReduxActionTypes.FETCH_ALL_USERS_INIT, - payload: { - orgId, - }, - }), - fetchAllRoles: (orgId: string) => - dispatch({ - type: ReduxActionTypes.FETCH_ALL_ROLES_INIT, - payload: { - orgId, - }, - }), -}); + return ( + <> + + + {currentOrg.name} + + { + const settingsStartIndex = location.pathname.indexOf("settings"); + const settingsEndIndex = settingsStartIndex + "settings".length; + const hasSlash = location.pathname[settingsEndIndex] === "/"; + let newUrl = ""; -export default connect(mapStateToProps, mapDispatchToProps)(OrgSettings); + if (hasSlash) { + newUrl = `${location.pathname.substr(0, settingsEndIndex)}/${ + tabArr[index].key + }`; + } else { + newUrl = `${location.pathname}/${tabArr[index].key}`; + } + history.push(newUrl); + }} + > + + ); +} diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx index 22098afbf7..2a892ba8c5 100644 --- a/app/client/src/reducers/index.tsx +++ b/app/client/src/reducers/index.tsx @@ -31,7 +31,7 @@ import { ApiNameReduxState } from "./uiReducers/apiNameReducer"; import { ExplorerReduxState } from "./uiReducers/explorerReducer"; import { PageDSLsReduxState } from "./uiReducers/pageDSLReducer"; import { ConfirmRunActionReduxState } from "./uiReducers/confirmRunActionReducer"; -import { AppDataState } from "@appsmith/reducers/entityReducers/appReducer"; +import { AppDataState } from "reducers/entityReducers/appReducer"; import { DatasourceNameReduxState } from "./uiReducers/datasourceNameReducer"; const appReducer = combineReducers({ diff --git a/app/client/src/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/reducers/uiReducers/applicationsReducer.tsx index 8630099289..faf84eea1a 100644 --- a/app/client/src/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/reducers/uiReducers/applicationsReducer.tsx @@ -15,6 +15,7 @@ const initialState: ApplicationsReduxState = { applicationList: [], creatingApplication: false, deletingApplication: false, + duplicatingApplication: false, userOrgs: [], }; @@ -139,6 +140,26 @@ const applicationsReducer = createReducer(initialState, { searchKeyword: action.payload.keyword, }; }, + [ReduxActionTypes.DUPLICATE_APPLICATION_INIT]: ( + state: ApplicationsReduxState, + ) => { + return { ...state, duplicatingApplication: true }; + }, + [ReduxActionTypes.DUPLICATE_APPLICATION_SUCCESS]: ( + state: ApplicationsReduxState, + action: ReduxAction, + ) => { + return { + ...state, + duplicatingApplication: false, + applicationList: [...state.applicationList, action.payload], + }; + }, + [ReduxActionTypes.DUPLICATE_APPLICATION_ERROR]: ( + state: ApplicationsReduxState, + ) => { + return { ...state, duplicatingApplication: false }; + }, }); export interface ApplicationsReduxState { @@ -150,6 +171,7 @@ export interface ApplicationsReduxState { creatingApplication: boolean; createApplicationError?: string; deletingApplication: boolean; + duplicatingApplication: boolean; currentApplication?: ApplicationPayload; userOrgs: any; } diff --git a/app/client/src/reducers/uiReducers/editorReducer.tsx b/app/client/src/reducers/uiReducers/editorReducer.tsx index 2276b126f7..7f2466f805 100644 --- a/app/client/src/reducers/uiReducers/editorReducer.tsx +++ b/app/client/src/reducers/uiReducers/editorReducer.tsx @@ -33,6 +33,15 @@ const editorReducer = createReducer(initialState, { [ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS]: (state: EditorReduxState) => { return { ...state, initialized: true }; }, + [ReduxActionTypes.UPDATE_PAGE_SUCCESS]: ( + state: EditorReduxState, + action: ReduxAction<{ id: string; name: string }>, + ) => { + if (action.payload.id === state.currentPageId) { + return { ...state, currentPageName: action.payload.name }; + } + return state; + }, [ReduxActionTypes.FETCH_PAGE_INIT]: (state: EditorReduxState) => ({ ...state, loadingStates: { @@ -102,6 +111,7 @@ const editorReducer = createReducer(initialState, { currentLayoutId, pageWidgetId, currentApplicationId, + currentPageId, } = action.payload; state.loadingStates.publishing = false; state.loadingStates.publishingError = false; @@ -111,6 +121,7 @@ const editorReducer = createReducer(initialState, { currentLayoutId, pageWidgetId, currentApplicationId, + currentPageId, }; }, [ReduxActionTypes.CLONE_PAGE_INIT]: (state: EditorReduxState) => { @@ -166,6 +177,7 @@ export interface EditorReduxState { pageWidgetId?: string; currentLayoutId?: string; currentPageName?: string; + currentPageId?: string; loadingStates: { saving: boolean; savingError: boolean; diff --git a/app/client/src/reducers/uiReducers/orgReducer.ts b/app/client/src/reducers/uiReducers/orgReducer.ts index 1b832b5104..890e4cae1e 100644 --- a/app/client/src/reducers/uiReducers/orgReducer.ts +++ b/app/client/src/reducers/uiReducers/orgReducer.ts @@ -1,4 +1,4 @@ -import { createReducer } from "utils/AppsmithUtils"; +import { createImmerReducer } from "utils/AppsmithUtils"; import { ReduxAction, ReduxActionTypes, @@ -11,6 +11,7 @@ const initialState: OrgReduxState = { fetchingRoles: false, isFetchAllRoles: false, isFetchAllUsers: false, + isFetchingOrg: false, }, orgUsers: [], orgRoles: [], @@ -20,161 +21,121 @@ const initialState: OrgReduxState = { }, }; -const orgReducer = createReducer(initialState, { - [ReduxActionTypes.FETCH_ORG_ROLES_INIT]: (state: OrgReduxState) => ({ - ...state, - loadingStates: { - ...state.loadingStates, - fetchingRoles: true, - }, - }), - [ReduxActionTypes.FETCH_ALL_ROLES_INIT]: (state: OrgReduxState) => ({ - ...state, - loadingStates: { - ...state.loadingStates, - isFetchAllRoles: true, - }, - }), - [ReduxActionTypes.FETCH_ALL_USERS_INIT]: (state: OrgReduxState) => ({ - ...state, - loadingStates: { - ...state.loadingStates, - isFetchAllUsers: true, - }, - }), - +const orgReducer = createImmerReducer(initialState, { + [ReduxActionTypes.FETCH_ORG_ROLES_INIT]: (draftState: OrgReduxState) => { + draftState.loadingStates.isFetchAllRoles = true; + }, + [ReduxActionTypes.FETCH_ALL_ROLES_INIT]: (draftState: OrgReduxState) => { + draftState.loadingStates.isFetchAllRoles = true; + }, + [ReduxActionTypes.FETCH_ALL_USERS_INIT]: (draftState: OrgReduxState) => { + draftState.loadingStates.isFetchAllUsers = true; + }, [ReduxActionTypes.FETCH_ORG_ROLES_SUCCESS]: ( - state: OrgReduxState, + draftState: OrgReduxState, action: ReduxAction, - ) => ({ - ...state, - roles: action.payload, - loadingStates: { - ...state.loadingStates, - fetchingRoles: false, - }, - }), - [ReduxActionErrorTypes.FETCH_ORG_ROLES_ERROR]: (state: OrgReduxState) => ({ - ...state, - loadingStates: { - ...state.loadingStates, - fetchingRoles: false, - }, - }), + ) => { + draftState.orgRoles = action.payload; + draftState.loadingStates.fetchingRoles = false; + }, + [ReduxActionErrorTypes.FETCH_ORG_ROLES_ERROR]: ( + draftState: OrgReduxState, + ) => { + draftState.loadingStates.fetchingRoles = false; + }, [ReduxActionTypes.FETCH_ALL_USERS_SUCCESS]: ( - state: OrgReduxState, - action: ReduxAction, - ) => ({ - ...state, - orgUsers: action.payload, - loadingStates: { - ...state.loadingStates, - isFetchAllUsers: false, - }, - }), + draftState: OrgReduxState, + action: ReduxAction, + ) => { + draftState.orgUsers = action.payload; + draftState.loadingStates.isFetchAllUsers = false; + }, [ReduxActionTypes.FETCH_ALL_ROLES_SUCCESS]: ( - state: OrgReduxState, + draftState: OrgReduxState, action: ReduxAction, - ) => ({ - ...state, - orgRoles: action.payload, - loadingStates: { - ...state.loadingStates, - isFetchAllRoles: false, - }, - }), + ) => { + draftState.orgRoles = action.payload; + draftState.loadingStates.isFetchAllRoles = false; + }, [ReduxActionTypes.CHANGE_ORG_USER_ROLE_SUCCESS]: ( - state: OrgReduxState, + draftState: OrgReduxState, action: ReduxAction<{ username: string; roleName: string }>, ) => { - const _orgUsers = state.orgUsers.map((user: OrgUser) => { + draftState.orgUsers.forEach((user: OrgUser) => { if (user.username === action.payload.username) { - return { - ...user, - roleName: action.payload.roleName, - isChangingRole: false, - }; + user.roleName = action.payload.roleName; } - return user; }); - return { - ...state, - orgUsers: _orgUsers, - }; }, [ReduxActionTypes.CHANGE_ORG_USER_ROLE_INIT]: ( - state: OrgReduxState, + draftState: OrgReduxState, action: ReduxAction<{ username: string }>, ) => { - const _orgUsers = state.orgUsers.map((user: OrgUser) => { + draftState.orgUsers.forEach((user: OrgUser) => { if (user.username === action.payload.username) { - return { - ...user, - isChangingRole: true, - }; + user.isChangingRole = true; } - return user; }); - return { ...state, orgUsers: _orgUsers }; }, [ReduxActionTypes.DELETE_ORG_USER_INIT]: ( - state: OrgReduxState, + draftState: OrgReduxState, action: ReduxAction<{ username: string }>, ) => { - const _orgUsers = state.orgUsers.map((user: OrgUser) => { + draftState.orgUsers.forEach((user: OrgUser) => { if (user.username === action.payload.username) { - return { - ...user, - isDeleting: true, - }; + user.isDeleting = true; } - return user; }); - return { ...state, orgUsers: _orgUsers }; }, [ReduxActionTypes.DELETE_ORG_USER_SUCCESS]: ( - state: OrgReduxState, + draftState: OrgReduxState, action: ReduxAction<{ username: string }>, ) => { - const _orgUsers = state.orgUsers.filter( + draftState.orgUsers = draftState.orgUsers.filter( (user: OrgUser) => user.username !== action.payload.username, ); - return { - ...state, - orgUsers: _orgUsers, - }; }, - [ReduxActionTypes.CHANGE_ORG_USER_ROLE_ERROR]: (state: OrgReduxState) => { - const _orgUsers = state.orgUsers.map(user => ({ - ...user, - isChangingRole: false, - })); - return { ...state, orgUsers: _orgUsers }; + [ReduxActionErrorTypes.CHANGE_ORG_USER_ROLE_ERROR]: ( + draftState: OrgReduxState, + ) => { + draftState.orgUsers.forEach((user: OrgUser) => { + //TODO: This will change the status to false even if one role change api fails. + user.isChangingRole = false; + }); }, - [ReduxActionTypes.DELETE_ORG_USER_ERROR]: (state: OrgReduxState) => { - const _orgUsers = state.orgUsers.map(user => ({ - ...user, - isDeleting: false, - })); - return { ...state, orgUsers: _orgUsers }; + [ReduxActionErrorTypes.DELETE_ORG_USER_ERROR]: ( + draftState: OrgReduxState, + ) => { + draftState.orgUsers.forEach((user: OrgUser) => { + //TODO: This will change the status to false even if one delete fails. + user.isDeleting = false; + }); }, [ReduxActionTypes.SET_CURRENT_ORG_ID]: ( - state: OrgReduxState, + draftState: OrgReduxState, action: ReduxAction<{ orgId: string }>, - ) => ({ - ...state, - currentOrg: { - ...state.currentOrg, - id: action.payload.orgId, - }, - }), - [ReduxActionTypes.FETCH_ORG_SUCCESS]: ( - state: OrgReduxState, + ) => { + draftState.currentOrg.id = action.payload.orgId; + }, + [ReduxActionTypes.SET_CURRENT_ORG]: ( + draftState: OrgReduxState, action: ReduxAction, - ) => ({ - ...state, - currentOrg: action.payload, - }), + ) => { + draftState.currentOrg = action.payload; + }, + [ReduxActionTypes.FETCH_CURRENT_ORG]: (draftState: OrgReduxState) => { + draftState.loadingStates.isFetchingOrg = true; + }, + [ReduxActionTypes.FETCH_ORG_SUCCESS]: ( + draftState: OrgReduxState, + action: ReduxAction, + ) => { + draftState.currentOrg = action.payload; + draftState.loadingStates.isFetchingOrg = false; + }, + [ReduxActionErrorTypes.FETCH_ORG_ERROR]: (draftState: OrgReduxState) => { + draftState.loadingStates.isFetchingOrg = false; + }, }); export interface OrgReduxState { @@ -184,6 +145,7 @@ export interface OrgReduxState { fetchingRoles: boolean; isFetchAllRoles: boolean; isFetchAllUsers: boolean; + isFetchingOrg: boolean; }; orgUsers: OrgUser[]; orgRoles: any; diff --git a/app/client/src/reducers/uiReducers/pageDSLReducer.ts b/app/client/src/reducers/uiReducers/pageDSLReducer.ts index dd01bbd4f6..95f4276f7c 100644 --- a/app/client/src/reducers/uiReducers/pageDSLReducer.ts +++ b/app/client/src/reducers/uiReducers/pageDSLReducer.ts @@ -4,8 +4,8 @@ import { ReduxActionErrorTypes, ReduxAction, } from "constants/ReduxActionConstants"; -import { ContainerWidgetProps } from "@appsmith/widgets/ContainerWidget"; -import { WidgetProps } from "@appsmith/widgets/BaseWidget"; +import { ContainerWidgetProps } from "widgets/ContainerWidget"; +import { WidgetProps } from "widgets/BaseWidget"; export type PageDSLsReduxState = { [pageId: string]: ContainerWidgetProps; diff --git a/app/client/src/sagas/ApplicationSagas.tsx b/app/client/src/sagas/ApplicationSagas.tsx index 2608f968bf..f477094998 100644 --- a/app/client/src/sagas/ApplicationSagas.tsx +++ b/app/client/src/sagas/ApplicationSagas.tsx @@ -18,6 +18,7 @@ import ApplicationApi, { OrganizationApplicationObject, ApplicationObject, ChangeAppViewAccessRequest, + DuplicateApplicationRequest, } from "api/ApplicationApi"; import { getDefaultPageId } from "./SagaUtils"; import { call, put, takeLatest, all, select } from "redux-saga/effects"; @@ -30,6 +31,11 @@ import { BUILDER_PAGE_URL } from "constants/routes"; import { AppState } from "reducers"; import { setDefaultApplicationPageSuccess } from "actions/applicationActions"; import AnalyticsUtil from "utils/AnalyticsUtil"; +import { AppToaster } from "components/editorComponents/ToastComponent"; +import { + DUPLICATING_APPLICATION, + DELETING_APPLICATION, +} from "constants/messages"; export function* publishApplicationSaga( requestAction: ReduxAction, @@ -192,6 +198,7 @@ export function* deleteApplicationSaga( action: ReduxAction, ) { try { + AppToaster.show({ message: DELETING_APPLICATION }); const request: DeleteApplicationRequest = action.payload; const response: ApiResponse = yield call( ApplicationApi.deleteApplication, @@ -214,6 +221,46 @@ export function* deleteApplicationSaga( } } +export function* duplicateApplicationSaga( + action: ReduxAction, +) { + try { + AppToaster.show({ message: DUPLICATING_APPLICATION }); + const request: DuplicateApplicationRequest = action.payload; + const response: ApiResponse = yield call( + ApplicationApi.duplicateApplication, + request, + ); + const isValidResponse = yield validateResponse(response); + if (isValidResponse) { + const application: ApplicationPayload = { + id: response.data.id, + name: response.data.name, + organizationId: response.data.organizationId, + pageCount: response.data.pages ? response.data.pages.length : 0, + defaultPageId: getDefaultPageId(response.data.pages), + appIsExample: response.data.appIsExample, + }; + yield put({ + type: ReduxActionTypes.DUPLICATE_APPLICATION_SUCCESS, + payload: response.data, + }); + const pageURL = BUILDER_PAGE_URL( + application.id, + application.defaultPageId, + ); + history.push(pageURL); + } + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.DUPLICATE_APPLICATION_ERROR, + payload: { + error, + }, + }); + } +} + export function* changeAppViewAccessSaga( requestAction: ReduxAction, ) { @@ -344,5 +391,9 @@ export default function* applicationSagas() { setDefaultApplicationPageSaga, ), takeLatest(ReduxActionTypes.DELETE_APPLICATION_INIT, deleteApplicationSaga), + takeLatest( + ReduxActionTypes.DUPLICATE_APPLICATION_INIT, + duplicateApplicationSaga, + ), ]); } diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index e6ea0cf5f5..ace0723e3f 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -155,14 +155,9 @@ export function* deleteDatasourceSaga( } } -function* updateDatasourceSaga( - actionPayload: ReduxAction<{ - datasource: Datasource; - reinitializeForm: boolean; - }>, -) { +function* updateDatasourceSaga(actionPayload: ReduxAction) { try { - const datasourcePayload = _.omit(actionPayload.payload.datasource, "name"); + const datasourcePayload = _.omit(actionPayload.payload, "name"); const response: GenericApiResponse = yield DatasourcesApi.updateDatasource( datasourcePayload, @@ -184,9 +179,6 @@ function* updateDatasourceSaga( id: response.data.id, }, }); - if (actionPayload.payload.reinitializeForm) { - yield put(initialize(DATASOURCE_DB_FORM, datasourcePayload)); - } } } catch (error) { yield put({ diff --git a/app/client/src/sagas/OrgSagas.ts b/app/client/src/sagas/OrgSagas.ts index 4fec6ed57c..d503d39567 100644 --- a/app/client/src/sagas/OrgSagas.ts +++ b/app/client/src/sagas/OrgSagas.ts @@ -1,4 +1,4 @@ -import { call, takeLatest, put, all } from "redux-saga/effects"; +import { call, takeLatest, put, all, select } from "redux-saga/effects"; import { ReduxActionTypes, ReduxAction, @@ -26,6 +26,7 @@ import OrgApi, { import { ApiResponse } from "api/ApiResponses"; import { AppToaster } from "components/editorComponents/ToastComponent"; import { ToastType } from "react-toastify"; +import { getCurrentOrg } from "selectors/organizationSelectors"; export function* fetchRolesSaga() { try { @@ -114,6 +115,9 @@ export function* changeOrgUserRoleSaga( } catch (error) { yield put({ type: ReduxActionErrorTypes.CHANGE_ORG_USER_ROLE_ERROR, + payload: { + error, + }, }); } } @@ -172,6 +176,17 @@ export function* saveOrgSaga(action: ReduxAction) { const response: ApiResponse = yield call(OrgApi.saveOrg, request); const isValidResponse = yield validateResponse(response); if (isValidResponse) { + const currentOrg = yield select(getCurrentOrg); + if (currentOrg && currentOrg.id === request.id) { + const updatedOrg = { + ...currentOrg, + ...request, + }; + yield put({ + type: ReduxActionTypes.SET_CURRENT_ORG, + payload: updatedOrg, + }); + } yield put({ type: ReduxActionTypes.SAVE_ORG_SUCCESS, }); diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index a80fdbce93..0d51c3aff8 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -158,6 +158,15 @@ export function* fetchPageSaga( yield put(fetchPageSuccess()); // Execute page load actions yield put(executePageLoadActions(canvasWidgetsPayload.pageActions)); + + // Add this to the page DSLs for entity explorer + yield put({ + type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, + payload: { + pageId: id, + dsl: extractCurrentDSL(fetchPageResponse), + }, + }); } } catch (error) { console.log(error); @@ -457,6 +466,14 @@ export function* updateWidgetNameSaga( yield updateCanvasWithDSL(response.data, pageId, layoutId); yield put(updateWidgetNameSuccess()); + // Add this to the page DSLs for entity explorer + yield put({ + type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, + payload: { + pageId: pageId, + dsl: response.data.dsl, + }, + }); } } else { yield put({ diff --git a/app/client/src/selectors/applicationSelectors.tsx b/app/client/src/selectors/applicationSelectors.tsx index f437a0527a..ed1750ce14 100644 --- a/app/client/src/selectors/applicationSelectors.tsx +++ b/app/client/src/selectors/applicationSelectors.tsx @@ -37,6 +37,8 @@ const getApplicationSearchKeyword = (state: AppState) => state.ui.applications.searchKeyword; export const getIsDeletingApplication = (state: AppState) => state.ui.applications.deletingApplication; +export const getIsDuplicatingApplication = (state: AppState) => + state.ui.applications.duplicatingApplication; export const getUserApplicationsOrgs = (state: AppState) => { return state.ui.applications.userOrgs; }; diff --git a/app/client/src/selectors/organizationSelectors.tsx b/app/client/src/selectors/organizationSelectors.tsx index 45d00c51e8..f027668244 100644 --- a/app/client/src/selectors/organizationSelectors.tsx +++ b/app/client/src/selectors/organizationSelectors.tsx @@ -6,6 +6,14 @@ export const getRolesFromState = (state: AppState) => { return state.ui.orgs.roles; }; +export const getOrgLoadingStates = (state: AppState) => { + return { + isFetchingOrg: state.ui.orgs.loadingStates.isFetchingOrg, + isFetchingAllUsers: state.ui.orgs.loadingStates.isFetchAllUsers, + isFetchingAllRoles: state.ui.orgs.loadingStates.isFetchAllRoles, + }; +}; + export const getCurrentOrgId = (state: AppState) => state.ui.orgs.currentOrg.id; export const getOrgs = (state: AppState) => { return state.ui.applications.userOrgs; diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx index 2acf54b047..2e2f49c7ce 100644 --- a/app/client/src/utils/AppsmithUtils.tsx +++ b/app/client/src/utils/AppsmithUtils.tsx @@ -10,6 +10,7 @@ import * as log from "loglevel"; import { LogLevelDesc } from "loglevel"; import FeatureFlag from "utils/featureFlags"; import { appCardColors } from "constants/AppConstants"; +import produce from "immer"; export const createReducer = ( initialState: any, @@ -24,6 +25,19 @@ export const createReducer = ( }; }; +export const createImmerReducer = ( + initialState: any, + handlers: { [type: string]: any }, +) => { + return function reducer(state = initialState, action: ReduxAction) { + if (handlers.hasOwnProperty(action.type)) { + return produce(handlers[action.type])(state, action); + } else { + return state; + } + }; +}; + export const appInitializer = () => { FormControlRegistry.registerFormControlBuilders(); const appsmithConfigs = getAppsmithConfigs(); diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx index f23530e969..afa52083d3 100644 --- a/app/client/src/utils/WidgetRegistry.tsx +++ b/app/client/src/utils/WidgetRegistry.tsx @@ -1,42 +1,81 @@ import BaseWidget, { WidgetProps } from "widgets/BaseWidget"; import { WidgetTypes } from "constants/WidgetConstants"; -import ContainerWidget, { ContainerWidgetProps } from "widgets/ContainerWidget"; -import TextWidget, { TextWidgetProps } from "widgets/TextWidget"; -import InputWidget, { InputWidgetProps } from "widgets/InputWidget"; -import CheckboxWidget, { CheckboxWidgetProps } from "widgets/CheckboxWidget"; +import ContainerWidget, { + ContainerWidgetProps, + ProfiledContainerWidget, +} from "widgets/ContainerWidget"; +import TextWidget, { + TextWidgetProps, + ProfiledTextWidget, +} from "widgets/TextWidget"; +import InputWidget, { + InputWidgetProps, + ProfiledInputWidget, +} from "widgets/InputWidget"; +import CheckboxWidget, { + CheckboxWidgetProps, + ProfiledCheckboxWidget, +} from "widgets/CheckboxWidget"; import RadioGroupWidget, { RadioGroupWidgetProps, + ProfiledRadioGroupWidget, } from "widgets/RadioGroupWidget"; import WidgetFactory from "./WidgetFactory"; import React from "react"; -import ButtonWidget, { ButtonWidgetProps } from "widgets/ButtonWidget"; -import DropdownWidget, { DropdownWidgetProps } from "widgets/DropdownWidget"; -import ImageWidget, { ImageWidgetProps } from "widgets/ImageWidget"; -import TableWidget, { TableWidgetProps } from "widgets/TableWidget"; +import ButtonWidget, { + ButtonWidgetProps, + ProfiledButtonWidget, +} from "widgets/ButtonWidget"; +import DropdownWidget, { + DropdownWidgetProps, + ProfiledDropDownWidget, +} from "widgets/DropdownWidget"; +import ImageWidget, { + ImageWidgetProps, + ProfiledImageWidget, +} from "widgets/ImageWidget"; +import TableWidget, { + TableWidgetProps, + ProfiledTableWidget, +} from "widgets/TableWidget"; import TabsWidget, { TabsWidgetProps, TabContainerWidgetProps, + ProfiledTabsWidget, } from "widgets/TabsWidget"; -import ModalWidget, { ModalWidgetProps } from "widgets/ModalWidget"; +import { ModalWidgetProps, ProfiledModalWidget } from "widgets/ModalWidget"; import RichTextEditorWidget, { RichTextEditorWidgetProps, + ProfiledRichTextEditorWidget, } from "widgets/RichTextEditorWidget"; -import ChartWidget, { ChartWidgetProps } from "widgets/ChartWidget"; -import MapWidget, { MapWidgetProps } from "widgets/MapWidget"; +import ChartWidget, { + ChartWidgetProps, + ProfiledChartWidget, +} from "widgets/ChartWidget"; +import MapWidget, { + MapWidgetProps, + ProfiledMapWidget, +} from "widgets/MapWidget"; import FilePickerWidget, { FilePickerWidgetProps, + ProfiledFilePickerWidget, } from "widgets/FilepickerWidget"; import DatePickerWidget, { DatePickerWidgetProps, + ProfiledDatePickerWidget, } from "widgets/DatePickerWidget"; -import FormWidget from "widgets/FormWidget"; +import FormWidget, { ProfiledFormWidget } from "widgets/FormWidget"; import FormButtonWidget, { FormButtonWidgetProps, + ProfiledFormButtonWidget, } from "widgets/FormButtonWidget"; -import IconWidget, { IconWidgetProps } from "widgets/IconWidget"; +import IconWidget, { + IconWidgetProps, + ProfiledIconWidget, +} from "widgets/IconWidget"; -import CanvasWidget from "widgets/CanvasWidget"; +import CanvasWidget, { ProfiledCanvasWidget } from "widgets/CanvasWidget"; export default class WidgetBuilderRegistry { static registerWidgetBuilders() { WidgetFactory.registerWidgetBuilder( @@ -45,7 +84,7 @@ export default class WidgetBuilderRegistry { buildWidget( widgetData: ContainerWidgetProps, ): JSX.Element { - return ; + return ; }, }, ContainerWidget.getPropertyValidationMap(), @@ -59,7 +98,7 @@ export default class WidgetBuilderRegistry { "TEXT_WIDGET", { buildWidget(widgetData: TextWidgetProps): JSX.Element { - return ; + return ; }, }, TextWidget.getPropertyValidationMap(), @@ -73,7 +112,7 @@ export default class WidgetBuilderRegistry { "BUTTON_WIDGET", { buildWidget(widgetData: ButtonWidgetProps): JSX.Element { - return ; + return ; }, }, ButtonWidget.getPropertyValidationMap(), @@ -87,7 +126,7 @@ export default class WidgetBuilderRegistry { "INPUT_WIDGET", { buildWidget(widgetData: InputWidgetProps): JSX.Element { - return ; + return ; }, }, InputWidget.getPropertyValidationMap(), @@ -101,7 +140,7 @@ export default class WidgetBuilderRegistry { "CHECKBOX_WIDGET", { buildWidget(widgetData: CheckboxWidgetProps): JSX.Element { - return ; + return ; }, }, CheckboxWidget.getPropertyValidationMap(), @@ -115,7 +154,7 @@ export default class WidgetBuilderRegistry { "DROP_DOWN_WIDGET", { buildWidget(widgetData: DropdownWidgetProps): JSX.Element { - return ; + return ; }, }, DropdownWidget.getPropertyValidationMap(), @@ -129,7 +168,7 @@ export default class WidgetBuilderRegistry { "RADIO_GROUP_WIDGET", { buildWidget(widgetData: RadioGroupWidgetProps): JSX.Element { - return ; + return ; }, }, RadioGroupWidget.getPropertyValidationMap(), @@ -143,7 +182,7 @@ export default class WidgetBuilderRegistry { "IMAGE_WIDGET", { buildWidget(widgetData: ImageWidgetProps): JSX.Element { - return ; + return ; }, }, ImageWidget.getPropertyValidationMap(), @@ -156,7 +195,7 @@ export default class WidgetBuilderRegistry { "TABLE_WIDGET", { buildWidget(widgetData: TableWidgetProps): JSX.Element { - return ; + return ; }, }, TableWidget.getPropertyValidationMap(), @@ -169,7 +208,7 @@ export default class WidgetBuilderRegistry { "FILE_PICKER_WIDGET", { buildWidget(widgetData: FilePickerWidgetProps): JSX.Element { - return ; + return ; }, }, FilePickerWidget.getPropertyValidationMap(), @@ -182,7 +221,7 @@ export default class WidgetBuilderRegistry { "DATE_PICKER_WIDGET", { buildWidget(widgetData: DatePickerWidgetProps): JSX.Element { - return ; + return ; }, }, DatePickerWidget.getPropertyValidationMap(), @@ -197,7 +236,7 @@ export default class WidgetBuilderRegistry { buildWidget( widgetProps: TabsWidgetProps, ): JSX.Element { - return ; + return ; }, }, TabsWidget.getPropertyValidationMap(), @@ -210,7 +249,7 @@ export default class WidgetBuilderRegistry { WidgetTypes.MODAL_WIDGET, { buildWidget(widgetProps: ModalWidgetProps): JSX.Element { - return ; + return ; }, }, BaseWidget.getPropertyValidationMap(), @@ -223,7 +262,7 @@ export default class WidgetBuilderRegistry { "RICH_TEXT_EDITOR_WIDGET", { buildWidget(widgetData: RichTextEditorWidgetProps): JSX.Element { - return ; + return ; }, }, RichTextEditorWidget.getPropertyValidationMap(), @@ -236,7 +275,7 @@ export default class WidgetBuilderRegistry { "CHART_WIDGET", { buildWidget(widgetData: ChartWidgetProps): JSX.Element { - return ; + return ; }, }, ChartWidget.getPropertyValidationMap(), @@ -251,7 +290,7 @@ export default class WidgetBuilderRegistry { buildWidget( widgetProps: ContainerWidgetProps, ): JSX.Element { - return ; + return ; }, }, FormWidget.getPropertyValidationMap(), @@ -265,7 +304,7 @@ export default class WidgetBuilderRegistry { "FORM_BUTTON_WIDGET", { buildWidget(widgetProps: FormButtonWidgetProps): JSX.Element { - return ; + return ; }, }, FormButtonWidget.getPropertyValidationMap(), @@ -279,7 +318,7 @@ export default class WidgetBuilderRegistry { "MAP_WIDGET", { buildWidget(widgetProps: MapWidgetProps): JSX.Element { - return ; + return ; }, }, MapWidget.getPropertyValidationMap(), @@ -295,7 +334,7 @@ export default class WidgetBuilderRegistry { buildWidget( widgetData: ContainerWidgetProps, ): JSX.Element { - return ; + return ; }, }, CanvasWidget.getPropertyValidationMap(), @@ -309,7 +348,7 @@ export default class WidgetBuilderRegistry { WidgetTypes.ICON_WIDGET, { buildWidget(widgetProps: IconWidgetProps): JSX.Element { - return ; + return ; }, }, IconWidget.getPropertyValidationMap(), diff --git a/app/client/src/utils/hooks/useClick.tsx b/app/client/src/utils/hooks/useClick.tsx new file mode 100644 index 0000000000..ba9455a278 --- /dev/null +++ b/app/client/src/utils/hooks/useClick.tsx @@ -0,0 +1,36 @@ +import { MutableRefObject, MouseEvent, useEffect } from "react"; + +export default ( + currentRef: MutableRefObject, + singleClk: (e: MouseEvent) => void, + doubleClk?: (e: MouseEvent) => void, +) => { + useEffect(() => { + let clickCount = 0; + let timeoutId = 0; + + const handleClick = (e: any) => { + if (!doubleClk) { + singleClk(e); + } else { + clickCount++; + if (clickCount === 2 && doubleClk) { + doubleClk(e); + clearTimeout(timeoutId); + clickCount = 0; + } else { + timeoutId = setTimeout(() => { + singleClk(e); + clickCount = 0; + }, 200); + } + } + }; + + const el = currentRef.current; + el?.addEventListener("click", handleClick); + return () => { + el?.removeEventListener("click", handleClick); + }; + }, [currentRef, singleClk, doubleClk]); +}; diff --git a/app/client/src/widgets/ButtonWidget.tsx b/app/client/src/widgets/ButtonWidget.tsx index 69889c4685..964f8004a6 100644 --- a/app/client/src/widgets/ButtonWidget.tsx +++ b/app/client/src/widgets/ButtonWidget.tsx @@ -11,6 +11,7 @@ import { } from "utils/ValidationFactory"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; +import * as Sentry from "@sentry/react"; class ButtonWidget extends BaseWidget { onButtonClickBound: (event: React.MouseEvent) => void; @@ -100,3 +101,4 @@ interface ButtonWidgetState extends WidgetState { } export default ButtonWidget; +export const ProfiledButtonWidget = Sentry.withProfiler(ButtonWidget); diff --git a/app/client/src/widgets/CanvasWidget.tsx b/app/client/src/widgets/CanvasWidget.tsx index 09d1e2d482..38c8339191 100644 --- a/app/client/src/widgets/CanvasWidget.tsx +++ b/app/client/src/widgets/CanvasWidget.tsx @@ -5,6 +5,7 @@ import { WidgetTypes, GridDefaults } from "constants/WidgetConstants"; import DropTargetComponent from "components/editorComponents/DropTargetComponent"; import { getCanvasSnapRows } from "utils/WidgetPropsUtils"; import { getCanvasClassName } from "utils/generators"; +import * as Sentry from "@sentry/react"; class CanvasWidget extends ContainerWidget { getWidgetType = () => { @@ -61,3 +62,4 @@ class CanvasWidget extends ContainerWidget { } export default CanvasWidget; +export const ProfiledCanvasWidget = Sentry.withProfiler(CanvasWidget); diff --git a/app/client/src/widgets/ChartWidget.tsx b/app/client/src/widgets/ChartWidget.tsx index e6695f4c38..7b20fd9ddf 100644 --- a/app/client/src/widgets/ChartWidget.tsx +++ b/app/client/src/widgets/ChartWidget.tsx @@ -4,6 +4,7 @@ import { WidgetType } from "constants/WidgetConstants"; import { WidgetPropertyValidationType } from "utils/ValidationFactory"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import Skeleton from "components/utils/Skeleton"; +import * as Sentry from "@sentry/react"; const ChartComponent = lazy(() => import( @@ -74,3 +75,4 @@ export interface ChartWidgetProps extends WidgetProps { } export default ChartWidget; +export const ProfiledChartWidget = Sentry.withProfiler(ChartWidget); diff --git a/app/client/src/widgets/CheckboxWidget.tsx b/app/client/src/widgets/CheckboxWidget.tsx index 523f4ab596..c330263428 100644 --- a/app/client/src/widgets/CheckboxWidget.tsx +++ b/app/client/src/widgets/CheckboxWidget.tsx @@ -12,6 +12,7 @@ import { TriggerPropertiesMap, DerivedPropertiesMap, } from "utils/WidgetFactory"; +import * as Sentry from "@sentry/react"; class CheckboxWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { @@ -87,3 +88,4 @@ export interface CheckboxWidgetProps extends WidgetProps { } export default CheckboxWidget; +export const ProfiledCheckboxWidget = Sentry.withProfiler(CheckboxWidget); diff --git a/app/client/src/widgets/ContainerWidget.tsx b/app/client/src/widgets/ContainerWidget.tsx index 99cae4eb89..099df25fc3 100644 --- a/app/client/src/widgets/ContainerWidget.tsx +++ b/app/client/src/widgets/ContainerWidget.tsx @@ -13,6 +13,7 @@ import { } from "constants/WidgetConstants"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; +import * as Sentry from "@sentry/react"; class ContainerWidget extends BaseWidget< ContainerWidgetProps, @@ -98,3 +99,4 @@ export interface ContainerWidgetProps } export default ContainerWidget; +export const ProfiledContainerWidget = Sentry.withProfiler(ContainerWidget); diff --git a/app/client/src/widgets/DatePickerWidget.tsx b/app/client/src/widgets/DatePickerWidget.tsx index 4a3a22ee11..a01f358afd 100644 --- a/app/client/src/widgets/DatePickerWidget.tsx +++ b/app/client/src/widgets/DatePickerWidget.tsx @@ -12,6 +12,7 @@ import { DerivedPropertiesMap, TriggerPropertiesMap, } from "utils/WidgetFactory"; +import * as Sentry from "@sentry/react"; class DatePickerWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { @@ -105,3 +106,4 @@ export interface DatePickerWidgetProps extends WidgetProps { } export default DatePickerWidget; +export const ProfiledDatePickerWidget = Sentry.withProfiler(DatePickerWidget); diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx index 70c044fadb..f595739dc0 100644 --- a/app/client/src/widgets/DropdownWidget.tsx +++ b/app/client/src/widgets/DropdownWidget.tsx @@ -13,6 +13,7 @@ import { TriggerPropertiesMap } from "utils/WidgetFactory"; import { VALIDATORS } from "utils/Validators"; import { DataTree } from "entities/DataTree/dataTreeFactory"; import { Intent as BlueprintIntent } from "@blueprintjs/core"; +import * as Sentry from "@sentry/react"; class DropdownWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { @@ -207,3 +208,4 @@ export interface DropdownWidgetProps extends WidgetProps { } export default DropdownWidget; +export const ProfiledDropDownWidget = Sentry.withProfiler(DropdownWidget); diff --git a/app/client/src/widgets/FilepickerWidget.tsx b/app/client/src/widgets/FilepickerWidget.tsx index f6c28b5a4e..104a888111 100644 --- a/app/client/src/widgets/FilepickerWidget.tsx +++ b/app/client/src/widgets/FilepickerWidget.tsx @@ -20,6 +20,7 @@ import { import Dashboard from "@uppy/dashboard"; import shallowequal from "shallowequal"; import _ from "lodash"; +import * as Sentry from "@sentry/react"; class FilePickerWidget extends BaseWidget< FilePickerWidgetProps, @@ -231,3 +232,4 @@ export interface FilePickerWidgetProps extends WidgetProps { } export default FilePickerWidget; +export const ProfiledFilePickerWidget = Sentry.withProfiler(FilePickerWidget); diff --git a/app/client/src/widgets/FormButtonWidget.tsx b/app/client/src/widgets/FormButtonWidget.tsx index d2195460b8..37664573be 100644 --- a/app/client/src/widgets/FormButtonWidget.tsx +++ b/app/client/src/widgets/FormButtonWidget.tsx @@ -11,6 +11,7 @@ import { } from "utils/ValidationFactory"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; +import * as Sentry from "@sentry/react"; class FormButtonWidget extends BaseWidget< FormButtonWidgetProps, @@ -119,3 +120,4 @@ export interface FormButtonWidgetState extends WidgetState { } export default FormButtonWidget; +export const ProfiledFormButtonWidget = Sentry.withProfiler(FormButtonWidget); diff --git a/app/client/src/widgets/FormWidget.tsx b/app/client/src/widgets/FormWidget.tsx index 4ed043e286..a672212c6a 100644 --- a/app/client/src/widgets/FormWidget.tsx +++ b/app/client/src/widgets/FormWidget.tsx @@ -5,6 +5,7 @@ import { WidgetType } from "constants/WidgetConstants"; import ContainerWidget, { ContainerWidgetProps } from "widgets/ContainerWidget"; import { ContainerComponentProps } from "components/designSystems/appsmith/ContainerComponent"; import shallowEqual from "shallowequal"; +import * as Sentry from "@sentry/react"; class FormWidget extends ContainerWidget { checkInvalidChildren = (children: WidgetProps[]): boolean => { @@ -72,3 +73,4 @@ export interface FormWidgetProps extends ContainerComponentProps { } export default FormWidget; +export const ProfiledFormWidget = Sentry.withProfiler(FormWidget); diff --git a/app/client/src/widgets/IconWidget.tsx b/app/client/src/widgets/IconWidget.tsx index 57ba12a34a..d2928f6a31 100644 --- a/app/client/src/widgets/IconWidget.tsx +++ b/app/client/src/widgets/IconWidget.tsx @@ -7,6 +7,7 @@ import IconComponent, { IconType, } from "components/designSystems/appsmith/IconComponent"; import { EventType, ExecutionResult } from "constants/ActionConstants"; +import * as Sentry from "@sentry/react"; const IconWrapper = styled.div` display: flex; @@ -69,3 +70,4 @@ export interface IconWidgetProps extends WidgetProps { } export default IconWidget; +export const ProfiledIconWidget = Sentry.withProfiler(IconWidget); diff --git a/app/client/src/widgets/ImageWidget.tsx b/app/client/src/widgets/ImageWidget.tsx index b43722ea76..25d57d6c6e 100644 --- a/app/client/src/widgets/ImageWidget.tsx +++ b/app/client/src/widgets/ImageWidget.tsx @@ -7,6 +7,7 @@ import { BASE_WIDGET_VALIDATION, } from "utils/ValidationFactory"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; +import * as Sentry from "@sentry/react"; class ImageWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { @@ -42,3 +43,4 @@ export interface ImageWidgetProps extends WidgetProps { } export default ImageWidget; +export const ProfiledImageWidget = Sentry.withProfiler(ImageWidget); diff --git a/app/client/src/widgets/InputWidget.tsx b/app/client/src/widgets/InputWidget.tsx index d2c17d7ee2..05d791fcc6 100644 --- a/app/client/src/widgets/InputWidget.tsx +++ b/app/client/src/widgets/InputWidget.tsx @@ -16,6 +16,7 @@ import { TriggerPropertiesMap, } from "utils/WidgetFactory"; import _ from "lodash"; +import * as Sentry from "@sentry/react"; class InputWidget extends BaseWidget { debouncedHandleTextChanged = _.debounce( @@ -221,3 +222,4 @@ interface InputWidgetState extends WidgetState { } export default InputWidget; +export const ProfiledInputWidget = Sentry.withProfiler(InputWidget); diff --git a/app/client/src/widgets/MapWidget.tsx b/app/client/src/widgets/MapWidget.tsx index fca4ffdce4..7d81407e50 100644 --- a/app/client/src/widgets/MapWidget.tsx +++ b/app/client/src/widgets/MapWidget.tsx @@ -8,6 +8,7 @@ import { EventType } from "constants/ActionConstants"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; import { getAppsmithConfigs } from "configs"; import styled from "styled-components"; +import * as Sentry from "@sentry/react"; const { google } = getAppsmithConfigs(); @@ -198,3 +199,4 @@ export interface MapWidgetProps extends WidgetProps { } export default MapWidget; +export const ProfiledMapWidget = Sentry.withProfiler(MapWidget); diff --git a/app/client/src/widgets/ModalWidget.tsx b/app/client/src/widgets/ModalWidget.tsx index 1a28abc369..a8258dd168 100644 --- a/app/client/src/widgets/ModalWidget.tsx +++ b/app/client/src/widgets/ModalWidget.tsx @@ -11,6 +11,7 @@ import { GridDefaults, } from "constants/WidgetConstants"; import { generateClassName } from "utils/generators"; +import * as Sentry from "@sentry/react"; const MODAL_SIZE: { [id: string]: { width: number; height: number } } = { MODAL_SMALL: { @@ -126,5 +127,8 @@ const mapDispatchToProps = (dispatch: any) => ({ }); }, }); - -export default connect(null, mapDispatchToProps)(ModalWidget); +export default ModalWidget; +export const ProfiledModalWidget = connect( + null, + mapDispatchToProps, +)(Sentry.withProfiler(ModalWidget)); diff --git a/app/client/src/widgets/RadioGroupWidget.tsx b/app/client/src/widgets/RadioGroupWidget.tsx index 0a304dbf42..1bfdc4372f 100644 --- a/app/client/src/widgets/RadioGroupWidget.tsx +++ b/app/client/src/widgets/RadioGroupWidget.tsx @@ -9,6 +9,7 @@ import { } from "utils/ValidationFactory"; import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { TriggerPropertiesMap } from "utils/WidgetFactory"; +import * as Sentry from "@sentry/react"; class RadioGroupWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { @@ -96,3 +97,4 @@ export interface RadioGroupWidgetProps extends WidgetProps { } export default RadioGroupWidget; +export const ProfiledRadioGroupWidget = Sentry.withProfiler(RadioGroupWidget); diff --git a/app/client/src/widgets/RichTextEditorWidget.tsx b/app/client/src/widgets/RichTextEditorWidget.tsx index 9f82a0f6a5..2f7727d859 100644 --- a/app/client/src/widgets/RichTextEditorWidget.tsx +++ b/app/client/src/widgets/RichTextEditorWidget.tsx @@ -9,6 +9,7 @@ import { DerivedPropertiesMap, } from "utils/WidgetFactory"; import Skeleton from "components/utils/Skeleton"; +import * as Sentry from "@sentry/react"; const RichtextEditorComponent = lazy(() => import( @@ -103,3 +104,6 @@ export interface RichTextEditorWidgetProps extends WidgetProps { } export default RichTextEditorWidget; +export const ProfiledRichTextEditorWidget = Sentry.withProfiler( + RichTextEditorWidget, +); diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index 72aec3cd76..1d01469ab2 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -21,6 +21,7 @@ import { ColumnAction } from "components/propertyControls/ColumnActionSelectorCo import { TriggerPropertiesMap } from "utils/WidgetFactory"; import Skeleton from "components/utils/Skeleton"; import moment from "moment"; +import * as Sentry from "@sentry/react"; const ReactTableComponent = lazy(() => import("components/designSystems/appsmith/ReactTableComponent"), ); @@ -612,3 +613,4 @@ export interface TableWidgetProps extends WidgetProps { } export default TableWidget; +export const ProfiledTableWidget = Sentry.withProfiler(TableWidget); diff --git a/app/client/src/widgets/TabsWidget.tsx b/app/client/src/widgets/TabsWidget.tsx index fcb4b9785f..fb978667de 100644 --- a/app/client/src/widgets/TabsWidget.tsx +++ b/app/client/src/widgets/TabsWidget.tsx @@ -9,6 +9,7 @@ import { VALIDATION_TYPES } from "constants/WidgetValidation"; import _ from "lodash"; import { EventType } from "constants/ActionConstants"; import { WidgetOperations } from "widgets/BaseWidget"; +import * as Sentry from "@sentry/react"; class TabsWidget extends BaseWidget< TabsWidgetProps, @@ -217,3 +218,4 @@ export interface TabsWidgetProps } export default TabsWidget; +export const ProfiledTabsWidget = Sentry.withProfiler(TabsWidget); diff --git a/app/client/src/widgets/TextWidget.tsx b/app/client/src/widgets/TextWidget.tsx index a683a7c92e..46afe1121f 100644 --- a/app/client/src/widgets/TextWidget.tsx +++ b/app/client/src/widgets/TextWidget.tsx @@ -8,6 +8,7 @@ import { BASE_WIDGET_VALIDATION, } from "utils/ValidationFactory"; import { DerivedPropertiesMap } from "utils/WidgetFactory"; +import * as Sentry from "@sentry/react"; const LINE_HEIGHTS: { [key in TextStyle]: number } = { // The following values are arrived at by multiplying line-height with font-size @@ -72,3 +73,4 @@ export interface TextWidgetProps extends WidgetProps { } export default TextWidget; +export const ProfiledTextWidget = Sentry.withProfiler(TextWidget); diff --git a/app/client/tsconfig.json b/app/client/tsconfig.json index a7559d371e..1438862bb8 100644 --- a/app/client/tsconfig.json +++ b/app/client/tsconfig.json @@ -1,5 +1,4 @@ { - "extends": "./tsconfig.path.json", "compilerOptions": { "target": "es5", "lib": [ diff --git a/app/client/yarn.lock b/app/client/yarn.lock index f53b5a0f12..dba6aba800 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -2719,14 +2719,14 @@ dependencies: any-observable "^0.3.0" -"@sentry/browser@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.0.tgz#d019fddbd885a1b735436b573067e79c81909043" - integrity sha512-+wsMegqvhdC4H/qlKvGFvYff9VIztENBiTHsSXqOHTFuVeQgyESbeHsJae2Fm4kuYDUtpAmUoIZe7cwwbPJNew== +"@sentry/browser@5.22.2": + version "5.22.2" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.2.tgz#c37c61c8612a169059ddcd8c08cd09441da7776c" + integrity sha512-kkNRFMcNErtWvz9WI0bG5Va2W+mRWhk5CxaJKWUMdMcGR2rIrl3D+kcMdpYDi9tNYPHUdUzTCb3vJQfO8o6TbA== dependencies: - "@sentry/core" "5.22.0" - "@sentry/types" "5.22.0" - "@sentry/utils" "5.22.0" + "@sentry/core" "5.22.2" + "@sentry/types" "5.22.2" + "@sentry/utils" "5.22.2" tslib "^1.9.3" "@sentry/cli@^1.55.0": @@ -2740,69 +2740,69 @@ progress "^2.0.3" proxy-from-env "^1.1.0" -"@sentry/core@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.0.tgz#2fa51c9f547e8b6d7ec820419c7880635f8648a5" - integrity sha512-VV9qbjHDlfmpwEi59xS3GN2Fz0tsxKCB4rTqqUpvsM5BCOxV162Q0f3MCwP1nBRSk5DnOvKuTiNAWg7b3kpX+g== +"@sentry/core@5.22.2": + version "5.22.2" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.2.tgz#3b57300d92c13163c26174311ad82482a08b9266" + integrity sha512-Tj3FlHiqK8uveKh56QP3PhNNrH13LTWqN1TwRwE2B2FLiqwIHGmJsCQNfyslQdBAkNeGRnnQrQxqH53KeuJGGA== dependencies: - "@sentry/hub" "5.22.0" - "@sentry/minimal" "5.22.0" - "@sentry/types" "5.22.0" - "@sentry/utils" "5.22.0" + "@sentry/hub" "5.22.2" + "@sentry/minimal" "5.22.2" + "@sentry/types" "5.22.2" + "@sentry/utils" "5.22.2" tslib "^1.9.3" -"@sentry/hub@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.0.tgz#b2bf2e99c9bd752605c63b606ab11431909ebe87" - integrity sha512-OuKaEGsreQxHCKXcyQipygYxBD17PaBH0vqzDWl3d+/ydZbhpl0e5kjeHviJiZ6JWZ2cIZFvAzfvNVQoymF8BQ== +"@sentry/hub@5.22.2": + version "5.22.2" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.2.tgz#812c8250970e44c63ee2bf1f779777d32dd88f54" + integrity sha512-6McBsonfpOY5hzlowzDfdLZklFQ1wWTGtiA0eByKxS/H1GePJc+UUSsu6D3bZJG0bIjFoq5vxLQFSZq6C7BPlQ== dependencies: - "@sentry/types" "5.22.0" - "@sentry/utils" "5.22.0" + "@sentry/types" "5.22.2" + "@sentry/utils" "5.22.2" tslib "^1.9.3" -"@sentry/minimal@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.0.tgz#7e24625a84af37ef2d2317f6f691dab27ff2ca02" - integrity sha512-iq7wPxVdPCOS2gDw3PENO66wOfMv6mq+Nup7EmTteKtO7CUVqVFIXjXZYBHMG49sZWMCT+ZsPI/a9xCDaJytBQ== +"@sentry/minimal@5.22.2": + version "5.22.2" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.2.tgz#bb050a49158c48596094184cff2806ce8cb63c9d" + integrity sha512-jfth6bY19FXE/kQc6hLBCKg5CjfX1MG+weyEXnPFstCb5JFMvSt6YPRI3OsY1hG3rQLxTX0mVSbe2YrBJE5kXA== dependencies: - "@sentry/hub" "5.22.0" - "@sentry/types" "5.22.0" + "@sentry/hub" "5.22.2" + "@sentry/types" "5.22.2" tslib "^1.9.3" -"@sentry/react@^5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.22.0.tgz#d1b312035f21302ce6877d62b83d12c793275043" - integrity sha512-Z7RCb9fCiR4bSe0PLyA8w/v7tvhpoA9G5x5LAncJuxR5ihnwVa2RYt5e/fl5vIazh2EuB6pveBPvpm4ntpmYeQ== +"@sentry/react@^5.22.2": + version "5.22.2" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-5.22.2.tgz#8a0f9ee475659faf780080569ca7c7c5cb92b218" + integrity sha512-DgpLWZV5htumjXaE13E/KMODwKyr56bNtk1gaTOPbm368b6zdcM/bwutdNQSTuNlhkJ4bhnhpVs7OzErJkcDLw== dependencies: - "@sentry/browser" "5.22.0" - "@sentry/minimal" "5.22.0" - "@sentry/types" "5.22.0" - "@sentry/utils" "5.22.0" + "@sentry/browser" "5.22.2" + "@sentry/minimal" "5.22.2" + "@sentry/types" "5.22.2" + "@sentry/utils" "5.22.2" hoist-non-react-statics "^3.3.2" tslib "^1.9.3" -"@sentry/tracing@^5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.22.0.tgz#d9cb85d22340b30afe2a8554c6d66742236cde56" - integrity sha512-dtDX9LDC/yAckXK+cifPt7vu/JWUiGFkalh5m/WgkhV2eveZ1o+bbv9YM0em8t4Kz3lOmJM/R9iO6YaeJKsqlQ== +"@sentry/tracing@^5.22.2": + version "5.22.2" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-5.22.2.tgz#6a326bf43b3491c3e462330f8e7d6cd52a975a2d" + integrity sha512-M4F4CN85luWoHBuBiQMk2/dtpn5L2CriBTcLsKQNkA/fk6lv8CbE1WZ/SkCMWzK4hxo5I2vSyzLlb2OwcMHmdQ== dependencies: - "@sentry/hub" "5.22.0" - "@sentry/minimal" "5.22.0" - "@sentry/types" "5.22.0" - "@sentry/utils" "5.22.0" + "@sentry/hub" "5.22.2" + "@sentry/minimal" "5.22.2" + "@sentry/types" "5.22.2" + "@sentry/utils" "5.22.2" tslib "^1.9.3" -"@sentry/types@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.0.tgz#7017dc5f36aa05b7042c3ef703a740b397db8230" - integrity sha512-PAeOQ8yxTkeTdJSYbPw6Tb7Wtx7/MWggqH6qj2G41u1yCvOoPURhlmd3pSayad+lWs2qm2kjVdkJKbPVJ4kojQ== +"@sentry/types@5.22.2": + version "5.22.2" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.2.tgz#32d7f82537afe7712971fd6222c7744f0d8a27fe" + integrity sha512-Ko9pri0D0TNaSHocLVLQQZlnTtMXrBhP5AZYjB193aYqc1x52dFchQlhiKLEgyFCKjTEIlD/J9ZD7QkQoeYT+A== -"@sentry/utils@5.22.0": - version "5.22.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.0.tgz#5b13fda94698fb1094535bc2b9b38a351894ba7e" - integrity sha512-MvHB+PAVI4PAffZOiRhgODmj1CgmViO2257abGtBY2hM1wGqc6tmLw1mn6rF+fh+Id2UDfa4miIJL2VY1E2aaw== +"@sentry/utils@5.22.2": + version "5.22.2" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.2.tgz#7296cd9036e8d34638743a27fe8c6fe0e70db902" + integrity sha512-HGpPohNgwRhR+7bf2OlziX84JVdwQAauesqcL4Y78e+U09+E06cQAEQkRIQJfn2Ai2NvVmMVhdj51v+AqmaAWQ== dependencies: - "@sentry/types" "5.22.0" + "@sentry/types" "5.22.2" tslib "^1.9.3" "@sentry/webpack-plugin@^1.12.1": diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json index e3c8ce97e7..77f8704598 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json @@ -41,7 +41,9 @@ { "label": "Host Address", "configProperty": "datasourceConfiguration.endpoints[*].host", - "controlType": "KEYVALUE_ARRAY" + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" }, { "label": "Port", 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 63f11277bf..7ade6e76ac 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 @@ -25,9 +25,11 @@ import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; @@ -43,6 +45,8 @@ public class MySqlPlugin extends BasePlugin { private static final String PASSWORD = "password"; private static final int VALIDITY_CHECK_TIMEOUT = 5; + private static final String DATE_COLUMN_TYPE_NAME = "date"; + public MySqlPlugin(PluginWrapper wrapper) { super(wrapper); } @@ -88,11 +92,38 @@ public class MySqlPlugin extends BasePlugin { ResultSetMetaData metaData = resultSet.getMetaData(); int colCount = metaData.getColumnCount(); while (resultSet.next()) { - Map row = new HashMap<>(colCount); - for (int i = 1; i <= colCount; i++) { - row.put(metaData.getColumnName(i), resultSet.getObject(i)); - } + // Use `LinkedHashMap` here so that the column ordering is preserved in the response. + Map row = new LinkedHashMap<>(colCount); rowsList.add(row); + + for (int i = 1; i <= colCount; i++) { + Object value; + final String typeName = metaData.getColumnTypeName(i); + + if (resultSet.getObject(i) == null) { + value = null; + + } else if (DATE_COLUMN_TYPE_NAME.equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE.format(resultSet.getDate(i).toLocalDate()); + + } else if ("datetime".equalsIgnoreCase(typeName) || "timestamp".equalsIgnoreCase(typeName)) { + value = DateTimeFormatter.ISO_DATE_TIME.format( + LocalDateTime.of( + resultSet.getDate(i).toLocalDate(), + resultSet.getTime(i).toLocalTime() + ) + ) + "Z"; + + } else if ("year".equalsIgnoreCase(typeName)) { + value = resultSet.getDate(i).toLocalDate().getYear(); + + } else { + value = resultSet.getObject(i); + + } + + row.put(metaData.getColumnLabel(i), value); + } } } else { diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json index 65765dd23f..6df259b8c7 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/form.json @@ -27,7 +27,9 @@ { "label": "Host Address", "configProperty": "datasourceConfiguration.endpoints[*].host", - "controlType": "KEYVALUE_ARRAY" + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" }, { "label": "Port", diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java index 6e143e56e8..17af5596ed 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java @@ -1,6 +1,13 @@ package com.external.plugins; -import com.appsmith.external.models.*; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.Endpoint; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import lombok.extern.log4j.Log4j; import org.junit.Before; import org.junit.ClassRule; @@ -10,21 +17,31 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.sql.Connection; +import java.sql.DriverManager; import java.sql.SQLException; -import java.util.ArrayList; +import java.sql.Statement; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Properties; import java.util.Set; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; @Log4j public class MySqlPluginTest { MySqlPlugin.MySqlPluginExecutor pluginExecutor = new MySqlPlugin.MySqlPluginExecutor(); + @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional. @ClassRule public static MySQLContainer mySQLContainer = new MySQLContainer() - .withUsername("mysql").withPassword("password").withDatabaseName("mysql"); + .withUsername("mysql") + .withPassword("password") + .withDatabaseName("mysql"); String address; Integer port; @@ -34,11 +51,67 @@ public class MySqlPluginTest { @Before public void setUp() { + if (address != null) { + return; + } + address = mySQLContainer.getContainerIpAddress(); port = mySQLContainer.getFirstMappedPort(); username = mySQLContainer.getUsername(); password = mySQLContainer.getPassword(); createDatasourceConfiguration(); + + Properties properties = new Properties(); + properties.putAll(Map.of( + "user", username, + "password", password + )); + + try (Connection connection = DriverManager.getConnection( + "jdbc:mysql://" + address + ":" + port + "/" + username, + properties + )) { + + try (Statement statement = connection.createStatement()) { + statement.execute("DROP TABLE IF EXISTS users"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute("CREATE TABLE users (\n" + + " id serial PRIMARY KEY,\n" + + " username VARCHAR (50) UNIQUE NOT NULL,\n" + + " password VARCHAR (50) NOT NULL,\n" + + " email VARCHAR (355) UNIQUE NOT NULL,\n" + + " spouse_dob DATE,\n" + + " dob DATE NOT NULL,\n" + + " yob YEAR NOT NULL,\n" + + " time1 TIME NOT NULL,\n" + + " created_on TIMESTAMP NOT NULL,\n" + + " updated_on DATETIME NOT NULL\n" + + ")"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute( + "INSERT INTO users VALUES (" + + "1, 'Jack', 'jill', 'jack@exemplars.com', NULL, '2018-12-31', 2018," + + " '18:32:45'," + + " '2018-11-30 20:45:15', '2018-11-30 20:45:15'" + + ")"); + } + + try (Statement statement = connection.createStatement()) { + statement.execute( + "INSERT INTO users VALUES (" + + "2, 'Jill', 'jack', 'jill@exemplars.com', NULL, '2019-12-31', 2019," + + " '15:45:30'," + + " '2019-11-30 23:59:59', '2019-11-30 23:59:59'" + + ")"); + } + + } catch (SQLException throwable) { + throwable.printStackTrace(); + } } private DatasourceConfiguration createDatasourceConfiguration() { @@ -137,7 +210,79 @@ public class MySqlPluginTest { } }) .verifyComplete(); + } + @Test + public void testAliasColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "user_id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + }) + .verifyComplete(); + } + + @Test + public void testExecuteDataTypes() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertEquals("2018-12-31", node.get("dob").asText()); + assertEquals("2018", node.get("yob").asText()); + assertTrue(node.get("time1").asText().matches("\\d{2}:\\d{2}:\\d{2}")); + assertTrue(node.get("created_on").asText().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); + assertTrue(node.get("updated_on").asText().matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); + + assertArrayEquals( + new String[]{ + "id", + "username", + "password", + "email", + "spouse_dob", + "dob", + "yob", + "time1", + "created_on", + "updated_on" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + }) + .verifyComplete(); } } \ No newline at end of file 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 0d3994e1f0..688be19ce6 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 @@ -47,9 +47,10 @@ public class PostgresPlugin extends BasePlugin { private static final String USER = "user"; private static final String PASSWORD = "password"; private static final String SSL = "ssl"; - private static final String DATE_COLUMN_TYPE_NAME = "date"; private static final int VALIDITY_CHECK_TIMEOUT = 5; + private static final String DATE_COLUMN_TYPE_NAME = "date"; + public PostgresPlugin(PluginWrapper wrapper) { super(wrapper); } diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json index 65fe25e6a4..8a161ef3e7 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/form.json @@ -27,7 +27,9 @@ { "label": "Host Address", "configProperty": "datasourceConfiguration.endpoints[*].host", - "controlType": "KEYVALUE_ARRAY" + "controlType": "KEYVALUE_ARRAY", + "validationMessage": "Please enter a valid host", + "validationRegex": "^((?![/:]).)*$" }, { "label": "Port", diff --git a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java index cc5472e82b..d67980502c 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java @@ -38,6 +38,7 @@ public class PostgresPluginTest { PostgresPlugin.PostgresPluginExecutor pluginExecutor = new PostgresPlugin.PostgresPluginExecutor(); + @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional. @ClassRule public static final PostgreSQLContainer pgsqlContainer = new PostgreSQLContainer<>("postgres:alpine") .withExposedPorts(5432) @@ -79,7 +80,7 @@ public class PostgresPluginTest { } try (Statement statement = connection.createStatement()) { - statement.execute("CREATE TABLE users(\n" + + statement.execute("CREATE TABLE users (\n" + " id serial PRIMARY KEY,\n" + " username VARCHAR (50) UNIQUE NOT NULL,\n" + " password VARCHAR (50) NOT NULL,\n" + @@ -114,8 +115,8 @@ public class PostgresPluginTest { ")"); } - } catch (SQLException throwables) { - throwables.printStackTrace(); + } catch (SQLException throwable) { + throwable.printStackTrace(); } } @@ -151,6 +152,33 @@ public class PostgresPluginTest { .verifyComplete(); } + @Test + public void testAliasColumnNames() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("SELECT id as user_id FROM users WHERE id = 1"); + + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + + StepVerifier.create(executeMono) + .assertNext(result -> { + final JsonNode node = ((ArrayNode) result.getBody()).get(0); + assertArrayEquals( + new String[]{ + "user_id" + }, + new ObjectMapper() + .convertValue(node, LinkedHashMap.class) + .keySet() + .toArray() + ); + }) + .verifyComplete(); + } + @Test public void testExecute() { DatasourceConfiguration dsConfig = createDatasourceConfiguration(); @@ -159,12 +187,11 @@ public class PostgresPluginTest { ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setBody("SELECT * FROM users WHERE id = 1"); - Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); + Mono executeMono = dsConnectionMono + .flatMap(conn -> pluginExecutor.execute(conn, dsConfig, actionConfiguration)); StepVerifier.create(executeMono) - .assertNext(obj -> { - ActionExecutionResult result = (ActionExecutionResult) obj; - + .assertNext(result -> { assertNotNull(result); assertTrue(result.getIsExecutionSuccess()); assertNotNull(result.getBody()); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java index 10e593f358..6d0b248f03 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java @@ -26,4 +26,5 @@ public interface Url { String MARKETPLACE_URL = BASE_URL + VERSION + "/marketplace"; String API_TEMPLATE_URL = BASE_URL + VERSION + "/templates"; String MARKETPLACE_ITEM_URL = BASE_URL + VERSION + "/items"; + String ASSET_URL = BASE_URL + VERSION + "/assets"; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/AssetController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/AssetController.java new file mode 100644 index 0000000000..8ae02015fe --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/AssetController.java @@ -0,0 +1,46 @@ +package com.appsmith.server.controllers; + +import com.appsmith.server.constants.Url; +import com.appsmith.server.services.AssetService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.buffer.DefaultDataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping(Url.ASSET_URL) +@Slf4j +@RequiredArgsConstructor +public class AssetController { + + private final AssetService service; + + @GetMapping("/{id}") + public Mono getById(@PathVariable String id, ServerWebExchange exchange) { + log.debug("Returning asset with ID '{}'.", id); + + final ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(HttpStatus.OK); + + final Mono imageBufferMono = service.getById(id) + .map(asset -> { + final String contentType = asset.getContentType(); + if (contentType != null) { + response.getHeaders().set(HttpHeaders.CONTENT_TYPE, contentType); + } + return new DefaultDataBufferFactory().wrap(asset.getData()); + }); + + return response.writeWith(imageBufferMono); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java index 1b43ef6c8f..2c2893f121 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java @@ -8,12 +8,15 @@ import com.appsmith.server.services.OrganizationService; import com.appsmith.server.services.UserOrganizationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.codec.multipart.Part; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @@ -52,4 +55,13 @@ public class OrganizationController extends BaseController new ResponseDTO<>(HttpStatus.OK.value(), user, null)); } + + @PostMapping("/{organizationId}/logo") + public Mono> uploadLogo(@PathVariable String organizationId, + @RequestPart("file") Mono fileMono) { + return fileMono + .flatMap(filePart -> service.uploadLogo(organizationId, filePart)) + .map(url -> new ResponseDTO<>(HttpStatus.OK.value(), url, null)); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Asset.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Asset.java new file mode 100644 index 0000000000..d41cb783c3 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Asset.java @@ -0,0 +1,22 @@ +package com.appsmith.server.domains; + +import com.appsmith.external.models.BaseDomain; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.http.MediaType; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class Asset extends BaseDomain { + + public Asset(MediaType mediaType, byte[] data) { + this(mediaType == null ? null : mediaType.toString(), data); + } + + String contentType; + + byte[] data; + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Organization.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Organization.java index 0dfa59a74f..022abe78c3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Organization.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Organization.java @@ -1,6 +1,7 @@ package com.appsmith.server.domains; import com.appsmith.external.models.BaseDomain; +import com.appsmith.server.constants.Url; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.NoArgsConstructor; @@ -37,6 +38,9 @@ public class Organization extends BaseDomain { @JsonIgnore private List userRoles; + @JsonIgnore + private String logoAssetId; + public String makeSlug() { return toSlug(name); } @@ -44,4 +48,9 @@ public class Organization extends BaseDomain { public static String toSlug(String text) { return text == null ? null : text.replaceAll("[^\\w\\d]+", "-").toLowerCase(); } + + public String getLogoUrl() { + return Url.ASSET_URL + "/" + logoAssetId; + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PluginExecutorHelper.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PluginExecutorHelper.java index e20cc6dd04..3f46bfffa2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PluginExecutorHelper.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PluginExecutorHelper.java @@ -14,7 +14,7 @@ import java.util.List; @Component public class PluginExecutorHelper { - private PluginManager pluginManager; + private final PluginManager pluginManager; @Autowired public PluginExecutorHelper(PluginManager pluginManager) { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/AssetRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/AssetRepository.java new file mode 100644 index 0000000000..4f66c63435 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/AssetRepository.java @@ -0,0 +1,8 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.Asset; +import org.springframework.stereotype.Repository; + +@Repository +public interface AssetRepository extends BaseRepository { +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AssetService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AssetService.java new file mode 100644 index 0000000000..40ae3d4ce5 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AssetService.java @@ -0,0 +1,10 @@ +package com.appsmith.server.services; + +import com.appsmith.server.domains.Asset; +import reactor.core.publisher.Mono; + +public interface AssetService { + + Mono getById(String id); + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AssetServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AssetServiceImpl.java new file mode 100644 index 0000000000..5919413c72 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/AssetServiceImpl.java @@ -0,0 +1,22 @@ +package com.appsmith.server.services; + +import com.appsmith.server.domains.Asset; +import com.appsmith.server.repositories.AssetRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AssetServiceImpl implements AssetService { + + private final AssetRepository repository; + + @Override + public Mono getById(String id) { + return repository.findById(id); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java index dace4aefd5..708a64f47e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java @@ -3,6 +3,7 @@ package com.appsmith.server.services; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.Policy; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.acl.AclPermission; @@ -10,6 +11,8 @@ import com.appsmith.server.acl.PolicyGenerator; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Organization; +import com.appsmith.server.domains.Plugin; +import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -164,6 +167,7 @@ public class DatasourceServiceImpl extends BaseService validateDatasource(Datasource datasource) { Set invalids = new HashSet<>(); + datasource.setInvalids(invalids); if (!StringUtils.hasText(datasource.getName())) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.NAME)); @@ -171,13 +175,11 @@ public class DatasourceServiceImpl extends BaseService { invalids.add(AppsmithError.PLUGIN_NOT_INSTALLED.getMessage(datasource.getPluginId())); - datasource.setInvalids(invalids); return Mono.just(new Organization()); })); @@ -193,18 +194,30 @@ public class DatasourceServiceImpl extends BaseService pluginExecutorMono = pluginExecutorHelper.getPluginExecutor(pluginService.findById(datasource.getPluginId())) + final Mono pluginMono = pluginService.findById(datasource.getPluginId()).cache(); + Mono pluginExecutorMono = pluginExecutorHelper.getPluginExecutor(pluginMono) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PLUGIN, datasource.getPluginId()))); return checkPluginInstallationAndThenReturnOrganizationMono - .then(pluginExecutorMono) + .then(pluginMono) + .flatMap(plugin -> { + if (PluginType.DB.equals(plugin.getType()) + && datasource.getDatasourceConfiguration() != null + && datasource.getDatasourceConfiguration().getEndpoints() != null) { + for (final Endpoint endpoint : datasource.getDatasourceConfiguration().getEndpoints()) { + if (endpoint.getHost().contains("/") || endpoint.getHost().contains(":")) { + invalids.add("Host value cannot contain `/` or `:` characters. Found `" + endpoint.getHost() + "`."); + } + } + } + return pluginExecutorMono; + }) .flatMap(pluginExecutor -> { DatasourceConfiguration datasourceConfiguration = datasource.getDatasourceConfiguration(); if (datasourceConfiguration != null && !pluginExecutor.isDatasourceValid(datasourceConfiguration)) { invalids.addAll(pluginExecutor.validateDatasource(datasourceConfiguration)); } - datasource.setInvalids(invalids); return Mono.just(datasource); }); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java index 87c2913c01..e09a1f98aa 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationService.java @@ -4,6 +4,7 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.User; import com.appsmith.server.domains.UserRole; +import org.springframework.http.codec.multipart.Part; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -34,4 +35,6 @@ public interface OrganizationService extends CrudService { Mono> getUserRolesForOrganization(String orgId); Mono> getOrganizationMembers(String orgId); + + Mono uploadLogo(String organizationId, Part filePart); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java index 71500d8c2d..a9fa811d76 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/OrganizationServiceImpl.java @@ -4,6 +4,7 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.AppsmithRole; import com.appsmith.server.acl.RoleGraph; import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Asset; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.OrganizationPlugin; import com.appsmith.server.domains.OrganizationSetting; @@ -13,13 +14,17 @@ import com.appsmith.server.domains.UserRole; import com.appsmith.server.dtos.OrganizationPluginStatus; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.repositories.AssetRepository; import com.appsmith.server.repositories.OrganizationRepository; import com.appsmith.server.repositories.PluginRepository; import com.appsmith.server.repositories.UserRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.http.codec.multipart.Part; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; @@ -51,6 +56,7 @@ public class OrganizationServiceImpl extends BaseService uploadLogo(String organizationId, Part filePart) { + return Mono + .zip( + repository + .findById(organizationId, MANAGE_ORGANIZATIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, organizationId))), + filePart.content().single() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + final DataBuffer dataBuffer = tuple.getT2(); + + final String prevAssetId = organization.getLogoAssetId(); + + byte[] data = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(data); + DataBufferUtils.release(dataBuffer); + + return assetRepository + .save(new Asset(filePart.headers().getContentType(), data)) + .flatMap(asset -> { + organization.setLogoAssetId(asset.getId()); + return repository.save(organization); + }) + .flatMap(savedOrganization -> + prevAssetId != null + ? assetRepository.deleteById(prevAssetId).thenReturn(savedOrganization) + : Mono.just(savedOrganization) + ); + }); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java index 22d1b646dd..9620350b04 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java @@ -29,6 +29,7 @@ import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import reactor.core.Exceptions; import reactor.core.publisher.Flux; @@ -742,4 +743,10 @@ public class UserServiceImpl extends BaseService i return Mono.just(Boolean.TRUE); } + @Override + public Flux get(MultiValueMap params) { + // Get All Users should not be supported. Return an error + return Flux.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + } diff --git a/app/server/appsmith-server/src/main/resources/examples-organization.json b/app/server/appsmith-server/src/main/resources/examples-organization.json index a437d9f2dc..554056914c 100644 --- a/app/server/appsmith-server/src/main/resources/examples-organization.json +++ b/app/server/appsmith-server/src/main/resources/examples-organization.json @@ -7,6 +7,18 @@ "policies": [], "_class": "com.appsmith.server.domains.Organization", "$datasources": [ + { + "name": "FreshDesk API", + "$pluginPackageName": "restapi-plugin", + "datasourceConfiguration": { + "sshProxyEnabled": false, + "url": "https://appsmithhelp.freshdesk.com" + }, + "invalids": [], + "deleted": false, + "policies": [], + "_class": "com.appsmith.server.domains.Datasource" + }, { "name": "Mock Database", "$pluginPackageName": "postgres-plugin", @@ -31,18 +43,6 @@ "deleted": false, "policies": [], "_class": "com.appsmith.server.domains.Datasource" - }, - { - "name": "FreshDesk API", - "$pluginPackageName": "restapi-plugin", - "datasourceConfiguration": { - "sshProxyEnabled": false, - "url": "https://appsmithhelp.freshdesk.com" - }, - "invalids": [ ], - "deleted": false, - "policies": [], - "_class": "com.appsmith.server.domains.Datasource" } ], "$applications": [ @@ -77,7 +77,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -88,10 +88,10 @@ }, "pluginType": "DB", "executeOnLoad": true, - "dynamicBindingPathList": [ ], + "dynamicBindingPathList": [], "isValid": true, - "invalids": [ ], - "jsonPathKeys": [ ], + "invalids": [], + "jsonPathKeys": [], "deleted": false, "policies": [], "_class": "com.appsmith.server.domains.Action" @@ -114,9 +114,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -276,9 +274,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -429,7 +425,7 @@ { "name": "getSignupCount", "pluginType": "DB", - "jsonPathKeys": [ ], + "jsonPathKeys": [], "timeoutInMillisecond": 10000 } ] @@ -439,7 +435,7 @@ { "name": "getSignupCount", "pluginType": "DB", - "jsonPathKeys": [ ], + "jsonPathKeys": [], "timeoutInMillisecond": 10000 } ] @@ -454,7 +450,7 @@ "Chart1" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -488,7 +484,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -505,7 +501,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "startDatePicker.selectedDate", "endDatePicker.selectedDate" @@ -532,9 +528,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -798,9 +792,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -1090,7 +1082,7 @@ "Chart1" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -1119,9 +1111,9 @@ "datasourceConfiguration": { "url": "https://api.razorpay.com" }, - "invalids": [ ], + "invalids": [], "deleted": false, - "policies": [ ] + "policies": [] }, "actionConfiguration": { "timeoutInMillisecond": 10000, @@ -1151,10 +1143,10 @@ "httpMethod": "POST" }, "pluginType": "API", - "dynamicBindingPathList": [ ], + "dynamicBindingPathList": [], "isValid": true, - "invalids": [ ], - "jsonPathKeys": [ ], + "invalids": [], + "jsonPathKeys": [], "deleted": false, "policies": [], "_class": "com.appsmith.server.domains.Action" @@ -1182,7 +1174,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -1198,7 +1190,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "ordersTable.selectedRow.id" ], @@ -1229,7 +1221,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -1245,7 +1237,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "usersTable.selectedRow.id" ], @@ -1276,7 +1268,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -1293,7 +1285,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "usersTable.searchText", "(usersTable.pageNo - 1) * 10" @@ -1325,7 +1317,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -1341,7 +1333,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "ordersTable.selectedRow.id" ], @@ -1358,9 +1350,9 @@ "datasourceConfiguration": { "url": "https://api.razorpay.com" }, - "invalids": [ ], + "invalids": [], "deleted": false, - "policies": [ ] + "policies": [] }, "actionConfiguration": { "timeoutInMillisecond": 10000, @@ -1399,7 +1391,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "ordersTable.selectedRow.orderAmount * 100", "ordersTable.selectedRow.paymentId" @@ -1431,7 +1423,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -1447,7 +1439,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "phoneInput.text", "addressInput.text", @@ -1477,9 +1469,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -1546,7 +1536,7 @@ "onPageChange": true }, "onRowSelected": "{{getOrders.run()}}", - "columnActions": [ ], + "columnActions": [], "columnSizeMap": { "address": 204, "name": 268, @@ -1567,9 +1557,7 @@ "canOutsideClickClose": true, "type": "MODAL_WIDGET", "canEscapeKeyClose": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "0", "shouldScrollContents": true, "blueprint": { @@ -1653,7 +1641,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "isVisible": true, "isDisabled": false, "canExtend": true @@ -1819,9 +1807,7 @@ "bottomRow": 14, "isVisible": true, "type": "BUTTON_WIDGET", - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "9pgyrkail5", "isLoading": false, "leftColumn": 12, @@ -2779,7 +2765,7 @@ "isVisible": true, "isDisabled": false, "shouldScrollContents": false, - "children": [ ], + "children": [], "blueprint": { "view": [ { @@ -2904,9 +2890,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -2973,7 +2957,7 @@ "onPageChange": true }, "onRowSelected": "{{getOrders.run()}}", - "columnActions": [ ], + "columnActions": [], "columnSizeMap": { "address": 204, "name": 268, @@ -2994,9 +2978,7 @@ "canOutsideClickClose": true, "type": "MODAL_WIDGET", "canEscapeKeyClose": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "0", "shouldScrollContents": true, "blueprint": { @@ -3080,7 +3062,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "isVisible": true, "isDisabled": false, "canExtend": true @@ -3246,9 +3228,7 @@ "bottomRow": 14, "isVisible": true, "type": "BUTTON_WIDGET", - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "9pgyrkail5", "isLoading": false, "leftColumn": 12, @@ -4206,7 +4186,7 @@ "isVisible": true, "isDisabled": false, "shouldScrollContents": false, - "children": [ ], + "children": [], "blueprint": { "view": [ { @@ -4393,7 +4373,7 @@ "Button6" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -4413,9 +4393,9 @@ "datasourceConfiguration": { "url": "https://appsmithhelp.freshdesk.com" }, - "invalids": [ ], + "invalids": [], "deleted": false, - "policies": [ ] + "policies": [] }, "actionConfiguration": { "timeoutInMillisecond": 10000, @@ -4444,10 +4424,10 @@ "httpMethod": "GET" }, "pluginType": "API", - "dynamicBindingPathList": [ ], + "dynamicBindingPathList": [], "isValid": true, - "invalids": [ ], - "jsonPathKeys": [ ], + "invalids": [], + "jsonPathKeys": [], "deleted": false, "policies": [], "_class": "com.appsmith.server.domains.Action" @@ -4470,9 +4450,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -4626,9 +4604,7 @@ "parentRowSpace": 40, "isVisible": true, "type": "BUTTON_WIDGET", - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "6rczwfuxqd", "isLoading": false, "parentColumnSpace": 34.5, @@ -4718,7 +4694,7 @@ "isLoading": false, "parentColumnSpace": 74, "leftColumn": 0, - "columnActions": [ ], + "columnActions": [], "columnSizeMap": { "subject": 217, "id": 115, @@ -4806,9 +4782,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -4962,9 +4936,7 @@ "parentRowSpace": 40, "isVisible": true, "type": "BUTTON_WIDGET", - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "6rczwfuxqd", "isLoading": false, "parentColumnSpace": 34.5, @@ -5054,7 +5026,7 @@ "isLoading": false, "parentColumnSpace": 74, "leftColumn": 0, - "columnActions": [ ], + "columnActions": [], "columnSizeMap": { "subject": 217, "id": 115, @@ -5133,7 +5105,7 @@ { "name": "getTickets", "pluginType": "API", - "jsonPathKeys": [ ], + "jsonPathKeys": [], "timeoutInMillisecond": 10000 } ] @@ -5143,7 +5115,7 @@ { "name": "getTickets", "pluginType": "API", - "jsonPathKeys": [ ], + "jsonPathKeys": [], "timeoutInMillisecond": 10000 } ] @@ -5163,7 +5135,7 @@ "MainContainer" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -5206,7 +5178,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -5222,7 +5194,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "statusRadio.selectedOptionValue", "genderDropdown.selectedOptionValue", @@ -5252,9 +5224,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -5334,7 +5304,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "containerStyle": "none", "canExtend": false } @@ -5933,9 +5903,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -6015,7 +5983,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "containerStyle": "none", "canExtend": false } @@ -6600,8 +6568,8 @@ } ] }, - "layoutOnLoadActions": [ ], - "publishedLayoutOnLoadActions": [ ], + "layoutOnLoadActions": [], + "publishedLayoutOnLoadActions": [], "widgetNames": [ "Form1", "Text9", @@ -6631,7 +6599,7 @@ "Canvas1" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -6665,7 +6633,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -6681,7 +6649,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "scheduleDatePicker.selectedDate", "ideaInput.text", @@ -6710,9 +6678,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -6792,7 +6758,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "containerStyle": "none", "canExtend": false } @@ -6904,9 +6870,7 @@ "bottomRow": 14, "isVisible": true, "type": "FORM_BUTTON_WIDGET", - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "pqa1d7o4vt", "isLoading": false, "disabledWhenInvalid": true, @@ -7194,7 +7158,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "isVisible": true, "isDisabled": false, "canExtend": true @@ -7522,9 +7486,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -7604,7 +7566,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "containerStyle": "none", "canExtend": false } @@ -7716,9 +7678,7 @@ "bottomRow": 14, "isVisible": true, "type": "FORM_BUTTON_WIDGET", - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "pqa1d7o4vt", "isLoading": false, "disabledWhenInvalid": true, @@ -8006,7 +7966,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "isVisible": true, "isDisabled": false, "canExtend": true @@ -8320,8 +8280,8 @@ } ] }, - "layoutOnLoadActions": [ ], - "publishedLayoutOnLoadActions": [ ], + "layoutOnLoadActions": [], + "publishedLayoutOnLoadActions": [], "widgetNames": [ "Form1", "Button1", @@ -8351,7 +8311,7 @@ "Canvas2" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -8385,7 +8345,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -8402,7 +8362,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "appsmith.URL.queryParams.email" ], @@ -8433,7 +8393,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -8449,7 +8409,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "genderDropdown.selectedOptionValue", "dobPicker.selectedDate", @@ -8479,9 +8439,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -8561,7 +8519,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "containerStyle": "none", "canExtend": false } @@ -8673,9 +8631,7 @@ "bottomRow": 12, "isVisible": true, "type": "FORM_BUTTON_WIDGET", - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "epboelq954", "isLoading": false, "disabledWhenInvalid": true, @@ -9042,9 +8998,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -9124,7 +9078,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "containerStyle": "none", "canExtend": false } @@ -9236,9 +9190,7 @@ "bottomRow": 12, "isVisible": true, "type": "FORM_BUTTON_WIDGET", - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "epboelq954", "isLoading": false, "disabledWhenInvalid": true, @@ -9638,7 +9590,7 @@ "dobPicker" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -9652,7 +9604,6 @@ }, { "name": "Table Tutorial", - "organizationId": "5f2a944780ca1f6faaed4e38", "isPublic": true, "$pages": [ { @@ -9682,7 +9633,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -9699,7 +9650,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "(usersTable.pageNo - 1) * 5" ], @@ -9716,9 +9667,9 @@ "datasourceConfiguration": { "url": "https://mock-api.appsmith.com" }, - "invalids": [ ], + "invalids": [], "deleted": false, - "policies": [ ] + "policies": [] }, "actionConfiguration": { "timeoutInMillisecond": 10000, @@ -9758,7 +9709,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "getUsersAPI.data.previous", "getUsersAPI.data.next" @@ -9785,9 +9736,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -10118,9 +10067,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -10495,7 +10442,7 @@ "usersTableFormatted" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -10529,7 +10476,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -10546,7 +10493,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "usersTable.searchText || \"\"", "(usersTable.pageNo - 1) * 10", @@ -10574,9 +10521,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -10801,9 +10746,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -11054,7 +10997,7 @@ "MainContainer" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -11088,7 +11031,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -11105,7 +11048,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "(usersTable.pageNo - 1) * 10" ], @@ -11131,9 +11074,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -11544,9 +11485,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -11987,7 +11926,7 @@ "roleText" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -12021,7 +11960,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -12038,7 +11977,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "(usersTable.pageNo - 1) * 5" ], @@ -12069,7 +12008,7 @@ }, "sshProxyEnabled": false }, - "invalids": [ ], + "invalids": [], "deleted": false, "policies": [] }, @@ -12085,7 +12024,7 @@ } ], "isValid": true, - "invalids": [ ], + "invalids": [], "jsonPathKeys": [ "genderRadio.selectedOptionValue", "roleDropdown.selectedOptionValue", @@ -12117,9 +12056,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -12181,7 +12118,7 @@ }, "columnActions": [ { - "actionPayloads": [ ], + "actionPayloads": [], "label": "Edit", "id": "6apm73t6ss", "dynamicTrigger": "{{showModal('edit_user_modal')}}" @@ -12206,9 +12143,7 @@ "canOutsideClickClose": true, "type": "MODAL_WIDGET", "canEscapeKeyClose": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "0", "shouldScrollContents": true, "blueprint": { @@ -12292,7 +12227,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "isVisible": true, "isDisabled": false, "canExtend": true @@ -12466,9 +12401,7 @@ }, "text": "Update", "isDisabled": false, - "dynamicBindings": { - - } + "dynamicBindings": {} }, { "widgetName": "Text3", @@ -12841,9 +12774,7 @@ "parentRowSpace": 1, "type": "CANVAS_WIDGET", "canExtend": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "version": 5, "minHeight": 1292, "parentColumnSpace": 1, @@ -12905,7 +12836,7 @@ }, "columnActions": [ { - "actionPayloads": [ ], + "actionPayloads": [], "label": "Edit", "id": "6apm73t6ss", "dynamicTrigger": "{{showModal('edit_user_modal')}}" @@ -12930,9 +12861,7 @@ "canOutsideClickClose": true, "type": "MODAL_WIDGET", "canEscapeKeyClose": true, - "dynamicBindings": { - - }, + "dynamicBindings": {}, "parentId": "0", "shouldScrollContents": true, "blueprint": { @@ -13016,7 +12945,7 @@ ] }, "detachFromLayout": true, - "children": [ ], + "children": [], "isVisible": true, "isDisabled": false, "canExtend": true @@ -13190,9 +13119,7 @@ }, "text": "Update", "isDisabled": false, - "dynamicBindings": { - - } + "dynamicBindings": {} }, { "widgetName": "Text3", @@ -13603,7 +13530,7 @@ "addressInput" ], "deleted": false, - "policies": [ ] + "policies": [] } ], "deleted": false, @@ -13616,4 +13543,4 @@ "_class": "com.appsmith.server.domains.Application" } ] -} +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/SeedMongoData.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/SeedMongoData.java index 1ea08c7c37..3962f4ce53 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/SeedMongoData.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/configurations/SeedMongoData.java @@ -11,6 +11,7 @@ import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.User; import com.appsmith.server.domains.UserRole; import com.appsmith.server.domains.UserState; +import com.appsmith.server.dtos.OrganizationPluginStatus; import com.appsmith.server.repositories.ApplicationRepository; import com.appsmith.server.repositories.OrganizationRepository; import com.appsmith.server.repositories.PageRepository; @@ -140,6 +141,7 @@ public class SeedMongoData { }; Object[][] pluginData = { {"Installed Plugin Name", PluginType.API, "installed-plugin"}, + {"Installed DB Plugin Name", PluginType.DB, "installed-db-plugin"}, {"Not Installed Plugin Name", PluginType.API, "not-installed-plugin"} }; @@ -171,42 +173,38 @@ public class SeedMongoData { .cache(); // Seed the organization data into the DB + Flux organizationFlux = mongoTemplate + .find(new Query().addCriteria(where("name").in(pluginData[0][0], pluginData[1][0])), Plugin.class) + .map(plugin -> new OrganizationPlugin(plugin.getId(), OrganizationPluginStatus.FREE)) + .collectList() + .cache() + .repeat() + .zipWithIterable(List.of(orgData)) + .map(tuple -> { + final List orgPlugins = tuple.getT1(); + final Object[] orgArray = tuple.getT2(); + Organization organization = new Organization(); + organization.setName((String) orgArray[0]); + organization.setDomain((String) orgArray[1]); + organization.setWebsite((String) orgArray[2]); + organization.setSlug((String) orgArray[3]); + organization.setPolicies((Set) orgArray[4]); + organization.setPlugins(orgPlugins); - Flux organizationFlux = mongoTemplate.findOne( - new Query().addCriteria(where("name").is(pluginData[0][0])), Plugin.class - ) - .map(plugin -> plugin.getId()) - .flatMapMany(pluginId -> Flux.just(orgData) - .map(array -> { - log.debug("In the orgFlux for pluginId: {}", pluginId); - Organization organization = new Organization(); - organization.setName((String) array[0]); - organization.setDomain((String) array[1]); - organization.setWebsite((String) array[2]); - organization.setSlug((String) array[3]); - organization.setPolicies((Set) array[4]); + List userRoles = new ArrayList<>(); + UserRole userRole = new UserRole(); + String roleName = "Administrator"; + userRole.setRole(AppsmithRole.generateAppsmithRoleFromName(roleName)); + userRole.setUsername(API_USER_EMAIL); + userRole.setRoleName(roleName); + userRoles.add(userRole); + organization.setUserRoles(userRoles); - OrganizationPlugin orgPlugin = new OrganizationPlugin(); - orgPlugin.setPluginId(pluginId); - List orgPlugins = new ArrayList<>(); - orgPlugins.add(orgPlugin); - organization.setPlugins(orgPlugins); - - List userRoles = new ArrayList<>(); - UserRole userRole = new UserRole(); - String roleName = "Administrator"; - userRole.setRole(AppsmithRole.generateAppsmithRoleFromName(roleName)); - userRole.setUsername(API_USER_EMAIL); - userRole.setRoleName(roleName); - userRoles.add(userRole); - organization.setUserRoles(userRoles); - - - log.debug("In the orgFlux. Create Organization: {}", organization); - return organization; - }).flatMap(organizationRepository::save) - ); + log.debug("In the orgFlux. Create Organization: {}", organization); + return organization; + }) + .flatMap(organizationRepository::save); Flux organizationFlux1 = organizationRepository.deleteAll() .thenMany(pluginFlux) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java index da4a971d79..7ecb196982 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java @@ -5,6 +5,7 @@ import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.Connection; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.Policy; import com.appsmith.external.models.SSLDetails; import com.appsmith.external.models.UploadedFile; @@ -37,6 +38,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.util.function.Tuple2; +import java.util.ArrayList; import java.util.Set; import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES; @@ -523,4 +525,50 @@ public class DatasourceServiceTest { }) .verifyComplete(); } + + @Test + @WithUserDetails(value = "api_user") + public void createDatasourceWithInvalidCharsInHost() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + + Mono pluginMono = pluginService.findByPackageName("installed-db-plugin"); + Datasource datasource = new Datasource(); + datasource.setName("test datasource name with invalid hostnames"); + datasource.setOrganizationId(orgId); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setEndpoints(new ArrayList<>()); + datasourceConfiguration.getEndpoints().add(new Endpoint("hostname/", 5432L)); + datasourceConfiguration.getEndpoints().add(new Endpoint("hostname:", 5432L)); + datasource.setDatasourceConfiguration(datasourceConfiguration); + Mono datasourceMono = pluginMono.map(plugin -> { + datasource.setPluginId(plugin.getId()); + return datasource; + }).flatMap(datasourceService::create); + + StepVerifier + .create(datasourceMono) + .assertNext(createdDatasource -> { + assertThat(createdDatasource.getId()).isNotEmpty(); + assertThat(createdDatasource.getPluginId()).isEqualTo(datasource.getPluginId()); + assertThat(createdDatasource.getName()).isEqualTo(datasource.getName()); + assertThat(createdDatasource.getInvalids()).containsExactlyInAnyOrder( + "Host value cannot contain `/` or `:` characters. Found `hostname/`.", + "Host value cannot contain `/` or `:` characters. Found `hostname:`." + ); + + Policy manageDatasourcePolicy = Policy.builder().permission(MANAGE_DATASOURCES.getValue()) + .users(Set.of("api_user")) + .build(); + Policy readDatasourcePolicy = Policy.builder().permission(READ_DATASOURCES.getValue()) + .users(Set.of("api_user")) + .build(); + Policy executeDatasourcePolicy = Policy.builder().permission(EXECUTE_DATASOURCES.getValue()) + .users(Set.of("api_user")) + .build(); + + assertThat(createdDatasource.getPolicies()).isNotEmpty(); + assertThat(createdDatasource.getPolicies()).containsAll(Set.of(manageDatasourcePolicy, readDatasourcePolicy, executeDatasourcePolicy)); + }) + .verifyComplete(); + } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java index 6a5b8b9b24..3bee52a4f5 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/OrganizationServiceTest.java @@ -6,6 +6,7 @@ import com.appsmith.server.acl.AppsmithRole; import com.appsmith.server.acl.RoleGraph; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Asset; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.User; @@ -13,23 +14,35 @@ import com.appsmith.server.domains.UserRole; import com.appsmith.server.dtos.InviteUsersDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.repositories.AssetRepository; import com.appsmith.server.repositories.DatasourceRepository; import com.appsmith.server.repositories.OrganizationRepository; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.NotImplementedException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.codec.multipart.Part; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import reactor.util.annotation.NonNull; import reactor.util.function.Tuple2; import reactor.util.function.Tuple3; +import java.io.IOException; +import java.io.InputStream; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -80,6 +93,9 @@ public class OrganizationServiceTest { @Autowired RoleGraph roleGraph; + @Autowired + private AssetRepository assetRepository; + Organization organization; @Before @@ -593,7 +609,7 @@ public class OrganizationServiceTest { assertThat(datasource.getPolicies()).isNotEmpty(); assertThat(datasource.getPolicies()).containsAll(Set.of(manageDatasourcePolicy, readDatasourcePolicy, - executeDatasourcePolicy)); + executeDatasourcePolicy)); }) .verifyComplete(); @@ -935,4 +951,74 @@ public class OrganizationServiceTest { .verifyComplete(); } + @Test + @WithUserDetails(value = "api_user") + public void uploadOrganizationLogo() throws IOException { + final InputStream imageResourceStream = getClass().getClassLoader() + .getResourceAsStream("test_assets/OrganizationServiceTest/my_organization_logo.png"); + assertThat(imageResourceStream).isNotNull(); + + final byte[] bytes = imageResourceStream.readAllBytes(); + final InMemoryFilePart filePart = new InMemoryFilePart(bytes, MediaType.IMAGE_PNG); + + final String organizationId = organizationRepository + .findByName("Spring Test Organization") + .blockOptional(Duration.ofSeconds(3)) + .map(Organization::getId) + .orElse(null); + + assertThat(organizationId).isNotNull(); + + final Mono> resultMono = organizationService + .uploadLogo(organizationId, filePart) + .flatMap(organizationWithLogo -> Mono.zip( + Mono.just(organizationWithLogo), + assetRepository.findById(organizationWithLogo.getLogoAssetId()) + )); + + StepVerifier.create(resultMono) + .assertNext(tuple -> { + final Organization organizationWithLogo = tuple.getT1(); + assertThat(organizationWithLogo.getLogoUrl()).isNotNull(); + assertThat(organizationWithLogo.getLogoUrl()).contains(organizationWithLogo.getLogoAssetId()); + + final Asset asset = tuple.getT2(); + assertThat(asset).isNotNull(); + assertThat(asset.getData()).isEqualTo(bytes); + }) + .verifyComplete(); + } + + private static class InMemoryFilePart implements Part { + + private final DataBuffer buffer; + private final HttpHeaders headers; + + public InMemoryFilePart(byte[] bytes, MediaType contentType) { + this.buffer = new DefaultDataBufferFactory().wrap(bytes); + headers = new HttpHeaders(); + headers.setContentType(contentType); + } + + @Override + @NonNull + public String name() { + throw new NotImplementedException( + "This is a FilePart made for testing. The name() method is not implemented."); + } + + @Override + @NonNull + public HttpHeaders headers() { + return headers; + } + + @Override + @NonNull + public Flux content() { + return Flux.just(buffer); + } + + } + } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserServiceTest.java index 3dedc30cb0..f3c607c5b8 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserServiceTest.java @@ -23,6 +23,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedCaseInsensitiveMap; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -372,5 +375,17 @@ public class UserServiceTest { .verifyComplete(); } + + @Test + @WithUserDetails(value = "api_user") + public void getAllUsersTest() { + Flux userFlux = userService.get(CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>())); + + StepVerifier.create(userFlux) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException && + throwable.getMessage().equals(AppsmithError.UNSUPPORTED_OPERATION.getMessage())) + .verify(); + + } } diff --git a/app/server/appsmith-server/src/test/resources/test_assets/OrganizationServiceTest/my_organization_logo.png b/app/server/appsmith-server/src/test/resources/test_assets/OrganizationServiceTest/my_organization_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..552086caa23d457a831789d6206fbac959158e71 GIT binary patch literal 1811 zcmZA2c~BB~8wYT7OT3S!G_MT2w#B2jj5bV7Ma6AZG)SfL%oEIm6}8PHuf)_$Q$xkp zv-Ko1Q$yD~G(|K+M+xvgTEWZ{&(~&W=Y8vs&-0ymW}d&E-#kAe($-2!TuB@N07zLQ z;0}j*`zukA!x}V|g*pteAcQj>0MJwaN+`V4ga81DbX&tsQQ_k}asU$Lr5r03wSz-He`l9dC0%QQYo_8%Wf@iwm~(~I1sWPqpnvpK!o zdPtBFB`0vS%Cx*3^d2|IeIUc#u)W3m)ZtaPl1G2rCS&qPYO3bl-ebokBuEV*nv?eS zAPD^YnA$Tzv24t2KtR{?vTY`oQdu0)EgsWqfxx0q%=deL4j0a5I5lPDKQacpPj(pM z-{lH+R~O`Ui+HPyy>}>dDX&dv$|Z6=aIv4FZ5zZdd;r*TbXGii=b}Y&$9TKx7ggzu z4ZcNr_3X|AwYT^c4_|4y(2T8SmwJ^C%Y-E)Lj_in9rr1iN@Fg<@RqtjCi}x-W4n5+ zilg`Bz7X>bj)al+w=y?xgLQUx`s4k|G18xUc`PRK_Am0G!=IRLXWbewG}VTjEcfs^ zMgBm~XY6z^wS3U`&*)V;(d9U`r1dQus2^vf8JZ6h41VL1P}H9oC(%yjKF2M8L^el8fjCdZ zFW`2MLYhZ=otH#nNy!S?IJy8^T3VuN{HqhZ%Vv3UGg}@oSLL$zeEqk{$^NJ*F%>j= zt-ij{4sp^ks;M+#+BLF!oncI^b&-}#i6k&bLhdpvf&<;ZEp)w0a8jQ) zX8s!iDv$;qZVral^c*g$W#8y~o4W9_t`|l~0qVJyA?$x#T6A}}u<#Wl+E|13*GGf- z>^^3Vo}QCVNdP+;ikmX^?eEu^dTiwzy=-)El=4V*d70N8P@CR}&EYCvmU~xvRbBmb z($H~vMX?x4E0?lVu*voDvla{1%H+0CanKL^JMTAn>R!yBz}^FJW%nR&dJ90Hf$We1ZZ83UQ6lUTI?`2 z_rC1Os1*yC9Xa;}r|IRn=IanKRs*obI5j}zWu4Fh9=5zR<8cq8l<4!Ubh>GNfz^`N zq~Fq&GL&%9$s2B&A>dO1`9J+Qfpb=Lq*4_(1Sh}TZJG%gY7A6Mqp4X@-GoSXCXs!c zB!gUGSPNb|(pK9%IriMywr;iPk|7U)Pv*mG^j&(MAE!0?cGrHx!Mr9Cbx*zT;x{LJ9~ICkx?J!mLUI)y7)b<0rx9!Jj1J5_7vm`=8}QB^IO z6DO0g)u-QBQ7Nb{^VVQhjutcMnzb*|pTr7xtZ(<)Xv0*SG&Nu8Vc@hvb4ln0IW)2@ z-%3prWJErA`s;#L1iv-EFV}y-5tTC2lAFg|L|AziW27tUUT^4=wKXhz(u*dxCVl|Z z=VBr#NN?YubmQllb%tTF?J@s3kKeX)K72ail@&c(+t^pITL_7t?H$aP(^Q4}-a{b7 zl!tj7GZebp1(OWDkTn(&0WtxnL_|ood=}oL)3-1;NRP8DKJIO1I-uf%#Vs2w_iMnf z()BO>d#V3FMZxg+@5q9{@NdP6%7Okza4^$KQaDJH(rm>Y{&E0o3tM=NnOp3C0pyKs ADgXcg literal 0 HcmV?d00001 diff --git a/app/server/scripts/acl-migration/.editorconfig b/app/server/scripts/node/.editorconfig similarity index 100% rename from app/server/scripts/acl-migration/.editorconfig rename to app/server/scripts/node/.editorconfig diff --git a/app/server/scripts/acl-migration/README.md b/app/server/scripts/node/README.md similarity index 94% rename from app/server/scripts/acl-migration/README.md rename to app/server/scripts/node/README.md index 71b10e167c..4a71290816 100644 --- a/app/server/scripts/acl-migration/README.md +++ b/app/server/scripts/node/README.md @@ -42,7 +42,7 @@ Assuming you have node (>=v12), use the following command to run the migration: ```sh npm install -node main.js 'https://localhost/api/v1/' 'mongodb://localhost:27017/mobtools' +node acl-migration.js 'https://localhost/api/v1/' 'mongodb://localhost:27017/mobtools' ``` The first argument should be a running API endpoint, and the second argument should be a URI to the database that this diff --git a/app/server/scripts/acl-migration/main.js b/app/server/scripts/node/acl-migration.js similarity index 100% rename from app/server/scripts/acl-migration/main.js rename to app/server/scripts/node/acl-migration.js diff --git a/app/server/scripts/node/dump-examples-org.js b/app/server/scripts/node/dump-examples-org.js new file mode 100644 index 0000000000..67321180fd --- /dev/null +++ b/app/server/scripts/node/dump-examples-org.js @@ -0,0 +1,206 @@ +if (process.argv.length !== 5) { + console.error("Takes three arguments, the MongoDB URL (like 'mongodb://localhost:27017/mobtools'),\n" + + "\tthe encryption salt and the encryption password used by the server connecting to this DB."); + process.exit(1); +} + +const MONGODB_URL = process.argv[2]; +const ENCRYPTION_SALT = process.argv[3]; +const ENCRYPTION_PASSWORD = process.argv[4]; + +const { MongoClient, ObjectID } = require("mongodb"); +const fs = require("fs"); +const path = require("path"); +const CryptoJS = require("crypto-js"); + +const mongoClient = new MongoClient(MONGODB_URL, { + useNewUrlParser: true, + useUnifiedTopology: true, +}); + +console.time("total time taken"); +main() + .then(() => console.log("\nFinished Successfully.")) + .catch(error => console.error(error)) + .finally(() => { + mongoClient.close(); + console.timeEnd("total time taken"); + console.log(); + }); + +async function main() { + const con = await mongoClient.connect(); + const db = con.db(); + + const pluginPackageNameByIds = {}; + for (const plugin of await db.collection("plugin").find().toArray()) { + pluginPackageNameByIds[plugin._id.toString()] = plugin.packageName; + } + + const templateOrganizationId = (await db.collection("config").findOne({name: "template-organization"})).config.organizationId; + + const organization = await db.collection("organization").findOne({_id: ObjectID(templateOrganizationId)}); + + const $datasources = await db.collection("datasource") + .find({organizationId: templateOrganizationId, deleted: false}) + .map(datasource => { + const datasourceConfiguration = datasource.datasourceConfiguration; + + if (datasourceConfiguration.authentication && datasourceConfiguration.authentication.password) { + datasourceConfiguration.authentication.password = decrypt(datasourceConfiguration.authentication.password); + } + + return { + name: datasource.name, + $pluginPackageName: pluginPackageNameByIds[datasource.pluginId], + datasourceConfiguration: datasourceConfiguration, + invalids: datasource.invalids, + deleted: false, + policies: [], + _class: datasource._class, + }; + }) + .toArray(); + + const allPageIds = []; + const allDefaultPageIds = new Set(); + const $applications = await db.collection("application") + .find({organizationId: templateOrganizationId, deleted: false, isPublic: true}) + .map(application => { + allPageIds.push(...application.pages.map(page => ObjectID(page._id))); + allDefaultPageIds.add(application.pages.filter(page => page.isDefault)[0]._id.toString()); + return { + name: application.name, + isPublic: true, + $pages: [], + pages: application.pages, + deleted: false, + policies: [], + _class: application._class, + }; + }) + .toArray(); + + const actionsByPageId = {}; + for (const action of await db.collection("action").find({organizationId: templateOrganizationId, deleted: false}).toArray()) { + if (!actionsByPageId[action.pageId]) { + actionsByPageId[action.pageId] = []; + } + let $isEmbedded = typeof action.datasource._id === "undefined"; + actionsByPageId[action.pageId].push({ + name: action.name, + datasource: { + $isEmbedded, + name: action.datasource.name, + $pluginPackageName: pluginPackageNameByIds[action.datasource.pluginId], + datasourceConfiguration: action.datasource.datasourceConfiguration, + invalids: action.datasource.invalids, + deleted: false, + policies: [], + }, + actionConfiguration: action.actionConfiguration, + pluginType: action.pluginType, + executeOnLoad: action.executeOnLoad, + dynamicBindingPathList: action.dynamicBindingPathList, + isValid: action.isValid, + invalids: action.invalids, + jsonPathKeys: action.jsonPathKeys, + deleted: false, + policies: [], + _class: action._class, + }); + } + + const pagesById = {}; + for (const page of await db.collection("page").find({_id: {$in: allPageIds}}).toArray()) { + const pageId = page._id.toString(); + + for (const layout of page.layouts) { + delete layout._id; + for (const actionSet of layout.layoutOnLoadActions) { + for (const action of actionSet) { + delete action._id; + } + } + for (const actionSet of layout.publishedLayoutOnLoadActions) { + for (const action of actionSet) { + delete action._id; + } + } + } + + pagesById[pageId] = { + name: page.name, + $isDefault: allDefaultPageIds.has(pageId), + $actions: actionsByPageId[pageId], + layouts: page.layouts, + deleted: false, + policies: [], + _class: page._class, + }; + } + + for (const application of $applications) { + application.$pages = []; + for (const page of application.pages) { + application.$pages.push(pagesById[page._id]); + } + delete application.pages; + } + + const finalData = { + name: organization.name, + organizationSettings: organization.organizationSettings, + slug: organization.slug, + userRoles: [], + deleted: false, + policies: [], + _class: organization._class, + $datasources, + $applications, + }; + + if (finalData.slug !== "example-apps") { + console.warn("The slug of the organization in the generated dump is not `example-apps`. This might be significant."); + } + + fs.writeFileSync( + findExamplesJsonPath(), + JSON.stringify(finalData, null, 2) + ); +} + +function findExamplesJsonPath() { + let projectDir = __dirname; + + while (projectDir != null && !fs.existsSync(path.join(projectDir, "appsmith-server"))) { + projectDir = path.dirname(projectDir); + } + + return path.join(projectDir, "appsmith-server", "src", "main", "resources", "examples-organization.json"); +} + + +/*! +* Author: flohall +* date: 2019-11-05 +* file: module/textEncryptor.js +* Original: . +*/ +const key = CryptoJS.PBKDF2(ENCRYPTION_PASSWORD, ENCRYPTION_SALT, { + keySize: 256 / 32, + iterations: 1024 +}); + +const decryptConfig = { + // same as NULL_IV_GENERATOR of AesBytesEncryptor - so encryption creates always same cipher text for same input + iv: {words: [0, 0, 0, 0, 0, 0, 0, 0], sigBytes: 0}, + padding: CryptoJS.pad.Pkcs7, + mode: CryptoJS.mode.CBC +}; + +function decrypt(text) { + return CryptoJS.AES + .decrypt({ciphertext: CryptoJS.enc.Hex.parse(text)}, key, decryptConfig) + .toString(CryptoJS.enc.Utf8); +} diff --git a/app/server/scripts/acl-migration/package-lock.json b/app/server/scripts/node/package-lock.json similarity index 97% rename from app/server/scripts/acl-migration/package-lock.json rename to app/server/scripts/node/package-lock.json index 06538ef31c..35477ae6e7 100644 --- a/app/server/scripts/acl-migration/package-lock.json +++ b/app/server/scripts/node/package-lock.json @@ -45,6 +45,12 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "crypto-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", + "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==", + "dev": true + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", diff --git a/app/server/scripts/acl-migration/package.json b/app/server/scripts/node/package.json similarity index 70% rename from app/server/scripts/acl-migration/package.json rename to app/server/scripts/node/package.json index eda2d9fdc2..b7321f1943 100644 --- a/app/server/scripts/acl-migration/package.json +++ b/app/server/scripts/node/package.json @@ -3,15 +3,16 @@ "version": "1.0.0", "description": "", "private": true, - "main": "main.js", "scripts": { - "run": "node main.js" + "acl-migration": "node acl-migration.js", + "dump-examples-org": "node dump-examples-org.js" }, "keywords": [], "author": "Appsmith", "devDependencies": { "axios": "^0.19.2", "axios-cookiejar-support": "^1.0.0", + "crypto-js": "^4.0.0", "mongodb": "^3.5.8", "tough-cookie": "^4.0.0" } diff --git a/deploy/install.sh b/deploy/install.sh index 25592b37c5..711fcffd93 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -3,46 +3,68 @@ set -o errexit is_command_present() { - type "$1" >/dev/null 2>&1 + type "$1" >/dev/null 2>&1 +} + +is_mac() { + [[ $OSTYPE == darwin* ]] } # This function checks if the relevant ports required by Appsmith are available or not # The script should error out in case they aren't available check_ports_occupied() { - ports_occupied="$( - if [[ "$OSTYPE" == "darwin"* ]]; then - sudo netstat -anp tcp - else - sudo netstat -tupln tcp - fi | awk '$6 == "LISTEN" && $4 ~ /^.*[.:](80|443)$/' | wc -l | grep -o '[[:digit:]]\+' - )" + local port_check_output + local ports_pattern="80|443" + + if is_mac; then + port_check_output="$(netstat -anp tcp | awk '$6 == "LISTEN" && $4 ~ /^.*\.('"$ports_pattern"')$/')" + elif is_command_present ss; then + # The `ss` command seems to be a better/faster version of `netstat`, but is not available on all Linux + # distributions by default. Other distributions have `ss` but no `netstat`. So, we try for `ss` first, then + # fallback to `netstat`. + port_check_output="$(ss --all --numeric --tcp | awk '$1 == "LISTEN" && $4 ~ /^.*:('"$ports_pattern"')$/')" + elif is_command_present netstat; then + port_check_output="$(netstat --all --numeric --tcp | awk '$6 == "LISTEN" && $4 ~ /^.*:('"$ports_pattern"')$/')" + fi + + if [[ -n $port_check_output ]]; then + echo "+++++++++++ ERROR ++++++++++++++++++++++" + echo "Appsmith requires ports 80 & 443 to be open. Please shut down any other service(s) that may be running on these ports." + echo "++++++++++++++++++++++++++++++++++++++++" + echo "" + bye + fi } install_docker() { - if [[ $package_manager -eq apt-get ]];then - echo "++++++++++++++++++++++++" - echo "Setting up docker repos" - sudo $package_manager update --quiet + echo "++++++++++++++++++++++++" + echo "Setting up docker repos" - sudo apt-get -y --quiet install gnupg-agent + if [[ $package_manager == apt-get ]]; then + apt_cmd="sudo apt-get --yes --quiet" + $apt_cmd update + $apt_cmd install gnupg-agent curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository \ - "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ - $(lsb_release -cs) \ - stable" + "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + $apt_cmd update + echo "Installing docker" + $apt_cmd install docker-ce docker-ce-cli containerd.io + else - sudo yum install -y yum-utils + yum_cmd="sudo yum --assumeyes --quiet" + $yum_cmd install yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + echo "Installing docker" + $yum_cmd install docker-ce docker-ce-cli containerd.io + fi - sudo ${package_manager} -y update --quiet - echo "Installing docker" - sudo ${package_manager} -y install docker-ce docker-ce-cli containerd.io --quiet } install_docker_compose() { - if [ $package_manager == "apt-get" -o $package_manager == "yum" ];then - if [ ! -f /usr/bin/docker-compose ];then + if [[ $package_manager == "apt-get" || $package_manager == "yum" ]]; then + if [[ ! -f /usr/bin/docker-compose ]];then echo "Installing docker-compose..." sudo curl -L "https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose @@ -67,29 +89,29 @@ start_docker() { } check_os() { - if [[ "$OSTYPE" == "darwin"* ]]; then + if is_mac; then package_manager="brew" desired_os=1 return fi - os_name=`cat /etc/*-release | egrep "^NAME="` - os_name="${os_name#*=}" + os_name="$(cat /etc/*-release | awk -F= '$1 == "NAME" { gsub(/"/, ""); print $2; exit }')" - case "${os_name}" in - \"Ubuntu*\") + case "$os_name" in + Ubuntu*) desired_os=1 package_manager="apt-get" ;; - \"Red\ Hat*\") - desired_os=0 + Red\ Hat*) + desired_os=1 package_manager="yum" ;; - \"CentOS*\") - desired_os=0 + CentOS*) + desired_os=1 package_manager="yum" ;; - *) desired_os=0 + *) + desired_os=0 esac } @@ -98,16 +120,11 @@ overwrite_file() { local template_file="$2" local full_path="$install_dir/$relative_path" - if [[ -f $full_path ]]; then - read -p "File $relative_path already exists. Would you like to replace it? [Y]: " value - value=${value:-Y} - - if ! [[ $value == "Y" || $value == "y" || $value == "yes" || $value == "Yes" ]]; then - echo "You chose not to replace existing file: '$full_path'." - rm -f "$template_file" - echo "File $template_file removed from source directory." - echo "" - fi + if [[ -f $full_path ]] && ! confirm y "File $relative_path already exists. Would you like to replace it?"; then + echo "You chose NOT to replace existing file: '$full_path'." + rm -f "$template_file" + echo "File $template_file removed from source directory." + echo "" else mv -f "$template_file" "$full_path" echo "File $full_path moved successfully!" @@ -116,30 +133,28 @@ overwrite_file() { # This function prompts the user for an input for a non-empty Mongo root password. read_mongo_password() { - read -sp 'Set the mongo password: ' mongo_root_password - while [[ -z $mongo_root_password ]] - do + read -srp 'Set the mongo password: ' mongo_root_password + while [[ -z $mongo_root_password ]]; do echo "" echo "" echo "+++++++++++ ERROR ++++++++++++++++++++++" echo "The mongo password cannot be empty. Please input a valid password string." echo "++++++++++++++++++++++++++++++++++++++++" echo "" - read -sp 'Set the mongo password: ' mongo_root_password + read -srp 'Set the mongo password: ' mongo_root_password done } # This function prompts the user for an input for a non-empty Mongo username. read_mongo_username() { - read -p 'Set the mongo root user: ' mongo_root_user - while [[ -z $mongo_root_user ]] - do + read -rp 'Set the mongo root user: ' mongo_root_user + while [[ -z $mongo_root_user ]]; do echo "" echo "+++++++++++ ERROR ++++++++++++++++++++++" echo "The mongo username cannot be empty. Please input a valid username string." echo "++++++++++++++++++++++++++++++++++++++++" echo "" - read -p 'Set the mongo root user: ' mongo_root_user + read -rp 'Set the mongo root user: ' mongo_root_user done } @@ -163,7 +178,7 @@ wait_for_containers_start() { urlencode() { # urlencode - old_lc_collate=$LC_COLLATE + local old_lc_collate="$LC_COLLATE" LC_COLLATE=C local length="${#1}" @@ -175,13 +190,122 @@ urlencode() { esac done - LC_COLLATE=$old_lc_collate + LC_COLLATE="$old_lc_collate" +} + +generate_password() { + # Picked up the following method of generation from : https://gist.github.com/earthgecko/3089509 + LC_CTYPE=C tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 13 | head -n 1 +} + +confirm() { + local default="$1" # Should be `y` or `n`. + local prompt="$2" + + local options="y/N" + if [[ $default == y || $default == Y ]]; then + options="Y/n" + fi + + local answer + read -n1 -rp "$prompt [$options] " answer + if [[ -z $answer ]]; then + # No answer given, the user just hit the Enter key. Take the default value as the answer. + answer="$default" + else + # An answer was given. This means the user didn't get to hit Enter so the cursor on the same line. Do an empty + # echo so the cursor moves to a new line. + echo + fi + + [[ yY =~ $answer ]] +} + +init_ssl_cert() { + local domain="$1" + echo "Creating certificate for '$domain'." + + local rsa_key_size=4096 + local data_path="./data/certbot" + + if [[ -d "$data_path" ]]; then + if ! confirm n "Existing certificate data found at '$data_path'. Continue and replace existing certificate?"; then + return + fi + fi + + mkdir -p "$data_path"/{conf,www} + + if ! [[ -e "$data_path/conf/options-ssl-nginx.conf" && -e "$data_path/conf/ssl-dhparams.pem" ]]; then + echo "### Downloading recommended TLS parameters..." + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" + echo + fi + + echo "### Requesting Let's Encrypt certificate for '$domain'..." + + local email + read -rp 'Enter email address to create SSL certificate: (Optional, but strongly recommended): ' email + if [[ -z $email ]]; then + local email_arg="--register-unsafely-without-email" + else + local email_arg="--email $email --no-eff-email" + fi + + if confirm n 'Do you want to create certificate in staging mode (which is used for dev purposes and is not subject to rate limits)?'; then + local staging_arg="--staging" + else + local staging_arg="" + fi + + echo "### Generating OpenSSL key for '$domain'..." + local live_path="/etc/letsencrypt/live/$domain" + certbot_cmd \ + "sh -c \"mkdir -p '$live_path' && openssl req -x509 -nodes -newkey rsa:1024 -days 1 \ + -keyout '$live_path/privkey.pem' \ + -out '$live_path/fullchain.pem' \ + -subj '/CN=localhost' \ + \"" + echo + + echo "### Starting nginx..." + sudo docker-compose up --force-recreate --detach nginx + echo + + echo "### Removing key now that validation is done for $domain..." + certbot_cmd \ + "rm -Rfv /etc/letsencrypt/live/$domain /etc/letsencrypt/archive/$domain /etc/letsencrypt/renewal/$domain.conf" + echo + + # The following command exits with a non-zero status code even if the certificate was generated, but some checks failed. + # So we explicitly ignore such failure with a `|| true` in the end, to avoid bash quitting on us because this looks like + # a failed command. + certbot_cmd "certbot certonly --webroot --webroot-path=/var/www/certbot \ + $staging_arg \ + $email_arg \ + --domains $domain \ + --rsa-key-size $rsa_key_size \ + --agree-tos \ + --force-renewal" \ + || true + echo + + echo "### Reloading nginx..." + sudo docker-compose exec nginx nginx -s reload +} + +certbot_cmd() { + sudo docker-compose run --rm --entrypoint "$1" certbot +} + +echo_contact_support() { + echo "Please contact with your OS details and version${1:-.}" } bye() { # Prints a friendly good bye message and exits the script. - echo "" - echo -e "Exiting for now. Bye! \U1F44B" - exit + echo -e "\nExiting for now. Bye! \U1F44B\n" + exit 1 } echo -e "\U1F44B Thank you for trying out Appsmith! " @@ -196,32 +320,38 @@ check_os if [[ $desired_os -eq 0 ]];then echo "" echo "This script is currently meant to install Appsmith on Mac OS X | Ubuntu | RHEL | CentOS machines." - echo "Please contact support@appsmith.com with your OS details if you wish to extend this support" + echo_contact_support " if you wish to extend this support." bye else echo "You're on an OS that is supported by this installation script." echo "" fi -if [[ "$OSTYPE" == "darwin"* && "$EUID" -eq 0 ]]; then - echo "Please do not run this script with root permissions on macOS." - echo "Please contact support@appsmith.com with your OS details if you wish to extend this support" +if [[ $EUID -eq 0 ]]; then + echo "Please do not run this script as root/sudo." + echo_contact_support bye fi check_ports_occupied -if [[ $ports_occupied -ne 0 ]]; then - echo "+++++++++++ ERROR ++++++++++++++++++++++" - echo "Appsmith requires ports 80 & 443 to be open. Please shut down any other service(s) that may be running on these ports." - echo "++++++++++++++++++++++++++++++++++++++++" - echo "" - bye +read -rp 'Installation Directory [appsmith]: ' install_dir +install_dir="${install_dir:-appsmith}" +if [[ $install_dir != /* ]]; then + # If it's not an absolute path, prepend current working directory to it, to make it an absolute path. + install_dir="$PWD/$install_dir" +fi + +if [[ -e "$install_dir" ]]; then + echo "The path '$install_dir' is already present. Please run the script again with a different path to install new." + echo "If you're trying to update your existing installation, that happens automatically through WatchTower." + echo_contact_support " if you're facing problems with the auto-updates." + exit fi # Check is Docker daemon is installed and available. If not, the install & start Docker for Linux machines. We cannot automatically install Docker Desktop on Mac OS -if ! is_command_present docker ;then - if [ $package_manager == "apt-get" -o $package_manager == "yum" ];then +if ! is_command_present docker; then + if [[ $package_manager == "apt-get" || $package_manager == "yum" ]]; then install_docker else echo "" @@ -229,7 +359,7 @@ if ! is_command_present docker ;then echo "Docker Desktop must be installed manually on Mac OS to proceed. Docker can only be installed automatically on Ubuntu / Redhat / Cent OS" echo "https://docs.docker.com/docker-for-mac/install/" echo "++++++++++++++++++++++++++++++++++++++++++++++++" - exit + exit 1 fi fi @@ -243,46 +373,40 @@ if [[ $package_manager == "yum" || $package_manager == "apt-get" ]]; then start_docker fi -read -p 'Installation Directory [appsmith]: ' install_dir -install_dir="${install_dir:-appsmith}" -mkdir -p "$PWD/$install_dir" -install_dir="$PWD/$install_dir" -read -p 'Is this a fresh installation? [Y/n]' fresh_install -fresh_install="${fresh_install:-Y}" +echo "Installing Appsmith to '$install_dir'." +mkdir -p "$install_dir" echo "" -if [ $fresh_install == "N" -o $fresh_install == "n" -o $fresh_install == "no" -o $fresh_install == "No" ];then - read -p 'Enter your current mongo db host: ' mongo_host - read -p 'Enter your current mongo root user: ' mongo_root_user - read -sp 'Enter your current mongo password: ' mongo_root_password - read -p 'Enter your current mongo database name: ' mongo_database - # It is possible that this isn't the first installation. - echo "" - read -p 'Do you have any existing data in the database?[Y/n]: ' existing_encrypted_data - existing_encrypted_data=${existing_encrypted_data:-Y} - # In this case be more cautious of auto generating the encryption keys. Err on the side of not generating the encryption keys - if [ $existing_encrypted_data == "N" -o $existing_encrypted_data == "n" -o $existing_encrypted_data == "no" -o $existing_encrypted_data == "No" ];then - auto_generate_encryption="true" - else - auto_generate_encryption="false" - fi -elif [ $fresh_install == "Y" -o $fresh_install == "y" -o $fresh_install == "yes" -o $fresh_install == "Yes" ];then - echo "Appsmith needs to create a mongo db" +if confirm y "Is this a fresh installation?"; then + echo "Appsmith needs to create a MongoDB instance." mongo_host="mongo" mongo_database="appsmith" - + # We invoke functions to read the mongo credentials from the user because they MUST be non-empty read_mongo_username read_mongo_password # Since the mongo was automatically setup, this must be the first time installation. Generate encryption credentials for this scenario auto_generate_encryption="true" +else + read -rp 'Enter your current mongo db host: ' mongo_host + read -rp 'Enter your current mongo root user: ' mongo_root_user + read -srp 'Enter your current mongo password: ' mongo_root_password + read -rp 'Enter your current mongo database name: ' mongo_database + # It is possible that this isn't the first installation. + echo "" + # In this case be more cautious of auto generating the encryption keys. Err on the side of not generating the encryption keys + if confirm y "Do you have any existing data in the database?"; then + auto_generate_encryption="false" + else + auto_generate_encryption="true" + fi fi echo "" # urlencoding the Mongo username and password -encoded_mongo_root_user=$( urlencode $mongo_root_user ) -encoded_mongo_root_password=$( urlencode $mongo_root_password ) +encoded_mongo_root_user=$(urlencode "$mongo_root_user") +encoded_mongo_root_password=$(urlencode "$mongo_root_password") encryptionEnv=./template/encryption.env if test -f "$encryptionEnv"; then @@ -290,14 +414,14 @@ if test -f "$encryptionEnv"; then echo "1) No. Conserve the older encryption password and salt and continue" echo "2) Yes. Overwrite the existing encryption (NOT SUGGESTED) with autogenerated encryption password and salt" echo "3) Yes. Overwrite the existing encryption (NOT SUGGESTED) with manually entering the encryption password and salt" - read -p 'Enter option number [1]: ' overwrite_encryption + read -rp 'Enter option number [1]: ' overwrite_encryption overwrite_encryption=${overwrite_encryption:-1} auto_generate_encryption="false" if [[ $overwrite_encryption -eq 1 ]];then setup_encryption="false" elif [[ $overwrite_encryption -eq 2 ]];then setup_encryption="true" - auto_generate_encryption="true" + auto_generate_encryption="true" elif [[ $overwrite_encryption -eq 3 ]];then setup_encryption="true" auto_generate_encryption="false" @@ -309,22 +433,17 @@ fi if [[ "$setup_encryption" = "true" ]];then if [[ "$auto_generate_encryption" = "false" ]];then echo "Please enter the salt and password found in the encyption.env file of your previous appsmith installation " - read -p 'Enter your encryption password: ' user_encryption_password - read -p 'Enter your encryption salt: ' user_encryption_salt - elif [[ "$auto_generate_encryption" = "true" ]];then - # Picked up the following method of generation from : https://gist.github.com/earthgecko/3089509 - user_encryption_password=$(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 13 | head -n 1) - user_encryption_salt=$(cat /dev/urandom | LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 13 | head -n 1) + read -rp 'Enter your encryption password: ' user_encryption_password + read -rp 'Enter your encryption salt: ' user_encryption_salt + elif [[ "$auto_generate_encryption" = "true" ]]; then + user_encryption_password=$(generate_password) + user_encryption_salt=$(generate_password) fi fi echo "" -read -p 'Do you have a custom domain that you would like to link? (Only for cloud installations) [N/y]: ' setup_domain -setup_domain=${setup_domain:-N} -# Setting default value for the setup_ssl variable. Without this, the script errors out in the if condition later -setup_ssl='N' -if [ $setup_domain == "Y" -o $setup_domain == "y" -o $setup_domain == "yes" -o $setup_domain == "Yes" ];then +if confirm n "Do you have a custom domain that you would like to link? (Only for cloud installations)"; then echo "" echo "+++++++++++ IMPORTANT PLEASE READ ++++++++++++++++++++++" echo "Please update your DNS records with your domain registrar" @@ -333,12 +452,9 @@ if [ $setup_domain == "Y" -o $setup_domain == "y" -o $setup_domain == "yes" -o $ echo "+++++++++++++++++++++++++++++++++++++++++++++++" echo "" echo "Would you like to provision an SSL certificate for your custom domain / subdomain?" - read -p '(Your DNS records must be updated for us to proceed) [Y/n]: ' setup_ssl - setup_ssl=${setup_ssl:-Y} -fi - -if [ $setup_ssl == "Y" -o $setup_ssl == "y" -o $setup_ssl == "yes" -o $setup_ssl == "Yes" ]; then - read -p 'Enter the domain or subdomain on which you want to host appsmith (example.com / app.example.com): ' custom_domain + if confirm y '(Your DNS records must be updated for us to proceed)'; then + read -rp 'Enter the domain or subdomain on which you want to host appsmith (example.com / app.example.com): ' custom_domain + fi fi NGINX_SSL_CMNT="" @@ -348,39 +464,35 @@ fi echo "" echo "Downloading the configuration templates..." -mkdir -p template -( cd template -curl --remote-name-all --silent --show-error \ - https://raw.githubusercontent.com/appsmithorg/appsmith/release/deploy/template/docker-compose.yml.sh \ - https://raw.githubusercontent.com/appsmithorg/appsmith/release/deploy/template/init-letsencrypt.sh.sh \ - https://raw.githubusercontent.com/appsmithorg/appsmith/release/deploy/template/mongo-init.js.sh \ - https://raw.githubusercontent.com/appsmithorg/appsmith/release/deploy/template/docker.env.sh \ - https://raw.githubusercontent.com/appsmithorg/appsmith/release/deploy/template/nginx_app.conf.sh \ - https://raw.githubusercontent.com/appsmithorg/appsmith/release/deploy/template/encryption.env.sh +templates_dir="$(mktemp -d)" +mkdir -p "$templates_dir" + +( + cd "$templates_dir" + curl --remote-name-all --silent --show-error \ + https://raw.githubusercontent.com/appsmithorg/appsmith/master/deploy/template/docker-compose.yml.sh \ + https://raw.githubusercontent.com/appsmithorg/appsmith/master/deploy/template/mongo-init.js.sh \ + https://raw.githubusercontent.com/appsmithorg/appsmith/master/deploy/template/docker.env.sh \ + https://raw.githubusercontent.com/appsmithorg/appsmith/master/deploy/template/nginx_app.conf.sh \ + https://raw.githubusercontent.com/appsmithorg/appsmith/master/deploy/template/encryption.env.sh ) -# Role - Folder -for directory_name in nginx certbot/conf certbot/www mongo/db -do - mkdir -p "$install_dir/data/$directory_name" -done +# Create needed folder structure. +mkdir -p "$install_dir/data/"{nginx,mongo/db} echo "" echo "Generating the configuration files from the templates" -. ./template/nginx_app.conf.sh -. ./template/docker-compose.yml.sh -. ./template/mongo-init.js.sh -. ./template/init-letsencrypt.sh.sh -. ./template/docker.env.sh -if [[ "$setup_encryption" = "true" ]];then - . ./template/encryption.env.sh +bash "$templates_dir/nginx_app.conf.sh" "$NGINX_SSL_CMNT" "$custom_domain" > nginx_app.conf +bash "$templates_dir/docker-compose.yml.sh" "$mongo_root_user" "$mongo_root_password" "$mongo_database" > docker-compose.yml +bash "$templates_dir/mongo-init.js.sh" "$mongo_root_user" "$mongo_root_password" > mongo-init.js +bash "$templates_dir/docker.env.sh" "$encoded_mongo_root_user" "$encoded_mongo_root_password" "$mongo_host" > docker.env +if [[ "$setup_encryption" = "true" ]]; then + bash "$templates_dir/encryption.env.sh" "$user_encryption_password" "$user_encryption_salt" > encryption.env fi -chmod 0755 init-letsencrypt.sh overwrite_file "data/nginx/app.conf.template" "nginx_app.conf" overwrite_file "docker-compose.yml" "docker-compose.yml" overwrite_file "data/mongo/init.js" "mongo-init.js" -overwrite_file "init-letsencrypt.sh" "init-letsencrypt.sh" overwrite_file "docker.env" "docker.env" overwrite_file "encryption.env" "encryption.env" @@ -388,12 +500,13 @@ echo "" cd "$install_dir" if [[ -n $custom_domain ]]; then - echo "Running init-letsencrypt.sh..." - sudo ./init-letsencrypt.sh + init_ssl_cert "$custom_domain" else echo "No domain found. Skipping generation of SSL certificate." fi +rm -rf "$templates_dir" + echo "" echo "Pulling the latest container images" sudo docker-compose pull @@ -430,5 +543,4 @@ else echo "Join our Discord server https://discord.com/invite/rBTTVJp" fi -echo "" -echo -e "Peace out \U1F596\n" +echo -e "\nPeace out \U1F596\n" diff --git a/deploy/template/docker-compose.yml.sh b/deploy/template/docker-compose.yml.sh index f701eaaef6..21b15d0a52 100644 --- a/deploy/template/docker-compose.yml.sh +++ b/deploy/template/docker-compose.yml.sh @@ -1,10 +1,12 @@ -#!/bin/sh +#!/bin/bash -if [ ! -f docker-compose.yml ]; then - touch docker-compose.yml -fi +set -o nounset -cat >| docker-compose.yml << EOF +mongo_root_user="$1" +mongo_root_password="$2" +mongo_database="$3" + +cat <| docker.env << EOF +encoded_mongo_root_user="$1" +encoded_mongo_root_password="$2" +mongo_host="$3" + +cat << EOF # Read our documentation on how to configure these features # https://docs.appsmith.com/v/v1.1/enabling-3p-services diff --git a/deploy/template/encryption.env.sh b/deploy/template/encryption.env.sh index 27c64b9237..1c4b34cb82 100644 --- a/deploy/template/encryption.env.sh +++ b/deploy/template/encryption.env.sh @@ -1,11 +1,11 @@ -#!/bin/sh +#!/bin/bash -if [ ! -f encryption.env ]; then - touch encryption.env -fi +set -o nounset -cat >| encryption.env << EOF +user_encryption_password="$1" +user_encryption_salt="$2" + +cat <| init-letsencrypt.sh << EOF -#!/bin/bash - -if ! [ -x "\$(command -v docker-compose)" ]; then - echo 'Error: docker-compose is not installed.' >&2 - exit 1 -fi - -domains=($custom_domain) -rsa_key_size=4096 -data_path="./data/certbot" -email="" # Adding a valid address is strongly recommended -staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits - -echo "\$data_path" -if [ -d "\$data_path" ]; then - read -p "Existing data found for \$domains. Continue and replace existing certificate? [y/N] " decision - if [ "\$decision" != "Y" ] && [ "\$decision" != "y" ]; then - exit - fi -fi - - -if [ ! -e "\$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "\$data_path/conf/ssl-dhparams.pem" ]; then - echo "### Downloading recommended TLS parameters ..." - mkdir -p "\$data_path/conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "\$data_path/conf/options-ssl-nginx.conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "\$data_path/conf/ssl-dhparams.pem" - echo -fi - -echo "### Creating dummy certificate for \$domains ..." -path="/etc/letsencrypt/live/\$domains" -mkdir -p "\$data_path/conf/live/\$domains" -docker-compose run --rm --entrypoint "\\ - openssl req -x509 -nodes -newkey rsa:1024 -days 1\\ - -keyout '\$path/privkey.pem' \\ - -out '\$path/fullchain.pem' \\ - -subj '/CN=localhost'" certbot -echo - - -echo "### Starting nginx ..." -docker-compose up --force-recreate -d nginx -echo - -echo "### Deleting dummy certificate for \$domains ..." -docker-compose run --rm --entrypoint "\\ - rm -Rf /etc/letsencrypt/live/\$domains && \\ - rm -Rf /etc/letsencrypt/archive/\$domains && \\ - rm -Rf /etc/letsencrypt/renewal/\$domains.conf" certbot -echo - - -echo "### Requesting Let's Encrypt certificate for \$domains ..." -#Join \$domains to -d args -domain_args="" -for domain in "\${domains[@]}"; do - domain_args="\$domain_args -d \$domain" -done - -# Select appropriate email arg -case "\$email" in - "") email_arg="--register-unsafely-without-email" ;; - *) email_arg="--email \$email" ;; -esac - -# Enable staging mode if needed -if [ \$staging != "0" ]; then staging_arg="--staging"; fi - -docker-compose run --rm --entrypoint "\\ - certbot certonly --webroot -w /var/www/certbot \\ - \$staging_arg \\ - \$email_arg \\ - \$domain_args \\ - --rsa-key-size \$rsa_key_size \\ - --agree-tos \\ - --force-renewal" certbot -echo - -echo "### Reloading nginx ..." -docker-compose exec nginx nginx -s reload - - -EOF diff --git a/deploy/template/mongo-init.js.sh b/deploy/template/mongo-init.js.sh index b88b0bfef9..0b26c98456 100644 --- a/deploy/template/mongo-init.js.sh +++ b/deploy/template/mongo-init.js.sh @@ -1,12 +1,11 @@ -#!/bin/sh +#!/bin/bash -if [ ! -f mongo-init.js ]; then - touch mongo-init.js -fi +set -o nounset +mongo_root_user="$1" +mongo_root_password="$2" - -cat >| mongo-init.js << EOF +cat << EOF let error = false print("**** Going to start Mongo seed ****") @@ -1534,7 +1533,4 @@ printjson(res) if (error) { print('Error occurred while inserting the records') } - - - EOF diff --git a/deploy/template/nginx_app.conf.sh b/deploy/template/nginx_app.conf.sh index 78cca642a9..14c27a9eeb 100644 --- a/deploy/template/nginx_app.conf.sh +++ b/deploy/template/nginx_app.conf.sh @@ -1,15 +1,16 @@ -#!/bin/sh +#!/bin/bash -if [ ! -f nginx_app.conf ]; then - touch nginx_app.conf -fi +set -o nounset -# This template file is different from the others because of the sub_filter commands in the Nginx configuration -# Those variables are substituted inside the Docker container for appsmith-editor during bootup. -# Hence we wish to prevent environment substitution here. -# Relevant variables will be replaced at the end of this file via sed command +# In the config file, there's three types of variables, all represented with the syntax `$name`. The ones that are not +# escaped with a backslash are rendered within this script. Among the ones that are escaped with a backslash, the ones +# starting with `APPSMITH_` will be rendered at boot-up time by appsmith-editor docker container. The rest (like $scheme +# and $host) are for nginx to work out. -content=' +NGINX_SSL_CMNT="$1" +custom_domain="$2" + +cat <| nginx_app.conf +EOF