diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index df146f4858..08a755c150 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -148,13 +148,13 @@ jobs: with: name: build path: app/client/build - + - name: Pull release server docker container and start it locally if: github.ref == 'refs/heads/release' shell: bash run: | echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - + docker run -d --net=host --name appsmith-internal-server -p 8080:8080 \ --env APPSMITH_MONGODB_URI=mongodb://localhost:27017/appsmith \ --env APPSMITH_REDIS_URL=redis://localhost:6379 \ diff --git a/.github/workflows/duplicate-issue-detector.yml b/.github/workflows/duplicate-issue-detector.yml new file mode 100644 index 0000000000..b952182389 --- /dev/null +++ b/.github/workflows/duplicate-issue-detector.yml @@ -0,0 +1,30 @@ +name: Potential Duplicate Issues +on: + issues: + types: [opened, edited] +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: bubkoo/potential-duplicates@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Label to set, when potential duplicates are detected. + label: potential-duplicate + # Get issues with state to compare. Supported state: 'all', 'closed', 'open'. + state: open + # If similarity is higher than this threshold([0,1]), issue will be marked as duplicate. + threshold: 0.6 + # Reactions to be add to comment when potential duplicates are detected. + # Available reactions: "-1", "+1", "confused", "laugh", "heart", "hooray", "rocket", "eyes" + reactions: 'confused' + # Comment to post when potential duplicates are detected. + comment: > + We have found issues that are potential duplicates: + {{#issues}} + - [#{{ number }}] {{ title }} ({{ accuracy }}%) + {{/issues}} + + If any of the issues listed above are a duplicate, please consider closing this issue & upvoting the original one. + + Alternatively, if neither of the listed issues addresses your feature/bug, keep this issue open. \ No newline at end of file diff --git a/API.png b/API.png index eaa300a435..800601510d 100644 Binary files a/API.png and b/API.png differ diff --git a/Query.png b/Query.png index cbfe6764a8..bf619be0f3 100644 Binary files a/Query.png and b/Query.png differ diff --git a/README.md b/README.md index f66b49a150..6c2da1c1d3 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,96 @@ -
- - Appsmith.com logo

Appsmith

-
-

A plug and play web framework to build internal tools.

-

- -[![GitHub release](https://img.shields.io/github/v/release/appsmithorg/appsmith.svg?logo=GitHub)](https://github.com/appsmithorg/appsmith/releases/latest) -[![Website](https://img.shields.io/website?url=https%3A%2F%2Fappsmith.com&logo=Appsmith)](https://appsmith.com) -[![Chat on Discord](https://img.shields.io/badge/chat-Discord-violet?logo=discord)](https://discord.gg/rBTTVJp) -[![Docs](https://img.shields.io/badge/docs-v1.x-brightgreen.svg?style=flat)](https://docs.appsmith.com) - -[![All Contributors](https://img.shields.io/badge/all_contributors-50+-orange.svg?style=flat-square)](#-contributors) - - +

+ Appsmith.com logo +

+ +

+ Documentation + ยท + Latest Release + ยท + Discord +
+

-
----------------- -

Build apps by connecting UI widgets to database queries or APIs. Write any logic in JS.

+
+Appsmith is a JavaScript-based visual development platform to build and launch internal tools quickly. Drag-and-drop pre-built widgets, and connect them using JavaScript to create interactive pages. Connect UI to your APIs and Databases to build complex workflows in minutes.

-![UI Builder Demo](https://github.com/appsmithOrg/appsmith/blob/master/static/demo.gif) +**API Support**: REST APIs
+**Database Support**: PostgreSQL, MongoDB, MySQL, Redshift, Elastic Search, DynamoDB, Redis, & MSFT SQL Server
+**Hosting**: Cloud-hosted & On-premise + +Already familiar with Appsmith? [Quickly start building on your own](#%EF%B8%8F-quickstart).
-### Here's how you build something: -1. Compose a page using pre-built UI components like table, charts, map viewers and forms. -2. Connect the UI components to any REST API or databases like MySQL, Postgres, and MongoDB. Write any logic in JS. -3. Deploy the internal tool to a custom URL and invite users to sign in with their Google accounts. - -------------------- - -## ๐Ÿ“บ Demo Video - -* [Build a tool in 5 minutes!](http://bit.ly/appsmith-demo-github) - -## ๐Ÿ—‚ Example Applications - -* [Customer Support Dashboard](https://bit.ly/cs-dashboard-appsmith) -* [Job Application Tracker](https://bit.ly/3hbYtTi) - -## ๐Ÿƒโ€โ™€๏ธ Getting Started -You can try our online sandbox or deploy a Docker image on a server. -* [Online sandbox](https://bit.ly/appsmith-signup-github) -* [Deploy with Docker](https://bit.ly/appsmith-docker-github) - -## ๐Ÿ˜‡ Why Appsmith? - -When we build internal tools today, we turn to admin panels, UI frameworks or use a bootstrap theme. We took inspirations from the best admin panels, bootstrap themes, and brought back the easy UI builder of Visual Basic. - -Appsmith is a quicker way of building internal tools by visualising them as modular blocks (**Widgets, APIs, Queries, JS**) and giving developers a simple user interface to configure them. Building new features, creating UI, changing dataflows, and modifying business logic becomes simpler because you no longer have to trudge through large undocumented code bases or wrestle with HTML/CSS. -Appsmith doesn't take the fun out of coding, because it treats every block as an object and exposes it via javascript so that you can read, transform and manipulate it. Whether it's a widget, API or query, you get to decide where you need to configure using UI and where you need to code. + +

+ + + +

## ๐Ÿญ Features -* **5 minute setup**: Deploy Appsmith on your servers in 5 minutes. -* **Build custom UI**: Drag & drop, resize and style widgets **without HTML / CSS**. -* **Query data**: Query & update your database directly from the UI. Connect to **PostgreSQL, MongoDB, MySQL, REST & GraphQL APIs**. -* **JS Logic**: Write snippets of business logic using JS to transform data, manipulate UI or trigger workflows. Use popular libraries like lodash & moment anywhere in the app -* **Data Workflows**: Simple configuration to create flows when users interact with the UI. -* **Realtime Editor**: Changes in your application reflect instantly with every edit. No need to compile! -* **Works with existing, live databases**: Connect directly to any PostgreSQL, MySQL & MongoDB -* **Fine-grained access control**: Control who can edit / view your applications from a single control panel -* **App management**: Build and organise multiple applications on a single platform +* **5-minute setup**: Deploy Appsmith on your server, or use our cloud version to start building in 5 minutes. [Quick Start](#%EF%B8%8F-quickstart) +* **Frontend as a service**: Drag-and-drop to build sophisticated **dashboards** and **workflows, without writing HTML/CSS**. Write JavaScript anywhere to transform data, and dynamically control widget-properties. +* **Database CRUD**: Query & update your database directly by connecting it to the UI. Connect to **PostgreSQL, MongoDB, MySQL & more!** +* **Trigger APIs**: Connect to REST APIs to build custom apps. +* **Security**: DB Credentials are AES 256 encrypted and no data is stored by appsmith. Deploy behind your private VPC for additional security! +* **One-click deployment**: Managed deployment of your apps with a click of a button. +* **Access-control**: Assign users different roles & control who can edit / view your applications. +* **Supports OAuth**: Allow users to authenticate via Google Auth or GitHub Auth. -## ๐Ÿ“• Documentation & Support +## ๐Ÿ“บ Demo -If you have encountered a bug or need to get in touch with us, you can contact us using one of the following channels: +Unsure if Appsmith is for you? [Watch it in action here](http://bit.ly/appsmith-demo-github) +But if youโ€™d rather check out some real applications that can be built with Appsmith, check below: +* [Customer Support Dashboard](https://bit.ly/cs-dashboard-appsmith) +* [Job Application Tracker](https://bit.ly/3hbYtTi) + +## ๐Ÿƒโ€โ™€๏ธ Quickstart + +The following steps introduce you to building a simple user-list dashboard on Appsmith. +1. [Sign up on Appsmith Cloud](https://bit.ly/appsmith-signup-github) or [Deploy Appsmith via Docker](https://bit.ly/appsmith-docker-github). +2. Create a new app within the organization that has already been created for you. +3. Click on the `+` icon next to the `Queries` section to add a new query in the mock database + 1. Name the query `usersQuery`. + 2. Write the query `select * from users limit 5;`. + 3. Run the query. + 4. In query window, switch to the `Settings` tab, and enable `Run Query on Page Load`. +4. Click on the `+` icon next to the `Widgets` section and drag a table onto the screen +5. Link the table data property to the `usersQuery` using JavaScript `{{usersQuery.data}}` +6. Hit the Deploy button and checkout the view mode of the app. + +Congratulations ๐ŸŽ‰ You just built your first app on Appsmith! +Connect your own data to build apps for your team. [Read more here.](https://docs.appsmith.com/core-concepts/connecting-to-databases) + +## ๐Ÿ“• Support & Troubleshooting + +If you encountered a bug or need help troubleshooting an issue, you can use one of the following channels: + +* Self Help: [Documentation](https://docs.appsmith.com/) +* Community Support: [Discord](https://discord.gg/rBTTVJp) * Issue & bug tracking: [GitHub Issues](https://github.com/appsmithorg/appsmith/issues/new/choose) -* Community & Support: [Discord](https://discord.gg/rBTTVJp) -* Documentation: [Documentation](https://docs.appsmith.com) -We are committed to fostering an open and welcoming environment in the community. Please see the [Code of Conduct](CODE_OF_CONDUCT.md). +## ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ Contributing -## โˆž Contributing to Appsmith +If you're interested in contributing to Appsmith: +1. Start by reading our [Contribution Guide](https://github.com/appsmithorg/appsmith/blob/master/CONTRIBUTING.md). +2. Learn how to set up your local environment, in our [developer-guide](https://github.com/appsmithorg/appsmith/blob/master/contributions/CodeContributionsGuidelines.md#-setup-for-local-development). +3. Explore our list of [good first issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aissue+is%3Aopen+label%3A%22Good+First+Issue%22). -Read our [Contribution Guide](https://github.com/appsmithorg/appsmith/blob/master/CONTRIBUTING.md) and join our community of contributors! +We are committed to fostering an open and welcoming environment in the community. Please read our [Code of Conduct](CODE_OF_CONDUCT.md). ## ๐Ÿ“‘ License The Appsmith platform is available under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) (Apache-2.0). -## ๐Ÿง‘โ€๐Ÿคโ€๐Ÿง‘ Contributors +## Contributors - + diff --git a/app/client/README.old.md b/app/client/README.old.md index 9bc8469e36..353e626643 100644 --- a/app/client/README.old.md +++ b/app/client/README.old.md @@ -1,6 +1,6 @@ **Edit a file, create a new file, and clone from Bitbucket in under 2 minutes** -When you're done, you can delete the content in this README and update the file with details for others getting started with your repository. +When you're done, you can delete the content in this README and then update the file with details for others getting started with your repository. *We recommend that you open this README in another tab as you perform the tasks below. You can [watch our video](https://youtu.be/0ocf7u76WSo) for a full demo of all the steps in this tutorial. Open the video in a new tab to avoid leaving Bitbucket.* diff --git a/app/client/cypress/fixtures/buttondsl.json b/app/client/cypress/fixtures/buttondsl.json new file mode 100644 index 0000000000..4e125772d4 --- /dev/null +++ b/app/client/cypress/fixtures/buttondsl.json @@ -0,0 +1,42 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "dynamicBindings": {}, + "version": 6, + "minHeight": 1292, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "text": "Submit", + "buttonStyle": "PRIMARY_BUTTON", + "widgetName": "Button1", + "isDisabled": false, + "isDefaultClickDisabled": true, + "type": "BUTTON_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 5, + "rightColumn": 7, + "topRow": 2, + "bottomRow": 3, + "parentId": "0", + "widgetId": "3qg87le9t4" + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/fixtures/containerdsl.json b/app/client/cypress/fixtures/containerdsl.json new file mode 100644 index 0000000000..fb7627a974 --- /dev/null +++ b/app/client/cypress/fixtures/containerdsl.json @@ -0,0 +1,61 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "dynamicBindings": {}, + "version": 6, + "minHeight": 1292, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "backgroundColor": "#FFFFFF", + "widgetName": "Container1", + "containerStyle": "card", + "children": [ + { + "isVisible": true, + "widgetName": "Canvas1", + "containerStyle": "none", + "canExtend": false, + "detachFromLayout": true, + "children": [], + "minHeight": 400, + "type": "CANVAS_WIDGET", + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": 592, + "topRow": 0, + "bottomRow": 400, + "parentId": "76mznwu5lq", + "widgetId": "tik1oqc468" + } + ], + "type": "CONTAINER_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 3, + "rightColumn": 11, + "topRow": 16, + "bottomRow": 26, + "parentId": "0", + "widgetId": "76mznwu5lq" + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/fixtures/inputdsl.json b/app/client/cypress/fixtures/inputdsl.json new file mode 100644 index 0000000000..c4379f1a51 --- /dev/null +++ b/app/client/cypress/fixtures/inputdsl.json @@ -0,0 +1,40 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "dynamicBindings": {}, + "version": 6, + "minHeight": 1292, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "inputType": "TEXT", + "label": "", + "widgetName": "Input1", + "type": "INPUT_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 2, + "rightColumn": 7, + "topRow": 0, + "bottomRow": 1, + "parentId": "0", + "widgetId": "2lhsjdd5sg" + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index cdd21b19bc..e7ff6ff21a 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -172,5 +172,7 @@ "productName": "Avocado Panini", "orderAmount": 7.99 } - ] + ], + "addInputWidgetBinding": "{{Table1.selectedRow.date", + "externalPage": "https://www.appsmith.com/" } \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/Applications/UpdateApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Applications/UpdateApplication_spec.js new file mode 100644 index 0000000000..b46e2399ee --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Applications/UpdateApplication_spec.js @@ -0,0 +1,70 @@ +const homePage = require("../../../locators/HomePage.json"); +const commonlocators = require("../../../locators/commonlocators.json"); +import tinycolor from "tinycolor2"; + +describe("Update Application", function() { + let appname; + let iconname; + let colorname; + + it("Open the application menu and update name and then check whether update is reflected in the application card", function() { + cy.get(commonlocators.homeIcon).click({ force: true }); + appname = localStorage.getItem("AppName"); + cy.get(homePage.searchInput).type(appname); + cy.wait(2000); + + cy.get(homePage.applicationCard) + .first() + .trigger("mouseover"); + cy.get(homePage.appMoreIcon) + .first() + .click({ force: true }); + cy.get(homePage.applicationName).type(appname + "{enter}"); + cy.wait("@updateApplication").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get(homePage.applicationCardName).should("contain", appname); + }); + + it("Open the application menu and update icon and then check whether update is reflected in the application card", function() { + cy.get(homePage.applicationIconSelector) + .first() + .click(); + cy.wait("@updateApplication") + .then(xhr => { + iconname = xhr.response.body.data.icon; + }) + .should("have.nested.property", "response.body.responseMeta.status", 200); + cy.get(homePage.applicationCard) + .first() + .within(() => { + cy.get("a") + .invoke("attr", "name") + .should("equal", iconname); + }); + }); + + it("Open the application menu and update card color and then check whether update is reflected in the application card", function() { + cy.get(homePage.applicationColorSelector) + .first() + .click(); + cy.wait("@updateApplication") + .then(xhr => { + colorname = tinycolor(xhr.response.body.data.color).toRgbString(); + }) + .should("have.nested.property", "response.body.responseMeta.status", 200); + cy.wait(2000); + + cy.get(homePage.applicationCard) + .first() + .within(() => { + cy.get(homePage.applicationBackgroundColor).should( + "have.css", + "background-color", + colorname, + ); + }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/ButtonWidgets_NavigateTo_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/ButtonWidgets_NavigateTo_validation_spec.js new file mode 100644 index 0000000000..14a8ad64c3 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/ButtonWidgets_NavigateTo_validation_spec.js @@ -0,0 +1,43 @@ +const commonlocators = require("../../../locators/commonlocators.json"); +const formWidgetsPage = require("../../../locators/FormWidgets.json"); +const dsl = require("../../../fixtures/buttondsl.json"); +const pages = require("../../../locators/Pages.json"); +const widgetsPage = require("../../../locators/Widgets.json"); +const publish = require("../../../locators/publishWidgetspage.json"); +const testdata = require("../../../fixtures/testdata.json"); +const dsl2 = require("../../../fixtures/displayWidgetDsl.json"); +const explorer = require("../../../locators/explorerlocators.json"); + +describe("Binding the button Widgets and validating NavigateTo Page functionality", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Button widget with action navigate to page", function() { + cy.openPropertyPane("buttonwidget"); + cy.get(widgetsPage.actionSelect).click(); + cy.get(commonlocators.chooseAction) + .children() + .contains("Navigate To") + .click(); + cy.enterActionValue(testdata.externalPage); + cy.get(commonlocators.editPropCrossButton).click(); + cy.wait(300); + }); + + it("Button click should take the control to page link validation", function() { + cy.PublishtheApp(); + cy.get(publish.buttonWidget).click(); + cy.wait(500); + cy.get(publish.buttonWidget).should("not.be.visible"); + cy.go("back"); + cy.get(publish.backToEditor) + .first() + .click(); + cy.wait("@getPage").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Datasources/PostgresDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Datasources/PostgresDatasource_spec.js index c70aaf4367..fc223533cd 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Datasources/PostgresDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Datasources/PostgresDatasource_spec.js @@ -31,8 +31,6 @@ describe("Postgres datasource test cases", function() { 200, ); - cy.GlobalSearchEntity(`${datasourceName}`); - cy.get(`.t--entity-name:contains(${datasourceName})`).click(); - cy.deleteDataSource(); + cy.deletePostgresDatasource(datasourceName); }); }); 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 8e545ee661..437ad1416e 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 @@ -9,13 +9,7 @@ describe("Entity explorer API pane related testcases", function() { cy.NavigateToWidgetsInExplorer(); cy.get(explorer.NoWidgetsMsg).should("be.visible"); cy.NavigateToAPI_Panel(); - cy.get(explorer.NoApiMsg) - .should("be.visible") - .should("be.visible"); cy.NavigateToQueriesInExplorer(); - cy.get(explorer.NoQueryMsg) - .should("be.visible") - .should("be.visible"); cy.reload(); }); 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 index c3c10b3135..bd44eb457d 100644 --- 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 @@ -74,11 +74,12 @@ describe("Entity explorer tests related to copy query", function() { }); it("Delete query and rename datasource in explorer", function() { - cy.deleteQuery(); cy.get(commonlocators.entityExplorersearch).clear(); cy.NavigateToDatasourceEditor(); cy.GlobalSearchEntity(`${datasourceName}`); - cy.get(`.t--entity-name:contains(${datasourceName})`).click(); + cy.get(`.t--entity-name:contains(${datasourceName})`) + .last() + .click(); cy.generateUUID().then(uid => { updatedName = uid; cy.log("complete uid :" + updatedName); @@ -86,7 +87,6 @@ describe("Entity explorer tests related to copy query", function() { cy.log("sliced id :" + updatedName); cy.EditEntityNameByDoubleClick(datasourceName, updatedName); cy.SearchEntityandOpen(updatedName); - cy.get(datasource.editDatasource).click(); cy.testSaveDatasource(); cy.hoverAndClick(); cy.get(apiwidget.delete).click({ force: true }); @@ -97,5 +97,8 @@ describe("Entity explorer tests related to copy query", function() { 409, ); }); + + cy.SearchEntityandOpen("Query1Copy"); + cy.deleteQuery(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js index fd3ae30c1e..9f36557f4d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Datasource_Structure_spec.js @@ -2,6 +2,7 @@ const explorer = require("../../../locators/explorerlocators.json"); const queryEditor = require("../../../locators/QueryEditor.json"); const queryLocators = require("../../../locators/QueryEditor.json"); const commonlocators = require("../../../locators/commonlocators.json"); +const apiwidget = require("../../../locators/apiWidgetslocator.json"); let datasourceName; @@ -15,6 +16,23 @@ describe("Entity explorer datasource structure", function() { }); it("Entity explorer datasource structure", function() { + cy.NavigateToQueryEditor(); + cy.contains(".t--datasource-name", datasourceName) + .find(queryLocators.createQuery) + .click(); + cy.wait("@createNewApi").should( + "have.nested.property", + "response.body.responseMeta.status", + 201, + ); + + cy.get(apiwidget.apiTxt) + .clear() + .type("MyQuery", { force: true }) + .should("have.value", "MyQuery") + .blur(); + cy.WaitAutoSave(); + cy.GlobalSearchEntity(datasourceName); cy.wait("@getDatasourceStructure").should( "have.nested.property", @@ -51,10 +69,27 @@ describe("Entity explorer datasource structure", function() { 200, ); + cy.GlobalSearchEntity("MyQuery"); + cy.get(`.t--entity-name:contains(MyQuery)`).click(); + cy.get(queryEditor.deleteQuery).click(); + cy.wait("@deleteAction").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + + cy.get(commonlocators.entityExplorersearch).clear(); + cy.deletePostgresDatasource(datasourceName); }); it("Refresh datasource structure", function() { + cy.NavigateToQueryEditor(); + cy.contains(".t--datasource-name", datasourceName) + .find(queryLocators.createQuery) + .click(); + cy.get(queryLocators.templateMenu).click(); + cy.GlobalSearchEntity(datasourceName); cy.get(`.t--entity.datasource:contains(${datasourceName})`) .find(explorer.collapse) @@ -68,12 +103,6 @@ describe("Entity explorer datasource structure", function() { cy.get(commonlocators.entityExplorersearch).clear(); - cy.NavigateToQueryEditor(); - cy.contains(".t--datasource-name", datasourceName) - .find(queryLocators.createQuery) - .click(); - cy.get(queryLocators.templateMenu).click(); - const tableName = Math.random() .toString(36) .replace(/[^a-z]+/g, ""); @@ -127,6 +156,8 @@ describe("Entity explorer datasource structure", function() { "response.body.responseMeta.status", 200, ); + + cy.get(commonlocators.entityExplorersearch).clear(); cy.deletePostgresDatasource(datasourceName); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js index 5bb8ed757c..db4e0b64cc 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Query_Datasource_spec.js @@ -4,8 +4,15 @@ const apiwidget = require("../../../locators/apiWidgetslocator.json"); const commonlocators = require("../../../locators/commonlocators.json"); const pageid = "MyPage"; +let datasourceName; describe("Entity explorer tests related to query and datasource", function() { + before(() => { + cy.generateUUID().then(uid => { + datasourceName = uid; + }); + }); + it("Create a page/moveQuery/rename/delete in explorer", function() { cy.NavigateToDatasourceEditor(); cy.get(datasource.PostgreSQL).click(); @@ -16,15 +23,17 @@ describe("Entity explorer tests related to query and datasource", function() { cy.testSaveDatasource(); + cy.get(".t--edit-datasource-name").click(); + cy.get(".t--edit-datasource-name input") + .clear() + .type(datasourceName, { force: true }) + .should("have.value", datasourceName) + .blur(); + cy.NavigateToQueryEditor(); - - cy.get("@createDatasource").then(httpResponse => { - const datasourceName = httpResponse.response.body.data.name; - - cy.contains(".t--datasource-name", datasourceName) - .find(queryLocators.createQuery) - .click(); - }); + cy.contains(".t--datasource-name", datasourceName) + .find(queryLocators.createQuery) + .click(); cy.get("@getPluginForm").should( "have.nested.property", @@ -40,15 +49,11 @@ describe("Entity explorer tests related to query and datasource", function() { cy.EvaluateCurrentValue("select * from users"); - cy.get("@createDatasource").then(httpResponse => { - const 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()}}"); - }); + 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()}}"); }); cy.Createpage(pageid); cy.GlobalSearchEntity("Query1"); @@ -62,14 +67,17 @@ describe("Entity explorer tests related to query and datasource", function() { cy.MoveAPIToPage(pageid); cy.SearchEntityandOpen("MyQuery"); cy.runQuery(); + cy.deleteQuery(); - cy.get(commonlocators.entityExplorersearch).clear(); - cy.NavigateToDatasourceEditor(); - cy.get("@createDatasource").then(httpResponse => { - const datasourceName = httpResponse.response.body.data.name; - cy.GlobalSearchEntity(`${datasourceName}`); - cy.get(`.t--entity-name:contains(${datasourceName})`).click(); - }); - cy.deleteDataSource(); + + cy.contains(".t--datasource-name", datasourceName) + .find(".t--edit-datasource") + .click(); + cy.get(".t--delete-datasource").click(); + cy.wait("@deleteDatasource").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Container_spec.js b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Container_spec.js index b5f310d718..56cc93725d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Container_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/LayoutWidgets/Container_spec.js @@ -1,6 +1,6 @@ const commonlocators = require("../../../locators/commonlocators.json"); const widgetsPage = require("../../../locators/Widgets.json"); -const dsl = require("../../../fixtures/layoutdsl.json"); +const dsl = require("../../../fixtures/containerdsl.json"); describe("Container Widget Functionality", function() { before(() => { 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 4f11d6e725..4c0f39bf6f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js @@ -29,7 +29,9 @@ describe("Create new org and share with a user", function() { cy.wait(2000); cy.get(homePage.appsContainer).contains(orgid); cy.xpath(homePage.ShareBtn).should("not.exist"); - cy.get(homePage.applicationCard).trigger("mouseover"); + cy.get(homePage.applicationCard) + .first() + .trigger("mouseover"); cy.get(homePage.appEditIcon).should("not.exist"); cy.launchApp(appid); cy.LogOut(); @@ -62,7 +64,9 @@ describe("Create new org and share with a user", function() { cy.xpath(homePage.ShareBtn) .first() .should("be.visible"); - cy.get(homePage.applicationCard).trigger("mouseover"); + cy.get(homePage.applicationCard) + .first() + .trigger("mouseover"); cy.get(homePage.appEditIcon) .first() .click({ force: true }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/ShareAppTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/ShareAppTests_spec.js index d67219cb9c..48843ba430 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/ShareAppTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/ShareAppTests_spec.js @@ -1,128 +1,129 @@ -// /// -// -// const homePage = require("../../../locators/HomePage.json"); -// const publish = require("../../../locators/publishWidgetspage.json"); -// -// describe("Create new org and share with a user", function() { -// let orgid; -// let appid; -// let currentUrl; -// -// it("create org and then share with a user from Application share option within application", function() { -// cy.NavigateToHome(); -// cy.generateUUID().then(uid => { -// orgid = uid; -// appid = uid; -// localStorage.setItem("OrgName", orgid); -// cy.createOrg(orgid); -// cy.CreateAppForOrg(orgid, appid); -// cy.wait("@getPagesForApp").should( -// "have.nested.property", -// "response.body.responseMeta.status", -// 200, -// ); -// cy.get("h2").contains("Drag and drop a widget here"); -// cy.get(homePage.shareApp).click(); -// cy.shareApp(Cypress.env("TESTUSERNAME1"), homePage.viewerRole); -// }); -// cy.LogOut(); -// }); -// -// it("login as invited user and then validate viewer privilage", function() { -// cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); -// cy.get(homePage.searchInput).type(appid); -// cy.wait(2000); -// cy.get(homePage.appsContainer).contains(orgid); -// cy.xpath(homePage.ShareBtn).should("not.exist"); -// cy.get(homePage.applicationCard).trigger("mouseover"); -// cy.get(homePage.appEditIcon).should("not.exist"); -// cy.launchApp(appid); -// cy.LogOut(); -// }); -// -// it("Enable public access to Application", function() { -// cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); -// cy.visit("/applications"); -// cy.wait("@applications").should( -// "have.nested.property", -// "response.body.responseMeta.status", -// 200, -// ); -// cy.SearchApp(appid); -// cy.wait("@getPagesForApp").should( -// "have.nested.property", -// "response.body.responseMeta.status", -// 200, -// ); -// cy.get("h2").contains("Drag and drop a widget here"); -// cy.get(homePage.shareApp).click(); -// cy.enablePublicAccess(); -// cy.PublishtheApp(); -// currentUrl = cy.url(); -// cy.url().then(url => { -// currentUrl = url; -// cy.log(currentUrl); -// }); -// cy.get(publish.backToEditor).click(); -// cy.LogOut(); -// }); -// -// it("login as uninvited user and then validate public access of Application", function() { -// cy.LogintoApp(Cypress.env("TESTUSERNAME2"), Cypress.env("TESTPASSWORD2")); -// cy.visit(currentUrl); -// cy.wait("@getPagesForApp").should( -// "have.nested.property", -// "response.body.responseMeta.status", -// 200, -// ); -// cy.get(publish.pageInfo) -// .invoke("text") -// .then(text => { -// const someText = text; -// expect(someText).to.equal("This page seems to be blank"); -// }); -// cy.LogOut(); -// }); -// it("login as Owner and disable public access", function() { -// cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); -// cy.visit("/applications"); -// cy.wait("@applications").should( -// "have.nested.property", -// "response.body.responseMeta.status", -// 200, -// ); -// cy.SearchApp(appid); -// cy.wait("@getPagesForApp").should( -// "have.nested.property", -// "response.body.responseMeta.status", -// 200, -// ); -// cy.get("h2").contains("Drag and drop a widget here"); -// cy.get(homePage.shareApp).click(); -// cy.enablePublicAccess(); -// cy.LogOut(); -// }); -// -// it("login as uninvited user and then validate public access disable feature", function() { -// cy.LogintoApp(Cypress.env("TESTUSERNAME2"), Cypress.env("TESTPASSWORD2")); -// cy.visit(currentUrl); -// cy.wait("@getPagesForApp").should( -// "have.nested.property", -// "response.body.responseMeta.status", -// 404, -// ); -// cy.LogOut(); -// }); -// -// it("login as owner and delete App ", function() { -// cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); -// cy.visit("/applications"); -// cy.wait("@applications").should( -// "have.nested.property", -// "response.body.responseMeta.status", -// 200, -// ); -// cy.SearchApp(appid); -// cy.get("#loading").should("not.exist"); -// }); -// }); +/// + +const homePage = require("../../../locators/HomePage.json"); +const publish = require("../../../locators/publishWidgetspage.json"); + +describe("Create new org and share with a user", function() { + let orgid; + let appid; + let currentUrl; + + it("create org and then share with a user from Application share option within application", function() { + cy.NavigateToHome(); + cy.generateUUID().then(uid => { + orgid = uid; + appid = uid; + localStorage.setItem("OrgName", orgid); + cy.createOrg(orgid); + cy.CreateAppForOrg(orgid, appid); + cy.wait("@getPagesForCreateApp").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get("h2").contains("Drag and drop a widget here"); + cy.get(homePage.shareApp).click({ force: true }); + cy.shareApp(Cypress.env("TESTUSERNAME1"), homePage.viewerRole); + }); + cy.LogOut(); + }); + + it("login as invited user and then validate viewer privilage", function() { + cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); + cy.get(homePage.searchInput).type(appid); + cy.wait(2000); + cy.get(homePage.appsContainer).contains(orgid); + cy.xpath(homePage.ShareBtn).should("not.exist"); + cy.get(homePage.applicationCard).trigger("mouseover"); + cy.get(homePage.appEditIcon).should("not.exist"); + cy.launchApp(appid); + cy.LogOut(); + }); + + it("Enable public access to Application", function() { + cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); + cy.visit("/applications"); + cy.wait("@applications").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.SearchApp(appid); + cy.wait("@getPagesForCreateApp").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get("h2").contains("Drag and drop a widget here"); + cy.get(homePage.shareApp).click(); + cy.enablePublicAccess(); + cy.PublishtheApp(); + currentUrl = cy.url(); + cy.url().then(url => { + currentUrl = url; + cy.log(currentUrl); + }); + cy.get(publish.backToEditor).click(); + cy.LogOut(); + }); + + it("login as uninvited user and then validate public access of Application", function() { + cy.LogintoApp(Cypress.env("TESTUSERNAME2"), Cypress.env("TESTPASSWORD2")); + cy.visit(currentUrl); + cy.wait("@getPagesForViewApp").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get(publish.pageInfo) + .invoke("text") + .then(text => { + const someText = text; + expect(someText).to.equal("This page seems to be blank"); + }); + cy.LogOut(); + }); + + it("login as Owner and disable public access", function() { + cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); + cy.visit("/applications"); + cy.wait("@applications").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.SearchApp(appid); + cy.wait("@getPagesForCreateApp").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get("h2").contains("Drag and drop a widget here"); + cy.get(homePage.shareApp).click(); + cy.enablePublicAccess(); + cy.LogOut(); + }); + + it("login as uninvited user and then validate public access disable feature", function() { + cy.LogintoApp(Cypress.env("TESTUSERNAME2"), Cypress.env("TESTPASSWORD2")); + cy.visit(currentUrl); + cy.wait("@viewApp").should( + "have.nested.property", + "response.body.responseMeta.status", + 404, + ); + cy.LogOut(); + }); + + it("login as owner and delete App ", function() { + cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); + cy.visit("/applications"); + cy.wait("@applications").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.SearchApp(appid); + cy.get("#loading").should("not.exist"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/UpdateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/UpdateOrgTests_spec.js new file mode 100644 index 0000000000..ad922ce41f --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/UpdateOrgTests_spec.js @@ -0,0 +1,71 @@ +const homePage = require("../../../locators/HomePage.json"); + +describe("Update Organization", function() { + let orgid; + + it("Open the org general settings and update org name. The update should reflect in the org. It should also reflect in the org names on the left side and the org dropdown. ", function() { + cy.NavigateToHome(); + cy.generateUUID().then(uid => { + orgid = uid; + localStorage.setItem("OrgName", orgid); + cy.createOrg(orgid); + cy.get(homePage.orgList.concat(orgid).concat(")")) + .scrollIntoView() + .should("be.visible") + .within(() => { + cy.get(".t--org-name") + .first() + .click(); + }); + cy.get(homePage.orgSettingOption).click(); + }); + cy.generateUUID().then(uid => { + orgid = uid; + localStorage.setItem("OrgName", orgid); + cy.get(homePage.orgNameInput).clear(); + cy.get(homePage.orgNameInput).type(orgid); + cy.wait(2000); + cy.get(homePage.orgHeaderName).should("have.text", orgid); + }); + cy.NavigateToHome(); + cy.get(homePage.leftPanelContainer).within(() => { + cy.get("span").should(item => { + expect(item).to.contain.text(orgid); + }); + }); + }); + + it("Open the org general settings and update org email. The update should reflect in the org.", function() { + cy.get(homePage.orgList.concat(orgid).concat(")")) + .scrollIntoView() + .should("be.visible") + .within(() => { + cy.get(".t--org-name") + .first() + .click(); + }); + cy.get(homePage.orgSettingOption).click(); + cy.get(homePage.orgEmailInput).clear(); + cy.get(homePage.orgEmailInput).type(Cypress.env("TESTUSERNAME2")); + cy.wait("@updateOrganization").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get(homePage.orgEmailInput).should( + "have.value", + Cypress.env("TESTUSERNAME2"), + ); + }); + + it("Open the org general settings and update org website. The update should reflect in the org.", function() { + cy.get(homePage.orgWebsiteInput).clear(); + cy.get(homePage.orgWebsiteInput).type("demowebsite"); + cy.wait("@updateOrganization").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get(homePage.orgWebsiteInput).should("have.value", "demowebsite"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/AddWidgetTableAndBind_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/AddWidgetTableAndBind_spec.js new file mode 100644 index 0000000000..ac8ba866f3 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/AddWidgetTableAndBind_spec.js @@ -0,0 +1,78 @@ +const queryLocators = require("../../../locators/QueryEditor.json"); +const queryEditor = require("../../../locators/QueryEditor.json"); +const dsl = require("../../../fixtures/inputdsl.json"); +const pages = require("../../../locators/Pages.json"); +const widgetsPage = require("../../../locators/Widgets.json"); +const publish = require("../../../locators/publishWidgetspage.json"); +const testdata = require("../../../fixtures/testdata.json"); +const commonlocators = require("../../../locators/commonlocators.json"); + +let datasourceName; + +describe("Addwidget from Query and bind with other widgets", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Create a PostgresDataSource", () => { + cy.createPostgresDatasource(); + cy.get("@createDatasource").then(httpResponse => { + datasourceName = httpResponse.response.body.data.name; + }); + }); + + it("Create a query and populate response by choosing addWidget and validate in Table Widget", () => { + cy.NavigateToQueryEditor(); + cy.contains(".t--datasource-name", datasourceName) + .find(queryLocators.createQuery) + .click(); + cy.get(queryLocators.templateMenu).click(); + cy.get(".CodeMirror textarea") + .first() + .focus() + .type('SELECT * FROM public."covidCases" LIMIT 10;'); + cy.wait(500); + cy.get(queryEditor.runQuery).click(); + cy.wait("@postExecute").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + cy.get(".t--add-widget").click(); + cy.SearchEntityandOpen("Table1"); + cy.isSelectRow(1); + cy.readTabledataPublish("1", "0").then(tabData => { + const tabValue = tabData; + cy.log("the value is" + tabValue); + expect(tabValue).to.be.equal("2020-01-23"); + }); + }); + + it("Input widget test with default value from table widget", () => { + cy.SearchEntityandOpen("Input1"); + cy.get(widgetsPage.defaultInput).type(testdata.addInputWidgetBinding); + cy.get(commonlocators.editPropCrossButton).click(); + cy.wait("@updateLayout").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + }); + + it("validation of data displayed in input widget based on row data selected", function() { + cy.isSelectRow(1); + cy.readTabledataPublish("1", "0").then(tabData => { + const tabValue = tabData; + cy.log("the value is" + tabValue); + expect(tabValue).to.be.equal("2020-01-23"); + cy.get(publish.inputWidget + " " + "input") + .first() + .invoke("attr", "value") + .should("contain", tabValue); + cy.get(publish.inputWidget + " " + "input") + .last() + .invoke("attr", "value") + .should("contain", tabValue); + }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/AddWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/AddWidget_spec.js index 4aa8a2605a..435f92c7f2 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/AddWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/AddWidget_spec.js @@ -22,16 +22,20 @@ describe("Add widget", function() { .first() .focus() .type("select * from configs"); - + cy.wait(500); cy.get(queryEditor.runQuery).click(); cy.wait("@postExecute").should( "have.nested.property", "response.body.responseMeta.status", 200, ); - cy.get(".t--add-widget").click(); - cy.SearchEntityandOpen("Table1"); + cy.isSelectRow(1); + cy.readTabledataPublish("1", "0").then(tabData => { + const tabValue = tabData; + expect(tabValue).to.be.equal("5"); + cy.log("the value is " + tabValue); + }); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js index e013ec788f..92a10fa831 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js @@ -2,6 +2,8 @@ const queryLocators = require("../../../locators/QueryEditor.json"); const plugins = require("../../../fixtures/plugins.json"); const datasource = require("../../../locators/DatasourcesEditor.json"); +let datasourceName; + describe("Create a query with a mongo datasource, run, save and then delete the query", function() { it("Create a query with a mongo datasource, run, save and then delete the query", function() { cy.NavigateToDatasourceEditor(); @@ -16,7 +18,7 @@ describe("Create a query with a mongo datasource, run, save and then delete the cy.NavigateToQueryEditor(); cy.get("@createDatasource").then(httpResponse => { - const datasourceName = httpResponse.response.body.data.name; + datasourceName = httpResponse.response.body.data.name; cy.contains(".t--datasource-name", datasourceName) .find(queryLocators.createQuery) @@ -38,19 +40,10 @@ describe("Create a query with a mongo datasource, run, save and then delete the cy.EvaluateCurrentValue(`{"find": "planets"}`); cy.runAndDeleteQuery(); - cy.NavigateToDatasourceEditor(); cy.get("@createDatasource").then(httpResponse => { - const datasourceName = httpResponse.response.body.data.name; + datasourceName = httpResponse.response.body.data.name; - cy.get(`.t--entity-name:contains(${datasourceName})`).click(); + cy.deletePostgresDatasource(datasourceName); }); - - cy.get(datasource.editDatasource).click(); - cy.get(".t--delete-datasource").click(); - cy.wait("@deleteDatasource").should( - "have.nested.property", - "response.body.responseMeta.status", - 200, - ); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js index f11014d18b..0d6ccc14a2 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js @@ -48,9 +48,10 @@ describe("Create a query with a postgres datasource, run, save and then delete t cy.runAndDeleteQuery(); }); it("Deletes a datasource", () => { - cy.NavigateToDatasourceEditor(); - cy.get(`.t--entity-name:contains(${datasourceName})`).click(); - cy.get(datasource.editDatasource).click(); + cy.NavigateToQueryEditor(); + cy.contains(".t--datasource-name", datasourceName) + .find(".t--edit-datasource") + .click(); cy.get(".t--delete-datasource").click(); cy.wait("@deleteDatasource").should( diff --git a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js index 1453b4b428..67ff5a2732 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js @@ -30,4 +30,20 @@ describe("Login from UI and check the functionality", function() { cy.wait(500); cy.url().should("include", "user/login"); }); + + it("Theme change test and validation", function() { + cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); + cy.get(homePage.profileMenu).click(); + cy.get(homePage.themeText).should("have.attr", "value", "true"); + cy.get("span") + .contains("Light") + .click({ force: true }); + cy.get(homePage.profileMenu).click(); + cy.get(homePage.themeText).should("have.attr", "value", "false"); + cy.get("span") + .contains("Dark") + .click({ force: true }); + cy.get(homePage.profileMenu).click(); + cy.get(homePage.themeText).should("have.attr", "value", "true"); + }); }); diff --git a/app/client/cypress/locators/ApiEditor.json b/app/client/cypress/locators/ApiEditor.json index 0ba7c8ca51..b5e9c1dd11 100644 --- a/app/client/cypress/locators/ApiEditor.json +++ b/app/client/cypress/locators/ApiEditor.json @@ -4,7 +4,7 @@ "createBlankApiCard": ".t--createBlankApiCard", "eachProviderCard": ".t--eachProviderCard", "nameOfApi": ".t--nameOfApi", - "ApiNameField": ".bp3-editable-text", + "ApiNameField": ".t--action-name-edit-field", "addToPageBtn": ".t--addToPageBtn", "ApiDeleteBtn": ".t--apiFormDeleteBtn", "ApiRunBtn": ".t--apiFormRunBtn", diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json index 9fd3710f5c..7360ed384c 100644 --- a/app/client/cypress/locators/DatasourcesEditor.json +++ b/app/client/cypress/locators/DatasourcesEditor.json @@ -13,7 +13,6 @@ "PostgreSQL": ".t--plugin-name:contains('PostgreSQL')", "sectionAuthentication": "[data-cy=section-Authentication]", "sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]", - "addDatasourceEntity": ".plugins .t--entity-add-btn", "PostgresEntity": ".t--entity-name:contains(PostgreSQL)", "createQuerty": ".t--create-query", "editDatasource": ".t--edit-datasource" diff --git a/app/client/cypress/locators/HomePage.json b/app/client/cypress/locators/HomePage.json index 4516962264..3a31c08531 100644 --- a/app/client/cypress/locators/HomePage.json +++ b/app/client/cypress/locators/HomePage.json @@ -45,10 +45,21 @@ "orgSection": "a:contains(", "createAppFrOrg": ") .t--create-app-popup", "shareApp": ".t--application-share-btn", - "enablePublicAccess": ".bp3-control-indicator", + "enablePublicAccess": ".slider", "closeBtn": ".bp3-dialog-close-button", "applicationName": ".t--application-name", "profileMenu": ".bp3-popover-wrapper.profile-menu", "signOutIcon": ".t--logout-icon", - "headerAppSmithLogo": ".t--Appsmith-logo-image" -} \ No newline at end of file + "headerAppSmithLogo": ".t--Appsmith-logo-image", + "applicationCardName": "[data-cy=t--app-card-name]", + "applicationIconSelector": ".t--icon-not-selected", + "applicationColorSelector": ".t--color-not-selected", + "applicationBackgroundColor": ".t--application-card-background", + "orgSettingOption": "[data-cy=t--org-setting]", + "orgNameInput": "[data-cy=t--org-name-input]", + "orgEmailInput": "[data-cy=t--org-email-input]", + "orgWebsiteInput": "[data-cy=t--org-website-input]", + "orgHeaderName": ".t--organization-header", + "leftPanelContainer": "[data-cy=t--left-panel]", + "themeText": "label div" +} diff --git a/app/client/cypress/locators/Pages.json b/app/client/cypress/locators/Pages.json index 8ef9748aa0..206e7b874c 100644 --- a/app/client/cypress/locators/Pages.json +++ b/app/client/cypress/locators/Pages.json @@ -5,7 +5,7 @@ "viewWidgets": ".t--page-sidebar-ViewWidgets", "widgetsEditor": ".t--nav-link-widgets-editor", "AddPage": ".pages .t--entity-add-btn", - "editInput": "input.bp3-editable-text-input", + "editInput": ".t--entity-name.editing", "Menuaction": ".bp3-overlay-open>.bp3-transition-container", "Delete": ":nth-child(2) > .bp3-menu-item", "apiEditorIcon": ".t--nav-link-api-editor", @@ -14,7 +14,7 @@ "entityTable": ".t--entity-name:contains('Table1')", "entityText": ".t--entity-name:contains('Text1')", "entityExplorer": ".t--nav-link-entity-explorer", - "popover": "//div[contains(@class,'t--entity page')]//*[local-name()='g' and @id='Icon/Outline/more-vertical']", + "popover": "//div[contains(@class,'t--entity page')]//*[last()]//*[local-name()='g' and @id='Icon/Outline/more-vertical']", "editName": ".single-select >div:contains('Edit Name')", "clonePage": ".single-select >div:contains('Clone')", "deletePage": ".single-select >div:contains('Delete')", diff --git a/app/client/cypress/locators/QueryEditor.json b/app/client/cypress/locators/QueryEditor.json index 64d78d0159..b5cb009914 100644 --- a/app/client/cypress/locators/QueryEditor.json +++ b/app/client/cypress/locators/QueryEditor.json @@ -5,5 +5,6 @@ "saveQuery": ".t--save-query", "deleteQuery": ".t--delete-query", "createQuery": ".t--create-query", - "addQueryEntity": ".//div[contains(@class,'t--entity group queries')]//div[contains(@class,'t--entity-add-btn')]" + "addQueryEntity": ".//div[contains(@class,'t--entity group queries')]//div[contains(@class,'t--entity-add-btn')]", + "addDatasource": ".t--add-datasource" } diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index e782a531d4..e44bb52a71 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -30,7 +30,7 @@ "labelTextStyle": ".bp3-ui-text span", "bodyTextStyle": ".bp3-running-text span", "headingTextStyle": ".bp3-heading span", - "editWidgetName": ".bp3-editable-text", + "editWidgetName": ".t--propery-page-title", "dropDownIcon": ".t--property-control-textstyle span.bp3-icon-chevron-down", "onDateSelectedField": ".t--property-control-ondateselected", "TableRow": ".t--draggable-tablewidget .tbody", diff --git a/app/client/cypress/locators/explorerlocators.json b/app/client/cypress/locators/explorerlocators.json index 938a57bfe0..23c42bc461 100644 --- a/app/client/cypress/locators/explorerlocators.json +++ b/app/client/cypress/locators/explorerlocators.json @@ -1,6 +1,6 @@ { "NoApiMsg": "p:contains('No APIs yet.')", - "NoQueryMsg": "p:contains('No Queries yet.')", + "NoQueryMsg": "p:contains('No DB Queries yet.')", "NoWidgetsMsg": "p:contains('No widgets yet.')", "AddPage": ".pages .t--entity-add-btn", "addEntityAPI": ".apis .t--entity-add-btn", @@ -24,5 +24,6 @@ "entity":".t--entity-name", "addWidget":".widgets .t--entity-add-btn", "dropHere":".appsmith_widget_0", - "closeWidgets":".t--close-widgets-sidebar" + "closeWidgets":".t--close-widgets-sidebar", + "addDBQueryEntity": ".dbqueries .t--entity-add-btn" } \ No newline at end of file diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index a747790c17..b32c1322ea 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -202,7 +202,7 @@ Cypress.Commands.add("launchApp", appName => { .first() .click(); cy.get("#loading").should("not.exist"); - cy.wait("@getPagesForApp").should( + cy.wait("@getPagesForViewApp").should( "have.nested.property", "response.body.responseMeta.status", 200, @@ -214,28 +214,37 @@ Cypress.Commands.add("CreateAppForOrg", (orgName, appname) => { .scrollIntoView() .should("be.visible") .click(); - cy.get(homePage.inputAppName).type(appname); - cy.get(homePage.CreateApp) - .contains("Submit") - .click({ force: true }); - cy.get("#loading").should("not.exist"); + cy.wait("@createNewApplication").should( + "have.nested.property", + "response.body.responseMeta.status", + 201, + ); + cy.wait(1000); + cy.get(homePage.applicationName).type(appname + "{enter}"); + cy.wait("@updateApplication").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); }); Cypress.Commands.add("CreateApp", appname => { cy.get(homePage.createNew) .first() .click({ force: true }); - cy.get(homePage.inputAppName).type(appname); - cy.get(homePage.CreateApp) - .contains("Submit") - .click({ force: true }); + cy.wait("@createNewApplication").should( + "have.nested.property", + "response.body.responseMeta.status", + 201, + ); cy.get("#loading").should("not.exist"); - cy.wait("@getPagesForApp").should( + cy.wait(1000); + cy.get(homePage.applicationName).type(appname + "{enter}"); + cy.wait("@updateApplication").should( "have.nested.property", "response.body.responseMeta.status", 200, ); - cy.get("h2").contains("Drag and drop a widget here"); }); Cypress.Commands.add("DeleteApp", appName => { @@ -301,7 +310,9 @@ Cypress.Commands.add("DeleteApp", appName => { cy.get(commonlocators.homeIcon).click({ force: true }); cy.get(homePage.searchInput).type(appName); cy.wait(2000); - cy.get(homePage.applicationCard).trigger("mouseover"); + cy.get(homePage.applicationCard) + .first() + .trigger("mouseover"); cy.get(homePage.appMoreIcon) .first() .click({ force: true }); @@ -552,9 +563,9 @@ Cypress.Commands.add("SearchEntityandOpen", apiname1 => { cy.get( commonlocators.entitySearchResult.concat(apiname1).concat("')"), ).should("be.visible"); - cy.get( - commonlocators.entitySearchResult.concat(apiname1).concat("')"), - ).click({ force: true }); + cy.get(commonlocators.entitySearchResult.concat(apiname1).concat("')")) + .last() + .click({ force: true }); }); Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => { @@ -723,7 +734,7 @@ Cypress.Commands.add("MoveAPIToPage", pageName => { cy.get(apiwidget.page) .contains(pageName) .click(); - cy.wait("@saveAction").should( + cy.wait("@moveAction").should( "have.nested.property", "response.body.responseMeta.status", 200, @@ -1304,11 +1315,14 @@ Cypress.Commands.add("importCurl", () => { }); Cypress.Commands.add("NavigateToDatasourceEditor", () => { - cy.get(datasourceEditor.addDatasourceEntity).click({ force: true }); + cy.get(explorer.addDBQueryEntity) + .last() + .click({ force: true }); + cy.get(queryEditor.addDatasource).click(); }); Cypress.Commands.add("NavigateToQueryEditor", () => { - cy.xpath(queryEditor.addQueryEntity).click({ force: true }); + cy.get(explorer.addDBQueryEntity).click({ force: true }); }); Cypress.Commands.add("testDatasource", () => { @@ -1390,9 +1404,11 @@ Cypress.Commands.add("createPostgresDatasource", () => { }); Cypress.Commands.add("deletePostgresDatasource", datasourceName => { - cy.NavigateToDatasourceEditor(); - cy.get(`.t--entity-name:contains(${datasourceName})`).click(); - cy.get(datasourceEditor.editDatasource).click(); + cy.NavigateToQueryEditor(); + + cy.contains(".t--datasource-name", datasourceName) + .find(".t--edit-datasource") + .click(); cy.get(".t--delete-datasource").click(); cy.wait("@deleteDatasource").should( "have.nested.property", @@ -1591,7 +1607,9 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("POST", "/api/v1/logout").as("postLogout"); cy.route("GET", "/api/v1/datasources").as("getDataSources"); - cy.route("GET", "/api/v1/pages/application/*").as("getPagesForApp"); + cy.route("GET", "/api/v1/pages/application/*").as("getPagesForCreateApp"); + cy.route("GET", "/api/v1/applications/view/*").as("getPagesForViewApp"); + cy.route("POST"); cy.route("GET", "/api/v1/pages/*").as("getPage"); cy.route("GET", "/api/v1/actions*").as("getActions"); @@ -1640,8 +1658,9 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("DELETE", "/api/v1/datasources/*").as("deleteDatasource"); cy.route("DELETE", "/api/v1/applications/*").as("deleteApplication"); cy.route("POST", "/api/v1/applications/?orgId=*").as("createNewApplication"); - cy.route("PUT", "/api/v1/applications/*").as("updateApplicationName"); + cy.route("PUT", "/api/v1/applications/*").as("updateApplication"); cy.route("PUT", "/api/v1/actions/*").as("saveAction"); + cy.route("PUT", "/api/v1/actions/move").as("moveAction"); cy.route("POST", "/api/v1/organizations").as("createOrg"); cy.route("POST", "/api/v1/users/invite").as("postInvite"); @@ -1652,6 +1671,9 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("POST", "/api/v1/pages").as("createPage"); cy.route("POST", "/api/v1/pages/clone/*").as("clonePage"); cy.route("PUT", "/api/v1/applications/*/changeAccess").as("changeAccess"); + + cy.route("PUT", "/api/v1/organizations/*").as("updateOrganization"); + cy.route("GET", "/api/v1/pages/view/application/*").as("viewApp"); }); Cypress.Commands.add("alertValidate", text => { @@ -1777,15 +1799,3 @@ Cypress.Commands.add("callApi", apiname => { Cypress.Commands.add("assertPageSave", () => { cy.get(commonlocators.saveStatusSuccess); }); - -Cypress.Commands.add("EditApp", appName => { - cy.get(homePage.searchInput).type(appName); - cy.wait(2000); - cy.get(homePage.applicationCard) - .first() - .trigger("mouseover"); - cy.get(homePage.appEditIcon) - .first() - .click({ force: true }); - cy.get("#loading").should("not.exist"); -}); diff --git a/app/client/docker/templates/nginx-mac.conf.template b/app/client/docker/templates/nginx-mac.conf.template index 5ebbe8481e..e36919c4e8 100644 --- a/app/client/docker/templates/nginx-mac.conf.template +++ b/app/client/docker/templates/nginx-mac.conf.template @@ -46,21 +46,21 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass https://release-api.appsmith.com; + proxy_pass http://host.docker.internal:8080; } location /oauth2 { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass https://release-api.appsmith.com; + proxy_pass http://host.docker.internal:8080; } location /login { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass https://release-api.appsmith.com; + proxy_pass http://host.docker.internal:8080; } } @@ -114,20 +114,20 @@ server { location /api { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass https://release-api.appsmith.com; + proxy_pass http://host.docker.internal:8080; } location /oauth2 { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass https://release-api.appsmith.com; + proxy_pass http://host.docker.internal:8080; } location /login { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; - proxy_pass https://release-api.appsmith.com; + proxy_pass http://host.docker.internal:8080; } } diff --git a/app/client/package.json b/app/client/package.json index ae1cb29ab7..b766d9d8fe 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -102,6 +102,7 @@ "react-toastify": "^5.5.0", "react-transition-group": "^4.3.0", "react-use-gesture": "^7.0.4", + "react-zoom-pan-pinch": "^1.6.1", "redux": "^4.0.1", "redux-form": "^8.2.6", "redux-saga": "^1.1.3", diff --git a/app/client/src/AppRouter.tsx b/app/client/src/AppRouter.tsx index 2b9daf0615..fb0bd5ef32 100644 --- a/app/client/src/AppRouter.tsx +++ b/app/client/src/AppRouter.tsx @@ -36,13 +36,15 @@ import { connect } from "react-redux"; import * as Sentry from "@sentry/react"; import AnalyticsUtil from "utils/AnalyticsUtil"; +import { trimTrailingSlash } from "utils/helpers"; + const SentryRoute = Sentry.withSentryRouting(Route); const loadingIndicator = ; function changeAppBackground(currentTheme: any) { if ( - window.location.pathname === "/applications" || + trimTrailingSlash(window.location.pathname) === "/applications" || window.location.pathname.indexOf("/settings/") !== -1 ) { document.body.style.backgroundColor = diff --git a/app/client/src/actions/applicationActions.ts b/app/client/src/actions/applicationActions.ts index 8afecb4024..9bfdd46847 100644 --- a/app/client/src/actions/applicationActions.ts +++ b/app/client/src/actions/applicationActions.ts @@ -1,4 +1,6 @@ -import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; +import { EditorModes } from "../components/editorComponents/CodeEditor/EditorConfig"; +import { APP_MODE } from "../reducers/entityReducers/appReducer"; import { UpdateApplicationPayload } from "api/ApplicationApi"; export const setDefaultApplicationPageSuccess = ( @@ -20,6 +22,24 @@ export const fetchApplications = () => { }; }; +export interface FetchApplicationPayload { + applicationId: string; + mode: APP_MODE; +} + +export const fetchApplication = ( + applicationId: string, + mode: APP_MODE, +): ReduxAction => { + return { + type: ReduxActionTypes.FETCH_APPLICATION_INIT, + payload: { + applicationId, + mode, + }, + }; +}; + export const updateApplication = ( id: string, data: UpdateApplicationPayload, @@ -33,15 +53,6 @@ export const updateApplication = ( }; }; -export const fetchApplication = (applicationId: string) => { - return { - type: ReduxActionTypes.FETCH_APPLICATION_INIT, - payload: { - applicationId, - }, - }; -}; - export const publishApplication = (applicationId: string) => { return { type: ReduxActionTypes.PUBLISH_APPLICATION_INIT, diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index 465be95758..f240ce802f 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -2,23 +2,29 @@ import { FetchPageRequest, SavePageResponse } from "api/PageApi"; import { WidgetOperation, WidgetProps } from "widgets/BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; import { - ReduxActionTypes, ReduxAction, + ReduxActionTypes, UpdateCanvasPayload, - FetchPageListPayload, } from "constants/ReduxActionConstants"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { ContainerWidgetProps } from "widgets/ContainerWidget"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { APP_MODE, UrlDataState } from "reducers/entityReducers/appReducer"; +export interface FetchPageListPayload { + applicationId: string; + mode: APP_MODE; +} + export const fetchPageList = ( applicationId: string, + mode: APP_MODE, ): ReduxAction => { return { type: ReduxActionTypes.FETCH_PAGE_LIST_INIT, payload: { applicationId, + mode, }, }; }; diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index 085eb768a7..5c353cc8c5 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -44,13 +44,10 @@ export interface ActionCreateUpdateResponse extends ApiResponse { export type PaginationField = "PREV" | "NEXT"; export interface ExecuteActionRequest extends APIRequest { - action: Pick | Omit; + actionId: string; params?: Property[]; paginationField?: PaginationField; -} - -export interface ExecuteQueryRequest extends APIRequest { - action: Pick | Omit; + viewMode: boolean; } export interface ExecuteActionResponse extends ApiResponse { diff --git a/app/client/src/api/ApplicationApi.tsx b/app/client/src/api/ApplicationApi.tsx index 82d8486590..2c8ccc69c4 100644 --- a/app/client/src/api/ApplicationApi.tsx +++ b/app/client/src/api/ApplicationApi.tsx @@ -129,6 +129,12 @@ class ApplicationApi extends Api { return Api.get(ApplicationApi.baseURL + applicationId); } + static fetchApplicationForViewMode( + applicationId: string, + ): AxiosPromise { + return Api.get(ApplicationApi.baseURL + `view/${applicationId}`); + } + static createApplication( request: CreateApplicationRequest, ): AxiosPromise { diff --git a/app/client/src/api/PageApi.tsx b/app/client/src/api/PageApi.tsx index 51f651b92e..c0e05681de 100644 --- a/app/client/src/api/PageApi.tsx +++ b/app/client/src/api/PageApi.tsx @@ -161,6 +161,12 @@ class PageApi extends Api { return Api.get(PageApi.url + "/application/" + applicationId); } + static fetchPageListViewMode( + applicationId: string, + ): AxiosPromise { + return Api.get(PageApi.url + "/view/application/" + applicationId); + } + static deletePage(request: DeletePageRequest): AxiosPromise { return Api.delete(PageApi.url + "/" + request.id); } diff --git a/app/client/src/assets/icons/ads/bag.svg b/app/client/src/assets/icons/ads/bag.svg index 04096a467d..8ce3bef7ce 100644 --- a/app/client/src/assets/icons/ads/bag.svg +++ b/app/client/src/assets/icons/ads/bag.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/billing.svg b/app/client/src/assets/icons/ads/billing.svg index daf4b246e5..99d809fa80 100644 --- a/app/client/src/assets/icons/ads/billing.svg +++ b/app/client/src/assets/icons/ads/billing.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/book.svg b/app/client/src/assets/icons/ads/book.svg index f016135fcd..51821b49d1 100644 --- a/app/client/src/assets/icons/ads/book.svg +++ b/app/client/src/assets/icons/ads/book.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/calender.svg b/app/client/src/assets/icons/ads/calender.svg index 8be12ddf06..9177c399f4 100644 --- a/app/client/src/assets/icons/ads/calender.svg +++ b/app/client/src/assets/icons/ads/calender.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/camera.svg b/app/client/src/assets/icons/ads/camera.svg index d1b23590c0..15c623c91f 100644 --- a/app/client/src/assets/icons/ads/camera.svg +++ b/app/client/src/assets/icons/ads/camera.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/chat.svg b/app/client/src/assets/icons/ads/chat.svg index 6fbbcfc754..13920a93e3 100644 --- a/app/client/src/assets/icons/ads/chat.svg +++ b/app/client/src/assets/icons/ads/chat.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/close.svg b/app/client/src/assets/icons/ads/close.svg index afaca4c528..0575beb7f9 100644 --- a/app/client/src/assets/icons/ads/close.svg +++ b/app/client/src/assets/icons/ads/close.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/context-menu.svg b/app/client/src/assets/icons/ads/context-menu.svg index 5c50eab87d..1fda6ea2b9 100644 --- a/app/client/src/assets/icons/ads/context-menu.svg +++ b/app/client/src/assets/icons/ads/context-menu.svg @@ -1,14 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/create-new.svg b/app/client/src/assets/icons/ads/create-new.svg index def6849ebf..9264739e83 100644 --- a/app/client/src/assets/icons/ads/create-new.svg +++ b/app/client/src/assets/icons/ads/create-new.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/delete.svg b/app/client/src/assets/icons/ads/delete.svg index a39c50e042..d1e8c04c04 100644 --- a/app/client/src/assets/icons/ads/delete.svg +++ b/app/client/src/assets/icons/ads/delete.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/down_arrow.svg b/app/client/src/assets/icons/ads/down_arrow.svg index bf868b04ee..098793c09e 100644 --- a/app/client/src/assets/icons/ads/down_arrow.svg +++ b/app/client/src/assets/icons/ads/down_arrow.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/duplicate.svg b/app/client/src/assets/icons/ads/duplicate.svg index 0c31bfc467..0488d31d56 100644 --- a/app/client/src/assets/icons/ads/duplicate.svg +++ b/app/client/src/assets/icons/ads/duplicate.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/edit.svg b/app/client/src/assets/icons/ads/edit.svg index 93eaaa6946..5956addc8e 100644 --- a/app/client/src/assets/icons/ads/edit.svg +++ b/app/client/src/assets/icons/ads/edit.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/error.svg b/app/client/src/assets/icons/ads/error.svg index 43f866b350..b55ff30df6 100644 --- a/app/client/src/assets/icons/ads/error.svg +++ b/app/client/src/assets/icons/ads/error.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/file.svg b/app/client/src/assets/icons/ads/file.svg index 4e83e5a8ae..be191cc8d3 100644 --- a/app/client/src/assets/icons/ads/file.svg +++ b/app/client/src/assets/icons/ads/file.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/flight.svg b/app/client/src/assets/icons/ads/flight.svg index 0a6daa90ab..9374e31d0d 100644 --- a/app/client/src/assets/icons/ads/flight.svg +++ b/app/client/src/assets/icons/ads/flight.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/frame.svg b/app/client/src/assets/icons/ads/frame.svg index 3bd9d4a502..372560ab88 100644 --- a/app/client/src/assets/icons/ads/frame.svg +++ b/app/client/src/assets/icons/ads/frame.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/general.svg b/app/client/src/assets/icons/ads/general.svg index 6204dd6c96..f99736c21e 100644 --- a/app/client/src/assets/icons/ads/general.svg +++ b/app/client/src/assets/icons/ads/general.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/globe.svg b/app/client/src/assets/icons/ads/globe.svg index 228290a5d6..2ca63fec36 100644 --- a/app/client/src/assets/icons/ads/globe.svg +++ b/app/client/src/assets/icons/ads/globe.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/heart.svg b/app/client/src/assets/icons/ads/heart.svg index 372367768d..1587bfbb99 100644 --- a/app/client/src/assets/icons/ads/heart.svg +++ b/app/client/src/assets/icons/ads/heart.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/invite-users.svg b/app/client/src/assets/icons/ads/invite-users.svg index 4b0b6cd9a0..4236df2b7f 100644 --- a/app/client/src/assets/icons/ads/invite-users.svg +++ b/app/client/src/assets/icons/ads/invite-users.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/launch.svg b/app/client/src/assets/icons/ads/launch.svg index 83d1bd28fb..75b837aad9 100644 --- a/app/client/src/assets/icons/ads/launch.svg +++ b/app/client/src/assets/icons/ads/launch.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/logout.svg b/app/client/src/assets/icons/ads/logout.svg index f301b2b502..ca6ba7a170 100644 --- a/app/client/src/assets/icons/ads/logout.svg +++ b/app/client/src/assets/icons/ads/logout.svg @@ -1,15 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/manage.svg b/app/client/src/assets/icons/ads/manage.svg index 27ba5fc70a..6e50631a1a 100644 --- a/app/client/src/assets/icons/ads/manage.svg +++ b/app/client/src/assets/icons/ads/manage.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/product.svg b/app/client/src/assets/icons/ads/product.svg index 9237160df7..02cf6f70df 100644 --- a/app/client/src/assets/icons/ads/product.svg +++ b/app/client/src/assets/icons/ads/product.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/search.svg b/app/client/src/assets/icons/ads/search.svg index e3e23f0e53..fd0abc79ae 100644 --- a/app/client/src/assets/icons/ads/search.svg +++ b/app/client/src/assets/icons/ads/search.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/share.svg b/app/client/src/assets/icons/ads/share.svg index 8db07e7a08..35fd9ca343 100644 --- a/app/client/src/assets/icons/ads/share.svg +++ b/app/client/src/assets/icons/ads/share.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/shopper.svg b/app/client/src/assets/icons/ads/shopper.svg index e8deb2e930..1a7b7eea3b 100644 --- a/app/client/src/assets/icons/ads/shopper.svg +++ b/app/client/src/assets/icons/ads/shopper.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/success.svg b/app/client/src/assets/icons/ads/success.svg index c5c3d40e41..db032c423d 100644 --- a/app/client/src/assets/icons/ads/success.svg +++ b/app/client/src/assets/icons/ads/success.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/upper_arrow.svg b/app/client/src/assets/icons/ads/upper_arrow.svg index 022072bf4a..15e203070a 100644 --- a/app/client/src/assets/icons/ads/upper_arrow.svg +++ b/app/client/src/assets/icons/ads/upper_arrow.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/user.svg b/app/client/src/assets/icons/ads/user.svg index e9e4b45e3d..d01937d7f2 100644 --- a/app/client/src/assets/icons/ads/user.svg +++ b/app/client/src/assets/icons/ads/user.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/view-all.svg b/app/client/src/assets/icons/ads/view-all.svg index 0689c29ced..f2edf62f0d 100644 --- a/app/client/src/assets/icons/ads/view-all.svg +++ b/app/client/src/assets/icons/ads/view-all.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/ads/workspace.svg b/app/client/src/assets/icons/ads/workspace.svg index 0c314d51fb..76b6cccd67 100644 --- a/app/client/src/assets/icons/ads/workspace.svg +++ b/app/client/src/assets/icons/ads/workspace.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/alert/error.svg b/app/client/src/assets/icons/alert/error.svg index addd38233f..ff3581b77b 100644 --- a/app/client/src/assets/icons/alert/error.svg +++ b/app/client/src/assets/icons/alert/error.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/alert/info.svg b/app/client/src/assets/icons/alert/info.svg index 145547a06f..3a608999b3 100644 --- a/app/client/src/assets/icons/alert/info.svg +++ b/app/client/src/assets/icons/alert/info.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/alert/success.svg b/app/client/src/assets/icons/alert/success.svg index c3221e7b06..3df2b9b839 100644 --- a/app/client/src/assets/icons/alert/success.svg +++ b/app/client/src/assets/icons/alert/success.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/alert/warning.svg b/app/client/src/assets/icons/alert/warning.svg index 6887ec5239..8ef6d43960 100644 --- a/app/client/src/assets/icons/alert/warning.svg +++ b/app/client/src/assets/icons/alert/warning.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/address.svg b/app/client/src/assets/icons/control/address.svg index 21acef28d8..cb38ae84af 100644 --- a/app/client/src/assets/icons/control/address.svg +++ b/app/client/src/assets/icons/control/address.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/bold.svg b/app/client/src/assets/icons/control/bold.svg index f3e136da0f..6a5ea86324 100644 --- a/app/client/src/assets/icons/control/bold.svg +++ b/app/client/src/assets/icons/control/bold.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/center-align.svg b/app/client/src/assets/icons/control/center-align.svg index 34e6f1a462..5071e005fc 100644 --- a/app/client/src/assets/icons/control/center-align.svg +++ b/app/client/src/assets/icons/control/center-align.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/chevron-down.svg b/app/client/src/assets/icons/control/chevron-down.svg index 783082577d..612b83e1e0 100644 --- a/app/client/src/assets/icons/control/chevron-down.svg +++ b/app/client/src/assets/icons/control/chevron-down.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/close.svg b/app/client/src/assets/icons/control/close.svg index aaeccc056d..c36a21cd8f 100644 --- a/app/client/src/assets/icons/control/close.svg +++ b/app/client/src/assets/icons/control/close.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/collapse.svg b/app/client/src/assets/icons/control/collapse.svg index fc7b106736..3eb84e3cee 100644 --- a/app/client/src/assets/icons/control/collapse.svg +++ b/app/client/src/assets/icons/control/collapse.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/columns-visibility.svg b/app/client/src/assets/icons/control/columns-visibility.svg index a44a40d859..78c503e371 100755 --- a/app/client/src/assets/icons/control/columns-visibility.svg +++ b/app/client/src/assets/icons/control/columns-visibility.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/compact.svg b/app/client/src/assets/icons/control/compact.svg index 8cbefd7126..82a827774e 100755 --- a/app/client/src/assets/icons/control/compact.svg +++ b/app/client/src/assets/icons/control/compact.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/copy.svg b/app/client/src/assets/icons/control/copy.svg index e9536f2cda..4af7dfe517 100644 --- a/app/client/src/assets/icons/control/copy.svg +++ b/app/client/src/assets/icons/control/copy.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/currency.svg b/app/client/src/assets/icons/control/currency.svg index 2115953d27..9a6b2a0402 100644 --- a/app/client/src/assets/icons/control/currency.svg +++ b/app/client/src/assets/icons/control/currency.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/datepicker.svg b/app/client/src/assets/icons/control/datepicker.svg index 959f2763c8..4bfed6bf77 100644 --- a/app/client/src/assets/icons/control/datepicker.svg +++ b/app/client/src/assets/icons/control/datepicker.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/decimal.svg b/app/client/src/assets/icons/control/decimal.svg index a6adb8fcb5..2d12715170 100644 --- a/app/client/src/assets/icons/control/decimal.svg +++ b/app/client/src/assets/icons/control/decimal.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/decrease.svg b/app/client/src/assets/icons/control/decrease.svg index f8ffdccb6e..c48db3214e 100644 --- a/app/client/src/assets/icons/control/decrease.svg +++ b/app/client/src/assets/icons/control/decrease.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/delete.svg b/app/client/src/assets/icons/control/delete.svg index 273eb462b8..d3783c6540 100644 --- a/app/client/src/assets/icons/control/delete.svg +++ b/app/client/src/assets/icons/control/delete.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/download-table.svg b/app/client/src/assets/icons/control/download-table.svg index cea728e494..129ee6b5ad 100755 --- a/app/client/src/assets/icons/control/download-table.svg +++ b/app/client/src/assets/icons/control/download-table.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/drag.svg b/app/client/src/assets/icons/control/drag.svg index a432baa4a2..92df55d4e5 100644 --- a/app/client/src/assets/icons/control/drag.svg +++ b/app/client/src/assets/icons/control/drag.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/draggable.svg b/app/client/src/assets/icons/control/draggable.svg index 4be5a7b6d6..b7e2354e33 100644 --- a/app/client/src/assets/icons/control/draggable.svg +++ b/app/client/src/assets/icons/control/draggable.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/edit-white.svg b/app/client/src/assets/icons/control/edit-white.svg index 12b10c0305..234ebcbf9a 100644 --- a/app/client/src/assets/icons/control/edit-white.svg +++ b/app/client/src/assets/icons/control/edit-white.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/edit.svg b/app/client/src/assets/icons/control/edit.svg index f2ec080903..e70ce4c6d1 100644 --- a/app/client/src/assets/icons/control/edit.svg +++ b/app/client/src/assets/icons/control/edit.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/email.svg b/app/client/src/assets/icons/control/email.svg index 16d7748e2e..59dad51c38 100644 --- a/app/client/src/assets/icons/control/email.svg +++ b/app/client/src/assets/icons/control/email.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/filter-icon.svg b/app/client/src/assets/icons/control/filter-icon.svg index 0e1b16406a..1723d3efe8 100755 --- a/app/client/src/assets/icons/control/filter-icon.svg +++ b/app/client/src/assets/icons/control/filter-icon.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/help.svg b/app/client/src/assets/icons/control/help.svg index b2986f1fb7..1dbe805af1 100644 --- a/app/client/src/assets/icons/control/help.svg +++ b/app/client/src/assets/icons/control/help.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/increase.svg b/app/client/src/assets/icons/control/increase.svg index e20986e3e8..db795e36ad 100644 --- a/app/client/src/assets/icons/control/increase.svg +++ b/app/client/src/assets/icons/control/increase.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/info.svg b/app/client/src/assets/icons/control/info.svg index 300bcd9a06..95aa0a81ae 100644 --- a/app/client/src/assets/icons/control/info.svg +++ b/app/client/src/assets/icons/control/info.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/input.svg b/app/client/src/assets/icons/control/input.svg index aeac1584b3..ad382f9790 100644 --- a/app/client/src/assets/icons/control/input.svg +++ b/app/client/src/assets/icons/control/input.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/integer.svg b/app/client/src/assets/icons/control/integer.svg index cd3e6a56c7..25969f3b1a 100644 --- a/app/client/src/assets/icons/control/integer.svg +++ b/app/client/src/assets/icons/control/integer.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/italics.svg b/app/client/src/assets/icons/control/italics.svg index 7ae3bbba66..51c317d687 100644 --- a/app/client/src/assets/icons/control/italics.svg +++ b/app/client/src/assets/icons/control/italics.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/js-toggle.svg b/app/client/src/assets/icons/control/js-toggle.svg index 616e8556f6..2698942924 100644 --- a/app/client/src/assets/icons/control/js-toggle.svg +++ b/app/client/src/assets/icons/control/js-toggle.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/launch.svg b/app/client/src/assets/icons/control/launch.svg index 09e2d1ce7a..74e7c04904 100644 --- a/app/client/src/assets/icons/control/launch.svg +++ b/app/client/src/assets/icons/control/launch.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/left-align.svg b/app/client/src/assets/icons/control/left-align.svg index 94eb13e628..d5c3b826c5 100644 --- a/app/client/src/assets/icons/control/left-align.svg +++ b/app/client/src/assets/icons/control/left-align.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/lightning.svg b/app/client/src/assets/icons/control/lightning.svg index 2c56892354..e611c04972 100644 --- a/app/client/src/assets/icons/control/lightning.svg +++ b/app/client/src/assets/icons/control/lightning.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/more-vertical.svg b/app/client/src/assets/icons/control/more-vertical.svg index e9a7600622..b8b10537a1 100755 --- a/app/client/src/assets/icons/control/more-vertical.svg +++ b/app/client/src/assets/icons/control/more-vertical.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/move.svg b/app/client/src/assets/icons/control/move.svg index 3cd36f1c26..9dce6c62a7 100644 --- a/app/client/src/assets/icons/control/move.svg +++ b/app/client/src/assets/icons/control/move.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/multiline.svg b/app/client/src/assets/icons/control/multiline.svg index 220bfad216..ba7bb1e4c1 100644 --- a/app/client/src/assets/icons/control/multiline.svg +++ b/app/client/src/assets/icons/control/multiline.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/password.svg b/app/client/src/assets/icons/control/password.svg index ad542c6f95..958ea5f324 100644 --- a/app/client/src/assets/icons/control/password.svg +++ b/app/client/src/assets/icons/control/password.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/phone.svg b/app/client/src/assets/icons/control/phone.svg index 33fc877272..8d43cad519 100644 --- a/app/client/src/assets/icons/control/phone.svg +++ b/app/client/src/assets/icons/control/phone.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/pick-location-initial.svg b/app/client/src/assets/icons/control/pick-location-initial.svg index 2572dd77b9..adc4d22906 100644 --- a/app/client/src/assets/icons/control/pick-location-initial.svg +++ b/app/client/src/assets/icons/control/pick-location-initial.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/pick-location-onclick.svg b/app/client/src/assets/icons/control/pick-location-onclick.svg index f8fb48fbe5..24e2db51f9 100644 --- a/app/client/src/assets/icons/control/pick-location-onclick.svg +++ b/app/client/src/assets/icons/control/pick-location-onclick.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/pick-location-selected.svg b/app/client/src/assets/icons/control/pick-location-selected.svg index 29d9c89835..edf7da2f08 100644 --- a/app/client/src/assets/icons/control/pick-location-selected.svg +++ b/app/client/src/assets/icons/control/pick-location-selected.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/pick-my-location.svg b/app/client/src/assets/icons/control/pick-my-location.svg index e519a17dd3..8655546aa2 100644 --- a/app/client/src/assets/icons/control/pick-my-location.svg +++ b/app/client/src/assets/icons/control/pick-my-location.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/play-icon.png b/app/client/src/assets/icons/control/play-icon.png index 52ddde9e4f..48ca301f33 100644 Binary files a/app/client/src/assets/icons/control/play-icon.png and b/app/client/src/assets/icons/control/play-icon.png differ diff --git a/app/client/src/assets/icons/control/redo.svg b/app/client/src/assets/icons/control/redo.svg index 71bfe8be4e..77c7dae9e9 100644 --- a/app/client/src/assets/icons/control/redo.svg +++ b/app/client/src/assets/icons/control/redo.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/remove.svg b/app/client/src/assets/icons/control/remove.svg index b3db7ea3ce..14e58f2cc5 100755 --- a/app/client/src/assets/icons/control/remove.svg +++ b/app/client/src/assets/icons/control/remove.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/right-align.svg b/app/client/src/assets/icons/control/right-align.svg index 967eb8d1fb..f62b86260a 100644 --- a/app/client/src/assets/icons/control/right-align.svg +++ b/app/client/src/assets/icons/control/right-align.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/search.svg b/app/client/src/assets/icons/control/search.svg index 3aa4f00565..364ae959ce 100644 --- a/app/client/src/assets/icons/control/search.svg +++ b/app/client/src/assets/icons/control/search.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/settings.svg b/app/client/src/assets/icons/control/settings.svg index 5bc7daef54..e6a03bf534 100644 --- a/app/client/src/assets/icons/control/settings.svg +++ b/app/client/src/assets/icons/control/settings.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/sort-icon.svg b/app/client/src/assets/icons/control/sort-icon.svg index f1862442de..02f54b7846 100755 --- a/app/client/src/assets/icons/control/sort-icon.svg +++ b/app/client/src/assets/icons/control/sort-icon.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/underline.svg b/app/client/src/assets/icons/control/underline.svg index 12a393304a..8447bfc39d 100644 --- a/app/client/src/assets/icons/control/underline.svg +++ b/app/client/src/assets/icons/control/underline.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/undo.svg b/app/client/src/assets/icons/control/undo.svg index 2ec8e0fb7c..e7b58feae4 100644 --- a/app/client/src/assets/icons/control/undo.svg +++ b/app/client/src/assets/icons/control/undo.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/view.svg b/app/client/src/assets/icons/control/view.svg index 66010d6eed..90d61a745b 100644 --- a/app/client/src/assets/icons/control/view.svg +++ b/app/client/src/assets/icons/control/view.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/zoomin.svg b/app/client/src/assets/icons/control/zoomin.svg index baa27c369d..2af30e2b17 100644 --- a/app/client/src/assets/icons/control/zoomin.svg +++ b/app/client/src/assets/icons/control/zoomin.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/control/zoomout.svg b/app/client/src/assets/icons/control/zoomout.svg index aceffa4ff6..6f1fb70136 100644 --- a/app/client/src/assets/icons/control/zoomout.svg +++ b/app/client/src/assets/icons/control/zoomout.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/form/add-new.svg b/app/client/src/assets/icons/form/add-new.svg index 6edbd99663..2b36469eb0 100644 --- a/app/client/src/assets/icons/form/add-new.svg +++ b/app/client/src/assets/icons/form/add-new.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/form/info-outline.svg b/app/client/src/assets/icons/form/info-outline.svg index 0cfe7d23e0..0782dabbbe 100644 --- a/app/client/src/assets/icons/form/info-outline.svg +++ b/app/client/src/assets/icons/form/info-outline.svg @@ -1,13 +1 @@ - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/form/lock.svg b/app/client/src/assets/icons/form/lock.svg new file mode 100644 index 0000000000..eba05a3753 --- /dev/null +++ b/app/client/src/assets/icons/form/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/assets/icons/form/trash.svg b/app/client/src/assets/icons/form/trash.svg index 04a04629f3..6ae5e921f8 100755 --- a/app/client/src/assets/icons/form/trash.svg +++ b/app/client/src/assets/icons/form/trash.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/header/deploy.svg b/app/client/src/assets/icons/header/deploy.svg index faa203e316..37bdd9d812 100644 --- a/app/client/src/assets/icons/header/deploy.svg +++ b/app/client/src/assets/icons/header/deploy.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/header/feedback.svg b/app/client/src/assets/icons/header/feedback.svg index 4eba24b0c4..bfb512b8c3 100644 --- a/app/client/src/assets/icons/header/feedback.svg +++ b/app/client/src/assets/icons/header/feedback.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/header/save-failure.svg b/app/client/src/assets/icons/header/save-failure.svg index d1d8ba1915..b6210197ea 100644 --- a/app/client/src/assets/icons/header/save-failure.svg +++ b/app/client/src/assets/icons/header/save-failure.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/header/save-loading.gif b/app/client/src/assets/icons/header/save-loading.gif index b7cfcf3cab..504e82ba8d 100644 Binary files a/app/client/src/assets/icons/header/save-loading.gif and b/app/client/src/assets/icons/header/save-loading.gif differ diff --git a/app/client/src/assets/icons/header/save-success.svg b/app/client/src/assets/icons/header/save-success.svg index a6b536bbe9..340e58cf32 100644 --- a/app/client/src/assets/icons/header/save-success.svg +++ b/app/client/src/assets/icons/header/save-success.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/header/share-white.svg b/app/client/src/assets/icons/header/share-white.svg index c967dc8d12..f90ae1bb14 100644 --- a/app/client/src/assets/icons/header/share-white.svg +++ b/app/client/src/assets/icons/header/share-white.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/app/client/src/assets/icons/help/discord.svg b/app/client/src/assets/icons/help/discord.svg index 4613aa9ae8..ee334bbc7e 100644 --- a/app/client/src/assets/icons/help/discord.svg +++ b/app/client/src/assets/icons/help/discord.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/client/src/assets/icons/help/document.svg b/app/client/src/assets/icons/help/document.svg index e0ba8bc5cb..bf8d13cf1e 100644 --- a/app/client/src/assets/icons/help/document.svg +++ b/app/client/src/assets/icons/help/document.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/help/github-icon.svg b/app/client/src/assets/icons/help/github-icon.svg index 3f18b45241..7126204ff8 100644 --- a/app/client/src/assets/icons/help/github-icon.svg +++ b/app/client/src/assets/icons/help/github-icon.svg @@ -1,27 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/help/help.svg b/app/client/src/assets/icons/help/help.svg index e041a79ee3..46d0b764c4 100644 --- a/app/client/src/assets/icons/help/help.svg +++ b/app/client/src/assets/icons/help/help.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/help/openlink.svg b/app/client/src/assets/icons/help/openlink.svg index 972fe06226..4b64b21ed3 100644 --- a/app/client/src/assets/icons/help/openlink.svg +++ b/app/client/src/assets/icons/help/openlink.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/api-colored.svg b/app/client/src/assets/icons/menu/api-colored.svg index 6d202ff595..939235b6b1 100644 --- a/app/client/src/assets/icons/menu/api-colored.svg +++ b/app/client/src/assets/icons/menu/api-colored.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/api.svg b/app/client/src/assets/icons/menu/api.svg index 78dcf2a32f..a81046b50d 100644 --- a/app/client/src/assets/icons/menu/api.svg +++ b/app/client/src/assets/icons/menu/api.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/data-sources.svg b/app/client/src/assets/icons/menu/data-sources.svg index ce5d38c669..63cc842f80 100644 --- a/app/client/src/assets/icons/menu/data-sources.svg +++ b/app/client/src/assets/icons/menu/data-sources.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/datasource-colored.svg b/app/client/src/assets/icons/menu/datasource-colored.svg index f779edcee5..ae022d43fc 100644 --- a/app/client/src/assets/icons/menu/datasource-colored.svg +++ b/app/client/src/assets/icons/menu/datasource-colored.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/datasource-column.svg b/app/client/src/assets/icons/menu/datasource-column.svg index da70f69a9d..695d40a98c 100644 --- a/app/client/src/assets/icons/menu/datasource-column.svg +++ b/app/client/src/assets/icons/menu/datasource-column.svg @@ -1,11 +1 @@ - - - Column - - - - - - - - \ No newline at end of file +Column \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/datasource-table.svg b/app/client/src/assets/icons/menu/datasource-table.svg index 76c3e9bf9b..a3c30a69fb 100644 --- a/app/client/src/assets/icons/menu/datasource-table.svg +++ b/app/client/src/assets/icons/menu/datasource-table.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/explorer.svg b/app/client/src/assets/icons/menu/explorer.svg index af233a1ee9..1284f5ad5f 100644 --- a/app/client/src/assets/icons/menu/explorer.svg +++ b/app/client/src/assets/icons/menu/explorer.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/foreign-key.svg b/app/client/src/assets/icons/menu/foreign-key.svg index 786add7c56..7003efae12 100644 --- a/app/client/src/assets/icons/menu/foreign-key.svg +++ b/app/client/src/assets/icons/menu/foreign-key.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/homepage.svg b/app/client/src/assets/icons/menu/homepage.svg index 2c34a49d48..0451341726 100644 --- a/app/client/src/assets/icons/menu/homepage.svg +++ b/app/client/src/assets/icons/menu/homepage.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/org.svg b/app/client/src/assets/icons/menu/org.svg index fcfd90d8e0..a644fb6d78 100644 --- a/app/client/src/assets/icons/menu/org.svg +++ b/app/client/src/assets/icons/menu/org.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/overflow-menu.svg b/app/client/src/assets/icons/menu/overflow-menu.svg index 1e1ffd2d40..3d9237e06c 100644 --- a/app/client/src/assets/icons/menu/overflow-menu.svg +++ b/app/client/src/assets/icons/menu/overflow-menu.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/page.svg b/app/client/src/assets/icons/menu/page.svg index dc184d19ce..5ad6c9eab1 100644 --- a/app/client/src/assets/icons/menu/page.svg +++ b/app/client/src/assets/icons/menu/page.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/pages.svg b/app/client/src/assets/icons/menu/pages.svg index fcafce2c0b..95114f63d8 100644 --- a/app/client/src/assets/icons/menu/pages.svg +++ b/app/client/src/assets/icons/menu/pages.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/primary-key.svg b/app/client/src/assets/icons/menu/primary-key.svg index 12311634e5..2ff17003e1 100644 --- a/app/client/src/assets/icons/menu/primary-key.svg +++ b/app/client/src/assets/icons/menu/primary-key.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/queries.svg b/app/client/src/assets/icons/menu/queries.svg index 44abd92bf2..e8d26e3a53 100644 --- a/app/client/src/assets/icons/menu/queries.svg +++ b/app/client/src/assets/icons/menu/queries.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/storage.svg b/app/client/src/assets/icons/menu/storage.svg index a856115925..315b78ac5c 100644 --- a/app/client/src/assets/icons/menu/storage.svg +++ b/app/client/src/assets/icons/menu/storage.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/widgets-colored.svg b/app/client/src/assets/icons/menu/widgets-colored.svg index f28f50fc3e..181f517469 100644 --- a/app/client/src/assets/icons/menu/widgets-colored.svg +++ b/app/client/src/assets/icons/menu/widgets-colored.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/menu/widgets.svg b/app/client/src/assets/icons/menu/widgets.svg index a48d556b21..d33c24c427 100644 --- a/app/client/src/assets/icons/menu/widgets.svg +++ b/app/client/src/assets/icons/menu/widgets.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/alert.svg b/app/client/src/assets/icons/widget/alert.svg index 74a2019404..71eeb2297a 100644 --- a/app/client/src/assets/icons/widget/alert.svg +++ b/app/client/src/assets/icons/widget/alert.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/button.svg b/app/client/src/assets/icons/widget/button.svg index 24c4944870..8acae73d9a 100644 --- a/app/client/src/assets/icons/widget/button.svg +++ b/app/client/src/assets/icons/widget/button.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/chart.svg b/app/client/src/assets/icons/widget/chart.svg index 98e609cb6c..5479998b18 100644 --- a/app/client/src/assets/icons/widget/chart.svg +++ b/app/client/src/assets/icons/widget/chart.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/checkbox.svg b/app/client/src/assets/icons/widget/checkbox.svg index 0947945076..7734916af5 100644 --- a/app/client/src/assets/icons/widget/checkbox.svg +++ b/app/client/src/assets/icons/widget/checkbox.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/collapse.svg b/app/client/src/assets/icons/widget/collapse.svg index 772bc19548..8cbcb11329 100644 --- a/app/client/src/assets/icons/widget/collapse.svg +++ b/app/client/src/assets/icons/widget/collapse.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/container.svg b/app/client/src/assets/icons/widget/container.svg index ed7bd00a1f..e97d99915a 100644 --- a/app/client/src/assets/icons/widget/container.svg +++ b/app/client/src/assets/icons/widget/container.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/datepicker.svg b/app/client/src/assets/icons/widget/datepicker.svg index 80c0c8bd7b..76980a5b33 100644 --- a/app/client/src/assets/icons/widget/datepicker.svg +++ b/app/client/src/assets/icons/widget/datepicker.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/dropdown.svg b/app/client/src/assets/icons/widget/dropdown.svg index 6caa5e0bf6..5e689f59c0 100644 --- a/app/client/src/assets/icons/widget/dropdown.svg +++ b/app/client/src/assets/icons/widget/dropdown.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/filepicker.svg b/app/client/src/assets/icons/widget/filepicker.svg index 3339c43747..88b52ea19c 100644 --- a/app/client/src/assets/icons/widget/filepicker.svg +++ b/app/client/src/assets/icons/widget/filepicker.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/form.svg b/app/client/src/assets/icons/widget/form.svg index 17484c70a3..f0153a9352 100644 --- a/app/client/src/assets/icons/widget/form.svg +++ b/app/client/src/assets/icons/widget/form.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/image.svg b/app/client/src/assets/icons/widget/image.svg index e1bb405a2d..c1eb966be5 100644 --- a/app/client/src/assets/icons/widget/image.svg +++ b/app/client/src/assets/icons/widget/image.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/input.svg b/app/client/src/assets/icons/widget/input.svg index 8dda85dd5b..8b24e7d3de 100644 --- a/app/client/src/assets/icons/widget/input.svg +++ b/app/client/src/assets/icons/widget/input.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/location-picker.svg b/app/client/src/assets/icons/widget/location-picker.svg index 254038ecd5..68150b62e1 100644 --- a/app/client/src/assets/icons/widget/location-picker.svg +++ b/app/client/src/assets/icons/widget/location-picker.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/map.svg b/app/client/src/assets/icons/widget/map.svg index 61c7b73472..7b57ee9c68 100755 --- a/app/client/src/assets/icons/widget/map.svg +++ b/app/client/src/assets/icons/widget/map.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/modal.svg b/app/client/src/assets/icons/widget/modal.svg index 1f1b1ad84b..edede7d2d6 100644 --- a/app/client/src/assets/icons/widget/modal.svg +++ b/app/client/src/assets/icons/widget/modal.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/plus.svg b/app/client/src/assets/icons/widget/plus.svg index fc1aa5b1f1..ca280afea0 100644 --- a/app/client/src/assets/icons/widget/plus.svg +++ b/app/client/src/assets/icons/widget/plus.svg @@ -1,4 +1 @@ - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/radio.svg b/app/client/src/assets/icons/widget/radio.svg index c609d61986..a2e3539fae 100644 --- a/app/client/src/assets/icons/widget/radio.svg +++ b/app/client/src/assets/icons/widget/radio.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/rich-text.svg b/app/client/src/assets/icons/widget/rich-text.svg index 99149884a5..437c2a12c4 100644 --- a/app/client/src/assets/icons/widget/rich-text.svg +++ b/app/client/src/assets/icons/widget/rich-text.svg @@ -1,8 +1 @@ - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/slash.svg b/app/client/src/assets/icons/widget/slash.svg index cd450db315..e32936f698 100644 --- a/app/client/src/assets/icons/widget/slash.svg +++ b/app/client/src/assets/icons/widget/slash.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/switch.svg b/app/client/src/assets/icons/widget/switch.svg index ba199d4f77..2336cc09da 100644 --- a/app/client/src/assets/icons/widget/switch.svg +++ b/app/client/src/assets/icons/widget/switch.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/table.svg b/app/client/src/assets/icons/widget/table.svg index 9d0ca98a83..743c7810f4 100644 --- a/app/client/src/assets/icons/widget/table.svg +++ b/app/client/src/assets/icons/widget/table.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/tabs.svg b/app/client/src/assets/icons/widget/tabs.svg index 0a155f14cd..9e9e748921 100644 --- a/app/client/src/assets/icons/widget/tabs.svg +++ b/app/client/src/assets/icons/widget/tabs.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/text.svg b/app/client/src/assets/icons/widget/text.svg index f4a474a5d9..14d8101ef8 100644 --- a/app/client/src/assets/icons/widget/text.svg +++ b/app/client/src/assets/icons/widget/text.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/icons/widget/video.svg b/app/client/src/assets/icons/widget/video.svg index 11426cec91..62b2bbc194 100644 --- a/app/client/src/assets/icons/widget/video.svg +++ b/app/client/src/assets/icons/widget/video.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/images/404-image.png b/app/client/src/assets/images/404-image.png index cba759370c..c962f6da35 100644 Binary files a/app/client/src/assets/images/404-image.png and b/app/client/src/assets/images/404-image.png differ diff --git a/app/client/src/assets/images/API.svg b/app/client/src/assets/images/API.svg index 9eca1b78bb..9c212ea28d 100644 --- a/app/client/src/assets/images/API.svg +++ b/app/client/src/assets/images/API.svg @@ -1,19 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/images/Curl-logo.svg b/app/client/src/assets/images/Curl-logo.svg index e574f5f631..08f86ba19f 100644 --- a/app/client/src/assets/images/Curl-logo.svg +++ b/app/client/src/assets/images/Curl-logo.svg @@ -1,10 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/images/Curl.png b/app/client/src/assets/images/Curl.png index 474feeef84..85f92e369a 100644 Binary files a/app/client/src/assets/images/Curl.png and b/app/client/src/assets/images/Curl.png differ diff --git a/app/client/src/assets/images/EditPen.svg b/app/client/src/assets/images/EditPen.svg index 0d6d1dba80..1742e7c90a 100644 --- a/app/client/src/assets/images/EditPen.svg +++ b/app/client/src/assets/images/EditPen.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/images/Github.png b/app/client/src/assets/images/Github.png index ea6ff545a2..dcf611c9c2 100644 Binary files a/app/client/src/assets/images/Github.png and b/app/client/src/assets/images/Github.png differ diff --git a/app/client/src/assets/images/Google.png b/app/client/src/assets/images/Google.png index f90e063212..fa90afe9fe 100644 Binary files a/app/client/src/assets/images/Google.png and b/app/client/src/assets/images/Google.png differ diff --git a/app/client/src/assets/images/NoSearchResult.svg b/app/client/src/assets/images/NoSearchResult.svg index d5a60d0c60..b733d943d9 100644 --- a/app/client/src/assets/images/NoSearchResult.svg +++ b/app/client/src/assets/images/NoSearchResult.svg @@ -1,39 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/images/Postman.png b/app/client/src/assets/images/Postman.png index cfd823454d..c1ebd3cc3b 100644 Binary files a/app/client/src/assets/images/Postman.png and b/app/client/src/assets/images/Postman.png differ diff --git a/app/client/src/assets/images/appsmith-datasource.svg b/app/client/src/assets/images/appsmith-datasource.svg new file mode 100644 index 0000000000..21f7ae5258 --- /dev/null +++ b/app/client/src/assets/images/appsmith-datasource.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/assets/images/appsmith_logo.png b/app/client/src/assets/images/appsmith_logo.png index 41dec42f1c..a6a492bbe1 100644 Binary files a/app/client/src/assets/images/appsmith_logo.png and b/app/client/src/assets/images/appsmith_logo.png differ diff --git a/app/client/src/assets/images/appsmith_logo_white.png b/app/client/src/assets/images/appsmith_logo_white.png index 3b01566236..872e5b2ce0 100644 Binary files a/app/client/src/assets/images/appsmith_logo_white.png and b/app/client/src/assets/images/appsmith_logo_white.png differ diff --git a/app/client/src/assets/images/email-not-configured.svg b/app/client/src/assets/images/email-not-configured.svg index 2ddfaaec3d..e93f6a8376 100644 --- a/app/client/src/assets/images/email-not-configured.svg +++ b/app/client/src/assets/images/email-not-configured.svg @@ -1,78 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/images/logo.svg b/app/client/src/assets/images/logo.svg index 6b60c1042f..9dfc1c058c 100755 --- a/app/client/src/assets/images/logo.svg +++ b/app/client/src/assets/images/logo.svg @@ -1,7 +1 @@ - - - - - - - + \ No newline at end of file diff --git a/app/client/src/assets/images/no_image.png b/app/client/src/assets/images/no_image.png index cb274965d4..f2a39de98b 100644 Binary files a/app/client/src/assets/images/no_image.png and b/app/client/src/assets/images/no_image.png differ diff --git a/app/client/src/assets/images/placeholder-image.svg b/app/client/src/assets/images/placeholder-image.svg index a3e57e6cd4..8c6be0447e 100644 --- a/app/client/src/assets/images/placeholder-image.svg +++ b/app/client/src/assets/images/placeholder-image.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/app/client/src/assets/images/query-image-outline.png b/app/client/src/assets/images/query-image-outline.png new file mode 100644 index 0000000000..5fa3fc7475 Binary files /dev/null and b/app/client/src/assets/images/query-image-outline.png differ diff --git a/app/client/src/assets/images/secure.svg b/app/client/src/assets/images/secure.svg new file mode 100644 index 0000000000..1567f28f1c --- /dev/null +++ b/app/client/src/assets/images/secure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/components/ads/ColorSelector.tsx b/app/client/src/components/ads/ColorSelector.tsx index ff01cfbac2..3fae16e43e 100644 --- a/app/client/src/components/ads/ColorSelector.tsx +++ b/app/client/src/components/ads/ColorSelector.tsx @@ -76,9 +76,14 @@ const ColorSelector = (props: ColorSelectorProps) => { selected={selected} color={hex} onClick={() => { - setSelected(hex); - props.onSelect && props.onSelect(hex); + if (selected !== hex) { + setSelected(hex); + props.onSelect && props.onSelect(hex); + } }} + className={ + selected === hex ? "t--color-selected" : "t--color-not-selected" + } /> ); })} diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx index 4377f0b0fb..75aa557ffd 100644 --- a/app/client/src/components/ads/EditableText.tsx +++ b/app/client/src/components/ads/EditableText.tsx @@ -21,28 +21,28 @@ export enum SavingState { ERROR = "ERROR", } -type EditableTextProps = CommonComponentProps & { +export type EditableTextProps = CommonComponentProps & { defaultValue: string; - onTextChanged: (value: string) => void; - placeholder: string; + placeholder?: string; + editInteractionKind: EditInteractionKind; + savingState: SavingState; + onBlur: (value: string) => void; + onTextChanged?: (value: string) => void; className?: string; valueTransform?: (value: string) => string; isEditingDefault?: boolean; forceDefault?: boolean; updating?: boolean; isInvalid?: (value: string) => string | boolean; - editInteractionKind: EditInteractionKind; hideEditIcon?: boolean; fill?: boolean; - savingState: SavingState; - onBlur: (value: string) => void; }; const EditableTextWrapper = styled.div<{ fill?: boolean; }>` width: ${props => (!props.fill ? "234px" : "100%")}; - .${Classes.TEXT} { + .error-message { margin-left: ${props => props.theme.spaces[5]}px; color: ${props => props.theme.colors.danger.main}; } @@ -70,10 +70,6 @@ const TextContainer = styled.div<{ }>` display: flex; align-items: center; - ${props => - props.isEditing && props.isInvalid - ? `margin-bottom: ${props.theme.spaces[2]}px` - : null}; .bp3-editable-text.bp3-editable-text-editing::before, .bp3-editable-text.bp3-disabled::before { display: none; @@ -143,7 +139,6 @@ export const EditableText = (props: EditableTextProps) => { const [savingState, setSavingState] = useState( SavingState.NOT_STARTED, ); - const valueRef = React.useRef(defaultValue); useEffect(() => { setSavingState(props.savingState); @@ -178,14 +173,14 @@ export const EditableText = (props: EditableTextProps) => { const onConfirm = useCallback( (_value: string) => { - if (savingState === SavingState.ERROR || isInvalid) { + if (savingState === SavingState.ERROR || isInvalid || _value === "") { setValue(lastValidValue); onBlur(lastValidValue); setSavingState(SavingState.NOT_STARTED); } else if (changeStarted) { - onTextChanged(_value); - onBlur(_value); + onTextChanged && onTextChanged(_value); } + onBlur(_value); setIsEditing(false); setChangeStarted(false); }, @@ -204,10 +199,9 @@ export const EditableText = (props: EditableTextProps) => { const finalVal: string = _value; const errorMessage = inputValidation && inputValidation(finalVal); const error = errorMessage ? errorMessage : false; - if (!error) { + if (!error && _value !== "") { setLastValidValue(finalVal); - valueRef.current = finalVal; - onTextChanged(finalVal); + onTextChanged && onTextChanged(finalVal); } setValue(finalVal); setIsInvalid(error); @@ -258,8 +252,8 @@ export const EditableText = (props: EditableTextProps) => { onChange={onInputchange} onConfirm={onConfirm} value={value} - selectAllOnFocus - placeholder={props.placeholder} + selectAllOnFocus={true} + placeholder={props.placeholder || defaultValue} className={props.className} onCancel={onConfirm} /> @@ -267,13 +261,15 @@ export const EditableText = (props: EditableTextProps) => { {savingState === SavingState.STARTED ? ( - ) : ( + ) : value ? ( - )} + ) : null} {isEditing && !!isInvalid ? ( - {isInvalid} + + {isInvalid} + ) : null} ); diff --git a/app/client/src/components/ads/EditableTextWrapper.tsx b/app/client/src/components/ads/EditableTextWrapper.tsx new file mode 100644 index 0000000000..7e64cd6ab5 --- /dev/null +++ b/app/client/src/components/ads/EditableTextWrapper.tsx @@ -0,0 +1,81 @@ +import EditableText, { EditableTextProps, SavingState } from "./EditableText"; +import React, { useEffect, useState } from "react"; +import styled from "styled-components"; +import { Classes } from "@blueprintjs/core"; + +type EditableTextWrapperProps = EditableTextProps & { + variant: "UNDERLINE" | "ICON"; + isNewApp: boolean; +}; + +const Container = styled.div<{ + isEditing?: boolean; + savingState: SavingState; + isInvalid: boolean; +}>` + &&& .${Classes.EDITABLE_TEXT}, .icon-wrapper { + padding: 5px 10px; + height: 25px; + text-decoration: ${props => (props.isEditing ? "unset" : "underline")}; + background-color: ${props => + (props.isInvalid && props.isEditing) || + props.savingState === SavingState.ERROR + ? props.theme.colors.editableText.dangerBg + : "transparent"}; + } + + &&& .${Classes.EDITABLE_TEXT_CONTENT}, &&& .${Classes.EDITABLE_TEXT_INPUT} { + text-align: center; + color: #d4d4d4; + font-size: ${props => props.theme.typography.h4.fontSize}px; + line-height: ${props => props.theme.typography.h4.lineHeight}px; + letter-spacing: ${props => props.theme.typography.h4.letterSpacing}px; + font-weight: ${props => props.theme.typography.h4.fontWeight}px; + } + + .error-message { + margin-top: 2px; + } +`; + +export default function EditableTextWrapper(props: EditableTextWrapperProps) { + const [isEditing, setIsEditing] = useState(props.isNewApp); + const [isValid, setIsValid] = useState(false); + + useEffect(() => { + setIsEditing(props.isNewApp); + }, [props.isNewApp]); + + return ( + + { + setIsEditing(false); + props.onBlur(value); + }} + className={props.className} + onTextChanged={(value: string) => setIsEditing(true)} + isInvalid={(value: string) => { + setIsEditing(true); + if (props.isInvalid) { + setIsValid(Boolean(props.isInvalid(value))); + return props.isInvalid(value); + } else { + return false; + } + }} + /> + + ); +} diff --git a/app/client/src/components/ads/IconSelector.tsx b/app/client/src/components/ads/IconSelector.tsx index 0dc17980fb..70bd3ba5d2 100644 --- a/app/client/src/components/ads/IconSelector.tsx +++ b/app/client/src/components/ads/IconSelector.tsx @@ -73,9 +73,16 @@ const IconSelector = (props: IconSelectorProps) => { { - setSelected(iconName); - props.onSelect && props.onSelect(iconName); + if (iconName !== selected) { + setSelected(iconName); + props.onSelect && props.onSelect(iconName); + } }} > diff --git a/app/client/src/components/ads/TagInputComponent.tsx b/app/client/src/components/ads/TagInputComponent.tsx index 2bfb538882..5e1db6998e 100644 --- a/app/client/src/components/ads/TagInputComponent.tsx +++ b/app/client/src/components/ads/TagInputComponent.tsx @@ -2,6 +2,9 @@ import React, { useState, useEffect } from "react"; import styled from "styled-components"; import { Classes, TagInput } from "@blueprintjs/core"; import { Intent } from "constants/DefaultTheme"; +import { WrappedFieldMetaProps } from "redux-form"; +import { INVITE_USERS_VALIDATION_EMAIL_LIST } from "constants/messages"; +import { isEmail } from "utils/formhelpers"; const TagInputWrapper = styled.div<{ intent?: Intent }>` margin-right: 8px; @@ -48,6 +51,7 @@ type TagInputProps = { /** Intent of the tags, which defines their color */ intent?: Intent; hasError?: boolean; + customError: (values: any) => void; }; /** @@ -71,10 +75,25 @@ const TagInputComponent = (props: TagInputProps) => { } }, [_values, values]); + const validateEmail = (newValues: string[]) => { + if (newValues && newValues.length > 0) { + let error = ""; + newValues.forEach((user: any) => { + if (!isEmail(user)) { + error = INVITE_USERS_VALIDATION_EMAIL_LIST; + } + }); + props.customError(error); + } else { + props.customError(""); + } + }; + const commitValues = (newValues: string[]) => { setValues(newValues); props.input.onChange && props.input.onChange(newValues.filter(Boolean).join(",")); + validateEmail(newValues); }; const onTagsChange = (values: React.ReactNode[]) => { diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx index 482f2477b7..1674a4bb02 100644 --- a/app/client/src/components/ads/TextInput.tsx +++ b/app/client/src/components/ads/TextInput.tsx @@ -175,7 +175,7 @@ const TextInput = forwardRef( ); return ( - + {ErrorMessage} diff --git a/app/client/src/components/designSystems/appsmith/ImageComponent.tsx b/app/client/src/components/designSystems/appsmith/ImageComponent.tsx index b05a024cad..365475af3d 100644 --- a/app/client/src/components/designSystems/appsmith/ImageComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ImageComponent.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { ComponentProps } from "./BaseComponent"; import styled from "styled-components"; +import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; export interface StyledImageProps { defaultImageUrl: string; @@ -16,7 +17,7 @@ export const StyledImage = styled.div< } >` position: relative; - display: flex; + display: flex; flex-direction: "row"; cursor: ${props => props.showHoverPointer && props.onClick ? "pointer" : "inherit"}; @@ -30,37 +31,133 @@ export const StyledImage = styled.div< width: 100%; `; +const Wrapper = styled.div` + height: 100%; + width: 100%; + .react-transform-element, + .react-transform-component { + height: 100%; + width: 100%; + } +`; + +enum ZoomingState { + MAX_ZOOMED_OUT = "MAX_ZOOMED_OUT", + MAX_ZOOMED_IN = "MAX_ZOOMED_IN", +} class ImageComponent extends React.Component< ImageComponentProps, { imageError: boolean; + zoomingState: ZoomingState; } > { + isPanning: boolean; constructor(props: ImageComponentProps) { super(props); + this.isPanning = false; this.state = { imageError: false, + zoomingState: ZoomingState.MAX_ZOOMED_OUT, }; } render() { + const { maxZoomLevel } = this.props; + const zoomActive = + maxZoomLevel !== undefined && maxZoomLevel > 1 && !this.isPanning; + const isZoomingIn = this.state.zoomingState === ZoomingState.MAX_ZOOMED_OUT; + let cursor = "inherit"; + if (zoomActive) { + cursor = isZoomingIn ? "zoom-in" : "zoom-out"; + } return ( - - + { + this.props.disableDrag(true); }} - alt={this.props.widgetName} - src={this.props.imageUrl} - onError={this.onImageError} - onLoad={this.onImageLoad} - onClick={this.props.onClick} - > - + onPanning={() => { + this.isPanning = true; + }} + onPanningStop={() => { + this.props.disableDrag(false); + }} + options={{ + maxScale: maxZoomLevel, + disabled: !zoomActive, + transformEnabled: zoomActive, + }} + pan={{ + disabled: !zoomActive, + }} + wheel={{ + disabled: !zoomActive, + }} + doubleClick={{ + disabled: true, + }} + onZoomChange={(zoom: any) => { + if (zoomActive) { + //Check max zoom + if ( + maxZoomLevel === zoom.scale && + // Added for preventing infinite loops + this.state.zoomingState !== ZoomingState.MAX_ZOOMED_IN + ) { + this.setState({ + zoomingState: ZoomingState.MAX_ZOOMED_IN, + }); + // Check min zoom + } else if ( + zoom.scale === 1 && + this.state.zoomingState !== ZoomingState.MAX_ZOOMED_OUT + ) { + this.setState({ + zoomingState: ZoomingState.MAX_ZOOMED_OUT, + }); + } + } + }} + > + {({ zoomIn, zoomOut, setScale, ...rest }: any) => ( + + + ) => { + if (!this.isPanning) { + if (isZoomingIn) { + zoomIn(event); + } else { + zoomOut(event); + } + this.props.onClick && this.props.onClick(event); + } + this.isPanning = false; + }} + > + {this.props.widgetName} + + + + )} + + ); } @@ -82,6 +179,8 @@ export interface ImageComponentProps extends ComponentProps { defaultImageUrl: string; isLoading: boolean; showHoverPointer?: boolean; + maxZoomLevel: number; + disableDrag: (disabled: boolean) => void; onClick?: (event: React.MouseEvent) => void; } diff --git a/app/client/src/components/designSystems/appsmith/MapComponent.tsx b/app/client/src/components/designSystems/appsmith/MapComponent.tsx index 80fa6067b6..09282d6df8 100644 --- a/app/client/src/components/designSystems/appsmith/MapComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/MapComponent.tsx @@ -73,62 +73,80 @@ const PickMyLocationWrapper = styled.div` `; const MyMapComponent = withScriptjs( - withGoogleMap((props: any) => ( - { - if (props.enableCreateMarker) { - props.saveMarker(e.latLng.lat(), e.latLng.lng()); + withGoogleMap((props: any) => { + const [mapCenter, setMapCenter] = React.useState< + | { + lat: number; + lng: number; + title?: string; + description?: string; } - }} - > - {props.enableSearch && ( - - - - )} - {props.markers.map((marker: any, index: number) => ( - ({ + ...props.center, + lng: props.center.long, + }); + return ( + { + if (props.enableCreateMarker) { + props.saveMarker(e.latLng.lat(), e.latLng.lng()); } - onClick={e => { - props.selectMarker(marker.lat, marker.long, marker.title); - }} - onDragEnd={de => { - props.updateMarker(de.latLng.lat(), de.latLng.lng(), index); - }} - /> - ))} - {props.enablePickLocation && ( - - - - )} - - )), + }} + > + {props.enableSearch && ( + + + + )} + {props.markers.map((marker: any, index: number) => ( + { + setMapCenter({ + ...marker, + lng: marker.long, + }); + props.selectMarker(marker.lat, marker.long, marker.title); + }} + onDragEnd={de => { + props.updateMarker(de.latLng.lat(), de.latLng.lng(), index); + }} + /> + ))} + {props.enablePickLocation && ( + + + + )} + + ); + }), ); class MapComponent extends React.Component { diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx index b0fb1f2a09..03dea321db 100644 --- a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -42,16 +42,16 @@ interface ReactTableComponentProps { width: number; height: number; pageSize: number; - tableData: object[]; + tableData: Array>; columnOrder?: string[]; disableDrag: (disable: boolean) => void; - onRowClick: (rowData: object, rowIndex: number) => void; + onRowClick: (rowData: Record, rowIndex: number) => void; onCommandClick: (dynamicTrigger: string, onComplete: () => void) => void; - updatePageNo: Function; + updatePageNo: (pageNo: number) => void; updateHiddenColumns: (hiddenColumns?: string[]) => void; sortTableColumn: (column: string, asc: boolean) => void; - nextPageClick: Function; - prevPageClick: Function; + nextPageClick: () => void; + prevPageClick: () => void; pageNo: number; serverSidePaginationEnabled: boolean; columnActions?: ColumnAction[]; @@ -68,10 +68,12 @@ interface ReactTableComponentProps { }; }; columnSizeMap?: { [key: string]: number }; - updateColumnType: Function; - updateColumnName: Function; - handleResizeColumn: Function; - handleReorderColumn: Function; + updateColumnType: (columnTypeMap: { + [key: string]: { type: string; format: string }; + }) => void; + updateColumnName: (columnNameMap: { [key: string]: string }) => void; + handleResizeColumn: (columnSizeMap: { [key: string]: number }) => void; + handleReorderColumn: (columnOrder: string[]) => void; searchTableData: (searchKey: any) => void; filters?: ReactTableFilter[]; applyFilter: (filters: ReactTableFilter[]) => void; @@ -277,7 +279,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { }; const selectTableRow = ( - row: { original: object; index: number }, + row: { original: Record; index: number }, isSelected: boolean, ) => { if (!isSelected || !!props.multiRowSelection) { diff --git a/app/client/src/components/designSystems/appsmith/Table.tsx b/app/client/src/components/designSystems/appsmith/Table.tsx index 824b33f5e3..51b6b6619b 100644 --- a/app/client/src/components/designSystems/appsmith/Table.tsx +++ b/app/client/src/components/designSystems/appsmith/Table.tsx @@ -32,19 +32,19 @@ interface TableProps { columns: ReactTableColumnProps[]; hiddenColumns?: string[]; updateHiddenColumns: (hiddenColumns?: string[]) => void; - data: object[]; + data: Array>; editMode: boolean; columnNameMap?: { [key: string]: string }; getColumnMenu: (columnIndex: number) => ColumnMenuOptionProps[]; handleColumnNameUpdate: (columnIndex: number, columnName: string) => void; sortTableColumn: (columnIndex: number, asc: boolean) => void; - handleResizeColumn: Function; + handleResizeColumn: (columnIndex: number, columnWidth: string) => void; selectTableRow: ( - row: { original: object; index: number }, + row: { original: Record; index: number }, isSelected: boolean, ) => void; pageNo: number; - updatePageNo: Function; + updatePageNo: (pageNo: number) => void; nextPageClick: () => void; prevPageClick: () => void; serverSidePaginationEnabled: boolean; diff --git a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx index 14e379d213..efe2f6a6b6 100644 --- a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx +++ b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx @@ -688,7 +688,7 @@ export const TableHeaderCell = (props: { handleColumnNameUpdate: (columnIndex: number, name: string) => void; getColumnMenu: (columnIndex: number) => ColumnMenuOptionProps[]; sortTableColumn: (columnIndex: number, asc: boolean) => void; - handleResizeColumn: Function; + handleResizeColumn: (columnIndex: number, columnWidth: string) => void; column: any; }) => { const { column } = props; @@ -765,7 +765,9 @@ export const TableHeaderCell = (props: { ); }; -export const getAllTableColumnKeys = (tableData: object[]) => { +export const getAllTableColumnKeys = ( + tableData: Array>, +) => { const columnKeys: string[] = []; for (let i = 0, tableRowCount = tableData.length; i < tableRowCount; i++) { const row = tableData[i]; @@ -814,7 +816,7 @@ export const reorderColumns = ( }; export function sortTableFunction( - filteredTableData: object[], + filteredTableData: Array>, columns: ReactTableColumnProps[], sortedColumn: string, sortOrder: boolean, @@ -881,7 +883,10 @@ export const ConditionFunctions: { return a !== "" && a !== undefined && a !== null; }, notEqualTo: (a: any, b: any) => { - return a !== b; + return a.toString() !== b.toString(); + }, + isEqualTo: (a: any, b: any) => { + return a.toString() === b.toString(); }, lessThan: (a: any, b: any) => { const numericB = Number(b); diff --git a/app/client/src/components/editorComponents/LightningMenu/helpers.tsx b/app/client/src/components/editorComponents/LightningMenu/helpers.tsx index 91ac42aa1a..b17dc48058 100644 --- a/app/client/src/components/editorComponents/LightningMenu/helpers.tsx +++ b/app/client/src/components/editorComponents/LightningMenu/helpers.tsx @@ -62,7 +62,7 @@ export const getApiOptions = ( text: LIGHTNING_MENU_DATA_API, }, openDirection: Directions.RIGHT, - openOnHover: false, + openOnHover: true, skin: skin, modifiers: { offset: { @@ -111,7 +111,7 @@ export const getQueryOptions = ( text: LIGHTNING_MENU_DATA_QUERY, }, openDirection: Directions.RIGHT, - openOnHover: false, + openOnHover: true, skin: skin, modifiers: { offset: { @@ -141,7 +141,7 @@ export const getWidgetOptions = ( text: LIGHTNING_MENU_DATA_WIDGET, }, openDirection: Directions.RIGHT, - openOnHover: false, + openOnHover: true, skin: skin, modifiers: { offset: { diff --git a/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx b/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx index 5f24b8150d..2ee33c83d7 100644 --- a/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx +++ b/app/client/src/components/editorComponents/actioncreator/ActionCreator.tsx @@ -18,7 +18,7 @@ import { KeyValueComponent } from "components/propertyControls/KeyValueComponent import { InputText } from "components/propertyControls/InputTextControl"; import { createModalAction } from "actions/widgetActions"; import { createNewApiName, createNewQueryName } from "utils/AppsmithUtils"; -import { isDynamicValue } from "utils/DynamicBindingUtils"; +import { getDynamicBindings, isDynamicValue } from "utils/DynamicBindingUtils"; import HightlightedCode from "components/editorComponents/HighlightedCode"; import TreeStructure from "components/utils/TreeStructure"; import { @@ -46,6 +46,7 @@ const FILE_TYPE_OPTIONS = [ { label: "SVG", value: "'image/svg+xml'", id: "image/svg+xml" }, ]; +const FUNC_ARGS_REGEX = /((["][^"]*["])|(['][^']*['])|([\(].*[\)[=][>][{].*[}])|([^'",][^,"+]*[^'",]*))*/gi; const ACTION_TRIGGER_REGEX = /^{{([\s\S]*?)\(([\s\S]*?)\)}}$/g; //Old Regex:: /\(\) => ([\s\S]*?)(\([\s\S]*?\))/g; const ACTION_ANONYMOUS_FUNC_REGEX = /\(\) => (({[\s\S]*?})|([\s\S]*?)(\([\s\S]*?\)))/g; @@ -78,23 +79,51 @@ export const modalGetter = (value: string) => { return name; }; -// const urlSetter = (changeValue: any, currentValue: string): string => { -// return currentValue.replace(ACTION_TRIGGER_REGEX, `{{$1('${changeValue}')}}`); -// }; +const stringToJS = (string: string): string => { + const { stringSegments, jsSnippets } = getDynamicBindings(string); + const js = stringSegments + .map((segment, index) => { + if (jsSnippets[index] && jsSnippets[index].length > 0) { + return jsSnippets[index]; + } else { + return `'${segment}'`; + } + }) + .join(" + "); + return js; +}; -// export const textGetter = (value: string) => { -// const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; -// if (matches.length) { -// const stringValue = matches[0][2]; -// return stringValue.substring(1, stringValue.length - 1); -// } -// return ""; -// }; +const JSToString = (js: string): string => { + const segments = js.split(" + "); + return segments + .map(segment => { + if (segment.charAt(0) === "'") { + return segment.substring(1, segment.length - 1); + } else return "{{" + segment + "}}"; + }) + .join(""); +}; -const alertTextSetter = (changeValue: any, currentValue: string): string => { +const argsStringToArray = (funcArgs: string): string[] => { + const argsplitMatches = [...funcArgs.matchAll(FUNC_ARGS_REGEX)]; + return argsplitMatches + .map(match => { + return match[0]; + }) + .filter(arg => { + return arg !== ""; + }); +}; + +const textSetter = ( + changeValue: any, + currentValue: string, + argNum: number, +): string => { const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)]; - const args = matches[0][2].split(","); - args[0] = `'${changeValue}'`; + const args = argsStringToArray(matches[0][2]); + const jsVal = stringToJS(changeValue); + args[argNum] = jsVal; const result = currentValue.replace( ACTION_TRIGGER_REGEX, `{{$1(${args.join(",")})}}`, @@ -102,40 +131,25 @@ const alertTextSetter = (changeValue: any, currentValue: string): string => { return result; }; -const alertTextGetter = (value: string) => { +const textGetter = (value: string, argNum: number) => { const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; if (matches.length) { - const funcArgs = matches[0][2]; - const arg = funcArgs.split(",")[0]; - return arg.substring(1, arg.length - 1); + const args = argsStringToArray(matches[0][2]); + const arg = args[argNum]; + const stringFromJS = arg ? JSToString(arg.trim()) : arg; + return stringFromJS; } return ""; }; -const alertTypeSetter = (changeValue: any, currentValue: string): string => { +const enumTypeSetter = ( + changeValue: any, + currentValue: string, + argNum: number, +): string => { const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)]; - const args = matches[0][2].split(","); - args[1] = changeValue as string; - return currentValue.replace( - ACTION_TRIGGER_REGEX, - `{{$1(${args.join(",")})}}`, - ); -}; - -const alertTypeGetter = (value: string) => { - const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; - if (matches.length) { - const funcArgs = matches[0][2]; - const arg = funcArgs.split(",")[1]; - return arg ? arg.trim() : "'primary'"; - } - return ""; -}; - -const storeKeyTextSetter = (changeValue: any, currentValue: string): string => { - const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)]; - const args = matches[0][2].split(","); - args[0] = `'${changeValue}'`; + const args = argsStringToArray(matches[0][2]); + args[argNum] = changeValue as string; const result = currentValue.replace( ACTION_TRIGGER_REGEX, `{{$1(${args.join(",")})}}`, @@ -143,104 +157,18 @@ const storeKeyTextSetter = (changeValue: any, currentValue: string): string => { return result; }; -const storeKeyTextGetter = (value: string) => { - const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; - if (matches.length) { - const funcArgs = matches[0][2]; - const arg = funcArgs.split(",")[0]; - return arg.substring(1, arg.length - 1); - } - return ""; -}; - -const storeValueTextSetter = ( - changeValue: any, - currentValue: string, +const enumTypeGetter = ( + value: string, + argNum: number, + defaultValue = "", ): string => { - const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)]; - const args = matches[0][2].split(","); - args[1] = `'${changeValue}'`; - return currentValue.replace( - ACTION_TRIGGER_REGEX, - `{{$1(${args.join(",")})}}`, - ); -}; - -const storeValueTextGetter = (value: string) => { const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; if (matches.length) { - const funcArgs = matches[0][2]; - const arg = funcArgs.split(",")[1]; - return arg ? arg.substring(1, arg.length - 1) : ""; + const args = argsStringToArray(matches[0][2]); + const arg = args[argNum]; + return arg ? arg.trim() : defaultValue; } - return ""; -}; - -const downloadDataSetter = (changeValue: any, currentValue: string): string => { - const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)]; - const args = matches[0][2].split(","); - args[0] = `'${changeValue}'`; - const result = currentValue.replace( - ACTION_TRIGGER_REGEX, - `{{$1(${args.join(",")})}}`, - ); - return result; -}; - -const downloadDataGetter = (value: string) => { - const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; - if (matches.length) { - const funcArgs = matches[0][2]; - const arg = funcArgs.split(",")[0]; - return arg.substring(1, arg.length - 1); - } - return ""; -}; - -const downloadFileNameSetter = ( - changeValue: any, - currentValue: string, -): string => { - const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)]; - const args = matches[0][2].split(","); - args[1] = `'${changeValue}'`; - return currentValue.replace( - ACTION_TRIGGER_REGEX, - `{{$1(${args.join(",")})}}`, - ); -}; - -const downloadFileNameGetter = (value: string) => { - const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; - if (matches.length) { - const funcArgs = matches[0][2]; - const arg = funcArgs.split(",")[1]; - return arg ? arg.substring(1, arg.length - 1) : ""; - } - return ""; -}; - -const downloadFileTypeSetter = ( - changeValue: any, - currentValue: string, -): string => { - const matches = [...currentValue.matchAll(ACTION_TRIGGER_REGEX)]; - const args = matches[0][2].split(","); - args[2] = changeValue as string; - return currentValue.replace( - ACTION_TRIGGER_REGEX, - `{{$1(${args.join(",")})}}`, - ); -}; - -const downloadFileTypeGetter = (value: string) => { - const matches = [...value.matchAll(ACTION_TRIGGER_REGEX)]; - if (matches.length) { - const funcArgs = matches[0][2]; - const arg = funcArgs.split(",")[2]; - return arg ? arg.trim() : ""; - } - return ""; + return defaultValue; }; type ActionCreatorProps = { @@ -436,10 +364,10 @@ const fieldConfigs: FieldConfigs = { }, [FieldType.PAGE_SELECTOR_FIELD]: { getter: (value: any) => { - return modalGetter(value); + return textGetter(value, 0); }, setter: (option: any, currentValue: string) => { - return modalSetter(option.value, currentValue); + return textSetter(option, currentValue, 0); }, view: ViewTypes.SELECTOR_VIEW, }, @@ -454,55 +382,73 @@ const fieldConfigs: FieldConfigs = { }, [FieldType.URL_FIELD]: { getter: (value: string) => { - return modalGetter(value); + return textGetter(value, 0); }, setter: (value: string, currentValue: string) => { - return modalSetter(value, currentValue); + return textSetter(value, currentValue, 0); }, view: ViewTypes.TEXT_VIEW, }, [FieldType.ALERT_TEXT_FIELD]: { getter: (value: string) => { - return alertTextGetter(value); + return textGetter(value, 0); }, setter: (value: string, currentValue: string) => { - return alertTextSetter(value, currentValue); + return textSetter(value, currentValue, 0); }, view: ViewTypes.TEXT_VIEW, }, [FieldType.ALERT_TYPE_SELECTOR_FIELD]: { getter: (value: any) => { - return alertTypeGetter(value); + return enumTypeGetter(value, 1, "success"); }, setter: (option: any, currentValue: string) => { - return alertTypeSetter(option.value, currentValue); + return enumTypeSetter(option.value, currentValue, 1); }, view: ViewTypes.SELECTOR_VIEW, }, [FieldType.KEY_TEXT_FIELD]: { - getter: storeKeyTextGetter, - setter: storeKeyTextSetter, + getter: (value: any) => { + return textGetter(value, 0); + }, + setter: (option: any, currentValue: string) => { + return textSetter(option, currentValue, 0); + }, view: ViewTypes.TEXT_VIEW, }, [FieldType.VALUE_TEXT_FIELD]: { - getter: storeValueTextGetter, - setter: storeValueTextSetter, + getter: (value: any) => { + return textGetter(value, 1); + }, + setter: (option: any, currentValue: string) => { + return textSetter(option, currentValue, 1); + }, view: ViewTypes.TEXT_VIEW, }, [FieldType.DOWNLOAD_DATA_FIELD]: { - getter: downloadDataGetter, - setter: downloadDataSetter, + getter: (value: any) => { + return textGetter(value, 0); + }, + setter: (option: any, currentValue: string) => { + return textSetter(option, currentValue, 0); + }, view: ViewTypes.TEXT_VIEW, }, [FieldType.DOWNLOAD_FILE_NAME_FIELD]: { - getter: downloadFileNameGetter, - setter: downloadFileNameSetter, + getter: (value: any) => { + return textGetter(value, 1); + }, + setter: (option: any, currentValue: string) => { + return textSetter(option, currentValue, 1); + }, view: ViewTypes.TEXT_VIEW, }, [FieldType.DOWNLOAD_FILE_TYPE_FIELD]: { - getter: downloadFileTypeGetter, + getter: (value: any) => { + return enumTypeGetter(value, 2); + }, setter: (option: any, currentValue: string) => - downloadFileTypeSetter(option.value, currentValue), + enumTypeSetter(option.value, currentValue, 2), view: ViewTypes.SELECTOR_VIEW, }, }; diff --git a/app/client/src/components/editorComponents/form/fields/DropdownWrapper.tsx b/app/client/src/components/editorComponents/form/fields/DropdownWrapper.tsx index 99200a880b..a83a90a5d8 100644 --- a/app/client/src/components/editorComponents/form/fields/DropdownWrapper.tsx +++ b/app/client/src/components/editorComponents/form/fields/DropdownWrapper.tsx @@ -19,10 +19,10 @@ const DropdownWrapper = (props: DropdownWrapperProps) => { }; useEffect(() => { - if (props.placeholder) { - setSelectedOption({ value: props.placeholder }); - } else if (props.input && props.input.value) { + if (props.input && props.input.value) { setSelectedOption({ value: props.input.value }); + } else if (props.placeholder) { + setSelectedOption({ value: props.placeholder }); } }, [props.input, props.placeholder]); diff --git a/app/client/src/components/editorComponents/form/fields/TagListField.tsx b/app/client/src/components/editorComponents/form/fields/TagListField.tsx index a2d38ccb27..ca30384eb9 100644 --- a/app/client/src/components/editorComponents/form/fields/TagListField.tsx +++ b/app/client/src/components/editorComponents/form/fields/TagListField.tsx @@ -26,6 +26,7 @@ type TagListFieldProps = { type: string; label: string; intent: Intent; + customError: (err: string) => void; }; const TagListField = (props: TagListFieldProps) => { diff --git a/app/client/src/components/formControls/DynamicTextFieldControl.tsx b/app/client/src/components/formControls/DynamicTextFieldControl.tsx index c8928db33a..bc4e5aa933 100644 --- a/app/client/src/components/formControls/DynamicTextFieldControl.tsx +++ b/app/client/src/components/formControls/DynamicTextFieldControl.tsx @@ -25,7 +25,7 @@ const Wrapper = styled.div` .dynamic-text-field { border-radius: 4px; font-size: 14px; - height: calc(100vh / 4); + min-height: calc(100vh / 4); } && { @@ -98,7 +98,7 @@ class DynamicTextControl extends BaseControl< export interface DynamicTextFieldProps extends ControlProps { actionName: string; - createTemplate: Function; + createTemplate: (template: any) => any; pluginId: string; responseType: string; } diff --git a/app/client/src/components/formControls/InputTextControl.tsx b/app/client/src/components/formControls/InputTextControl.tsx index 52a7bcb4a7..75f26e73e6 100644 --- a/app/client/src/components/formControls/InputTextControl.tsx +++ b/app/client/src/components/formControls/InputTextControl.tsx @@ -4,6 +4,17 @@ import { InputType } from "widgets/InputWidget"; import { ControlType } from "constants/PropertyControlConstants"; import TextField from "components/editorComponents/form/fields/TextField"; import FormLabel from "components/editorComponents/FormLabel"; +import { FormIcons } from "icons/FormIcons"; +import { Colors } from "constants/Colors"; +import styled from "styled-components"; + +const StyledInfo = styled.span` + font-weight: normal; + line-height: normal; + color: ${Colors.CADET_BLUE}; + font-size: 12px; + margin-left: 1px; +`; export function InputText(props: { label: string; @@ -14,13 +25,20 @@ export function InputText(props: { dataType?: string; isRequired?: boolean; name: string; + encrypted?: boolean; }) { const { name, placeholder, dataType, label, isRequired } = props; return (
- {label} {isRequired && "*"} + {label} {isRequired && "*"}{" "} + {props.encrypted && ( + <> + + Encrypted + + )} { validationMessage={validationMessage} placeholder={placeholderText} dataType={this.getType(dataType)} + encrypted={this.props.encrypted} /> ); } @@ -90,6 +109,7 @@ export interface InputControlProps extends ControlProps { placeholderText: string; inputType?: InputType; dataType?: InputType; + encrypted?: boolean; } export default InputTextControl; diff --git a/app/client/src/constants/FieldExpectedValue.ts b/app/client/src/constants/FieldExpectedValue.ts index 93872d9c24..1d7ff931e0 100644 --- a/app/client/src/constants/FieldExpectedValue.ts +++ b/app/client/src/constants/FieldExpectedValue.ts @@ -43,6 +43,7 @@ const FIELD_VALUES: Record< image: "string", defaultImage: "string", isVisible: "boolean", + maxZoomLevel: "number", }, RADIO_GROUP_WIDGET: { options: "Array<{ label: string, value: string }>", diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 30e6b55eec..692b88d1be 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -1,4 +1,4 @@ -import { WidgetProps, WidgetCardProps } from "widgets/BaseWidget"; +import { WidgetCardProps, WidgetProps } from "widgets/BaseWidget"; import { PageAction } from "constants/ActionConstants"; import { Org } from "./orgConstants"; @@ -485,7 +485,3 @@ export type InitializeEditorPayload = { applicationId: string; pageId: string; }; - -export type FetchPageListPayload = { - applicationId: string; -}; diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index c4fba06d0d..f8e936a8fe 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -92,7 +92,7 @@ export const NAVIGATE_TO_VALIDATION_ERROR = "Please enter a valid URL or page name"; export const INVITE_USERS_VALIDATION_EMAIL_LIST = - "Invalid Email address(es) found:"; + "Invalid Email address(es) found"; export const INVITE_USERS_VALIDATION_ROLE_EMPTY = "Please select a role"; export const INVITE_USERS_EMAIL_LIST_PLACEHOLDER = "Comma separated emails"; diff --git a/app/client/src/icons/FormIcons.tsx b/app/client/src/icons/FormIcons.tsx index 65b0d80d04..18d27e634e 100644 --- a/app/client/src/icons/FormIcons.tsx +++ b/app/client/src/icons/FormIcons.tsx @@ -5,6 +5,7 @@ import { IconProps, IconWrapper } from "constants/IconConstants"; import { ReactComponent as InfoIcon } from "assets/icons/form/info-outline.svg"; import { ReactComponent as DeleteIcon } from "assets/icons/form/trash.svg"; import { ReactComponent as AddNewIcon } from "assets/icons/form/add-new.svg"; +import { ReactComponent as LockIcon } from "assets/icons/form/lock.svg"; /* eslint-disable react/display-name */ @@ -50,4 +51,9 @@ export const FormIcons: { /> ), + LOCK_ICON: (props: IconProps) => ( + + + + ), }; diff --git a/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx b/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx index f9f699b722..32aa917df6 100644 --- a/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx +++ b/app/client/src/mockResponses/PropertyPaneConfigResponse.tsx @@ -1,7 +1,7 @@ import { PropertyPaneConfigsResponse } from "api/ConfigsApi"; const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = { - // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // eslint-disable-next-line // @ts-ignore config: { CONTAINER_WIDGET: [ @@ -294,6 +294,36 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = { controlType: "SWITCH", isJSConvertible: true, }, + { + id: "3.1.4", + helpText: "Controls the max zoom of the widget", + propertyName: "maxZoomLevel", + label: "Max Zoom Level", + controlType: "DROP_DOWN", + options: [ + { + label: "1x (No Zoom)", + value: 1, + }, + { + label: "2x", + value: 2, + }, + { + label: "4x", + value: 4, + }, + { + label: "8x", + value: 8, + }, + { + label: "16x", + value: 16, + }, + ], + isJSConvertible: true, + }, ], }, { diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 4e8a028792..d59a754e3c 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -36,6 +36,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { defaultImage: "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png", imageShape: "RECTANGLE", + maxZoomLevel: 1, image: "", rows: 3, columns: 4, diff --git a/app/client/src/pages/Applications/ApplicationCard.tsx b/app/client/src/pages/Applications/ApplicationCard.tsx index cf9075b054..263d8fa028 100644 --- a/app/client/src/pages/Applications/ApplicationCard.tsx +++ b/app/client/src/pages/Applications/ApplicationCard.tsx @@ -204,7 +204,6 @@ const AppNameWrapper = styled.div<{ isFetching: boolean }>` : null}; `; type ApplicationCardProps = { - activeAppCard?: boolean; application: ApplicationPayload; duplicate?: (applicationId: string) => void; share?: (applicationId: string) => void; @@ -275,11 +274,6 @@ export const ApplicationCard = (props: ApplicationCardProps) => { addDeleteOption(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - if (props.activeAppCard) { - setShowOverlay(true); - } - }, [props.activeAppCard]); const appIcon = (props.application?.icon || getApplicationIcon(props.application.id)) as AppIconName; @@ -458,7 +452,11 @@ export const ApplicationCard = (props: ApplicationCardProps) => { > <> { isFetching={isFetchingApplications} className={isFetchingApplications ? Classes.SKELETON : ""} > - {props.application.name} + + {props.application.name} + diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx index 39c4e3cf49..660c9374be 100644 --- a/app/client/src/pages/Applications/index.tsx +++ b/app/client/src/pages/Applications/index.tsx @@ -1,4 +1,4 @@ -import React, { Component, useState } from "react"; +import React, { Component, Fragment, useState } from "react"; import styled from "styled-components"; import { connect, useSelector, useDispatch } from "react-redux"; import { AppState } from "reducers"; @@ -53,11 +53,11 @@ import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; import { loadingUserOrgs } from "./ApplicationLoaders"; -import CreateApplicationForm from "./CreateApplicationForm"; import { creatingApplicationMap } from "reducers/uiReducers/applicationsReducer"; import CenteredWrapper from "../../components/designSystems/appsmith/CenteredWrapper"; import NoSearchImage from "../../assets/images/NoSearchResult.svg"; -import organizationList from "../../mockResponses/OrganisationListResponse"; +import { getNextEntityName } from "utils/AppsmithUtils"; +import Spinner from "components/ads/Spinner"; const OrgDropDown = styled.div` display: flex; @@ -334,7 +334,7 @@ function LeftPane() { heading="ORGANIZATIONS" isFetchingApplications={isFetchingApplications} > - + { } `; -const AddApplicationCard = ( - - - - Create New - - -); const NoSearchResultImg = styled.img` margin: 1em; `; @@ -487,6 +475,7 @@ const ApplicationsSection = (props: any) => { getOnSelectAction(DropdownOnSelectActions.REDIRECT, { path: `/org/${orgId}/settings/general`, @@ -512,6 +501,7 @@ const ApplicationsSection = (props: any) => { }; const createNewApplication = (applicationName: string, orgId: string) => { + console.log(applicationName, orgId); return dispatch({ type: ReduxActionTypes.CREATE_APPLICATION_INIT, payload: { @@ -600,14 +590,40 @@ const ApplicationsSection = (props: any) => { ) && !isFetchingApplications && ( - + { + if ( + Object.entries(creatingApplicationMap).length === 0 + ) { + createNewApplication( + getNextEntityName( + "Untitled application ", + applications.map((el: any) => el.name), + ), + organization.id, + ); + } + }} + > + {creatingApplicationMap && + creatingApplicationMap[organization.id] ? ( + + ) : ( + + + + Create New + + + )} + )} {applications.map((application: any) => { @@ -618,11 +634,6 @@ const ApplicationsSection = (props: any) => { key={application.id} application={application} orgId={organization.id} - activeAppCard={ - props.newApplicationList[ - props.newApplicationList.length - 1 - ] === application.id - } delete={deleteApplication} update={updateApplicationDispatch} duplicate={duplicateApplicationDispatch} @@ -663,14 +674,13 @@ type ApplicationProps = { }; class Applications extends Component< ApplicationProps, - { selectedOrgId: string; newApplicationList: any } + { selectedOrgId: string } > { constructor(props: ApplicationProps) { super(props); this.state = { selectedOrgId: "", - newApplicationList: [], }; } @@ -678,22 +688,6 @@ class Applications extends Component< PerformanceTracker.stopTracking(PerformanceTransactionName.LOGIN_CLICK); PerformanceTracker.stopTracking(PerformanceTransactionName.SIGN_UP); this.props.getAllApplication(); - if (this.props.applicationList.length > 0) { - this.setState({ - newApplicationList: this.props.applicationList.map(el => el.id), - }); - } - } - - componentDidUpdate() { - if ( - this.props.applicationList.length > 0 && - this.props.applicationList.length !== this.state.newApplicationList.length - ) { - this.setState({ - newApplicationList: this.props.applicationList.map(el => el.id), - }); - } } public render() { @@ -707,7 +701,6 @@ class Applications extends Component< }} /> diff --git a/app/client/src/pages/Editor/APIEditor/Form.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx index 3bab489594..961b65d3d7 100644 --- a/app/client/src/pages/Editor/APIEditor/Form.tsx +++ b/app/client/src/pages/Editor/APIEditor/Form.tsx @@ -201,6 +201,7 @@ const ApiEditorForm: React.FC = (props: Props) => { name="actionConfiguration.httpMethod" className="t--apiFormHttpMethod" options={HTTP_METHOD_OPTIONS} + isSearchable={false} /> { - this.setState({ - viewMode: false, - }); this.props.setDatasourceEditorMode( this.props.datasourceId, false, diff --git a/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx b/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx index 1d0adafdf0..29209e4b69 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/FormTitle.tsx @@ -94,6 +94,7 @@ const FormTitle = (props: FormTitleProps) => { return ( { publishApplication, } = props; + const dispatch = useDispatch(); + const isSavingName = useSelector(getIsSavingAppName); + const applicationList = useSelector(getApplicationList); + const handlePublish = () => { if (applicationId) { publishApplication(applicationId); @@ -180,6 +191,10 @@ export const EditorHeader = (props: EditorHeaderProps) => { } } + const updateApplicationDispatch = (id: string, data: { name: string }) => { + dispatch(updateApplication(id, data)); + }; + return ( @@ -192,8 +207,26 @@ export const EditorHeader = (props: EditorHeaderProps) => { - {currentApplication?.name}  - {pageName}  + {currentApplication ? ( + el.id === applicationId).length > 0 + } + onBlur={(value: string) => + updateApplicationDispatch(applicationId || "", { name: value }) + } + /> + ) : null} + {/* {pageName}  */} diff --git a/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx b/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx index 12764f882e..555e7d8790 100644 --- a/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Actions/ActionsGroup.tsx @@ -1,12 +1,10 @@ -import React, { ReactNode, useCallback, memo } from "react"; +import React, { ReactNode, memo } from "react"; import ExplorerActionEntity from "./ActionEntity"; import { Page } from "constants/ReduxActionConstants"; import { ExplorerURLParams, getActionIdFromURL } from "../helpers"; import { ActionGroupConfig } from "./helpers"; import { useParams } from "react-router"; -import EntityPlaceholder from "../Entity/Placeholder"; -import Entity from "../Entity"; -import history from "utils/history"; +import { Plugin } from "api/PluginApi"; type ExplorerActionsGroupProps = { actions: any[]; @@ -14,10 +12,11 @@ type ExplorerActionsGroupProps = { searchKeyword?: string; config: ActionGroupConfig; page: Page; + plugins: Record; }; export const ExplorerActionsGroup = memo((props: ExplorerActionsGroupProps) => { const params = useParams(); - let childNode: ReactNode = props.actions.map((action: any) => { + const childNode: ReactNode = props.actions.map((action: any) => { const url = props.config?.getURL( params.applicationId, props.page.pageId, @@ -26,9 +25,10 @@ export const ExplorerActionsGroup = memo((props: ExplorerActionsGroupProps) => { const actionId = getActionIdFromURL(); const active = actionId === action.config.id; - let method = undefined; - method = action.config.actionConfiguration.httpMethod; - const icon = props.config?.getIcon(method); + const icon = props.config?.getIcon( + action.config, + props.plugins[action.config.datasource.pluginId], + ); return ( { ); }); - if (!props.searchKeyword && (!childNode || !props.actions.length)) { - childNode = ( - - No {props.config?.groupName || "Actions"} yet. Please click the{" "} - + icon on - {props.config?.groupName || "Actions"} above, to - create. - - ); - } - - const switchToCreateActionPage = useCallback(() => { - const path = props.config?.generateCreatePageURL( - params?.applicationId, - props.page.pageId, - props.page.pageId, - ); - history.push(path); - }, [props.config, props.page.pageId, params]); - - return ( - - {childNode} - - ); + return <>{childNode}; }); ExplorerActionsGroup.displayName = "ExplorerActionsGroup"; diff --git a/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx b/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx index a4be0e4ec5..d7f89223a4 100644 --- a/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx +++ b/app/client/src/pages/Editor/Explorer/Actions/helpers.tsx @@ -1,5 +1,5 @@ import React, { ReactNode } from "react"; -import { apiIcon, queryIcon, MethodTag } from "../ExplorerIcons"; +import { apiIcon, dbQueryIcon, MethodTag, QueryIcon } from "../ExplorerIcons"; import { PluginType } from "entities/Action"; import { generateReactKey } from "utils/generators"; import { QUERIES_EDITOR_URL, API_EDITOR_URL } from "constants/routes"; @@ -11,8 +11,10 @@ import { } from "constants/routes"; import { Page } from "constants/ReduxActionConstants"; -import ExplorerActionsGroup from "./ActionsGroup"; import { ExplorerURLParams } from "../helpers"; +import { Datasource } from "api/DatasourcesApi"; +import { Plugin } from "api/PluginApi"; +import PluginGroup from "../PluginGroup/PluginGroup"; export type ActionGroupConfig = { groupName: string; @@ -25,7 +27,7 @@ export type ActionGroupConfig = { pageId: string, selectedPageId: string, ) => string; - getIcon: (method?: string) => ReactNode; + getIcon: (action: any, plugin: Plugin) => ReactNode; isGroupActive: (params: ExplorerURLParams, pageId: string) => boolean; isGroupExpanded: (params: ExplorerURLParams, pageId: string) => boolean; }; @@ -46,7 +48,9 @@ export const ACTION_PLUGIN_MAP: Array< getURL: (applicationId: string, pageId: string, id: string) => { return `${API_EDITOR_ID_URL(applicationId, pageId, id)}`; }, - getIcon: (method?: string) => { + getIcon: (action: any) => { + const method = action.actionConfiguration.httpMethod; + if (!method) return apiIcon; return ; }, @@ -61,14 +65,16 @@ export const ACTION_PLUGIN_MAP: Array< }; case PluginType.DB: return { - groupName: "Queries", + groupName: "DB Queries", type, - icon: queryIcon, + icon: dbQueryIcon, key: generateReactKey(), getURL: (applicationId: string, pageId: string, id: string) => `${QUERIES_EDITOR_ID_URL(applicationId, pageId, id)}`, - getIcon: () => { - return queryIcon; + getIcon: (action: any, plugin: Plugin) => { + if (plugin && plugin.iconLocation) + return ; + return dbQueryIcon; }, generateCreatePageURL: QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID, isGroupActive: (params: ExplorerURLParams, pageId: string) => @@ -84,30 +90,46 @@ export const ACTION_PLUGIN_MAP: Array< } }); -// Gets the Actions groups in the entity explorer -// ACTION_PLUGIN_MAP specifies the number of groups -// APIs, Queries, etc. -export const getActionGroups = ( +export const getPluginGroups = ( page: Page, step: number, - actions?: any[], + actions: any[], + datasources: Datasource[], + plugins: Plugin[], searchKeyword?: string, ) => { return ACTION_PLUGIN_MAP?.map((config?: ActionGroupConfig) => { if (!config) return null; + const entries = actions?.filter( (entry: any) => entry.config.pluginType === config?.type, ); - if (!entries || (entries.length === 0 && !!searchKeyword)) return null; + + const filteredPlugins = plugins.filter( + plugin => plugin.type === config.type, + ); + const filteredPluginIds = filteredPlugins.map(plugin => plugin.id); + const filteredDatasources = datasources.filter(datasource => { + return filteredPluginIds.includes(datasource.pluginId); + }); + + if ( + (!entries && !filteredDatasources) || + (entries.length === 0 && + filteredDatasources.length === 0 && + !!searchKeyword) + ) + return null; return ( - ); }); diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DataSourceContextMenu.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DataSourceContextMenu.tsx index 571ca3c7b3..3fd0d52ac0 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DataSourceContextMenu.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DataSourceContextMenu.tsx @@ -12,6 +12,7 @@ import { initExplorerEntityNameEdit } from "actions/explorerActions"; export const DataSourceContextMenu = (props: { datasourceId: string; + entityId: string; className?: string; }) => { const dispatch = useDispatch(); @@ -19,8 +20,8 @@ export const DataSourceContextMenu = (props: { dispatch(deleteDatasource({ id: props.datasourceId })); }, [dispatch, props.datasourceId]); const editDatasourceName = useCallback( - () => dispatch(initExplorerEntityNameEdit(props.datasourceId)), - [dispatch, props.datasourceId], + () => dispatch(initExplorerEntityNameEdit(props.entityId)), + [dispatch, props.entityId], ); const dispatchRefresh = useCallback(() => { dispatch(refreshDatasourceStructure(props.datasourceId)); diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx index ece7f57fa6..e63898ee35 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx @@ -27,6 +27,7 @@ type ExplorerDatasourceEntityProps = { datasource: Datasource; step: number; searchKeyword?: string; + pageId: string; }; export const ExplorerDatasourceEntity = ( @@ -56,7 +57,7 @@ export const ExplorerDatasourceEntity = ( const active = datasourceIdFromURL === props.datasource.id; const updateDatasourceName = (id: string, name: string) => - saveDatasourceName({ id, name }); + saveDatasourceName({ id: props.datasource.id, name }); const datasourceStructure = useSelector((state: AppState) => { return state.entities.datasources.structure[props.datasource.id]; @@ -79,7 +80,7 @@ export const ExplorerDatasourceEntity = ( return ( diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx index 215142f0b1..8c2ad179b1 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx @@ -50,7 +50,10 @@ export const DatasourceStructure = (props: DatasourceStructureProps) => { const [active, setActive] = useState(false); const lightningMenu = ( - + setActive(!active)} + > @@ -58,48 +61,48 @@ export const DatasourceStructure = (props: DatasourceStructureProps) => { ); - if (dbStructure.templates) - templateMenu = ( - setActive(true)} - onClosed={() => { - setActive(false); - }} - className={`${EntityClassNames.CONTEXT_MENU} t--structure-template-menu`} - minimal - position={Position.RIGHT_TOP} - boundary={"viewport"} - > - {lightningMenu} - - - ); + if (dbStructure.templates) templateMenu = lightningMenu; const columnsAndKeys = dbStructure.columns.concat(dbStructure.keys); return ( - { + if (!nextOpenState) { + setActive(false); + } + }} > - {columnsAndKeys.map((field, index) => { - return ( - - ); - })} - + setActive(!active)} + > + {columnsAndKeys.map((field, index) => { + return ( + + ); + })} + + + ); }; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx deleted file mode 100644 index 14be4e4bb2..0000000000 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourcesGroup.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useMemo } from "react"; -import { datasourceIcon } from "../ExplorerIcons"; -import Entity from "../Entity"; -import { keyBy } from "lodash"; -import { DATA_SOURCES_EDITOR_URL } from "constants/routes"; -import { useParams } from "react-router"; -import { ExplorerURLParams } from "../helpers"; -import history from "utils/history"; - -import { useSelector } from "react-redux"; -import { AppState } from "reducers"; -import { Datasource } from "api/DatasourcesApi"; -import ExplorerDatasourceEntity from "./DatasourceEntity"; - -type ExplorerDatasourcesGroupProps = { - step: number; - searchKeyword?: string; - datasources?: Datasource[]; -}; - -export const ExplorerDatasourcesGroup = ( - props: ExplorerDatasourcesGroupProps, -) => { - const params = useParams(); - const plugins = useSelector((state: AppState) => { - return state.entities.plugins.list; - }); - const { datasources = [] } = props; - const disableDatasourceGroup = !datasources || !datasources.length; - - const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]); - - if (disableDatasourceGroup && props.searchKeyword) return null; - return ( - -1 - } - isDefaultExpanded - disabled={disableDatasourceGroup} - onCreate={() => { - history.push( - DATA_SOURCES_EDITOR_URL(params.applicationId, params.pageId), - ); - }} - > - {datasources.map((datasource: Datasource) => { - return ( - - ); - })} - - ); -}; - -export default ExplorerDatasourcesGroup; diff --git a/app/client/src/pages/Editor/Explorer/ExplorerIcons.tsx b/app/client/src/pages/Editor/Explorer/ExplorerIcons.tsx index e64899c56d..aff9a36ba9 100644 --- a/app/client/src/pages/Editor/Explorer/ExplorerIcons.tsx +++ b/app/client/src/pages/Editor/Explorer/ExplorerIcons.tsx @@ -5,6 +5,7 @@ import { WidgetType } from "constants/WidgetConstants"; import { WidgetIcons } from "icons/WidgetIcons"; import { Plugin } from "api/PluginApi"; import ImageAlt from "assets/images/placeholder-image.svg"; +import QueryImageOutline from "assets/images/query-image-outline.png"; import styled from "styled-components"; import { HTTP_METHODS, @@ -47,11 +48,35 @@ export const apiIcon = ( ); -const QueryIcon = MenuIcons.DATASOURCES_COLORED_ICON; -export const queryIcon = ( - +const DBQueryIcon = MenuIcons.DATASOURCES_COLORED_ICON; +export const dbQueryIcon = ( + ); +const QueryIconWrapper = styled.div` + position: relative; + .inner-image { + position: absolute; + top: 5px; + left: 1.1px; + height: 11px; + width: 11px; + } + .outer-image { + height: 18px; + width: 14px; + } +`; + +export const QueryIcon = (props: { plugin: Plugin }) => { + return ( + + + + + ); +}; + const DataSourceIcon = MenuIcons.DATASOURCES_ICON; export const datasourceIcon = ( { const PluginIcon = styled.img` height: ${ENTITY_ICON_SIZE}px; width: ${ENTITY_ICON_SIZE}px; - margin-right: 4px; `; export const getPluginIcon = (plugin?: Plugin) => { diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx index 8e7aa65992..1c3877606d 100644 --- a/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Pages/PageEntity.tsx @@ -12,18 +12,23 @@ 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 { getPluginGroups } from "../Actions/helpers"; import ExplorerWidgetGroup from "../Widgets/WidgetGroup"; import { resolveAsSpaceChar } from "utils/helpers"; +import { Datasource } from "api/DatasourcesApi"; +import { Plugin } from "api/PluginApi"; type ExplorerPageEntityProps = { page: Page; widgets?: WidgetProps; actions: any[]; + datasources: Datasource[]; + plugins: Plugin[]; step: number; searchKeyword?: string; showWidgetsSidebar: () => void; }; + export const ExplorerPageEntity = (props: ExplorerPageEntityProps) => { const params = useParams(); @@ -76,10 +81,12 @@ export const ExplorerPageEntity = (props: ExplorerPageEntityProps) => { addWidgetsFn={addWidgetsFn} /> - {getActionGroups( + {getPluginGroups( props.page, props.step + 1, props.actions as DataTreeAction[], + props.datasources, + props.plugins, props.searchKeyword, )} diff --git a/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx b/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx index a0f0f05129..306672f9f3 100644 --- a/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Pages/PageGroup.tsx @@ -11,12 +11,16 @@ import { Page } from "constants/ReduxActionConstants"; import ExplorerPageEntity from "./PageEntity"; import { AppState } from "reducers"; import { WidgetProps } from "widgets/BaseWidget"; +import { Datasource } from "api/DatasourcesApi"; +import { Plugin } from "api/PluginApi"; type ExplorerPageGroupProps = { searchKeyword?: string; step: number; widgets?: Record; actions: Record; + datasources: Record; + plugins: Plugin[]; showWidgetsSidebar: () => void; }; @@ -38,13 +42,17 @@ export const ExplorerPageGroup = (props: ExplorerPageGroupProps) => { 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; + const datasources = props.datasources[page.pageId] || []; + if (!pageWidgets && pageActions.length === 0 && datasources.length === 0) + return null; return ( { + const params = useParams(); + const switchToCreateActionPage = useCallback(() => { + const path = props.actionConfig?.generateCreatePageURL( + params?.applicationId, + props.page.pageId, + props.page.pageId, + ); + history.push(path); + }, [props.actionConfig, props.page.pageId, params]); + + const plugins = useSelector((state: AppState) => { + return state.entities.plugins.list; + }); + const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]); + const disableGroup = + !!props.searchKeyword && !props.datasources.length && !props.actions.length; + + const isEmpty = + !props.searchKeyword && !props.datasources.length && !props.actions.length; + + const emptyNode = ( + + No {props.actionConfig?.groupName || "Plugin Groups"} yet. Please click + the + icon on + {props.actionConfig?.groupName || "Plugin Groups"}{" "} + above, to create. + + ); + + return ( + + {isEmpty ? ( + emptyNode + ) : ( + <> + + {props.datasources.map((datasource: Datasource) => { + return ( + + ); + })} + + )} + + ); +}); + +ExplorerPluginGroup.displayName = "ExplorerPluginGroup"; + +export default ExplorerPluginGroup; diff --git a/app/client/src/pages/Editor/Explorer/hooks.ts b/app/client/src/pages/Editor/Explorer/hooks.ts index be94a343de..00e0fca772 100644 --- a/app/client/src/pages/Editor/Explorer/hooks.ts +++ b/app/client/src/pages/Editor/Explorer/hooks.ts @@ -31,22 +31,41 @@ const findWidgets = (widgets: WidgetProps, keyword: string) => { const findDataSources = (dataSources: Datasource[], keyword: string) => { return dataSources.filter( (dataSource: Datasource) => - dataSource.name.toLowerCase().indexOf(keyword) > -1, + dataSource.name.toLowerCase().indexOf(keyword.toLowerCase()) > -1, ); }; export const useFilteredDatasources = (searchKeyword?: string) => { - const dataSources = useSelector((state: AppState) => { + const reducerDatasources = useSelector((state: AppState) => { return state.entities.datasources.list; }); + const actions = useActions(); - return useMemo( - () => - searchKeyword - ? findDataSources(dataSources, searchKeyword.toLowerCase()) - : dataSources, - [searchKeyword, dataSources], - ); + const datasources = useMemo(() => { + const datasourcesPageMap: Record = {}; + for (const [key, value] of Object.entries(actions)) { + const datasourceIds = value.map(action => action.config.datasource?.id); + const activeDatasources = reducerDatasources.filter(datasource => + datasourceIds.includes(datasource.id), + ); + datasourcesPageMap[key] = activeDatasources; + } + + return datasourcesPageMap; + }, [actions, reducerDatasources]); + + return useMemo(() => { + if (searchKeyword) { + const filteredDatasources = produce(datasources, draft => { + for (const [key, value] of Object.entries(draft)) { + draft[key] = findDataSources(value, searchKeyword); + } + }); + return filteredDatasources; + } + + return datasources; + }, [searchKeyword, datasources]); }; export const useActions = (searchKeyword?: string) => { diff --git a/app/client/src/pages/Editor/Explorer/index.tsx b/app/client/src/pages/Editor/Explorer/index.tsx index 2c24b53bb7..ec3b394c3c 100644 --- a/app/client/src/pages/Editor/Explorer/index.tsx +++ b/app/client/src/pages/Editor/Explorer/index.tsx @@ -9,7 +9,6 @@ import { } from "./hooks"; import Search from "./ExplorerSearch"; import ExplorerPageGroup from "./Pages/PageGroup"; -import ExplorerDatasourcesGroup from "./Datasources/DatasourcesGroup"; import { scrollbarDark } from "constants/DefaultTheme"; import { NonIdealState, Classes, IPanelProps } from "@blueprintjs/core"; import WidgetSidebar from "../WidgetSidebar"; @@ -21,6 +20,8 @@ import JSDependencies from "./JSDependencies"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; +import { useSelector } from "react-redux"; +import { getPlugins } from "selectors/entitiesSelector"; const Wrapper = styled.div` height: 100%; @@ -51,6 +52,8 @@ const EntityExplorer = (props: IPanelProps) => { const { searchKeyword, clearSearch } = useFilteredEntities(searchInputRef); const datasources = useFilteredDatasources(searchKeyword); + const plugins = useSelector(getPlugins); + const widgets = useWidgets(searchKeyword); const actions = useActions(searchKeyword); @@ -60,8 +63,10 @@ const EntityExplorer = (props: IPanelProps) => { const noActions = Object.values(actions).filter(actions => actions && actions.length > 0) .length === 0; - - const noDatasource = !datasources || datasources.length === 0; + const noDatasource = + Object.values(datasources).filter( + datasources => datasources && datasources.length > 0, + ).length === 0; noResults = noWidgets && noActions && noDatasource; } const { openPanel } = props; @@ -77,6 +82,8 @@ const EntityExplorer = (props: IPanelProps) => { step={0} widgets={widgets} actions={actions} + datasources={datasources} + plugins={plugins} showWidgetsSidebar={showWidgetsSidebar} /> {noResults && ( @@ -88,12 +95,6 @@ const EntityExplorer = (props: IPanelProps) => { /> )} - - ); diff --git a/app/client/src/pages/Editor/PageListSidebar/PageListItem.tsx b/app/client/src/pages/Editor/PageListSidebar/PageListItem.tsx index c6a7e3ba4a..8f0f06a1ac 100644 --- a/app/client/src/pages/Editor/PageListSidebar/PageListItem.tsx +++ b/app/client/src/pages/Editor/PageListSidebar/PageListItem.tsx @@ -100,6 +100,7 @@ const PageListItem = withTheme((props: PageListItemProps) => { editInteractionKind={EditInteractionKind.DOUBLE} onTextChanged={onEditPageName} hideEditIcon + className="t--page-list-item" />
diff --git a/app/client/src/pages/Editor/PropertyPaneTitle.tsx b/app/client/src/pages/Editor/PropertyPaneTitle.tsx index f4e25fd825..f1393f7aee 100644 --- a/app/client/src/pages/Editor/PropertyPaneTitle.tsx +++ b/app/client/src/pages/Editor/PropertyPaneTitle.tsx @@ -116,6 +116,7 @@ const PropertyPaneTitle = memo((props: PropertyPaneTitleProps) => { onBlur={exitEditMode} hideEditIcon minimal + className="t--propery-page-title" /> {updating && } diff --git a/app/client/src/pages/Editor/QueryEditor/AddDatasourceSecurely.tsx b/app/client/src/pages/Editor/QueryEditor/AddDatasourceSecurely.tsx new file mode 100644 index 0000000000..de64141b3b --- /dev/null +++ b/app/client/src/pages/Editor/QueryEditor/AddDatasourceSecurely.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import styled from "styled-components"; +import Secure from "assets/images/secure.svg"; +import AppsmithDatasource from "assets/images/appsmith-datasource.svg"; +import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; +import { Colors } from "constants/Colors"; + +const Wrapper = styled.div` + border: 2px solid #d6d6d6; + padding: 23px; + flex-direction: row; + display: flex; +`; + +const Header = styled.div` + font-weight: 600; + font-size: 24px; + color: ${Colors.OXFORD_BLUE}; + margin-top: 8px; +`; + +const Content = styled.div` + margin-top: 8px; + color: ${Colors.OXFORD_BLUE}; + max-width: 65%; +`; + +const ActionButton = styled(BaseButton)` + &&& { + max-width: 155px; + max-height: 36px; + margin-top: 18px; + } +`; + +type AddDatasourceSecurelyProps = { + onAddDatasource: () => void; +}; + +const AddDatasourceSecurely = (props: AddDatasourceSecurelyProps) => { + return ( + +
+ +
Secure & fast database connection
+ + Connect your database to start building read/write workflows. Your + Passwords are fully encrypted and we never store any of your data. + Thatโ€™s a promise. + + +
+ +
+ ); +}; + +export default AddDatasourceSecurely; diff --git a/app/client/src/pages/Editor/QueryEditor/DatasourceCard.tsx b/app/client/src/pages/Editor/QueryEditor/DatasourceCard.tsx index fb1e4a5b1f..2ff392fdd0 100644 --- a/app/client/src/pages/Editor/QueryEditor/DatasourceCard.tsx +++ b/app/client/src/pages/Editor/QueryEditor/DatasourceCard.tsx @@ -2,16 +2,20 @@ import { Datasource } from "api/DatasourcesApi"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import React from "react"; import { isNil } from "lodash"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { Colors } from "constants/Colors"; +import { useParams } from "react-router"; import { getPluginImages, getQueryActionsForCurrentPage, } from "selectors/entitiesSelector"; import styled from "styled-components"; import { AppState } from "reducers"; +import history from "utils/history"; import { renderDatasourceSection } from "pages/Editor/DataSourceEditor/DatasourceSection"; +import { DATA_SOURCES_EDITOR_ID_URL } from "constants/routes"; +import { setDatsourceEditorMode } from "actions/datasourceActions"; const Wrapper = styled.div` border: 2px solid #d6d6d6; @@ -20,9 +24,9 @@ const Wrapper = styled.div` `; const ActionButton = styled(BaseButton)` - &&& { + &&&& { + height: 36px; max-width: 120px; - min-height: 36px; } `; @@ -31,6 +35,14 @@ const DatasourceImage = styled.img` width: auto; `; +const EditDatasourceButton = styled(BaseButton)` + &&&& { + height: 36px; + max-width: 160px; + border: 1px solid ${Colors.GEYSER_LIGHT}; + } +`; + const DatasourceName = styled.span` margin-left: 10px; font-size: 16px; @@ -55,13 +67,23 @@ const Queries = styled.div` margin-top: 11px; `; +const ButtonsWrapper = styled.div` + flex-direction: row; + display: inline-flex; + gap: 10px; + flex: 1; + justify-content: flex-end; +`; + type DatasourceCardProps = { datasource: Datasource; onCreateQuery: (datasource: Datasource) => void; }; const DatasourceCard = (props: DatasourceCardProps) => { + const dispatch = useDispatch(); const pluginImages = useSelector(getPluginImages); + const params = useParams<{ applicationId: string; pageId: string }>(); const { datasource } = props; const datasourceFormConfigs = useSelector( (state: AppState) => state.entities.plugins.formConfigs, @@ -75,6 +97,17 @@ const DatasourceCard = (props: DatasourceCardProps) => { datasourceFormConfigs[datasource?.pluginId ?? ""]; const QUERY = queriesWithThisDatasource > 1 ? "queries" : "query"; + const editDatasource = () => { + dispatch(setDatsourceEditorMode({ id: datasource.id, viewMode: false })); + history.push( + DATA_SOURCES_EDITOR_ID_URL( + params.applicationId, + params.pageId, + datasource.id, + ), + ); + }; + return ( @@ -93,14 +126,22 @@ const DatasourceCard = (props: DatasourceCardProps) => { : "No query is using this datasource"} - props.onCreateQuery(datasource)} - /> + + + props.onCreateQuery(datasource)} + /> + {!isNil(currentFormConfig) ? renderDatasourceSection(currentFormConfig[0], datasource) diff --git a/app/client/src/pages/Editor/QueryEditor/Form.tsx b/app/client/src/pages/Editor/QueryEditor/Form.tsx index d6f87855a2..c9c780d579 100644 --- a/app/client/src/pages/Editor/QueryEditor/Form.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Form.tsx @@ -26,7 +26,6 @@ import { RestAction } from "entities/Action"; import { connect, useDispatch } from "react-redux"; import { AppState } from "reducers"; import ActionNameEditor from "components/editorComponents/ActionNameEditor"; -import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleHelp"; import { getPluginResponseTypes, getPluginDocumentationLinks, @@ -40,11 +39,10 @@ import { queryActionSettingsConfig } from "mockResponses/ActionSettings"; import { addTableWidgetFromQuery } from "actions/widgetActions"; const QueryFormContainer = styled.div` - padding: 20px 32px; + padding: 20px 0px; width: 100%; - display: flex; - flex-direction: column; height: calc(100vh - ${props => props.theme.headerHeight}); + overflow: auto; a { font-size: 14px; line-height: 20px; @@ -206,32 +204,33 @@ const NameWrapper = styled.div` } `; -const CollapsibleWrapper = styled.div` - width: 200px; -`; - const LoadingContainer = styled(CenteredWrapper)` height: 50%; `; const TabContainerView = styled.div` - height: calc(100vh / 3); - .react-tabs__tab-panel { - border: 1px solid #ebeff2; overflow: scroll; } .react-tabs__tab-list { margin: 0px; } + &&& { + ul.react-tabs__tab-list { + padding-left: 23px; + } + } + position: relative; + margin-top: 31px; `; const SettingsWrapper = styled.div` - padding: 5px 10px; + padding: 5px 23px; `; const AddWidgetButton = styled(BaseButton)` &&&& { + height: 36px; max-width: 125px; border: 1px solid ${Colors.GEYSER_LIGHT}; } @@ -241,7 +240,7 @@ const OutputHeader = styled.div` flex-direction: row; justify-content: space-between; display: flex; - margin-bottom: 10px; + margin: 10px 0px; align-items: center; `; @@ -249,6 +248,20 @@ const FieldWrapper = styled.div` margin-top: 15px; `; +const StyledFormRow = styled(FormRow)` + padding: 0px 24px; +`; + +const DocumentationLink = styled.a` + position: absolute; + right: 23px; + top: -6px; +`; + +const OutputWrapper = styled.div` + margin: 0px 23px; +`; + type QueryFormProps = { onDeleteClick: () => void; onRunClick: () => void; @@ -380,7 +393,7 @@ const QueryEditorForm: React.FC = (props: Props) => { return (
- + @@ -454,34 +467,19 @@ const QueryEditorForm: React.FC = (props: Props) => { )} - - -
-

Query Statement

- - {documentationLink && ( - - - - {"Documentation "} - - - - - )} -
+ + {documentationLink && ( + + {"Documentation "} + + + )} = (props: Props) => { )} {error && ( - <> +

Query error

{error} - +
)} {!error && output && dataSources.length && ( - <> +

{output.length ? "Query response" : "No data records to display"} @@ -567,7 +565,7 @@ const QueryEditorForm: React.FC = (props: Props) => { )} {isSQL ?

: } - + )} ); diff --git a/app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx b/app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx index e1ec2dc53d..e9d32dff9f 100644 --- a/app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx +++ b/app/client/src/pages/Editor/QueryEditor/QueryHomeScreen.tsx @@ -13,6 +13,7 @@ import { QUERY_EDITOR_URL_WITH_SELECTED_PAGE_ID, DATA_SOURCES_EDITOR_URL, } from "constants/routes"; +import AddDatasourceSecurely from "./AddDatasourceSecurely"; import { QueryAction } from "entities/Action"; import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; import DatasourceCard from "./DatasourceCard"; @@ -20,6 +21,7 @@ import { fetchDBPluginForms } from "actions/pluginActions"; const QueryHomePage = styled.div` padding: 20px; + padding-top: 30px; overflow: auto; display: flex; flex-direction: column; @@ -131,15 +133,24 @@ class QueryHomeScreen extends React.Component { Select a datasource to query or create a new one

- { - history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId)); - }} - fill - minimal - text="New Datasource" - icon={"plus"} - /> + {dataSources.length < 2 ? ( + { + history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId)); + }} + /> + ) : ( + { + history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId)); + }} + fill + minimal + text="New Datasource" + icon={"plus"} + /> + )} {dataSources.map(datasource => { return ( []; } -const StyledTableWrapped = styled(TableWrapper)` - min-height: 0px; - height: 100%; +const TABLE_SIZES = { + COLUMN_HEADER_HEIGHT: 38, + TABLE_HEADER_HEIGHT: 42, + ROW_HEIGHT: 40, + ROW_FONT_SIZE: 14, +}; + +export const TableWrapper = styled.div` + width: 100%; + height: auto; + background: white; + border: 1px solid ${Colors.GEYSER_LIGHT}; + box-sizing: border-box; + display: flex; + justify-content: space-between; + flex-direction: column; + overflow: hidden; .tableWrap { - display: flex; - flex: 1; + height: 100%; + display: block; + overflow-x: auto; + overflow-y: hidden; } .table { - display: flex; - flex: 1; - flex-direction: column; - height: 100%; + border-spacing: 0; + color: ${Colors.THUNDER}; + position: relative; + background: ${Colors.ATHENS_GRAY_DARKER}; display: table; width: 100%; + .thead, .tbody { + overflow: hidden; + } + .tbody { + overflow-y: scroll; + height: auto; + .tr { + width: 100%; + } + } + .tr { + overflow: hidden; + :nth-child(even) { + background: ${Colors.ATHENS_GRAY_DARKER}; + } + :nth-child(odd) { + background: ${Colors.WHITE}; + } + &.selected-row { + background: ${Colors.POLAR}; + &:hover { + background: ${Colors.POLAR}; + } + } + &:hover { + background: ${Colors.ATHENS_GRAY}; + } + } + .th, + .td { + margin: 0; + padding: 9px 10px; + border-bottom: 1px solid ${Colors.GEYSER_LIGHT}; + border-right: 1px solid ${Colors.GEYSER_LIGHT}; + position: relative; + font-size: ${TABLE_SIZES.ROW_FONT_SIZE}px; + line-height: ${TABLE_SIZES.ROW_FONT_SIZE}px; + :last-child { + border-right: 0; + } + .resizer { + display: inline-block; + width: 10px; + height: 100%; + position: absolute; + right: 0; + top: 0; + transform: translateX(50%); + z-index: 1; + ${"" /* prevents from scrolling while dragging on touch devices */} + touch-action:none; + &.isResizing { + cursor: isResizing; + } + } + } + .th { + padding: 0 10px 0 0; + height: ${TABLE_SIZES.COLUMN_HEADER_HEIGHT}px; + line-height: ${TABLE_SIZES.COLUMN_HEADER_HEIGHT}px; + background: ${Colors.ATHENS_GRAY_DARKER}; + } + .td { + height: ${TABLE_SIZES.ROW_HEIGHT}px; + line-height: ${TABLE_SIZES.ROW_HEIGHT}px; + padding: 0 10px; + } + } + .draggable-header, + .hidden-header { + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + color: ${Colors.OXFORD_BLUE}; + font-weight: 500; + padding-left: 10px; + &.sorted { + padding-left: 5px; + } + } + .draggable-header { + cursor: pointer; + &.reorder-line { + width: 1px; height: 100%; - overflow: auto; + } + } + .hidden-header { + opacity: 0.6; + } + .column-menu { + cursor: pointer; + height: ${TABLE_SIZES.COLUMN_HEADER_HEIGHT}px; + line-height: ${TABLE_SIZES.COLUMN_HEADER_HEIGHT}px; + } + .th { + display: flex; + justify-content: space-between; + &.highlight-left { + border-left: 2px solid ${Colors.GREEN}; + } + &.highlight-right { + border-right: 2px solid ${Colors.GREEN}; } } `; @@ -100,11 +214,7 @@ const Table = (props: TableProps) => { if (rows.length === 0 || headerGroups.length === 0) return null; return ( - +
{headerGroups.map((headerGroup: any, index: number) => ( @@ -156,7 +266,7 @@ const Table = (props: TableProps) => {
-
+ ); }; diff --git a/app/client/src/pages/common/ServerUnavailable.tsx b/app/client/src/pages/common/ServerUnavailable.tsx index 915cbaabc0..49504fac71 100644 --- a/app/client/src/pages/common/ServerUnavailable.tsx +++ b/app/client/src/pages/common/ServerUnavailable.tsx @@ -1,6 +1,9 @@ import React from "react"; import styled from "styled-components"; +import Button from "components/editorComponents/Button"; import PageUnavailableImage from "assets/images/404-image.png"; +import { APPLICATIONS_URL } from "constants/routes"; +import history from "utils/history"; const Wrapper = styled.div` text-align: center; @@ -28,6 +31,16 @@ const ServerUnavailable = () => {

Appsmith server is unavailable

Please try again after some time

+
); diff --git a/app/client/src/pages/organization/General.tsx b/app/client/src/pages/organization/General.tsx index 9e2448a7fa..480ee65103 100644 --- a/app/client/src/pages/organization/General.tsx +++ b/app/client/src/pages/organization/General.tsx @@ -86,6 +86,7 @@ export function GeneralSettings() { placeholder="Workspace name" onChange={onWorkspaceNameChange} defaultValue={currentOrg.name} + cypressSelector="t--org-name-input" > )} @@ -100,6 +101,7 @@ export function GeneralSettings() { placeholder="Your website" onChange={onWebsiteChange} defaultValue={currentOrg.website || ""} + cypressSelector="t--org-website-input" > )} @@ -115,6 +117,7 @@ export function GeneralSettings() { placeholder="Email" onChange={onEmailChange} defaultValue={currentOrg.email || ""} + cypressSelector="t--org-email-input" > )} diff --git a/app/client/src/pages/organization/OrgInviteUsersForm.tsx b/app/client/src/pages/organization/OrgInviteUsersForm.tsx index 18aabd68e1..d126b63665 100644 --- a/app/client/src/pages/organization/OrgInviteUsersForm.tsx +++ b/app/client/src/pages/organization/OrgInviteUsersForm.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, useEffect } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import styled from "styled-components"; import { useLocation } from "react-router-dom"; import TagListField from "components/editorComponents/form/fields/TagListField"; @@ -204,12 +204,23 @@ const validate = (values: any) => { errors["role"] = INVITE_USERS_VALIDATION_ROLE_EMPTY; } + if (values.users && values.users.length > 0) { + const _users = values.users.split(",").filter(Boolean); + + _users.forEach((user: string) => { + if (!isEmail(user)) { + errors["users"] = INVITE_USERS_VALIDATION_EMAIL_LIST; + } + }); + } return errors; }; const { mailEnabled } = getAppsmithConfigs(); const OrgInviteUsersForm = (props: any) => { + const [emailError, setEmailError] = useState(""); + const { handleSubmit, allUsers, @@ -296,6 +307,7 @@ const OrgInviteUsersForm = (props: any) => { label="Emails" intent="success" data-cy="t--invite-email-input" + customError={(err: string) => setEmailError(err)} /> { fill /> )} - {submitFailed && error && ( - + {((submitFailed && error) || emailError) && ( + )} {!pathRegex.test(currentPath) && canManage && ( diff --git a/app/client/src/pages/organization/settings.tsx b/app/client/src/pages/organization/settings.tsx index 1c4e24fb5e..edcf0b5567 100644 --- a/app/client/src/pages/organization/settings.tsx +++ b/app/client/src/pages/organization/settings.tsx @@ -81,7 +81,9 @@ export default function Settings() { - {currentOrg.name} + + {currentOrg.name} + , + ) => { + const datasources = action.payload; + + return state.map(action => { + const datasourceId = action.config.datasource.id; + if (datasourceId) { + const datasource = datasources.find( + datasource => datasource.id === datasourceId, + ); + + return { + ...action, + config: { + ...action.config, + datasource: datasource || action.config.datasource, // fallback to original datasource if datasource not available. + }, + }; + } + + return action; + }); + }, + [ReduxActionTypes.UPDATE_DATASOURCE_SUCCESS]: ( + state: ActionDataState, + action: ReduxAction, + ) => { + const datasource = action.payload; + + return state.map(action => { + const datasourceId = action.config.datasource.id; + if (datasourceId && datasource.id === datasourceId) { + return { + ...action, + config: { + ...action.config, + datasource: datasource, + }, + }; + } + + return action; + }); + }, }); export default actionsReducer; diff --git a/app/client/src/reducers/entityReducers/pageListReducer.tsx b/app/client/src/reducers/entityReducers/pageListReducer.tsx index e7fbb3bd78..5a8a753a6d 100644 --- a/app/client/src/reducers/entityReducers/pageListReducer.tsx +++ b/app/client/src/reducers/entityReducers/pageListReducer.tsx @@ -10,7 +10,7 @@ const initialState: PageListReduxState = { pages: [], }; -const pageListReducer = createReducer(initialState, { +export const pageListReducer = createReducer(initialState, { [ReduxActionTypes.DELETE_PAGE_INIT]: ( state: PageListReduxState, action: ReduxAction<{ id: string }>, diff --git a/app/client/src/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/reducers/uiReducers/applicationsReducer.tsx index e8ebe8f7fb..56228c84b3 100644 --- a/app/client/src/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/reducers/uiReducers/applicationsReducer.tsx @@ -222,7 +222,7 @@ const applicationsReducer = createReducer(initialState, { return { ...state, userOrgs: _organizations, - isSavingAppName: isSavingAppName, + isSavingAppName: true, }; }, [ReduxActionTypes.UPDATE_APPLICATION_SUCCESS]: ( diff --git a/app/client/src/reducers/uiReducers/explorerReducer.ts b/app/client/src/reducers/uiReducers/explorerReducer.ts index c888f08175..a1ec33ddfd 100644 --- a/app/client/src/reducers/uiReducers/explorerReducer.ts +++ b/app/client/src/reducers/uiReducers/explorerReducer.ts @@ -27,6 +27,23 @@ const setEntityUpdateSuccess = () => { return {}; }; +const setUpdatingDatasourceEntity = ( + state: ExplorerReduxState, + action: ReduxAction<{ id: string }>, +) => { + const pathParts = window.location.pathname.split("/"); + const pageId = pathParts[pathParts.indexOf("pages") + 1]; + + if (!state.updatingEntity?.includes(action.payload.id)) { + return { + updatingEntity: `${action.payload.id}-${pageId}`, + updateEntityError: undefined, + }; + } + + return state; +}; + const explorerReducer = createReducer(initialState, { [ReduxActionTypes.FETCH_PAGE_INIT]: setUpdatingEntity, [ReduxActionTypes.FETCH_PAGE_ERROR]: setEntityUpdateError, @@ -48,19 +65,19 @@ const explorerReducer = createReducer(initialState, { [ReduxActionErrorTypes.DELETE_ACTION_ERROR]: setEntityUpdateError, [ReduxActionTypes.DELETE_ACTION_SUCCESS]: setEntityUpdateSuccess, - [ReduxActionTypes.DELETE_DATASOURCE_INIT]: setUpdatingEntity, + [ReduxActionTypes.DELETE_DATASOURCE_INIT]: setUpdatingDatasourceEntity, [ReduxActionErrorTypes.DELETE_DATASOURCE_ERROR]: setEntityUpdateError, [ReduxActionTypes.DELETE_DATASOURCE_SUCCESS]: setEntityUpdateSuccess, - [ReduxActionTypes.UPDATE_DATASOURCE_INIT]: setUpdatingEntity, + [ReduxActionTypes.UPDATE_DATASOURCE_INIT]: setUpdatingDatasourceEntity, [ReduxActionErrorTypes.UPDATE_DATASOURCE_ERROR]: setEntityUpdateError, [ReduxActionTypes.UPDATE_DATASOURCE_SUCCESS]: setEntityUpdateSuccess, - [ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_INIT]: setUpdatingEntity, + [ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_INIT]: setUpdatingDatasourceEntity, [ReduxActionErrorTypes.FETCH_DATASOURCE_STRUCTURE_ERROR]: setEntityUpdateError, [ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_SUCCESS]: setEntityUpdateSuccess, - [ReduxActionTypes.REFRESH_DATASOURCE_STRUCTURE_INIT]: setUpdatingEntity, + [ReduxActionTypes.REFRESH_DATASOURCE_STRUCTURE_INIT]: setUpdatingDatasourceEntity, [ReduxActionErrorTypes.REFRESH_DATASOURCE_STRUCTURE_ERROR]: setEntityUpdateError, [ReduxActionTypes.REFRESH_DATASOURCE_STRUCTURE_SUCCESS]: setEntityUpdateSuccess, diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 46e69d5ebd..5581f621a3 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -74,7 +74,11 @@ import { getType, Types } from "utils/TypeHelpers"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; -import { getCurrentApplication } from "selectors/applicationSelectors"; +import { APP_MODE } from "reducers/entityReducers/appReducer"; +import { + getAppMode, + getCurrentApplication, +} from "selectors/applicationSelectors"; import { evaluateDynamicTrigger, evaluateSingleValue } from "./evaluationsSaga"; function* navigateActionSaga( @@ -321,10 +325,13 @@ export function* executeActionSaga( : event.type === EventType.ON_PREV_PAGE ? "PREV" : undefined; + const appMode = yield select(getAppMode); + const executeActionRequest: ExecuteActionRequest = { - action: { id: actionId }, + actionId: actionId, params: actionParams, paginationField: pagination, + viewMode: appMode === APP_MODE.PUBLISHED, }; const timeout = yield select(getActionTimeout, actionId); const response: ActionApiResponse = yield ActionAPI.executeAction( @@ -529,18 +536,20 @@ function* runActionSaga( yield take(ReduxActionTypes.UPDATE_ACTION_SUCCESS); } const actionObject = yield select(getAction, actionId); - const action: ExecuteActionRequest["action"] = { id: actionId }; const jsonPathKeys = actionObject.jsonPathKeys; const { paginationField } = reduxAction.payload; const params = yield call(getActionParams, jsonPathKeys); const timeout = yield select(getActionTimeout, actionId); + const appMode = yield select(getAppMode); + const viewMode = appMode === APP_MODE.PUBLISHED; const response: ActionApiResponse = yield ActionAPI.executeAction( { - action, + actionId, params, paginationField, + viewMode, }, timeout, ); @@ -623,9 +632,12 @@ function* executePageLoadAction(pageAction: PageAction) { getActionParams, pageAction.jsonPathKeys, ); + const appMode = yield select(getAppMode); + const viewMode = appMode === APP_MODE.PUBLISHED; const executeActionRequest: ExecuteActionRequest = { - action: { id: pageAction.id }, + actionId: pageAction.id, params, + viewMode, }; AnalyticsUtil.logEvent("EXECUTE_ACTION", { type: pageAction.pluginType, diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index fb845d99db..6323d95002 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -50,6 +50,7 @@ import { ActionData } from "reducers/entityReducers/actionsReducer"; import { getAction, getCurrentPageNameByActionId, + getDatasource, getPageNameByPageId, } from "selectors/entitiesSelector"; import { PLUGIN_TYPE_API } from "constants/ApiEditorConstants"; @@ -87,7 +88,19 @@ export function* createActionSaga(actionPayload: ReduxAction) { pageName: pageName, ...actionPayload.payload.eventData, }); - yield put(createActionSuccess(response.data)); + + let newAction = response.data; + + if (newAction.datasource.id) { + const datasource = yield select(getDatasource, newAction.datasource.id); + + newAction = { + ...newAction, + datasource, + }; + } + + yield put(createActionSuccess(newAction)); } } catch (error) { yield put({ @@ -234,10 +247,26 @@ export function* updateActionSaga(actionPayload: ReduxAction<{ id: string }>) { pageName: pageName, }); } + + let updatedAction = response.data; + + if (updatedAction.datasource.id) { + const datasource = yield select( + getDatasource, + updatedAction.datasource.id, + ); + + updatedAction = { + ...updatedAction, + datasource, + }; + } + PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.UPDATE_ACTION_API, ); - yield put(updateActionSuccess({ data: response.data })); + + yield put(updateActionSuccess({ data: updatedAction })); } } catch (error) { PerformanceTracker.stopAsyncTracking( diff --git a/app/client/src/sagas/ApplicationSagas.tsx b/app/client/src/sagas/ApplicationSagas.tsx index cf79512d42..4179f117fa 100644 --- a/app/client/src/sagas/ApplicationSagas.tsx +++ b/app/client/src/sagas/ApplicationSagas.tsx @@ -1,28 +1,28 @@ import { - ReduxActionTypes, - ReduxActionErrorTypes, - ReduxAction, ApplicationPayload, + ReduxAction, + ReduxActionErrorTypes, + ReduxActionTypes, } from "constants/ReduxActionConstants"; import ApplicationApi, { - PublishApplicationResponse, - PublishApplicationRequest, - FetchApplicationsResponse, + ApplicationObject, + ApplicationPagePayload, + ApplicationResponsePayload, + ChangeAppViewAccessRequest, CreateApplicationRequest, CreateApplicationResponse, - ApplicationResponsePayload, - ApplicationPagePayload, - SetDefaultPageRequest, DeleteApplicationRequest, + DuplicateApplicationRequest, + FetchApplicationsResponse, FetchUsersApplicationsOrgsResponse, OrganizationApplicationObject, - ApplicationObject, - ChangeAppViewAccessRequest, - DuplicateApplicationRequest, + PublishApplicationRequest, + PublishApplicationResponse, + SetDefaultPageRequest, UpdateApplicationRequest, } from "api/ApplicationApi"; import { getDefaultPageId } from "./SagaUtils"; -import { call, put, takeLatest, all, select } from "redux-saga/effects"; +import { all, call, put, select, takeLatest } from "redux-saga/effects"; import { validateResponse } from "./ErrorSagas"; import { getUserApplicationsOrgsList } from "selectors/applicationSelectors"; @@ -30,13 +30,17 @@ import { ApiResponse } from "api/ApiResponses"; import history from "utils/history"; import { BUILDER_PAGE_URL } from "constants/routes"; import { AppState } from "reducers"; -import { setDefaultApplicationPageSuccess } from "actions/applicationActions"; +import { + FetchApplicationPayload, + setDefaultApplicationPageSuccess, +} from "actions/applicationActions"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { AppToaster } from "components/editorComponents/ToastComponent"; import { - DUPLICATING_APPLICATION, DELETING_APPLICATION, + DUPLICATING_APPLICATION, } from "constants/messages"; +import { APP_MODE } from "../reducers/entityReducers/appReducer"; import { Organization } from "constants/orgConstants"; export function* publishApplicationSaga( @@ -134,14 +138,18 @@ export function* fetchApplicationListSaga() { } export function* fetchApplicationSaga( - action: ReduxAction<{ - applicationId: string; - }>, + action: ReduxAction, ) { try { - const applicationId: string = action.payload.applicationId; + const { mode, applicationId } = action.payload; + // Get endpoint based on app mode + const apiEndpoint = + mode === APP_MODE.EDIT + ? ApplicationApi.fetchApplication + : ApplicationApi.fetchApplicationForViewMode; + const response: FetchApplicationsResponse = yield call( - ApplicationApi.fetchApplication, + apiEndpoint, applicationId, ); @@ -204,6 +212,10 @@ export function* updateApplicationSaga( type: ReduxActionTypes.UPDATE_APPLICATION_SUCCESS, payload: response.data, }); + AppToaster.show({ + message: `Application updated`, + type: "success", + }); } } catch (error) { yield put({ @@ -212,6 +224,10 @@ export function* updateApplicationSaga( error, }, }); + AppToaster.show({ + message: error, + type: "error", + }); } } diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index f10d4ce8c6..8c312abef8 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -45,14 +45,15 @@ function* initializeEditorSaga( initializeEditorAction: ReduxAction, ) { const { applicationId, pageId } = initializeEditorAction.payload; + // Step 1: Set App Mode. Start getting all the data needed + yield put(setAppMode(APP_MODE.EDIT)); yield put({ type: ReduxActionTypes.START_EVALUATION }); - // Step 1: Start getting all the data needed by the yield all([ - put(fetchPageList(applicationId)), + put(fetchPageList(applicationId, APP_MODE.EDIT)), put(fetchEditorConfigs()), put(fetchActions(applicationId)), put(fetchPage(pageId)), - put(fetchApplication(applicationId)), + put(fetchApplication(applicationId, APP_MODE.EDIT)), ]); // Step 2: Wait for all data to be in the state yield all([ @@ -71,8 +72,7 @@ function* initializeEditorSaga( take(ReduxActionTypes.FETCH_DATASOURCES_SUCCESS), ]); - // Step 5: Set app mode - yield put(setAppMode(APP_MODE.EDIT)); + // Step 5: Set app store yield put(updateAppStore(getAppStore(applicationId))); const currentApplication = yield select(getCurrentApplication); @@ -152,18 +152,22 @@ export function* initializeAppViewerSaga( action: ReduxAction<{ applicationId: string; pageId: string }>, ) { const { applicationId, pageId } = action.payload; + yield put(setAppMode(APP_MODE.PUBLISHED)); yield put({ type: ReduxActionTypes.START_EVALUATION }); yield all([ + // TODO (hetu) Remove spl view call for fetch actions put(fetchActionsForView(applicationId)), - put(fetchPageList(applicationId)), - put(fetchApplication(applicationId)), + put(fetchPageList(applicationId, APP_MODE.PUBLISHED)), + put(fetchApplication(applicationId, APP_MODE.PUBLISHED)), ]); yield all([ take(ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS), take(ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS), + take(ReduxActionTypes.FETCH_APPLICATION_SUCCESS), ]); + yield put(updateAppStore(getAppStore(applicationId))); const defaultPageId = yield select(getDefaultPageId); const toLoadPageId = pageId || defaultPageId; diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index 9e0b5a8147..3dd72719fd 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -1,7 +1,6 @@ import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer"; import { AppState } from "reducers"; import { - FetchPageListPayload, PageListPayload, ReduxAction, ReduxActionErrorTypes, @@ -9,8 +8,9 @@ import { UpdateCanvasPayload, } from "constants/ReduxActionConstants"; import { - deletePageSuccess, clonePageSuccess, + deletePageSuccess, + FetchPageListPayload, fetchPageSuccess, fetchPublishedPageSuccess, savePageSuccess, @@ -20,6 +20,7 @@ import { updateWidgetNameSuccess, } from "actions/pageActions"; import PageApi, { + ClonePageRequest, CreatePageRequest, DeletePageRequest, FetchPageListResponse, @@ -32,7 +33,6 @@ import PageApi, { UpdatePageRequest, UpdateWidgetNameRequest, UpdateWidgetNameResponse, - ClonePageRequest, } from "api/PageApi"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { @@ -69,8 +69,8 @@ import { fetchActionsForPage, setActionsToExecuteOnPageLoad, } from "actions/actionActions"; +import { APP_MODE, UrlDataState } from "reducers/entityReducers/appReducer"; import { clearEvalCache } from "./evaluationsSaga"; -import { UrlDataState } from "reducers/entityReducers/appReducer"; import { getQueryParams } from "utils/AppsmithUtils"; import PerformanceTracker, { PerformanceTransactionName, @@ -86,11 +86,12 @@ export function* fetchPageListSaga( PerformanceTransactionName.FETCH_PAGE_LIST_API, ); try { - const { applicationId } = fetchPageListAction.payload; - const response: FetchPageListResponse = yield call( - PageApi.fetchPageList, - applicationId, - ); + const { applicationId, mode } = fetchPageListAction.payload; + const apiCall = + mode === APP_MODE.EDIT + ? PageApi.fetchPageList + : PageApi.fetchPageListViewMode; + const response: FetchPageListResponse = yield call(apiCall, applicationId); const isValidResponse = yield validateResponse(response); if (isValidResponse) { const orgId = response.data.organizationId; diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts index 3ad47c7f14..087d3d4f7b 100644 --- a/app/client/src/sagas/QueryPaneSagas.ts +++ b/app/client/src/sagas/QueryPaneSagas.ts @@ -23,12 +23,14 @@ import { getAction, getPluginEditorConfigs, getDatasource, + getPluginTemplates, } from "selectors/entitiesSelector"; import { RestAction } from "entities/Action"; import { setActionProperty } from "actions/actionActions"; import { fetchPluginForm } from "actions/pluginActions"; import { getQueryParams } from "utils/AppsmithUtils"; import { QUERY_CONSTANT } from "constants/QueryEditorConstants"; +import { isEmpty } from "lodash"; function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) { const { id } = actionPayload.payload; @@ -52,8 +54,8 @@ function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) { return; } - if (!editorConfigs[action.pluginId]) { - yield put(fetchPluginForm({ id: action.pluginId })); + if (!editorConfigs[action.datasource.pluginId]) { + yield put(fetchPluginForm({ id: action.datasource.pluginId })); } yield put(initialize(QUERY_EDITOR_FORM_NAME, action)); @@ -104,10 +106,17 @@ function* handleQueryCreatedSaga(actionPayload: ReduxAction) { yield put(initialize(QUERY_EDITOR_FORM_NAME, data)); const applicationId = yield select(getCurrentApplicationId); const pageId = yield select(getCurrentPageId); + const pluginTemplates = yield select(getPluginTemplates); + const queryTemplate = pluginTemplates[action.pluginId]; + // Do not show template view if the query has body(code) or if there are no templates + const showTemplate = !( + !!actionConfiguration.body || isEmpty(queryTemplate) + ); + history.replace( QUERIES_EDITOR_ID_URL(applicationId, pageId, id, { editName: "true", - showTemplate: actionConfiguration.body ? "false" : "true", + showTemplate, }), ); } diff --git a/app/client/src/selectors/applicationSelectors.tsx b/app/client/src/selectors/applicationSelectors.tsx index fe8e5c1c50..7a00830dfb 100644 --- a/app/client/src/selectors/applicationSelectors.tsx +++ b/app/client/src/selectors/applicationSelectors.tsx @@ -29,6 +29,7 @@ export const getCurrentApplication = ( }; export const getApplicationSearchKeyword = (state: AppState) => state.ui.applications.searchKeyword; +export const getAppMode = (state: AppState) => state.entities.app.mode; export const getIsDeletingApplication = (state: AppState) => state.ui.applications.deletingApplication; export const getIsDuplicatingApplication = (state: AppState) => diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index 826682644d..7b6dea68d0 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -117,3 +117,22 @@ export const isMac = () => { typeof navigator !== "undefined" ? navigator.platform : undefined; return !platform ? false : /Mac|iPod|iPhone|iPad/.test(platform); }; + +/** + * Removes the trailing slashes from the path + * @param path + * @example + * ```js + * let trimmedUrl = trimTrailingSlash('/url/') + * console.log(trimmedUrl) //will output /url + * ``` + * @example + * ```js + * let trimmedUrl = trimTrailingSlash('/yet-another-url//') + * console.log(trimmedUrl) // will output /yet-another-url + * ``` + */ +export const trimTrailingSlash = (path: string) => { + const trailingUrlRegex = /\/+$/; + return path.replace(trailingUrlRegex, ""); +}; diff --git a/app/client/src/widgets/ImageWidget.tsx b/app/client/src/widgets/ImageWidget.tsx index a8b2c3f5c5..9d99d5b565 100644 --- a/app/client/src/widgets/ImageWidget.tsx +++ b/app/client/src/widgets/ImageWidget.tsx @@ -22,6 +22,7 @@ class ImageWidget extends BaseWidget { image: VALIDATION_TYPES.TEXT, imageShape: VALIDATION_TYPES.TEXT, defaultImage: VALIDATION_TYPES.TEXT, + maxZoomLevel: VALIDATION_TYPES.NUMBER, }; } static getTriggerPropertyMap(): TriggerPropertiesMap { @@ -30,8 +31,13 @@ class ImageWidget extends BaseWidget { }; } getPageView() { + const { maxZoomLevel } = this.props; return ( { + this.disableDrag(disable); + }} + maxZoomLevel={maxZoomLevel} widgetId={this.props.widgetId} imageUrl={this.props.image || ""} onClick={this.props.onClick ? this.onImageClick : undefined} @@ -64,6 +70,7 @@ export interface ImageWidgetProps extends WidgetProps { image: string; imageShape: ImageShape; defaultImage: string; + maxZoomLevel: number; onClick?: string; } diff --git a/app/client/src/widgets/MapWidget.tsx b/app/client/src/widgets/MapWidget.tsx index 6bb0f5b36a..1e8160598f 100644 --- a/app/client/src/widgets/MapWidget.tsx +++ b/app/client/src/widgets/MapWidget.tsx @@ -28,6 +28,8 @@ const DisabledContainer = styled.div` color: #0a0b0e; } `; + +const DefaultCenter = { lat: -34.397, long: 150.644 }; class MapWidget extends BaseWidget { static getPropertyValidationMap(): WidgetPropertyValidationType { return { @@ -140,7 +142,7 @@ class MapWidget extends BaseWidget { isVisible={this.props.isVisible} zoomLevel={this.props.zoomLevel} allowZoom={this.props.allowZoom} - center={this.props.center || this.props.mapCenter} + center={this.props.center || this.props.mapCenter || DefaultCenter} enableCreateMarker selectedMarker={this.props.selectedMarker} updateCenter={this.updateCenter} diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 506f423104..87eb451ef7 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -15038,6 +15038,11 @@ react-window@^1.8.2: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" +react-zoom-pan-pinch@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-1.6.1.tgz#da16267c258ab37e8ebcdc7c252794a9633e91ec" + integrity sha512-J2eM0gZ04XiUWvmKZrOhSAB2zjyoK7kw2POIeN1X0yTTlmp6HPGV0zYfjnlkhgt8nQwpvXAbsF/oAnkuiwk1kA== + react@^16.12.0, react@^16.8.3, react@^16.8.5: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java index 82ef9d9a76..fbe05759ab 100644 --- a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java @@ -32,6 +32,8 @@ import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -107,7 +109,18 @@ public class ElasticSearchPlugin extends BasePlugin { final List hosts = new ArrayList<>(); for (Endpoint endpoint : datasourceConfiguration.getEndpoints()) { - hosts.add(new HttpHost(endpoint.getHost(), endpoint.getPort().intValue(), "http")); + URL url; + try { + url = new URL(endpoint.getHost()); + } catch (MalformedURLException e) { + return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Invalid host provided. It should be of the form http(s)://your-es-url.com")); + } + String scheme = "http"; + if (url.getProtocol() != null) { + scheme = url.getProtocol(); + } + + hosts.add(new HttpHost(url.getHost(), endpoint.getPort().intValue(), scheme)); } final RestClientBuilder clientBuilder = RestClient.builder(hosts.toArray(new HttpHost[]{})); @@ -156,6 +169,22 @@ public class ElasticSearchPlugin extends BasePlugin { if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { invalids.add("No endpoint provided. Please provide a host:port where ElasticSearch is reachable."); + } else { + for(Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + if (endpoint.getHost() == null) { + invalids.add("Missing host for endpoint"); + } else { + try { + URL url = new URL(endpoint.getHost()); + } catch (MalformedURLException e) { + invalids.add("Invalid host provided. It should be of the form http(s)://your-es-url.com"); + } + } + + if (endpoint.getPort() == null) { + invalids.add("Missing port for endpoint"); + } + } } return invalids; diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/form.json index e9f62cc4e3..3f3455124c 100644 --- a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/resources/form.json @@ -7,11 +7,11 @@ "sectionName": null, "children": [ { - "label": "Host Address", + "label": "Host URL", "configProperty": "datasourceConfiguration.endpoints[*].host", "controlType": "KEYVALUE_ARRAY", - "validationMessage": "Please enter a valid host", - "validationRegex": "^((?![/:]).)*$" + "validationMessage": "Please enter a valid host URL", + "validationRegex": "^(http|https)://" }, { "label": "Port", diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java b/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java index 921c09150f..95cbf26660 100644 --- a/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java @@ -2,12 +2,14 @@ package com.external.plugins; 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 lombok.extern.slf4j.Slf4j; import org.apache.http.HttpHost; import org.elasticsearch.client.Request; import org.elasticsearch.client.RestClient; +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -17,8 +19,10 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -35,13 +39,16 @@ public class ElasticSearchPluginTest { .withEnv("discovery.type", "single-node"); private static final DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + private static String host; + private static Integer port; @BeforeClass public static void setUp() throws IOException { - final Integer port = container.getMappedPort(9200); + port = container.getMappedPort(9200); + host = "http://" + container.getContainerIpAddress(); final RestClient client = RestClient.builder( - new HttpHost("localhost", port, "http") + new HttpHost(container.getContainerIpAddress(), port, "http") ).build(); Request request; @@ -60,7 +67,7 @@ public class ElasticSearchPluginTest { client.close(); - dsConfig.setEndpoints(List.of(new Endpoint("localhost", port.longValue()))); + dsConfig.setEndpoints(List.of(new Endpoint(host, port.longValue()))); } private Mono execute(HttpMethod method, String path, String body) { @@ -203,4 +210,81 @@ public class ElasticSearchPluginTest { .verifyComplete(); } + @Test + public void itShouldValidateDatasourceWithNoEndpoints() { + DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration(); + + Assert.assertEquals(Set.of("No endpoint provided. Please provide a host:port where ElasticSearch is reachable."), + pluginExecutor.validateDatasource(invalidDatasourceConfiguration)); + } + + @Test + public void itShouldValidateDatasourceWithEmptyPort() { + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + Endpoint endpoint = new Endpoint(); + endpoint.setHost(host); + datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + Assert.assertEquals(Set.of("Missing port for endpoint"), + pluginExecutor.validateDatasource(datasourceConfiguration)); + } + + @Test + public void itShouldValidateDatasourceWithEmptyHost() { + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + Endpoint endpoint = new Endpoint(); + endpoint.setPort(Long.valueOf(port)); + datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + Assert.assertEquals(Set.of("Missing host for endpoint"), + pluginExecutor.validateDatasource(datasourceConfiguration)); + } + + @Test + public void itShouldValidateDatasourceWithMissingEndpoint() { + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + + Endpoint endpoint = new Endpoint(); + datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + Assert.assertEquals(Set.of("Missing port for endpoint", "Missing host for endpoint"), + pluginExecutor.validateDatasource(datasourceConfiguration)); + } + + @Test + public void itShouldValidateDatasourceWithEndpointNoProtocol() { + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + Endpoint endpoint = new Endpoint(); + endpoint.setHost("localhost"); + endpoint.setPort(Long.valueOf(port)); + datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + Assert.assertEquals(Set.of("Invalid host provided. It should be of the form http(s)://your-es-url.com"), + pluginExecutor.validateDatasource(datasourceConfiguration) + ); + } + + @Test + public void itShouldTestDatasourceWithInvalidEndpoint() { + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + Endpoint endpoint = new Endpoint(); + endpoint.setHost("localhost"); + endpoint.setPort(Long.valueOf(port)); + datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + StepVerifier.create(pluginExecutor.testDatasource(datasourceConfiguration)) + .assertNext(result -> { + assertFalse(result.getInvalids().isEmpty()); + }) + .verifyComplete(); + } + + @Test + public void itShouldTestDatasource() { + StepVerifier.create(pluginExecutor.testDatasource(dsConfig)) + .assertNext(result -> { + assertTrue(result.getInvalids().isEmpty()); + }) + .verifyComplete(); + } } 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 8529e28a47..24e17b7c8d 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 @@ -292,8 +292,14 @@ public class MySqlPlugin extends BasePlugin { } if (StringUtils.isEmpty(datasourceConfiguration.getUrl()) && - CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { - invalids.add("Missing endpoint and url"); + CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + invalids.add("Missing endpoint and url"); + } else if (!CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { + for (final Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + if (endpoint.getHost().contains("/") || endpoint.getHost().contains(":")) { + invalids.add("Host value cannot contain `/` or `:` characters. Found `" + endpoint.getHost() + "`."); + } + } } if (datasourceConfiguration.getAuthentication() == null) { 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 ee53314f3c..85caf72611 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 @@ -236,6 +236,14 @@ public class MySqlPluginTest { assertTrue(output.contains("Missing endpoint and url")); } + @Test + public void testValidateDatasourceInvalidEndpoint() { + String hostname = "jdbc://localhost"; + dsConfig.getEndpoints().get(0).setHost(hostname); + Set output = pluginExecutor.validateDatasource(dsConfig); + assertTrue(output.contains("Host value cannot contain `/` or `:` characters. Found `" + hostname + "`.")); + } + /* checking that the connection is being closed after the datadourceDestroy method is being called NOT : this test case will fail in case of a SQL Exception */ 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 9b36f1cb17..9606f1f4a0 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 @@ -301,6 +301,14 @@ public class PostgresPlugin extends BasePlugin { if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) { invalids.add("Missing endpoint."); + } else { + for (final Endpoint endpoint : datasourceConfiguration.getEndpoints()) { + if (StringUtils.isEmpty(endpoint.getHost())) { + invalids.add("Missing hostname."); + } else if (endpoint.getHost().contains("/") || endpoint.getHost().contains(":")) { + invalids.add("Host value cannot contain `/` or `:` characters. Found `" + endpoint.getHost() + "`."); + } + } } if (datasourceConfiguration.getConnection() != null 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 a6b1e21fb1..e09490c7be 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 @@ -27,6 +27,7 @@ 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.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -159,6 +160,37 @@ public class PostgresPluginTest { .verifyComplete(); } + @Test + public void itShouldValidateDatasourceWithEmptyEndpoints() { + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + dsConfig.setEndpoints(new ArrayList<>()); + + Assert.assertEquals(Set.of("Missing endpoint."), + pluginExecutor.validateDatasource(dsConfig)); + } + + @Test + public void itShouldValidateDatasourceWithEmptyHost() { + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + dsConfig.getEndpoints().get(0).setHost(""); + + Assert.assertEquals(Set.of("Missing hostname."), + pluginExecutor.validateDatasource(dsConfig)); + } + + @Test + public void itShouldValidateDatasourceWithInvalidHostname() { + + String hostname = "jdbc://localhost"; + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + dsConfig.getEndpoints().get(0).setHost("jdbc://localhost"); + + Assert.assertEquals(Set.of("Host value cannot contain `/` or `:` characters. Found `" + hostname + "`."), + pluginExecutor.validateDatasource(dsConfig)); + } + @Test public void testAliasColumnNames() { DatasourceConfiguration dsConfig = createDatasourceConfiguration(); diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java index fadee2dc18..6f30556313 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java +++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java @@ -131,6 +131,9 @@ public class RedisPlugin extends BasePlugin { if (StringUtils.isNullOrEmpty(endpoint.getHost())) { invalids.add("Missing host for endpoint"); } + if (endpoint.getPort() == null) { + invalids.add("Missing port for endpoint"); + } } AuthenticationDTO auth = datasourceConfiguration.getAuthentication(); diff --git a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java index 9728700a9e..419b699d36 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java +++ b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java @@ -15,6 +15,7 @@ import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; +import org.testcontainers.utility.DockerImageName; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import redis.clients.jedis.Jedis; @@ -25,7 +26,7 @@ import java.util.Set; @Slf4j public class RedisPluginTest { @ClassRule - public static final GenericContainer redis = new GenericContainer("redis:5.0.3-alpine") + public static final GenericContainer redis = new GenericContainer(DockerImageName.parse("redis:5.0.3-alpine")) .withExposedPorts(6379); private static String host; private static Integer port; @@ -65,7 +66,8 @@ public class RedisPluginTest { public void itShouldValidateDatasourceWithNoEndpoints() { DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration(); - Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), Set.of("Missing endpoint(s)")); + Assert.assertEquals(Set.of("Missing endpoint(s)"), + pluginExecutor.validateDatasource(invalidDatasourceConfiguration)); } @Test @@ -75,7 +77,19 @@ public class RedisPluginTest { Endpoint endpoint = new Endpoint(); invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); - Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), Set.of("Missing host for endpoint")); + Assert.assertEquals(Set.of("Missing port for endpoint", "Missing host for endpoint"), + pluginExecutor.validateDatasource(invalidDatasourceConfiguration)); + } + + @Test + public void itShouldValidateDatasourceWithEmptyPort() { + DatasourceConfiguration invalidDatasourceConfiguration = new DatasourceConfiguration(); + + Endpoint endpoint = new Endpoint(); + endpoint.setHost("test-host"); + invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); + + Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), Set.of("Missing port for endpoint")); } @Test @@ -91,8 +105,9 @@ public class RedisPluginTest { invalidDatasourceConfiguration.setAuthentication(invalidAuth); invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); - Assert.assertEquals(pluginExecutor.validateDatasource(invalidDatasourceConfiguration), - Set.of("Missing username for authentication.", "Missing password for authentication.") + Assert.assertEquals( + Set.of("Missing port for endpoint", "Missing username for authentication.", "Missing password for authentication."), + pluginExecutor.validateDatasource(invalidDatasourceConfiguration) ); } @@ -107,7 +122,7 @@ public class RedisPluginTest { Endpoint endpoint = new Endpoint(); endpoint.setHost("test-host"); - + endpoint.setPort(Long.valueOf(port)); datasourceConfiguration.setAuthentication(auth); datasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); @@ -174,7 +189,7 @@ public class RedisPluginTest { Assert.assertNotNull(actionExecutionResult); Assert.assertNotNull(actionExecutionResult.getBody()); final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0); - Assert.assertEquals(node.get("result").asText(), "PONG"); + Assert.assertEquals("PONG", node.get("result").asText()); }).verifyComplete(); } @@ -193,7 +208,7 @@ public class RedisPluginTest { Assert.assertNotNull(actionExecutionResult); Assert.assertNotNull(actionExecutionResult.getBody()); final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0); - Assert.assertEquals(node.get("result").asText(), "null"); + Assert.assertEquals("null", node.get("result").asText()); }).verifyComplete(); // Setting a key @@ -206,7 +221,7 @@ public class RedisPluginTest { Assert.assertNotNull(actionExecutionResult); Assert.assertNotNull(actionExecutionResult.getBody()); final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0); - Assert.assertEquals(node.get("result").asText(), "OK"); + Assert.assertEquals("OK", node.get("result").asText()); }).verifyComplete(); // Getting the key @@ -217,7 +232,7 @@ public class RedisPluginTest { Assert.assertNotNull(actionExecutionResult); Assert.assertNotNull(actionExecutionResult.getBody()); final JsonNode node = ((ArrayNode) actionExecutionResult.getBody()).get(0); - Assert.assertEquals(node.get("result").asText(), "value"); + Assert.assertEquals("value", node.get("result").asText()); }).verifyComplete(); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/AuthenticationEntryPoint.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/AuthenticationEntryPoint.java index b90050593d..28c26d3a4d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/AuthenticationEntryPoint.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/AuthenticationEntryPoint.java @@ -23,7 +23,7 @@ public class AuthenticationEntryPoint implements ServerAuthenticationEntryPoint @Override public Mono commence(ServerWebExchange exchange, AuthenticationException e) { - log.debug("In the custom authenticationEntryPoint. Returning unauthorized from here"); + // In the custom authenticationEntryPoint. Returning unauthorized from here return Mono.fromRunnable(() -> { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); 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 3f8f6a212c..43232e8836 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 @@ -2,19 +2,21 @@ package com.appsmith.server.controllers; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.server.constants.Url; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Layout; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionMoveDTO; import com.appsmith.server.dtos.ActionViewDTO; import com.appsmith.server.dtos.ExecuteActionDTO; import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.services.ActionCollectionService; -import com.appsmith.server.services.ActionService; import com.appsmith.server.services.LayoutActionService; +import com.appsmith.server.services.NewActionService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -34,32 +36,33 @@ import java.util.List; @RestController @RequestMapping(Url.ACTION_URL) @Slf4j -public class ActionController extends BaseController { +public class ActionController { private final ActionCollectionService actionCollectionService; private final LayoutActionService layoutActionService; + private final NewActionService newActionService; @Autowired - public ActionController(ActionService service, - ActionCollectionService actionCollectionService, - LayoutActionService layoutActionService) { - super(service); + public ActionController(ActionCollectionService actionCollectionService, + LayoutActionService layoutActionService, + NewActionService newActionService) { this.actionCollectionService = actionCollectionService; this.layoutActionService = layoutActionService; + this.newActionService = newActionService; } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public Mono> create(@Valid @RequestBody Action resource, - @RequestHeader(name = "Origin", required = false) String originHeader, - ServerWebExchange exchange) { + public Mono> createAction(@Valid @RequestBody ActionDTO resource, + @RequestHeader(name = "Origin", required = false) String originHeader, + ServerWebExchange exchange) { log.debug("Going to create resource {}", resource.getClass().getName()); return actionCollectionService.createAction(resource) .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); } @PutMapping("/{id}") - public Mono> update(@PathVariable String id, @RequestBody Action resource) { + public Mono> updateAction(@PathVariable String id, @RequestBody ActionDTO resource) { log.debug("Going to update resource with id: {}", id); return actionCollectionService.updateAction(id, resource) .map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null)); @@ -67,12 +70,12 @@ public class ActionController extends BaseController> executeAction(@RequestBody ExecuteActionDTO executeActionDTO) { - return service.executeAction(executeActionDTO) + return newActionService.executeAction(executeActionDTO) .map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null)); } @PutMapping("/move") - public Mono> moveAction(@RequestBody @Valid ActionMoveDTO actionMoveDTO) { + public Mono> moveAction(@RequestBody @Valid ActionMoveDTO actionMoveDTO) { log.debug("Going to move action {} from page {} to page {}", actionMoveDTO.getAction().getName(), actionMoveDTO.getAction().getPageId(), actionMoveDTO.getDestinationPageId()); return layoutActionService.moveAction(actionMoveDTO) .map(action -> new ResponseDTO<>(HttpStatus.OK.value(), action, null)); @@ -86,14 +89,38 @@ public class ActionController extends BaseController>> getActionsForViewMode(@RequestParam String applicationId) { - return service.getActionsForViewMode(applicationId).collectList() + return newActionService.getActionsForViewMode(applicationId).collectList() .map(actions -> new ResponseDTO<>(HttpStatus.OK.value(), actions, null)); } @PutMapping("/executeOnLoad/{id}") - public Mono> setExecuteOnLoad(@PathVariable String id, @RequestParam Boolean flag) { + public Mono> setExecuteOnLoad(@PathVariable String id, @RequestParam Boolean flag) { log.debug("Going to set execute on load for action id {} to {}", id, flag); return layoutActionService.setExecuteOnLoad(id, flag) .map(action -> new ResponseDTO<>(HttpStatus.OK.value(), action, null)); } + + @DeleteMapping("/{id}") + public Mono> deleteAction(@PathVariable String id) { + log.debug("Going to delete unpublished action with id: {}", id); + return newActionService.deleteUnpublishedAction(id) + .map(deletedResource -> new ResponseDTO<>(HttpStatus.OK.value(), deletedResource, null)); + } + + /** + * This function fetches all actions in edit mode. + * To fetch the actions in view mode, check the function `getActionsForViewMode` + * + * The controller function is primarily used with param applicationId by the client to fetch the actions in edit + * mode. + * + * @param params + * @return + */ + @GetMapping("") + public Mono>> getAllUnpublishedActions(@RequestParam MultiValueMap params) { + log.debug("Going to get all actions"); + return newActionService.getUnpublishedActions(params).collectList() + .map(resources -> new ResponseDTO<>(HttpStatus.OK.value(), resources, null)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java index 7e815c694c..818d6051c8 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java @@ -60,7 +60,7 @@ public class ApplicationController extends BaseController> publish(@PathVariable String applicationId) { - return service.publish(applicationId) + return applicationPageService.publish(applicationId) .map(published -> new ResponseDTO<>(HttpStatus.OK.value(), published, null)); } @@ -97,4 +97,10 @@ public class ApplicationController extends BaseController new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); } + @GetMapping("/view/{applicationId}") + public Mono> getApplicationInViewMode(@PathVariable String applicationId) { + return service.getApplicationInViewMode(applicationId) + .map(application -> new ResponseDTO<>(HttpStatus.OK.value(), application, null)); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java index b8d6df3e86..63863dae59 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/BaseController.java @@ -38,6 +38,14 @@ public abstract class BaseController, T extends Bas .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); } + /** + * TODO : Remove this function completely if this is not being used. + * If not, atleast remove it for : + * 1. Page + * 2. Datasources + * @param params + * @return + */ @GetMapping("") public Mono>> getAll(@RequestParam MultiValueMap params) { log.debug("Going to get all resources"); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ItemController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ItemController.java index 9e93236dc2..88a2fc2a95 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ItemController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ItemController.java @@ -1,7 +1,7 @@ package com.appsmith.server.controllers; import com.appsmith.server.constants.Url; -import com.appsmith.server.domains.Action; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.AddItemToPageDTO; import com.appsmith.server.dtos.ItemDTO; import com.appsmith.server.dtos.ResponseDTO; @@ -37,7 +37,7 @@ public class ItemController { } @PostMapping("/addToPage") - public Mono> addItemToPage(@RequestBody AddItemToPageDTO addItemToPageDTO) { + public Mono> addItemToPage(@RequestBody AddItemToPageDTO addItemToPageDTO) { log.debug("Going to add item {} to page {} with new name {}", addItemToPageDTO.getMarketplaceElement().getItem().getName(), addItemToPageDTO.getPageId(), addItemToPageDTO.getName()); return service.addItemToPage(addItemToPageDTO) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java index 5bcba4b0ce..f2e643513d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/PageController.java @@ -1,103 +1,110 @@ package com.appsmith.server.controllers; import com.appsmith.server.constants.Url; -import com.appsmith.server.domains.Page; import com.appsmith.server.dtos.ApplicationPagesDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.dtos.ResponseDTO; -import com.appsmith.server.exceptions.AppsmithError; -import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.services.ApplicationPageService; -import com.appsmith.server.services.PageService; +import com.appsmith.server.services.NewPageService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; -import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.DeleteMapping; 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.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.validation.Valid; -import java.util.List; @RestController @RequestMapping(Url.PAGE_URL) @Slf4j -public class PageController extends BaseController { +public class PageController { private final ApplicationPageService applicationPageService; + private final NewPageService newPageService; @Autowired - public PageController(PageService service, ApplicationPageService applicationPageService) { - super(service); + public PageController(ApplicationPageService applicationPageService, + NewPageService newPageService) { this.applicationPageService = applicationPageService; + this.newPageService = newPageService; } @PostMapping @ResponseStatus(HttpStatus.CREATED) - public Mono> create(@Valid @RequestBody Page resource, - @RequestHeader(name = "Origin", required = false) String originHeader, - ServerWebExchange exchange) { + public Mono> createPage(@Valid @RequestBody PageDTO resource, + @RequestHeader(name = "Origin", required = false) String originHeader, + ServerWebExchange exchange) { log.debug("Going to create resource {}", resource.getClass().getName()); return applicationPageService.createPage(resource) .map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null)); } - @Override - public Mono>> getAll(@RequestParam MultiValueMap params) { - return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); - } - @Deprecated @GetMapping("/application/{applicationId}") public Mono> getPageNamesByApplicationId(@PathVariable String applicationId) { - return service.findNamesByApplicationId(applicationId) + return newPageService.findNamesByApplicationIdAndViewMode(applicationId, false) .map(resources -> new ResponseDTO<>(HttpStatus.OK.value(), resources, null)); } - @GetMapping("/application/name/{applicationName}") - public Mono> getPageNamesByApplicationName(@PathVariable String applicationName) { - return service.findNamesByApplicationName(applicationName) + @GetMapping("/view/application/{applicationId}") + public Mono> getPageNamesByApplicationIdInViewMode(@PathVariable String applicationId) { + return newPageService.findNamesByApplicationIdAndViewMode(applicationId, true) .map(resources -> new ResponseDTO<>(HttpStatus.OK.value(), resources, null)); } - @Override @GetMapping("/{pageId}") - public Mono> getById(@PathVariable String pageId) { + public Mono> getPageById(@PathVariable String pageId) { return applicationPageService.getPage(pageId, false) .map(page -> new ResponseDTO<>(HttpStatus.OK.value(), page, null)); } @GetMapping("/{pageId}/view") - public Mono> getPageView(@PathVariable String pageId) { + public Mono> getPageView(@PathVariable String pageId) { return applicationPageService.getPage(pageId, true) .map(page -> new ResponseDTO<>(HttpStatus.OK.value(), page, null)); } @GetMapping("{pageName}/application/{applicationName}/view") - public Mono> getPageViewByName(@PathVariable String applicationName, @PathVariable String pageName) { + public Mono> getPageViewByName(@PathVariable String applicationName, @PathVariable String pageName) { return applicationPageService.getPageByName(applicationName, pageName, true) .map(page -> new ResponseDTO<>(HttpStatus.OK.value(), page, null)); } + /** + * This only deletes the unpublished version of the page. + * In case the page has never been published, the page gets deleted. + * In case the page has been published, this page would eventually get deleted whenever the application is published + * next. + * @param id + * @return + */ @DeleteMapping("/{id}") - public Mono> delete(@PathVariable String id) { + public Mono> deletePage(@PathVariable String id) { log.debug("Going to delete page with id: {}", id); - return service.delete(id) + return applicationPageService.deleteUnpublishedPage(id) .map(deletedResource -> new ResponseDTO<>(HttpStatus.OK.value(), deletedResource, null)); } @PostMapping("/clone/{pageId}") - public Mono> clonePage(@PathVariable String pageId) { + public Mono> clonePage(@PathVariable String pageId) { return applicationPageService.clonePage(pageId) .map(page -> new ResponseDTO<>(HttpStatus.CREATED.value(), page, null)); } + + @PutMapping("/{id}") + public Mono> updatePage(@PathVariable String id, @RequestBody PageDTO resource) { + log.debug("Going to update page with id: {}", id); + return newPageService.updatePage(id, resource) + .map(updatedResource -> new ResponseDTO<>(HttpStatus.OK.value(), updatedResource, null)); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/RestApiImportController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/RestApiImportController.java index 8cc2b01a2c..32ec045d0e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/RestApiImportController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/RestApiImportController.java @@ -2,8 +2,8 @@ package com.appsmith.server.controllers; import com.appsmith.external.models.TemplateCollection; import com.appsmith.server.constants.Url; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.RestApiImporterType; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ResponseDTO; import com.appsmith.server.services.ApiImporter; import com.appsmith.server.services.CurlImporterService; @@ -41,12 +41,12 @@ public class RestApiImportController { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public Mono> create(@Valid @RequestBody Object input, - @RequestParam RestApiImporterType type, - @RequestParam String pageId, - @RequestParam String name, - @RequestParam String organizationId, - @RequestHeader(name = "Origin", required = false) String originHeader + public Mono> create(@Valid @RequestBody Object input, + @RequestParam RestApiImporterType type, + @RequestParam String pageId, + @RequestParam String name, + @RequestParam String organizationId, + @RequestHeader(name = "Origin", required = false) String originHeader ) { log.debug("Going to import API"); ApiImporter service; 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 77cada4a46..1703b7966e 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 @@ -20,6 +20,7 @@ import java.util.Set; @ToString @NoArgsConstructor @Document +@Deprecated public class Action extends BaseDomain { String name; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java index ca981c7c4f..cc1b64ae47 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java @@ -32,6 +32,13 @@ public class Application extends BaseDomain { List pages; + @JsonIgnore + List publishedPages; + + @JsonIgnore + @Transient + Boolean viewMode = false; + @Transient boolean appIsExample = false; @@ -42,4 +49,8 @@ public class Application extends BaseDomain { String icon; + public List getPages() { + return viewMode ? publishedPages : pages; + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationPage.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationPage.java index bf1a87e4b6..b304224291 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationPage.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ApplicationPage.java @@ -1,6 +1,7 @@ package com.appsmith.server.domains; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -12,6 +13,7 @@ import net.minidev.json.annotate.JsonIgnore; @ToString @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode public class ApplicationPage { String id; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Collection.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Collection.java index a2b8c2b07c..deda3c443e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Collection.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Collection.java @@ -25,5 +25,5 @@ public class Collection extends BaseDomain { Boolean shared; //To save space, when creating/updating collection, only add Action's id field instead of the entire action. - List actions; + List actions; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewAction.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewAction.java new file mode 100644 index 0000000000..f41c8c24c3 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewAction.java @@ -0,0 +1,38 @@ +package com.appsmith.server.domains; + +import com.appsmith.external.models.BaseDomain; +import com.appsmith.server.dtos.ActionDTO; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.data.mongodb.core.mapping.Document; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@Document +public class NewAction extends BaseDomain { + + // Fields in action that are not allowed to change between published and unpublished versions + String applicationId; + + String organizationId; + + PluginType pluginType; + + String pluginId; + + String templateId; //If action is created via a template, store the id here. + + String providerId; //If action is created via a template, store the template's provider id here. + + Documentation documentation; // Documentation for the template using which this action was created + + // Action specific fields that are allowed to change between published and unpublished versions + ActionDTO unpublishedAction; + + ActionDTO publishedAction; + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewPage.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewPage.java new file mode 100644 index 0000000000..7091919805 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/NewPage.java @@ -0,0 +1,21 @@ +package com.appsmith.server.domains; + +import com.appsmith.external.models.BaseDomain; +import com.appsmith.server.dtos.PageDTO; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.mongodb.core.mapping.Document; + +@Getter +@Setter +@NoArgsConstructor +@Document +public class NewPage extends BaseDomain { + + String applicationId; + + PageDTO unpublishedPage; + + PageDTO publishedPage; +} 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 f12b9252d1..eef0ba665a 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 @@ -10,6 +10,8 @@ import lombok.ToString; import org.springframework.data.mongodb.core.mapping.Document; import javax.validation.constraints.NotNull; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotBlank; import java.util.List; @@ -22,7 +24,7 @@ public class Organization extends BaseDomain { private String domain; - @NotNull(message = "Name is mandatory") + @NotBlank(message = "Name is mandatory") private String name; private String website; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Page.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Page.java index 300f1416de..d70ea2b93b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Page.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Page.java @@ -15,6 +15,7 @@ import java.util.List; @ToString @NoArgsConstructor @Document +@Deprecated public class Page extends BaseDomain { String name; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionDTO.java new file mode 100644 index 0000000000..0fa89257ce --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionDTO.java @@ -0,0 +1,117 @@ +package com.appsmith.server.dtos; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.Policy; +import com.appsmith.external.models.Property; +import com.appsmith.server.domains.ActionProvider; +import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.Documentation; +import com.appsmith.server.domains.PluginType; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.data.annotation.Transient; + +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Getter +@Setter +@NoArgsConstructor +@ToString +public class ActionDTO { + + @Transient + private String id; + + @Transient + String applicationId; + + @Transient + String organizationId; + + @Transient + PluginType pluginType; + + @Transient + String pluginId; + + String name; + + Datasource datasource; + + String pageId; + + String collectionId; + + ActionConfiguration actionConfiguration; + + Boolean executeOnLoad; + + /* + * This is a list of fields specified by the client to signify which fields have dynamic bindings in them. + * TODO: The server can use this field to simplify our Mustache substitutions in the future + */ + List dynamicBindingPathList; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + Boolean isValid; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + Set invalids; + + + // This is a list of keys that the client whose values the client needs to send during action execution. + // These are the Mustache keys that the server will replace before invoking the API + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + Set jsonPathKeys; + + @JsonIgnore + String cacheResponse; + + @Transient + String templateId; //If action is created via a template, store the id here. + + @Transient + String providerId; //If action is created via a template, store the template's provider id here. + + @Transient + ActionProvider provider; + + @JsonIgnore + Boolean userSetOnLoad = false; + + Boolean confirmBeforeExecute = false; + + @Transient + Documentation documentation; + + Instant deletedAt = null; + + @Transient + @JsonIgnore + protected Set policies = new HashSet<>(); + + @Transient + public Set userPermissions = new HashSet<>(); + + /** + * If the Datasource is null, create one and set the autoGenerated flag to true. This is required because spring-data + * cannot add the createdAt and updatedAt properties for null embedded objects. At this juncture, we couldn't find + * a way to disable the auditing for nested objects. + * + * @return + */ + public Datasource getDatasource() { + if (this.datasource == null) { + this.datasource = new Datasource(); + this.datasource.setIsAutoGenerated(true); + } + return datasource; + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionMoveDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionMoveDTO.java index c8b26daeb5..9c1d9c9a11 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionMoveDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionMoveDTO.java @@ -1,6 +1,5 @@ package com.appsmith.server.dtos; -import com.appsmith.server.domains.Action; import lombok.Getter; import lombok.Setter; @@ -9,8 +8,9 @@ import javax.validation.constraints.NotNull; @Getter @Setter public class ActionMoveDTO { + @NotNull - Action action; + ActionDTO action; @NotNull String destinationPageId; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionViewDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionViewDTO.java index 143ea29aa8..a8722ca4e3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionViewDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ActionViewDTO.java @@ -3,6 +3,7 @@ package com.appsmith.server.dtos; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; import java.util.Set; @@ -11,6 +12,7 @@ import static com.appsmith.external.constants.ActionConstants.DEFAULT_ACTION_EXE @Getter @Setter @NoArgsConstructor +@ToString public class ActionViewDTO { String id; String name; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ExecuteActionDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ExecuteActionDTO.java index 0ed85818a7..1890470744 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ExecuteActionDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ExecuteActionDTO.java @@ -6,17 +6,25 @@ import com.appsmith.server.domains.Action; import lombok.Getter; import lombok.Setter; -import javax.validation.constraints.NotNull; import java.util.List; @Getter @Setter public class ExecuteActionDTO { - @NotNull + /** + * action field was added to support dry run execution. Now that dry run functionality has been removed, + * actionId has been added to send only the id of the action. + * TODO : Remove the deprecated field. + */ + @Deprecated Action action; + String actionId; + List params; PaginationField paginationField; + + Boolean viewMode = false; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java new file mode 100644 index 0000000000..67072b2ade --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/PageDTO.java @@ -0,0 +1,42 @@ +package com.appsmith.server.dtos; + +import com.appsmith.external.models.Policy; +import com.appsmith.server.domains.Layout; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.springframework.data.annotation.Transient; + +import java.time.Instant; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Getter +@Setter +@NoArgsConstructor +@ToString +public class PageDTO { + + @Transient + private String id; + + String name; + + @Transient + String applicationId; + + List layouts; + + @Transient + public Set userPermissions = new HashSet<>(); + + @Transient + @JsonIgnore + protected Set policies = new HashSet<>(); + + Instant deletedAt = null; + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index 59622e0df4..757779655d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -17,7 +17,7 @@ public enum AppsmithError { USER_DOESNT_BELONG_ANY_ORGANIZATION(400, 4009, "User {0} does not belong to any organization"), USER_DOESNT_BELONG_TO_ORGANIZATION(400, 4010, "User {0} does not belong to an organization with id {1}"), NO_CONFIGURATION_FOUND_IN_DATASOURCE(400, 4011, "No datasource configuration found. Please configure it and try again."), - INVALID_ACTION(400, 4012, "Action {0} with id {1} is invalid: {3}"), + INVALID_ACTION(400, 4012, "Action {0} with id {1} is invalid: {2}"), INVALID_DATASOURCE(400, 4013, "Datasource is invalid. Please edit to make it valid. Details: {0}"), INVALID_ACTION_NAME(400, 4014, "Action name is invalid. Please input syntactically correct name"), INVALID_DATASOURCE_CONFIGURATION(400, 4015, "Datasource configuration is invalid"), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java index e2e691c57e..f74cd0846e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java @@ -4,15 +4,15 @@ import com.appsmith.external.models.BaseDomain; import com.appsmith.external.models.Policy; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.PolicyGenerator; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Datasource; -import com.appsmith.server.domains.Page; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.User; -import com.appsmith.server.repositories.ActionRepository; import com.appsmith.server.repositories.ApplicationRepository; import com.appsmith.server.repositories.DatasourceRepository; -import com.appsmith.server.repositories.PageRepository; +import com.appsmith.server.repositories.NewActionRepository; +import com.appsmith.server.repositories.NewPageRepository; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -26,25 +26,27 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; + @Component public class PolicyUtils { private final PolicyGenerator policyGenerator; private final ApplicationRepository applicationRepository; - private final PageRepository pageRepository; - private final ActionRepository actionRepository; private final DatasourceRepository datasourceRepository; + private final NewPageRepository newPageRepository; + private final NewActionRepository newActionRepository; public PolicyUtils(PolicyGenerator policyGenerator, ApplicationRepository applicationRepository, - PageRepository pageRepository, - ActionRepository actionRepository, - DatasourceRepository datasourceRepository) { + DatasourceRepository datasourceRepository, + NewPageRepository newPageRepository, + NewActionRepository newActionRepository) { this.policyGenerator = policyGenerator; this.applicationRepository = applicationRepository; - this.pageRepository = pageRepository; - this.actionRepository = actionRepository; this.datasourceRepository = datasourceRepository; + this.newPageRepository = newPageRepository; + this.newActionRepository = newActionRepository; } public T addPoliciesToExistingObject(Map policyMap, T obj) { @@ -157,6 +159,26 @@ public class PolicyUtils { .flatMapMany(updatedDatasources -> datasourceRepository.saveAll(updatedDatasources)); } + public Flux updateWithNewPoliciesToDatasourcesByDatasourceIds(Set ids, Map datasourcePolicyMap, boolean addPolicyToObject) { + + return datasourceRepository + .findAllByIds(ids, MANAGE_DATASOURCES) + // In case we have come across a datasource the current user is not allowed to manage, move on. + .switchIfEmpty(Mono.empty()) + .flatMap(datasource -> { + Datasource updatedDatasource; + if (addPolicyToObject) { + updatedDatasource = addPoliciesToExistingObject(datasourcePolicyMap, datasource); + } else { + updatedDatasource = removePoliciesFromExistingObject(datasourcePolicyMap, datasource); + } + + return Mono.just(updatedDatasource); + }) + .collectList() + .flatMapMany(datasources -> datasourceRepository.saveAll(datasources)); + } + public Flux updateWithNewPoliciesToApplicationsByOrgId(String orgId, Map newAppPoliciesMap, boolean addPolicyToObject) { return applicationRepository @@ -174,9 +196,13 @@ public class PolicyUtils { .flatMapMany(updatedApplications -> applicationRepository.saveAll(updatedApplications)); } - public Flux updateWithApplicationPermissionsToAllItsPages(String applicationId, Map newPagePoliciesMap, boolean addPolicyToObject) { + public Flux updateWithApplicationPermissionsToAllItsPages(String applicationId, Map newPagePoliciesMap, boolean addPolicyToObject) { - return pageRepository + // Instead of fetching pages from the application object, we fetch pages from the page repository. This ensures that all the published + // AND the unpublished pages are updated with the new policy change [This covers the edge cases where a page may exist + // in published app but has been deleted in the edit mode]. This means that we don't have to do any special treatment + // during deployment of the application to handle edge cases. + return newPageRepository .findByApplicationId(applicationId, AclPermission.MANAGE_PAGES) .switchIfEmpty(Mono.empty()) .map(page -> { @@ -187,13 +213,25 @@ public class PolicyUtils { } }) .collectList() - .flatMapMany(updatedPages -> pageRepository.saveAll(updatedPages)); + .flatMapMany(updatedPages -> newPageRepository + .saveAll(updatedPages)); } - public Flux updateWithPagePermissionsToAllItsActions(String pageId, Map newActionPoliciesMap, boolean addPolicyToObject) { + /** + * Instead of fetching actions by pageId, fetch actions by applicationId and then update the action policies + * using the new ActionPoliciesMap. This ensures the following : + * 1. Instead of bulk updating actions page wise, we do bulk update of actions in one go for the entire application. + * 2. If the action is associated with different pages (in published/unpublished page due to movement of action), fetching + * actions by applicationId ensures that we update ALL the actions and don't have to do special handling for the same. + * @param applicationId + * @param newActionPoliciesMap + * @param addPolicyToObject + * @return + */ + public Flux updateWithPagePermissionsToAllItsActions(String applicationId, Map newActionPoliciesMap, boolean addPolicyToObject) { - return actionRepository - .findByPageId(pageId, AclPermission.MANAGE_ACTIONS) + return newActionRepository + .findByApplicationId(applicationId) .switchIfEmpty(Mono.empty()) .map(action -> { if (addPolicyToObject) { @@ -202,14 +240,8 @@ public class PolicyUtils { return removePoliciesFromExistingObject(newActionPoliciesMap, action); } }) - .map(action -> { - if (action.getDatasource() == null) { - action.setDatasource(new Datasource()); - } - return action; - }) .collectList() - .flatMapMany(updatedActions -> actionRepository.saveAll(updatedActions)); + .flatMapMany(updatedActions -> newActionRepository.saveAll(updatedActions)); } public Map generateInheritedPoliciesFromSourcePolicies(Map sourcePolicyMap, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java index 7777d9715c..e4802b9f51 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java @@ -13,6 +13,8 @@ import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Group; import com.appsmith.server.domains.InviteUser; import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.OrganizationPlugin; import com.appsmith.server.domains.Page; @@ -27,8 +29,10 @@ import com.appsmith.server.domains.Role; import com.appsmith.server.domains.Sequence; import com.appsmith.server.domains.Setting; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.DslActionDTO; import com.appsmith.server.dtos.OrganizationPluginStatus; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.services.EncryptionService; import com.appsmith.server.services.OrganizationService; import com.github.cloudyrock.mongock.ChangeLog; @@ -68,6 +72,7 @@ import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS; import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; +import static com.appsmith.server.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; import static com.appsmith.server.repositories.BaseAppsmithRepositoryImpl.fieldName; import static org.springframework.data.mongodb.core.query.Criteria.where; import static org.springframework.data.mongodb.core.query.Query.query; @@ -116,6 +121,42 @@ public class DatabaseChangelog { } } + private ActionDTO copyActionToDTO(Action action) { + ActionDTO actionDTO = new ActionDTO(); + actionDTO.setName(action.getName()); + actionDTO.setDatasource(action.getDatasource()); + actionDTO.setPageId(action.getPageId()); + actionDTO.setActionConfiguration(action.getActionConfiguration()); + actionDTO.setExecuteOnLoad(action.getExecuteOnLoad()); + actionDTO.setDynamicBindingPathList(action.getDynamicBindingPathList()); + actionDTO.setIsValid(action.getIsValid()); + actionDTO.setInvalids(action.getInvalids()); + actionDTO.setJsonPathKeys(action.getJsonPathKeys()); + actionDTO.setCacheResponse(action.getCacheResponse()); + actionDTO.setUserSetOnLoad(action.getUserSetOnLoad()); + actionDTO.setConfirmBeforeExecute(action.getConfirmBeforeExecute()); + + return actionDTO; + } + + private void installPluginToAllOrganizations(MongoTemplate mongoTemplate, String pluginId) { + for (Organization organization : mongoTemplate.findAll(Organization.class)) { + if (CollectionUtils.isEmpty(organization.getPlugins())) { + organization.setPlugins(new ArrayList<>()); + } + + final Set installedPlugins = organization.getPlugins() + .stream().map(OrganizationPlugin::getPluginId).collect(Collectors.toSet()); + + if (!installedPlugins.contains(pluginId)) { + organization.getPlugins() + .add(new OrganizationPlugin(pluginId, OrganizationPluginStatus.FREE)); + } + + mongoTemplate.save(organization); + } + } + @ChangeSet(order = "001", id = "initial-plugins", author = "") public void initialPlugins(MongoTemplate mongoTemplate) { Plugin plugin1 = new Plugin(); @@ -901,6 +942,7 @@ public class DatabaseChangelog { if (unfixablePagesCount > 0) { log.info("Not all pages' onLoad actions could be fixed. Some old applications might not auto-run actions."); + } } @@ -994,7 +1036,7 @@ public class DatabaseChangelog { installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); } - @ChangeSet(order = "030", id = "add-msSql-plugin", author = "") + @ChangeSet(order = "031", id = "add-msSql-plugin", author = "") public void addMsSqlPlugin(MongoTemplate mongoTemplate) { Plugin plugin1 = new Plugin(); plugin1.setName("MsSQL"); @@ -1014,22 +1056,141 @@ public class DatabaseChangelog { installPluginToAllOrganizations(mongoTemplate, plugin1.getId()); } - private void installPluginToAllOrganizations(MongoTemplate mongoTemplate, String pluginId) { - for (Organization organization : mongoTemplate.findAll(Organization.class)) { - if (CollectionUtils.isEmpty(organization.getPlugins())) { - organization.setPlugins(new ArrayList<>()); + @ChangeSet(order = "037", id = "createNewPageIndexAfterDroppingNewPage", author = "") + public void addNewPageIndexAfterDroppingNewPage(MongoTemplate mongoTemplate) { + Index createdAtIndex = makeIndex("createdAt"); + + // Drop existing NewPage class + mongoTemplate.dropCollection(NewPage.class); + + // Now add an index + ensureIndexes(mongoTemplate, NewPage.class, + createdAtIndex + ); + } + + @ChangeSet(order = "038", id = "createNewActionIndexAfterDroppingNewAction", author = "") + public void addNewActionIndexAfterDroppingNewAction(MongoTemplate mongoTemplate) { + Index createdAtIndex = makeIndex("createdAt"); + + // Drop existing NewAction class + mongoTemplate.dropCollection(NewAction.class); + + // Now add an index + ensureIndexes(mongoTemplate, NewAction.class, + createdAtIndex + ); + } + + @ChangeSet(order = "039", id = "migrate-page-and-actions", author = "") + public void migratePage(MongoTemplate mongoTemplate) { + final List pages = mongoTemplate.find( + query(where("deletedAt").is(null)), + Page.class + ); + + List toBeInsertedPages = new ArrayList<>(); + + for (Page oldPage : pages) { + PageDTO unpublishedPage = new PageDTO(); + PageDTO publishedPage = new PageDTO(); + + unpublishedPage.setName(oldPage.getName()); + unpublishedPage.setLayouts(oldPage.getLayouts()); + + publishedPage.setName(oldPage.getName()); + publishedPage.setLayouts(oldPage.getLayouts()); + + if (oldPage.getLayouts() != null && !oldPage.getLayouts().isEmpty()) { + unpublishedPage.setLayouts(new ArrayList<>()); + publishedPage.setLayouts(new ArrayList<>()); + for (Layout layout : oldPage.getLayouts()) { + Layout unpublishedLayout = new Layout(); + copyNewFieldValuesIntoOldObject(layout, unpublishedLayout); + unpublishedLayout.setPublishedDsl(null); + unpublishedLayout.setPublishedLayoutOnLoadActions(null); + unpublishedPage.getLayouts().add(unpublishedLayout); + + Layout publishedLayout = new Layout(); + publishedLayout.setViewMode(true); + copyNewFieldValuesIntoOldObject(layout, publishedLayout); + publishedLayout.setDsl(publishedLayout.getDsl()); + publishedLayout.setLayoutOnLoadActions(publishedLayout.getPublishedLayoutOnLoadActions()); + publishedLayout.setPublishedDsl(null); + publishedLayout.setPublishedLayoutOnLoadActions(null); + publishedPage.getLayouts().add(publishedLayout); + } } - final Set installedPlugins = organization.getPlugins() - .stream().map(OrganizationPlugin::getPluginId).collect(Collectors.toSet()); + NewPage newPage = new NewPage(); + newPage.setApplicationId(oldPage.getApplicationId()); + newPage.setPublishedPage(publishedPage); + newPage.setUnpublishedPage(unpublishedPage); - if (!installedPlugins.contains(pluginId)) { - organization.getPlugins() - .add(new OrganizationPlugin(pluginId, OrganizationPluginStatus.FREE)); - } + //Set the base domain fields + newPage.setId(oldPage.getId()); + newPage.setCreatedAt(oldPage.getCreatedAt()); + newPage.setUpdatedAt(oldPage.getUpdatedAt()); + newPage.setPolicies(oldPage.getPolicies()); - mongoTemplate.save(organization); + toBeInsertedPages.add(newPage); } + mongoTemplate.insertAll(toBeInsertedPages); + + // Migrate Actions now + + Map pageIdApplicationIdMap = pages + .stream() + .collect(Collectors.toMap(Page::getId, Page::getApplicationId)); + + final List actions = mongoTemplate.find( + query(where("deletedAt").is(null)), + Action.class + ); + + List toBeInsertedActions = new ArrayList<>(); + + for (Action oldAction : actions) { + ActionDTO unpublishedAction = copyActionToDTO(oldAction); + ActionDTO publishedAction = copyActionToDTO(oldAction); + + NewAction newAction = new NewAction(); + + newAction.setOrganizationId(oldAction.getOrganizationId()); + newAction.setPluginType(oldAction.getPluginType()); + newAction.setPluginId(oldAction.getPluginId()); + newAction.setTemplateId(oldAction.getTemplateId()); + newAction.setProviderId(oldAction.getProviderId()); + newAction.setDocumentation(oldAction.getDocumentation()); + + // During the first migration, both the published and the unpublished action dtos would match the existing + // action because before this action only had a single instance (whether in edit/view mode) + newAction.setUnpublishedAction(unpublishedAction); + newAction.setPublishedAction(publishedAction); + + // Now set the application id for this action + String applicationId = pageIdApplicationIdMap.get(oldAction.getPageId()); + + if (applicationId != null) { + newAction.setApplicationId(applicationId); + } + + // Set the pluginId for the action + if (oldAction.getDatasource() != null) { + newAction.setPluginId(oldAction.getDatasource().getPluginId()); + } + + //Set the base domain fields + newAction.setId(oldAction.getId()); + newAction.setCreatedAt(oldAction.getCreatedAt()); + newAction.setUpdatedAt(oldAction.getUpdatedAt()); + newAction.setPolicies(oldAction.getPolicies()); + + toBeInsertedActions.add(newAction); + } + + mongoTemplate.insertAll(toBeInsertedActions); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepositoryImpl.java index 3718cce1fb..c89afbed96 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepositoryImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepositoryImpl.java @@ -84,6 +84,9 @@ public class CustomApplicationRepositoryImpl extends BaseAppsmithRepositoryImpl< @Override public Mono setDefaultPage(String applicationId, String pageId) { + // Since this can only happen during edit, the page in question is unpublished page. Hence the update should + // be to pages and not publishedPages + final Mono setAllAsNonDefaultMono = mongoOperations.updateFirst( Query.query(getIdCriteria(applicationId)).addCriteria(Criteria.where("pages.isDefault").is(true)), new Update().set("pages.$.isDefault", false), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepository.java index 82988c131e..809bacd86d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepository.java @@ -7,6 +7,8 @@ import com.mongodb.client.result.UpdateResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Set; + public interface CustomDatasourceRepository extends AppsmithRepository { Flux findAllByOrganizationId(String organizationId, AclPermission permission); @@ -14,5 +16,7 @@ public interface CustomDatasourceRepository extends AppsmithRepository findById(String id, AclPermission aclPermission); + Flux findAllByIds(Set ids, AclPermission permission); + Mono saveStructure(String datasourceId, DatasourceStructure structure); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepositoryImpl.java index 343a9f9d17..236dbcd0cb 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepositoryImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomDatasourceRepositoryImpl.java @@ -14,6 +14,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Set; import static org.springframework.data.mongodb.core.query.Criteria.where; import static org.springframework.data.mongodb.core.query.Query.query; @@ -38,6 +39,11 @@ public class CustomDatasourceRepositoryImpl extends BaseAppsmithRepositoryImpl findAllByIds(Set ids, AclPermission permission) { + Criteria idcriteria = where(fieldName(QDatasource.datasource.id)).in(ids); + return queryAll(List.of(idcriteria), permission); + } + public Mono saveStructure(String datasourceId, DatasourceStructure structure) { return mongoOperations.updateFirst( query(where(fieldName(QDatasource.datasource.id)).is(datasourceId)), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewActionRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewActionRepository.java new file mode 100644 index 0000000000..820599a3eb --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewActionRepository.java @@ -0,0 +1,36 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.domains.NewAction; +import org.springframework.data.domain.Sort; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Set; + +public interface CustomNewActionRepository extends AppsmithRepository { + Mono findByUnpublishedNameAndPageId(String name, String pageId, AclPermission aclPermission); + + Flux findByPageId(String pageId, AclPermission aclPermission); + + Flux findByPageId(String pageId); + + Flux findByPageIdAndViewMode(String pageId, Boolean viewMode, AclPermission aclPermission); + + Flux findUnpublishedActionsForRestApiOnLoad(Set names, + String pageId, + String httpMethod, + Boolean userSetOnLoad, AclPermission aclPermission); + + Flux findAllActionsByNameAndPageIdsAndViewMode(String name, List pageIds, Boolean viewMode, AclPermission aclPermission, Sort sort); + + Flux findUnpublishedActionsByNameInAndPageIdAndExecuteOnLoadTrue( + Set names, String pageId, AclPermission permission); + + Flux findByApplicationId(String applicationId, AclPermission aclPermission, Sort sort); + + Flux findByApplicationIdAndViewMode(String applicationId, Boolean viewMode, AclPermission aclPermission); + + Mono countByDatasourceId(String datasourceId); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewActionRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewActionRepositoryImpl.java new file mode 100644 index 0000000000..67a2c33738 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewActionRepositoryImpl.java @@ -0,0 +1,223 @@ +package com.appsmith.server.repositories; + +import com.appsmith.external.models.QActionConfiguration; +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.QNewAction; +import com.appsmith.server.domains.User; +import lombok.extern.slf4j.Slf4j; +import org.bson.types.ObjectId; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +@Component +@Slf4j +public class CustomNewActionRepositoryImpl extends BaseAppsmithRepositoryImpl + implements CustomNewActionRepository { + + public CustomNewActionRepositoryImpl(ReactiveMongoOperations mongoOperations, + MongoConverter mongoConverter) { + super(mongoOperations, mongoConverter); + } + + @Override + public Mono findByUnpublishedNameAndPageId(String name, String pageId, AclPermission aclPermission) { + Criteria nameCriteria = where(fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.name)).is(name); + Criteria pageCriteria = where(fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.pageId)).is(pageId); + + return queryOne(List.of(nameCriteria, pageCriteria), aclPermission); + } + + @Override + public Flux findByPageId(String pageId, AclPermission aclPermission) { + String unpublishedPage = fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.pageId); + String publishedPage = fieldName(QNewAction.newAction.publishedAction)+"."+fieldName(QNewAction.newAction.publishedAction.pageId); + + Criteria pageCriteria = new Criteria().orOperator( + where(unpublishedPage).is(pageId), + where(publishedPage).is(pageId) + ); + + return ReactiveSecurityContextHolder.getContext() + .map(ctx -> ctx.getAuthentication()) + .flatMapMany(auth -> { + User user = (User) auth.getPrincipal(); + Query query = new Query(); + + if (aclPermission == null) { + query.addCriteria(new Criteria().andOperator(notDeleted(), pageCriteria)); + } else { + query.addCriteria(new Criteria().andOperator(notDeleted(), userAcl(user, aclPermission), pageCriteria)); + } + + return mongoOperations.query(NewAction.class) + .matching(query) + .all() + .map(obj -> setUserPermissionsInObject(obj, user)); + }); + } + + @Override + public Flux findByPageId(String pageId) { + return this.findByPageId(pageId, null); + } + + @Override + public Flux findByPageIdAndViewMode(String pageId, Boolean viewMode, AclPermission aclPermission) { + Criteria pageCriteria; + + // Fetch published actions + if (Boolean.TRUE.equals(viewMode)) { + pageCriteria = where(fieldName(QNewAction.newAction.publishedAction)+"."+fieldName(QNewAction.newAction.publishedAction.pageId)).is(pageId); + } + // Fetch unpublished actions + else { + pageCriteria = where(fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.pageId)).is(pageId); + } + return queryAll(List.of(pageCriteria), aclPermission); + } + + @Override + public Flux findUnpublishedActionsForRestApiOnLoad( + Set names, + String pageId, + String httpMethod, + Boolean userSetOnLoad, + AclPermission aclPermission) { + Criteria namesCriteria = where(fieldName(QNewAction.newAction.unpublishedAction) + + "." + + fieldName(QNewAction.newAction.unpublishedAction.name)) + .in(names); + + Criteria pageCriteria = where(fieldName(QNewAction.newAction.unpublishedAction) + + "." + + fieldName(QNewAction.newAction.unpublishedAction.pageId)) + .is(pageId); + + Criteria userSetOnLoadCriteria = where(fieldName(QNewAction.newAction.unpublishedAction) + + "." + +fieldName(QNewAction.newAction.unpublishedAction.userSetOnLoad)) + .is(userSetOnLoad); + + String httpMethodQueryKey = fieldName(QNewAction.newAction.unpublishedAction) + + "." + + fieldName(QNewAction.newAction.unpublishedAction.actionConfiguration) + + "." + + fieldName(QActionConfiguration.actionConfiguration.httpMethod); + + Criteria httpMethodCriteria = where(httpMethodQueryKey).is(httpMethod); + List criterias = List.of(namesCriteria, pageCriteria, httpMethodCriteria, userSetOnLoadCriteria); + + return queryAll(criterias, aclPermission); + } + + @Override + public Flux findAllActionsByNameAndPageIdsAndViewMode(String name, + List pageIds, + Boolean viewMode, + AclPermission aclPermission, + Sort sort) { + /** + * TODO : This function is called by get(params) to get all actions by params and hence + * only covers criteria of few fields like page id, name, etc. Make this generic to cover + * all possible fields + */ + + List criteriaList = new ArrayList<>(); + + // Fetch published actions + if (Boolean.TRUE.equals(viewMode)) { + + if (name != null) { + Criteria nameCriteria = where(fieldName(QNewAction.newAction.publishedAction)+"."+fieldName(QNewAction.newAction.publishedAction.name)).is(name); + criteriaList.add(nameCriteria); + } + + if (pageIds != null && !pageIds.isEmpty()) { + Criteria pageCriteria = where(fieldName(QNewAction.newAction.publishedAction)+"."+fieldName(QNewAction.newAction.publishedAction.pageId)).in(pageIds); + criteriaList.add(pageCriteria); + } + } + // Fetch unpublished actions + else { + + if (name != null) { + Criteria nameCriteria = where(fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.name)).is(name); + criteriaList.add(nameCriteria); + } + + if (pageIds != null && !pageIds.isEmpty()) { + Criteria pageCriteria = where(fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.pageId)).in(pageIds); + criteriaList.add(pageCriteria); + } + } + + return queryAll(criteriaList, aclPermission, sort); + } + + @Override + public Flux findUnpublishedActionsByNameInAndPageIdAndExecuteOnLoadTrue(Set names, + String pageId, + AclPermission permission) { + List criteriaList = new ArrayList<>(); + if (names != null) { + Criteria namesCriteria = where(fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.name)).in(names); + criteriaList.add(namesCriteria); + } + Criteria pageCriteria = where(fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.pageId)).is(pageId); + criteriaList.add(pageCriteria); + + Criteria executeOnLoadCriteria = where(fieldName(QNewAction.newAction.unpublishedAction)+"."+fieldName(QNewAction.newAction.unpublishedAction.executeOnLoad)).is(Boolean.TRUE); + criteriaList.add(executeOnLoadCriteria); + + return queryAll(criteriaList, permission); + } + + @Override + public Flux findByApplicationId(String applicationId, AclPermission aclPermission, Sort sort) { + + Criteria applicationCriteria = where(fieldName(QNewAction.newAction.applicationId)).is(applicationId); + + return queryAll(List.of(applicationCriteria), aclPermission, sort); + } + + @Override + public Flux findByApplicationIdAndViewMode(String applicationId, + Boolean viewMode, + AclPermission aclPermission) { + + Criteria applicationCriteria = where(fieldName(QNewAction.newAction.applicationId)).is(applicationId); + + return queryAll(List.of(applicationCriteria), aclPermission); + } + + @Override + public Mono countByDatasourceId(String datasourceId) { + Criteria unpublishedDatasourceCriteria = where(fieldName(QNewAction.newAction.unpublishedAction) + + ".datasource._id") + .is(new ObjectId(datasourceId)); + Criteria publishedDatasourceCriteria = where(fieldName(QNewAction.newAction.publishedAction) + + ".datasource._id") + .is(new ObjectId(datasourceId)); + + Criteria datasourceCriteria = new Criteria().orOperator(unpublishedDatasourceCriteria, publishedDatasourceCriteria); + + Query query = new Query(); + query.addCriteria(datasourceCriteria); + + return mongoOperations.count(query, "newAction"); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewPageRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewPageRepository.java new file mode 100644 index 0000000000..81a27c084f --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewPageRepository.java @@ -0,0 +1,16 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.domains.NewPage; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface CustomNewPageRepository extends AppsmithRepository { + Flux findByApplicationId(String applicationId, AclPermission aclPermission); + + Mono findByIdAndLayoutsIdAndViewMode(String id, String layoutId, AclPermission aclPermission, Boolean viewMode); + + Mono findByNameAndViewMode(String name, AclPermission aclPermission, Boolean viewMode); + + Mono findByNameAndApplicationIdAndViewMode(String name, String applicationId, AclPermission aclPermission, Boolean viewMode); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewPageRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewPageRepositoryImpl.java new file mode 100644 index 0000000000..43b49c6b98 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNewPageRepositoryImpl.java @@ -0,0 +1,79 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.domains.QLayout; +import com.appsmith.server.domains.QNewPage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +@Component +@Slf4j +public class CustomNewPageRepositoryImpl extends BaseAppsmithRepositoryImpl + implements CustomNewPageRepository { + + public CustomNewPageRepositoryImpl(ReactiveMongoOperations mongoOperations, MongoConverter mongoConverter) { + super(mongoOperations, mongoConverter); + } + + @Override + public Flux findByApplicationId(String applicationId, AclPermission aclPermission) { + Criteria applicationIdCriteria = where(fieldName(QNewPage.newPage.applicationId)).is(applicationId); + return queryAll(List.of(applicationIdCriteria), aclPermission); + } + + @Override + public Mono findByIdAndLayoutsIdAndViewMode(String id, String layoutId, AclPermission aclPermission, Boolean viewMode) { + Criteria idCriterion = getIdCriteria(id); + String layoutsIdKey; + String layoutsKey; + + if (Boolean.TRUE.equals(viewMode)) { + layoutsKey = fieldName(QNewPage.newPage.publishedPage) + "." + fieldName(QNewPage.newPage.publishedPage.layouts); + } else { + layoutsKey = fieldName(QNewPage.newPage.unpublishedPage) + "." + fieldName(QNewPage.newPage.unpublishedPage.layouts); + } + layoutsIdKey = layoutsKey + "." + fieldName(QLayout.layout.id); + + Criteria layoutCriterion = where(layoutsIdKey).is(layoutId); + + List criteria = List.of(idCriterion, layoutCriterion); + return queryOne(criteria, aclPermission); + } + + @Override + public Mono findByNameAndViewMode(String name, AclPermission aclPermission, Boolean viewMode) { + Criteria nameCriterion = getNameCriterion(name, viewMode); + + return queryOne(List.of(nameCriterion), aclPermission); + } + + @Override + public Mono findByNameAndApplicationIdAndViewMode(String name, String applicationId, AclPermission aclPermission, Boolean viewMode) { + Criteria nameCriterion = getNameCriterion(name, viewMode); + + Criteria applicationIdCriterion = where(fieldName(QNewPage.newPage.applicationId)).is(applicationId); + + return queryOne(List.of(nameCriterion, applicationIdCriterion), aclPermission); + } + + private Criteria getNameCriterion(String name, Boolean viewMode) { + String nameKey; + + if (Boolean.TRUE.equals(viewMode)) { + nameKey = fieldName(QNewPage.newPage.publishedPage) + "." + fieldName(QNewPage.newPage.publishedPage.name); + } else { + nameKey = fieldName(QNewPage.newPage.unpublishedPage) + "." + fieldName(QNewPage.newPage.unpublishedPage.name); + } + return where(nameKey).is(name); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NewActionRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NewActionRepository.java new file mode 100644 index 0000000000..c87aea1fb2 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NewActionRepository.java @@ -0,0 +1,12 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.NewAction; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +@Repository +public interface NewActionRepository extends BaseRepository, CustomNewActionRepository { + + Flux findByApplicationId(String applicationId); + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NewPageRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NewPageRepository.java new file mode 100644 index 0000000000..acb2e15844 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NewPageRepository.java @@ -0,0 +1,12 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.NewPage; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +@Repository +public interface NewPageRepository extends BaseRepository, CustomNewPageRepository { + + Flux findByApplicationId(String applicationId); + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionCollectionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionCollectionService.java index 987c32333b..4aafe58d57 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionCollectionService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionCollectionService.java @@ -1,13 +1,13 @@ package com.appsmith.server.services; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Collection; +import com.appsmith.server.dtos.ActionDTO; import reactor.core.publisher.Mono; public interface ActionCollectionService { Mono createCollection(Collection collection); - Mono createAction(Action action); + Mono createAction(ActionDTO action); - Mono updateAction(String id, Action action); + Mono updateAction(String id, ActionDTO action); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionCollectionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionCollectionServiceImpl.java index 31644a6fb3..8b1ce56642 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionCollectionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ActionCollectionServiceImpl.java @@ -1,7 +1,8 @@ package com.appsmith.server.services; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Collection; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import lombok.extern.slf4j.Slf4j; @@ -13,17 +14,17 @@ import reactor.core.publisher.Mono; @Service @Slf4j public class ActionCollectionServiceImpl implements ActionCollectionService { - private final ActionService actionService; private final CollectionService collectionService; private final LayoutActionService layoutActionService; + private final NewActionService newActionService; @Autowired - public ActionCollectionServiceImpl(ActionService actionService, - CollectionService collectionService, - LayoutActionService layoutActionService) { - this.actionService = actionService; + public ActionCollectionServiceImpl(CollectionService collectionService, + LayoutActionService layoutActionService, + NewActionService newActionService) { this.collectionService = collectionService; this.layoutActionService = layoutActionService; + this.newActionService = newActionService; } /** @@ -47,13 +48,13 @@ public class ActionCollectionServiceImpl implements ActionCollectionService { .flatMap(action -> { if (action.getId() == null) { //Action doesn't exist. Create now. - return actionService.create(action); + return newActionService.createAction(action.getUnpublishedAction()); } - return Mono.just(action); + return Mono.just(action.getUnpublishedAction()); }) //Need to store only action Ids inside the collection. Extract only ids and return the same inside Action .map(action -> { - Action toSave = new Action(); + NewAction toSave = new NewAction(); toSave.setId(action.getId()); return toSave; }) @@ -74,19 +75,18 @@ public class ActionCollectionServiceImpl implements ActionCollectionService { * @return */ @Override - public Mono createAction(Action action) { + public Mono createAction(ActionDTO action) { if (action.getCollectionId() == null) { - return actionService.create(action); + return newActionService.createAction(action); } - Action finalAction = action; - return Mono.just(action) - .flatMap(actionService::create) + ActionDTO finalAction = action; + return newActionService.createAction(action) .flatMap(savedAction -> collectionService.addSingleActionToCollection(finalAction.getCollectionId(), savedAction)); } @Override - public Mono updateAction(String id, Action action) { + public Mono updateAction(String id, ActionDTO action) { // Since the policies are server only concept, we should first set this to null. action.setPolicies(null); @@ -96,30 +96,33 @@ public class ActionCollectionServiceImpl implements ActionCollectionService { return layoutActionService.updateAction(id, action); } else if (action.getCollectionId().length() == 0) { //The Action has been removed from existing collection. - return actionService + return newActionService .getById(id) - .flatMap(action1 -> collectionService.removeSingleActionFromCollection(action1.getCollectionId(), action1)) + .flatMap(action1 -> collectionService.removeSingleActionFromCollection(action1.getUnpublishedAction().getCollectionId(), + Mono.just(action1))) .flatMap(action1 -> { log.debug("Action {} has been removed from its collection.", action1.getId()); action.setCollectionId(null); - return layoutActionService.updateAction(id, action) - .flatMap(updatedAction -> { - updatedAction.setCollectionId(null); - return actionService.save(updatedAction); - }); + return layoutActionService.updateAction(id, action); }); } else { //If the code flow has reached this point, that means that the collectionId has been changed to another collection. //Remove the action from previous collection and add it to the new collection. - return actionService + return newActionService .getById(id) .flatMap(action1 -> { - if (action1.getCollectionId() != null) { - return collectionService.removeSingleActionFromCollection(action1.getCollectionId(), action1); + if (action1.getUnpublishedAction().getCollectionId() != null) { + return collectionService.removeSingleActionFromCollection(action1.getUnpublishedAction().getCollectionId(), + Mono.just(action1)); } - return Mono.just(action1); + return Mono.just(newActionService.generateActionByViewMode(action1, false)); + }) + .map(obj -> (NewAction) obj) + .flatMap(action1 -> { + ActionDTO unpublishedAction = action1.getUnpublishedAction(); + unpublishedAction.setId(action1.getId()); + return collectionService.addSingleActionToCollection(action.getCollectionId(), unpublishedAction); }) - .flatMap(action1 -> collectionService.addSingleActionToCollection(action.getCollectionId(), action1)) .flatMap(action1 -> { log.debug("Action {} removed from its previous collection and added to the new collection", action1.getId()); return layoutActionService.updateAction(id, action); 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 b0133d389b..77e0355234 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 @@ -19,6 +19,7 @@ import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Action; import com.appsmith.server.domains.ActionProvider; import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.PluginType; @@ -30,7 +31,6 @@ import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.MustacheHelper; import com.appsmith.server.helpers.PluginExecutorHelper; import com.appsmith.server.repositories.ActionRepository; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.ArrayUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -61,7 +61,6 @@ 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_DATASOURCES; -import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; import static com.appsmith.server.acl.AclPermission.READ_PAGES; @@ -72,13 +71,12 @@ public class ActionServiceImpl extends BaseService { - Page page = tuple.getT1(); + NewPage page = tuple.getT1(); User user = tuple.getT2(); // Inherit the action policies from the page. - generateAndSetActionPolicies(page, user, action); + generateAndSetActionPolicies(page, action); // If the datasource is embedded, check for organizationId and set it in action if (action.getDatasource() != null && @@ -554,8 +550,9 @@ public class ActionServiceImpl extends BaseService policySet = page.getPolicies().stream() - .filter(policy -> policy.getPermission().equals(MANAGE_PAGES.getValue()) - || policy.getPermission().equals(READ_PAGES.getValue())) - .collect(Collectors.toSet()); - Set documentPolicies = policyGenerator.getAllChildPolicies(policySet, Page.class, Action.class); + private void generateAndSetActionPolicies(NewPage page, Action action) { + Set documentPolicies = policyGenerator.getAllChildPolicies(page.getPolicies(), Page.class, Action.class); action.setPolicies(documentPolicies); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApiImporter.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApiImporter.java index 96ab7ce542..8cdf64f1e3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApiImporter.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApiImporter.java @@ -1,10 +1,10 @@ package com.appsmith.server.services; -import com.appsmith.server.domains.Action; +import com.appsmith.server.dtos.ActionDTO; import reactor.core.publisher.Mono; public interface ApiImporter { - Mono importAction(Object input, String pageId, String name, String orgId); + Mono importAction(Object input, String pageId, String name, String orgId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java index b170e0114e..4190c49497 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageService.java @@ -1,24 +1,24 @@ package com.appsmith.server.services; import com.appsmith.server.domains.Application; -import com.appsmith.server.domains.Page; +import com.appsmith.server.dtos.PageDTO; import com.mongodb.client.result.UpdateResult; import reactor.core.publisher.Mono; public interface ApplicationPageService { - Mono createPage(Page page); + Mono createPage(PageDTO page); - Mono addPageToApplication(Application application, Page page, Boolean isDefault); + Mono addPageToApplication(Application application, PageDTO page, Boolean isDefault); - Mono getPage(String pageId, boolean viewMode); + Mono getPage(String pageId, boolean viewMode); Mono createApplication(Application application); Mono createApplication(Application application, String orgId); - Mono getPageByName(String applicationName, String pageName, boolean viewMode); + Mono getPageByName(String applicationName, String pageName, boolean viewMode); - Mono makePageDefault(Page page); + Mono makePageDefault(PageDTO page); Mono makePageDefault(String applicationId, String pageId); @@ -26,7 +26,11 @@ public interface ApplicationPageService { Mono deleteApplication(String id); - Mono clonePage(String pageId); + Mono clonePage(String pageId); Mono cloneApplication(String applicationId); + + Mono deleteUnpublishedPage(String id); + + Mono publish(String applicationId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java index d1a9633d3b..9da96447e3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationPageServiceImpl.java @@ -4,14 +4,17 @@ import com.appsmith.external.models.Policy; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.PolicyGenerator; import com.appsmith.server.constants.FieldName; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ApplicationPagesDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.dtos.PageNameIdDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -26,6 +29,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.annotation.Nullable; +import java.time.Instant; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -39,11 +43,10 @@ import static com.appsmith.server.acl.AclPermission.ORGANIZATION_MANAGE_APPLICAT import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; import static com.appsmith.server.acl.AclPermission.READ_PAGES; -@Slf4j @Service +@Slf4j public class ApplicationPageServiceImpl implements ApplicationPageService { private final ApplicationService applicationService; - private final PageService pageService; private final SessionUserService sessionUserService; private final OrganizationService organizationService; private final LayoutActionService layoutActionService; @@ -52,29 +55,30 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { private final PolicyGenerator policyGenerator; private final ApplicationRepository applicationRepository; - private final ActionService actionService; + private final NewPageService newPageService; + private final NewActionService newActionService; public ApplicationPageServiceImpl(ApplicationService applicationService, - PageService pageService, SessionUserService sessionUserService, OrganizationService organizationService, LayoutActionService layoutActionService, AnalyticsService analyticsService, PolicyGenerator policyGenerator, ApplicationRepository applicationRepository, - ActionService actionService) { + NewPageService newPageService, + NewActionService newActionService) { this.applicationService = applicationService; - this.pageService = pageService; this.sessionUserService = sessionUserService; this.organizationService = organizationService; this.layoutActionService = layoutActionService; this.analyticsService = analyticsService; this.policyGenerator = policyGenerator; this.applicationRepository = applicationRepository; - this.actionService = actionService; + this.newPageService = newPageService; + this.newActionService = newActionService; } - public Mono createPage(Page page) { + public Mono createPage(PageDTO page) { if (page.getId() != null) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID)); } else if (page.getName() == null) { @@ -88,28 +92,25 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { layoutList = new ArrayList<>(); } if (layoutList.isEmpty()) { - layoutList.add(pageService.createDefaultLayout()); + layoutList.add(newPageService.createDefaultLayout()); page.setLayouts(layoutList); } Mono applicationMono = applicationService.findById(page.getApplicationId(), AclPermission.MANAGE_APPLICATIONS) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION_ID, page.getApplicationId()))); - Mono userMono = sessionUserService.getCurrentUser(); - Mono pageMono = Mono.zip(applicationMono, userMono) - .map(tuple -> { - Application application = tuple.getT1(); - User user = tuple.getT2(); - generateAndSetPagePolicies(application, user, page); + Mono pageMono = applicationMono + .map(application -> { + generateAndSetPagePolicies(application, page); return page; }); return pageMono - .flatMap(pageService::createDefault) + .flatMap(newPageService::createDefault) //After the page has been saved, update the application (save the page id inside the application) .zipWith(applicationMono) .flatMap(tuple -> { - final Page savedPage = tuple.getT1(); + final PageDTO savedPage = tuple.getT1(); final Application application = tuple.getT2(); return addPageToApplication(application, savedPage, false) .thenReturn(savedPage); @@ -125,7 +126,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { * @return UpdateResult object with details on how many documents have been updated, which should be 0 or 1. */ @Override - public Mono addPageToApplication(Application application, Page page, Boolean isDefault) { + public Mono addPageToApplication(Application application, PageDTO page, Boolean isDefault) { return applicationRepository.addPageToApplication(application.getId(), page.getId(), isDefault) .doOnSuccess(result -> { if (result.getModifiedCount() != 1) { @@ -135,23 +136,14 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { } @Override - public Mono getPage(String pageId, boolean viewMode) { + public Mono getPage(String pageId, boolean viewMode) { AclPermission permission = viewMode ? READ_PAGES : MANAGE_PAGES; - return pageService.findById(pageId, permission) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE, pageId))) - .map(page -> { - List layoutList = page.getLayouts(); - // Set the view mode for all the layouts in the page. This ensures that we send the correct DSL - // back to the client - layoutList.stream() - .forEach(layout -> layout.setViewMode(viewMode)); - page.setLayouts(layoutList); - return page; - }); + return newPageService.findPageById(pageId, permission, viewMode) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE, pageId))); } @Override - public Mono getPageByName(String applicationName, String pageName, boolean viewMode) { + public Mono getPageByName(String applicationName, String pageName, boolean viewMode) { AclPermission appPermission; AclPermission pagePermission; if (viewMode) { @@ -166,27 +158,20 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { return applicationService .findByName(applicationName, appPermission) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE + "by application name", applicationName))) - .flatMap(application -> pageService.findByNameAndApplicationId(pageName, application.getId(), pagePermission)) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE + "by page name", pageName))) - .map(page -> { - List layoutList = page.getLayouts(); - // Set the view mode for all the layouts in the page. This ensures that we send the correct DSL - // back to the client - layoutList.stream() - .forEach(layout -> layout.setViewMode(viewMode)); - page.setLayouts(layoutList); - return page; - }); + .flatMap(application -> newPageService.findByNameAndApplicationIdAndViewMode(pageName, application.getId(), pagePermission, viewMode)) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE + "by page name", pageName))); } @Override - public Mono makePageDefault(Page page) { + public Mono makePageDefault(PageDTO page) { return makePageDefault(page.getApplicationId(), page.getId()); } @Override public Mono makePageDefault(String applicationId, String pageId) { - return pageService.findById(pageId, AclPermission.MANAGE_PAGES) + // Since this can only happen during edit, the page in question is unpublished page. Set the view mode accordingly + Boolean viewMode = false; + return newPageService.findPageById(pageId, AclPermission.MANAGE_PAGES, viewMode) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE, pageId))) // Check if the page actually belongs to the application. .flatMap(page -> { @@ -195,7 +180,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { } return Mono.error(new AppsmithException(AppsmithError.PAGE_DOESNT_BELONG_TO_APPLICATION, page.getName(), applicationId)); }) - .then(applicationService.findById(applicationId)) + .then(applicationService.findById(applicationId, MANAGE_APPLICATIONS)) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION_ID, applicationId))) .flatMap(application -> applicationRepository @@ -219,29 +204,30 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORGANIZATION_ID)); } + application.setPublishedPages(new ArrayList<>()); + Mono userMono = sessionUserService.getCurrentUser().cache(); Mono applicationWithPoliciesMono = setApplicationPolicies(userMono, orgId, application); return applicationWithPoliciesMono .flatMap(applicationService::createDefault) - .zipWith(userMono) - .flatMap(tuple -> { - Application savedApplication = tuple.getT1(); - User user = tuple.getT2(); + .flatMap(savedApplication -> { - Page page = new Page(); + PageDTO page = new PageDTO(); page.setName(FieldName.DEFAULT_PAGE_NAME); page.setApplicationId(savedApplication.getId()); List layoutList = new ArrayList<>(); - layoutList.add(pageService.createDefaultLayout()); + layoutList.add(newPageService.createDefaultLayout()); page.setLayouts(layoutList); //Set the page policies - generateAndSetPagePolicies(savedApplication, user, page); + generateAndSetPagePolicies(savedApplication, page); - return pageService + return newPageService .createDefault(page) .flatMap(savedPage -> addPageToApplication(savedApplication, savedPage, true)) + // fetch the application again because the application.pages has changed post the addition of + // the newly created page to the application. .then(applicationService.findById(savedApplication.getId(), READ_APPLICATIONS)); }); } @@ -277,6 +263,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { application.setId(null); application.setPolicies(new HashSet<>()); application.setPages(new ArrayList<>()); + application.setPublishedPages(new ArrayList<>()); application.setIsPublic(false); Mono userMono = sessionUserService.getCurrentUser().cache(); @@ -286,12 +273,8 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { .flatMap(applicationService::createDefault); } - private void generateAndSetPagePolicies(Application application, User user, Page page) { - Set policySet = application.getPolicies().stream() - .filter(policy -> policy.getPermission().equals(MANAGE_APPLICATIONS.getValue()) - || policy.getPermission().equals(READ_APPLICATIONS.getValue())) - .collect(Collectors.toSet()); - Set documentPolicies = policyGenerator.getAllChildPolicies(policySet, Application.class, Page.class); + private void generateAndSetPagePolicies(Application application, PageDTO page) { + Set documentPolicies = policyGenerator.getAllChildPolicies(application.getPolicies(), Application.class, Page.class); page.setPolicies(documentPolicies); } @@ -309,9 +292,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "application", id))) .flatMap(application -> { log.debug("Archiving pages for applicationId: {}", id); - return pageService.findByApplicationId(id, READ_PAGES) - .flatMap(page -> pageService.delete(page.getId())) - .collectList() + return newPageService.archivePagesByApplicationId(id, MANAGE_PAGES) .thenReturn(application); }) .flatMap(applicationService::archive); @@ -321,19 +302,19 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { } @Override - public Mono clonePage(String pageId) { + public Mono clonePage(String pageId) { - return pageService.findById(pageId, MANAGE_PAGES) + return newPageService.findById(pageId, MANAGE_PAGES) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED))) .flatMap(page -> clonePageGivenApplicationId(pageId, page.getApplicationId(), " Copy")); } - private Mono clonePageGivenApplicationId(String pageId, String applicationId, + private Mono clonePageGivenApplicationId(String pageId, String applicationId, @Nullable String newPageNameSuffix) { // Find the source page and then prune the page layout fields to only contain the required fields that should be // copied. - Mono sourcePageMono = pageService.findById(pageId, MANAGE_PAGES) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACTION_IS_NOT_AUTHORIZED))) + Mono sourcePageMono = newPageService.findPageById(pageId, MANAGE_PAGES, false) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PAGE, pageId))) .flatMap(page -> Flux.fromIterable(page.getLayouts()) .map(layout -> layout.getDsl()) .map(dsl -> { @@ -350,14 +331,14 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { })); // This call is without - Flux sourceActionFlux = actionService.findByPageId(pageId, MANAGE_ACTIONS) + Flux sourceActionFlux = newActionService.findByPageId(pageId, MANAGE_ACTIONS) // In case there are no actions in the page being cloned, return empty .switchIfEmpty(Flux.empty()); return sourcePageMono .flatMap(page -> { - Mono pageNamesMono = pageService - .findNamesByApplicationId(page.getApplicationId()); + Mono pageNamesMono = newPageService + .findNamesByApplicationIdAndViewMode(page.getApplicationId(), false); return pageNamesMono // If a new page name suffix is given, // set a unique name for the cloned page and then create the page. @@ -383,16 +364,18 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { // Proceed with creating the copy of the page page.setId(null); page.setApplicationId(applicationId); - return pageService.createDefault(page); + return newPageService.createDefault(page); }); }) .flatMap(page -> { String newPageId = page.getId(); return sourceActionFlux .flatMap(action -> { - action.setId(null); - action.setPageId(newPageId); - return actionService.create(action); + // Set new page id in the actionDTO + action.getUnpublishedAction().setPageId(newPageId); + + // Now create the new action from the template of the source action. + return newActionService.createAction(action.getUnpublishedAction()); }) .collectList() .thenReturn(page); @@ -420,7 +403,7 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { }); } - private Mono clonePageGivenApplicationId(String pageId, String applicationId) { + private Mono clonePageGivenApplicationId(String pageId, String applicationId) { return clonePageGivenApplicationId(pageId, applicationId, null); } @@ -481,10 +464,157 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { // Set the cloned pages into the cloned application and save. .flatMap(clonedPages -> { savedApplication.setPages(clonedPages); - return applicationRepository.save(savedApplication); + return applicationService.save(savedApplication); }) ); }); } + /** + * This function archives the unpublished page. This also archives the unpublished action. The reason that the + * entire action is not deleted at this point is to handle the following edge case : + * An application is published with 1 page and 1 action. + * Post publish, create a new page and move the action from the existing page to the new page. Now delete this newly + * created page. + * In this scenario, if we were to delete all actions associated with the page, we would end up deleting an action + * which is currently in published state and is being used. + * + * @param id The pageId which needs to be archived. + * @return + */ + @Override + public Mono deleteUnpublishedPage(String id) { + + return newPageService.findById(id, AclPermission.MANAGE_PAGES) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PAGE_ID, id))) + .flatMap(page -> { + log.debug("Going to archive pageId: {} for applicationId: {}", page.getId(), page.getApplicationId()); + Mono applicationMono = applicationService.getById(page.getApplicationId()) + .flatMap(application -> { + application.getPages().removeIf(p -> p.getId().equals(page.getId())); + return applicationService.save(application); + }); + Mono newPageMono; + if (page.getPublishedPage() != null) { + PageDTO unpublishedPage = page.getUnpublishedPage(); + unpublishedPage.setDeletedAt(Instant.now()); + newPageMono = newPageService.save(page); + } else { + // This page was never published. This can be safely archived. + newPageMono = newPageService.archive(page); + } + + Mono archivedPageMono = newPageMono + .flatMap(analyticsService::sendDeleteEvent) + .flatMap(newPage -> newPageService.getPageByViewMode(newPage, false)); + + /** + * Only delete unpublished action and not the entire action. + */ + Mono> archivedActionsMono = newActionService.findByPageId(page.getId(), MANAGE_ACTIONS) + .flatMap(action -> { + log.debug("Going to archive actionId: {} for applicationId: {}", action.getId(), id); + return newActionService.deleteUnpublishedAction(action.getId()); + }).collectList(); + + return Mono.zip(archivedPageMono, archivedActionsMono, applicationMono) + .map(tuple -> { + PageDTO page1 = tuple.getT1(); + List actions = tuple.getT2(); + Application application = tuple.getT3(); + log.debug("Archived pageId: {} and {} actions for applicationId: {}", page1.getId(), actions.size(), application.getId()); + return page1; + }); + }); + } + + /** + * This function walks through all the pages in the application. In each page, it walks through all the layouts. + * In a layout, dsl and publishedDsl JSONObjects exist. Publish function is responsible for copying the dsl into + * the publishedDsl. + * + * @param applicationId The id of the application that will be published. + * @return Publishes a Boolean true, when the application has been published. + */ + @Override + public Mono publish(String applicationId) { + Mono applicationMono = applicationService.findById(applicationId, MANAGE_APPLICATIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "application", applicationId))); + + Flux publishApplicationAndPages = applicationMono + //Return all the pages in the Application + .flatMap(application -> { + List pages = application.getPages(); + if (pages == null) { + pages = new ArrayList<>(); + } + + // This is the time to delete any page which was deleted in edit mode but still exists in the published mode + List publishedPages = application.getPublishedPages(); + if (publishedPages == null) { + publishedPages = new ArrayList<>(); + } + Set publishedPageIds = publishedPages.stream().map(applicationPage -> applicationPage.getId()).collect(Collectors.toSet()); + Set editedPageIds = pages.stream().map(applicationPage -> applicationPage.getId()).collect(Collectors.toSet()); + + /** + * Now add the published page ids and edited page ids into a single set and then remove the edited + * page ids to get a set of page ids which have been deleted in the edit mode. + * For example : + * Published page ids : [ A, B, C ] + * Edited Page ids : [ B, C, D ] aka A has been deleted and D has been added + * Step 1. Add both the ids into a single set : [ A, B, C, D] + * Step 2. Remove Edited Page Ids : [ A ] + * Result : Page A which has been deleted in the edit mode + */ + publishedPageIds.addAll(editedPageIds); + publishedPageIds.removeAll(editedPageIds); + + Mono> archivePageListMono; + if (!publishedPageIds.isEmpty()) { + archivePageListMono = Flux.fromStream(publishedPageIds.stream()) + .flatMap(id -> newPageService.archiveById(id)) + .collectList(); + } else { + archivePageListMono = Mono.just(new ArrayList<>()); + } + + application.setPublishedPages(pages); + + // Archive the deleted pages and save the application changes and then return the pages so that + // the pages can also be published + return Mono.zip(archivePageListMono, applicationService.save(application)) + .thenReturn(pages); + }) + .flatMapMany(Flux::fromIterable) + //In each page, copy each layout's dsl to publishedDsl field + .flatMap(applicationPage -> newPageService + .findById(applicationPage.getId(), MANAGE_PAGES) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "page", applicationPage.getId()))) + .map(page -> { + page.setPublishedPage(page.getUnpublishedPage()); + return page; + })) + .collectList() + .flatMapMany(newPageService::saveAll); + + Flux publishedActionsFlux = newActionService + .findAllByApplicationIdAndViewMode(applicationId, false, MANAGE_ACTIONS, null) + .flatMap(newAction -> { + // If the action was deleted in edit mode, now this can be safely deleted from the repository + if (newAction.getUnpublishedAction().getDeletedAt() != null) { + return newActionService.delete(newAction.getId()) + .then(Mono.empty()); + } + // Publish the action by copying the unpublished actionDTO to published actionDTO + newAction.setPublishedAction(newAction.getUnpublishedAction()); + return Mono.just(newAction); + }) + .collectList() + .flatMapMany(actions -> newActionService.saveAll(actions)); + + return Mono.zip(publishApplicationAndPages.collectList(), publishedActionsFlux.collectList()) + .map(tuple -> true); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationService.java index 0ed6ad082c..f182a2a7b1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationService.java @@ -20,8 +20,6 @@ public interface ApplicationService extends CrudService { Mono findByName(String name, AclPermission permission); - Mono publish(String applicationId); - Mono save(Application application); Mono createDefault(Application object); @@ -31,4 +29,6 @@ public interface ApplicationService extends CrudService { Mono changeViewAccess (String id, ApplicationAccessDTO applicationAccessDTO); Flux findAllApplicationsByOrganizationId(String organizationId); + + Mono getApplicationInViewMode(String applicationId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java index 305fc8c2a4..df78137cb6 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ApplicationServiceImpl.java @@ -5,17 +5,16 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; -import com.appsmith.server.domains.ApplicationPage; -import com.appsmith.server.domains.Datasource; -import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ApplicationAccessDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.PolicyUtils; import com.appsmith.server.repositories.ApplicationRepository; -import com.appsmith.server.repositories.PageRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; @@ -27,14 +26,13 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import javax.validation.Validator; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES; import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS; -import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; @@ -42,11 +40,7 @@ import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; @Service public class ApplicationServiceImpl extends BaseService implements ApplicationService { - //Using PageRepository instead of PageService is because a cyclic dependency is introduced if PageService is used here. - //TODO : Solve for this across LayoutService, PageService and ApplicationService. - private final PageRepository pageRepository; private final PolicyUtils policyUtils; - private final DatasourceService datasourceService; private final ConfigService configService; @Autowired @@ -56,14 +50,10 @@ public class ApplicationServiceImpl extends BaseService publish(String applicationId) { - Mono applicationMono = findById(applicationId) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "application", applicationId))); - return applicationMono - //Return all the pages in the Application - .map(application -> { - List pages = application.getPages(); - if (pages == null) { - pages = new ArrayList<>(); - } - return pages; - }) - .flatMapMany(Flux::fromIterable) - //In each page, copy each layout's dsl to publishedDsl field - .flatMap(applicationPage -> pageRepository - .findById(applicationPage.getId()) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "page", applicationPage.getId()))) - .map(page -> { - List layoutList = page.getLayouts(); - for (Layout layout : layoutList) { - layout.setPublishedDsl(layout.getDsl()); - layout.setPublishedLayoutActions(layout.getLayoutActions()); - layout.setPublishedLayoutOnLoadActions(layout.getLayoutOnLoadActions()); - } - return page; - }) - .flatMap(pageRepository::save)) - .collectList() - .map(pages -> true); - } @Override public Mono changeViewAccess(String id, ApplicationAccessDTO applicationAccessDTO) { @@ -211,7 +162,16 @@ public class ApplicationServiceImpl extends BaseService generateAndSetPoliciesForPublicView(Application application, Boolean isPublic) { + @Override + public Mono getApplicationInViewMode(String applicationId) { + return repository.findById(applicationId, READ_APPLICATIONS) + .map(application -> { + application.setViewMode(true); + return application; + }); + } + + private Mono generateAndSetPoliciesForPublicView(Application application, Boolean isPublic) { AclPermission applicationPermission = READ_APPLICATIONS; AclPermission datasourcePermission = EXECUTE_DATASOURCES; @@ -225,36 +185,36 @@ public class ApplicationServiceImpl extends BaseService actionPolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(pagePolicyMap, Page.class, Action.class); Map datasourcePolicyMap = policyUtils.generatePolicyFromPermission(Set.of(datasourcePermission), user); - Flux updatedPagesFlux = policyUtils.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, isPublic); + Flux updatedPagesFlux = policyUtils.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, isPublic); - Flux updatedActionsFlux = updatedPagesFlux - .flatMap(page -> policyUtils.updateWithPagePermissionsToAllItsActions(page.getId(), actionPolicyMap, isPublic)); + Flux updatedActionsFlux = updatedPagesFlux + .collectList() + .then(Mono.just(application.getId())) + .flatMapMany(applicationId -> policyUtils.updateWithPagePermissionsToAllItsActions(application.getId(), actionPolicyMap, isPublic)); return updatedActionsFlux - .flatMap(action -> { - if (action.getDatasource() != null && action.getDatasource().getId() != null) { - return datasourceService - .findById(action.getDatasource().getId(), MANAGE_DATASOURCES) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, - FieldName.DATASOURCE, action.getDatasource().getId()))) - .map(datasource -> { - Datasource updatedDatasource; - if (isPublic) { - updatedDatasource = policyUtils.addPoliciesToExistingObject(datasourcePolicyMap, datasource); - } else { - updatedDatasource = policyUtils.removePoliciesFromExistingObject(datasourcePolicyMap, datasource); - } - - return datasourceService.save(updatedDatasource); - }) - // In case the datasource is not found, do not stop the processing for other actions. - .switchIfEmpty(Mono.empty()); - } - // In case of no datasource / embedded datasource, nothing else needs to be done here. - return Mono.empty(); - }) - .flatMap(obj -> obj) .collectList() + .flatMap(actions -> { + Set datasourceIds = new HashSet<>(); + for (NewAction action : actions) { + ActionDTO unpublishedAction = action.getUnpublishedAction(); + ActionDTO publishedAction = action.getPublishedAction(); + + if (unpublishedAction.getDatasource() != null && + unpublishedAction.getDatasource().getId() != null) { + datasourceIds.add(unpublishedAction.getDatasource().getId()); + } + + if (publishedAction != null && + publishedAction.getDatasource() != null && + publishedAction.getDatasource().getId() != null) { + datasourceIds.add(publishedAction.getDatasource().getId()); + } + } + + return policyUtils.updateWithNewPoliciesToDatasourcesByDatasourceIds(datasourceIds, datasourcePolicyMap, isPublic) + .collectList(); + }) .thenReturn(application) .flatMap(app -> { Application updatedApplication; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/BaseApiImporter.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/BaseApiImporter.java index 7f7cce0e81..a44a0161f3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/BaseApiImporter.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/BaseApiImporter.java @@ -1,10 +1,10 @@ package com.appsmith.server.services; -import com.appsmith.server.domains.Action; +import com.appsmith.server.dtos.ActionDTO; import reactor.core.publisher.Mono; public abstract class BaseApiImporter implements ApiImporter { - public abstract Mono importAction(Object input, String pageId, String name, String orgId); + public abstract Mono importAction(Object input, String pageId, String name, String orgId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CollectionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CollectionService.java index b47db12f7f..f1f07a98bc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CollectionService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CollectionService.java @@ -1,7 +1,8 @@ package com.appsmith.server.services; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Collection; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.dtos.ActionDTO; import reactor.core.publisher.Mono; import java.util.List; @@ -9,9 +10,9 @@ import java.util.List; public interface CollectionService extends CrudService { Mono findById(String id); - Mono addActionsToCollection(Collection collection, List actions); + Mono addActionsToCollection(Collection collection, List actions); - Mono addSingleActionToCollection(String collectionId, Action action); + Mono addSingleActionToCollection(String collectionId, ActionDTO action); - Mono removeSingleActionFromCollection(String collectionId, Action action); + Mono removeSingleActionFromCollection(String collectionId, Mono action); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CollectionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CollectionServiceImpl.java index 120fc86085..0cb68e5363 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CollectionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CollectionServiceImpl.java @@ -1,7 +1,8 @@ package com.appsmith.server.services; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Collection; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.repositories.CollectionRepository; @@ -36,14 +37,14 @@ public class CollectionServiceImpl extends BaseService addActionsToCollection(Collection collection, List actions) { + public Mono addActionsToCollection(Collection collection, List actions) { collection.setActions(actions); return repository.save(collection); } @Override - public Mono addSingleActionToCollection(String collectionId, Action action) { + public Mono addSingleActionToCollection(String collectionId, ActionDTO action) { if (collectionId == null) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "id")); } @@ -55,7 +56,7 @@ public class CollectionServiceImpl extends BaseService { - List actions = collection1.getActions(); + List actions = collection1.getActions(); if (actions == null) { actions = new ArrayList<>(); } @@ -65,7 +66,7 @@ public class CollectionServiceImpl extends BaseService removeSingleActionFromCollection(String collectionId, Action action) { + public Mono removeSingleActionFromCollection(String collectionId, Mono actionMono) { if (collectionId == null) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "id")); } - if (action.getId() == null) { - return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "action")); - } return repository .findById(collectionId) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "collectionId"))) - .flatMap(collection -> { - List actions = collection.getActions(); + .zipWith(actionMono) + .flatMap(tuple -> { + Collection collection = tuple.getT1(); + NewAction action = tuple.getT2(); + + if (action.getId() == null) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "action")); + } + + List actions = collection.getActions(); if (actions == null || actions.isEmpty()) { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "actionId or collectionId")); } - ListIterator actionIterator = actions.listIterator(); + ListIterator actionIterator = actions.listIterator(); while (actionIterator.hasNext()) { if (actionIterator.next().getId().equals(action.getId())) { actionIterator.remove(); break; } } + log.debug("Action {} removed from Collection {}", action.getId(), collection.getId()); return repository.save(collection); }) - .map(collection -> { - log.debug("Action {} removed from Collection {}", action.getId(), collection.getId()); - return action; - }); + .then(actionMono); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CurlImporterService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CurlImporterService.java index 5f4b712d61..5c7cfcf438 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CurlImporterService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CurlImporterService.java @@ -3,9 +3,9 @@ package com.appsmith.server.services; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Property; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Plugin; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import lombok.extern.slf4j.Slf4j; @@ -40,17 +40,17 @@ public class CurlImporterService extends BaseApiImporter { private static final String CONTENT_TYPE_URLENCODED = "application/x-www-form-urlencoded"; - private final ActionService actionService; + private final NewActionService newActionService; private final PluginService pluginService; - public CurlImporterService(ActionService actionService, PluginService pluginService) { - this.actionService = actionService; + public CurlImporterService(NewActionService newActionService, PluginService pluginService) { + this.newActionService = newActionService; this.pluginService = pluginService; } @Override - public Mono importAction(Object input, String pageId, String name, String orgId) { - Action action; + public Mono importAction(Object input, String pageId, String name, String orgId) { + ActionDTO action; try { action = curlToAction((String) input, pageId, name); @@ -66,7 +66,7 @@ public class CurlImporterService extends BaseApiImporter { // with embedded datasource return Mono.zip(Mono.just(action), pluginService.findByPackageName(RESTAPI_PLUGIN)) .flatMap(tuple -> { - final Action action1 = tuple.getT1(); + final ActionDTO action1 = tuple.getT1(); final Plugin plugin = tuple.getT2(); final Datasource datasource = action1.getDatasource(); final DatasourceConfiguration datasourceConfiguration = datasource.getDatasourceConfiguration(); @@ -75,11 +75,11 @@ public class CurlImporterService extends BaseApiImporter { datasource.setOrganizationId(orgId); return Mono.just(action1); }) - .flatMap(actionService::create); + .flatMap(newActionService::createAction); } - public Action curlToAction(String command, String pageId, String name) throws AppsmithException { - Action action = curlToAction(command); + public ActionDTO curlToAction(String command, String pageId, String name) throws AppsmithException { + ActionDTO action = curlToAction(command); if (action != null) { action.setPageId(pageId); action.setName(name); @@ -87,7 +87,7 @@ public class CurlImporterService extends BaseApiImporter { return action; } - public Action curlToAction(String command) throws AppsmithException { + public ActionDTO curlToAction(String command) throws AppsmithException { // Three stages of parsing the cURL command: // 1. lex: Split the string into tokens, respecting the quoting semantics of a POSIX-compliant shell. // 2. normalize: Normalize all the command line arguments of a curl command, into their long-form versions. @@ -248,7 +248,7 @@ public class CurlImporterService extends BaseApiImporter { return normalizedTokens; } - public Action parse(List tokens) throws AppsmithException { + public ActionDTO parse(List tokens) throws AppsmithException { // Curl argument parsing as per . if (!"curl".equals(tokens.get(0))) { @@ -256,7 +256,7 @@ public class CurlImporterService extends BaseApiImporter { return null; } - final Action action = new Action(); + final ActionDTO action = new ActionDTO(); final ActionConfiguration actionConfiguration = new ActionConfiguration(); action.setActionConfiguration(actionConfiguration); @@ -367,7 +367,7 @@ public class CurlImporterService extends BaseApiImporter { return action; } - private void trySaveURL(Action action, String token) throws MalformedURLException, URISyntaxException { + private void trySaveURL(ActionDTO action, String token) throws MalformedURLException, URISyntaxException { // If the URL appears to not have a protocol set, prepend the `https` protocol. if (!token.matches("\\w+://.*")) { token = "http://" + token; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java index 959827227e..1e15af6fa5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java @@ -6,6 +6,7 @@ import com.appsmith.server.domains.Datasource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; import java.util.Set; public interface DatasourceService extends CrudService { @@ -26,4 +27,5 @@ public interface DatasourceService extends CrudService { Flux findAllByOrganizationId(String organizationId, AclPermission readDatasources); + Flux saveAll(List datasourceList); } 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 708a64f47e..8ec077abd1 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 @@ -18,8 +18,8 @@ import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.MustacheHelper; import com.appsmith.server.helpers.PluginExecutorHelper; -import com.appsmith.server.repositories.ActionRepository; import com.appsmith.server.repositories.DatasourceRepository; +import com.appsmith.server.repositories.NewActionRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; @@ -35,6 +35,7 @@ import reactor.core.scheduler.Scheduler; import javax.validation.Validator; import javax.validation.constraints.NotNull; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -53,7 +54,7 @@ public class DatasourceServiceImpl extends BaseService { - 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; - }) + .then(pluginExecutorMono) .flatMap(pluginExecutor -> { DatasourceConfiguration datasourceConfiguration = datasource.getDatasourceConfiguration(); if (datasourceConfiguration != null && !pluginExecutor.isDatasourceValid(datasourceConfiguration)) { @@ -338,12 +327,17 @@ public class DatasourceServiceImpl extends BaseService saveAll(List datasourceList) { + return repository.saveAll(datasourceList); + } + @Override public Mono delete(String id) { return repository .findById(id, MANAGE_DATASOURCES) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.DATASOURCE, id))) - .zipWhen(datasource -> actionRepository.countByDatasourceId(datasource.getId())) + .zipWhen(datasource -> newActionRepository.countByDatasourceId(datasource.getId())) .flatMap(objects -> { final Long actionsCount = objects.getT2(); if (actionsCount > 0) { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemService.java index baf3133823..22670af012 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemService.java @@ -1,6 +1,6 @@ package com.appsmith.server.services; -import com.appsmith.server.domains.Action; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.AddItemToPageDTO; import com.appsmith.server.dtos.ItemDTO; import org.springframework.util.MultiValueMap; @@ -10,5 +10,5 @@ import reactor.core.publisher.Mono; public interface ItemService { Flux get(MultiValueMap params); - Mono addItemToPage(AddItemToPageDTO addItemToPageDTO); + Mono addItemToPage(AddItemToPageDTO addItemToPageDTO); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java index 8c402fe509..33f3f6c91a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ItemServiceImpl.java @@ -2,9 +2,9 @@ package com.appsmith.server.services; import com.appsmith.external.models.ApiTemplate; import com.appsmith.server.constants.FieldName; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Documentation; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.AddItemToPageDTO; import com.appsmith.server.dtos.ItemDTO; import com.appsmith.server.dtos.ItemType; @@ -20,19 +20,19 @@ import reactor.core.publisher.Mono; @Slf4j public class ItemServiceImpl implements ItemService { private final ApiTemplateService apiTemplateService; - private final ActionService actionService; private final PluginService pluginService; private final MarketplaceService marketplaceService; + private final NewActionService newActionService; private static final String RAPID_API_PLUGIN = "rapidapi-plugin"; public ItemServiceImpl(ApiTemplateService apiTemplateService, - ActionService actionService, PluginService pluginService, - MarketplaceService marketplaceService) { + MarketplaceService marketplaceService, + NewActionService newActionService) { this.apiTemplateService = apiTemplateService; - this.actionService = actionService; this.pluginService = pluginService; this.marketplaceService = marketplaceService; + this.newActionService = newActionService; } @Override @@ -60,7 +60,7 @@ public class ItemServiceImpl implements ItemService { } @Override - public Mono addItemToPage(AddItemToPageDTO addItemToPageDTO) { + public Mono addItemToPage(AddItemToPageDTO addItemToPageDTO) { if (!addItemToPageDTO.getMarketplaceElement().getType().equals(ItemType.TEMPLATE)) { log.debug("Only templates can currently be added to the page. Any other type is unsupported."); return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); @@ -72,7 +72,7 @@ public class ItemServiceImpl implements ItemService { ApiTemplate apiTemplate = addItemToPageDTO.getMarketplaceElement().getItem(); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName(addItemToPageDTO.getName()); action.setPageId(addItemToPageDTO.getPageId()); action.setTemplateId(apiTemplate.getId()); @@ -108,6 +108,6 @@ public class ItemServiceImpl implements ItemService { action.setPluginType(plugin.getType()); return action; }) - .flatMap(actionService::create); + .flatMap(newActionService::createAction); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionService.java index 6d1007bf06..0d0b388088 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionService.java @@ -1,7 +1,7 @@ package com.appsmith.server.services; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Layout; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionMoveDTO; import com.appsmith.server.dtos.RefactorNameDTO; import reactor.core.publisher.Mono; @@ -9,13 +9,13 @@ import reactor.core.publisher.Mono; public interface LayoutActionService { public Mono updateLayout(String pageId, String layoutId, Layout layout); - public Mono moveAction(ActionMoveDTO actionMoveDTO); + public Mono moveAction(ActionMoveDTO actionMoveDTO); public Mono refactorWidgetName(RefactorNameDTO refactorNameDTO); public Mono refactorActionName(RefactorNameDTO refactorNameDTO); - public Mono updateAction(String id, Action action); + public Mono updateAction(String id, ActionDTO action); - Mono setExecuteOnLoad(String id, Boolean isExecuteOnLoad); + Mono setExecuteOnLoad(String id, Boolean isExecuteOnLoad); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java index 76ddcbc60b..65ee57fc83 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutActionServiceImpl.java @@ -1,13 +1,12 @@ package com.appsmith.server.services; import com.appsmith.external.models.ActionConfiguration; -import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Layout; -import com.appsmith.server.domains.Page; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionMoveDTO; import com.appsmith.server.dtos.DslActionDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -36,17 +35,16 @@ import java.util.regex.Pattern; import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; -import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; -import static com.appsmith.server.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; import static java.util.stream.Collectors.toSet; @Service @Slf4j public class LayoutActionServiceImpl implements LayoutActionService { - private final ActionService actionService; - private final PageService pageService; + private final ObjectMapper objectMapper; private final AnalyticsService analyticsService; + private final NewPageService newPageService; + private final NewActionService newActionService; /* * This pattern finds all the String which have been extracted from the mustache dynamic bindings. * e.g. for the given JS function using action with name "fetchUsers" @@ -63,14 +61,14 @@ public class LayoutActionServiceImpl implements LayoutActionService { private final String preWord = "\\b("; private final String postWord = ")\\b"; - public LayoutActionServiceImpl(ActionService actionService, - PageService pageService, - ObjectMapper objectMapper, - AnalyticsService analyticsService) { - this.actionService = actionService; - this.pageService = pageService; + public LayoutActionServiceImpl(ObjectMapper objectMapper, + AnalyticsService analyticsService, + NewPageService newPageService, + NewActionService newActionService) { this.objectMapper = objectMapper; this.analyticsService = analyticsService; + this.newPageService = newPageService; + this.newActionService = newActionService; } @Override @@ -97,12 +95,13 @@ public class LayoutActionServiceImpl implements LayoutActionService { Mono>> onLoadActionsMono = findOnLoadActionsInPage(dynamicBindingNames, pageId); - return pageService.findByIdAndLayoutsId(pageId, layoutId, MANAGE_PAGES) + // fetch the unpublished page and layout id combination + return newPageService.findByIdAndLayoutsId(pageId, layoutId, MANAGE_PAGES, false) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE_ID + " or " + FieldName.LAYOUT_ID, pageId + ", " + layoutId))) .zipWith(onLoadActionsMono) .map(tuple -> { - Page page = tuple.getT1(); + PageDTO page = tuple.getT1(); List> onLoadActions = tuple.getT2(); List layoutList = page.getLayouts(); @@ -110,25 +109,19 @@ public class LayoutActionServiceImpl implements LayoutActionService { //Because the findByIdAndLayoutsId call returned non-empty result, we are guaranteed to find the layoutId here. for (Layout storedLayout : layoutList) { if (storedLayout.getId().equals(layoutId)) { - //Copy the variables to conserve before update - JSONObject publishedDsl = storedLayout.getPublishedDsl(); - List> publishedLayoutOnLoadActions = storedLayout.getPublishedLayoutOnLoadActions(); //Update layout.setLayoutOnLoadActions(onLoadActions); BeanUtils.copyProperties(layout, storedLayout); storedLayout.setId(layoutId); - //Copy back the conserved variables. - storedLayout.setPublishedDsl(publishedDsl); - storedLayout.setPublishedLayoutOnLoadActions(publishedLayoutOnLoadActions); break; } } page.setLayouts(layoutList); return page; }) - .flatMap(pageService::save) + .flatMap(newPageService::saveUnpublishedPage) .flatMap(page -> { List layoutList = page.getLayouts(); for (Layout storedLayout : layoutList) { @@ -149,33 +142,32 @@ public class LayoutActionServiceImpl implements LayoutActionService { return Mono.just(onLoadActions); } Set bindingNames = new HashSet<>(); - return actionService.findOnLoadActionsInPage(dynamicBindingNames, pageId) - .flatMap(action -> { + return newActionService.findUnpublishedOnLoadActionsInPage(dynamicBindingNames, pageId) + .flatMap(newAction -> { + ActionDTO action = newAction.getUnpublishedAction(); if (!CollectionUtils.isEmpty(action.getJsonPathKeys())) { for (String mustacheKey : action.getJsonPathKeys()) { extractWordsAndAddToSet(bindingNames, mustacheKey); } bindingNames.remove(action.getName()); } - DslActionDTO newAction = new DslActionDTO(); - newAction.setId(action.getId()); - newAction.setPluginType(action.getPluginType()); - newAction.setJsonPathKeys(action.getJsonPathKeys()); - newAction.setName(action.getName()); + DslActionDTO actionDTO = new DslActionDTO(); + actionDTO.setId(newAction.getId()); + actionDTO.setPluginType(newAction.getPluginType()); + actionDTO.setJsonPathKeys(action.getJsonPathKeys()); + actionDTO.setName(action.getName()); if (action.getActionConfiguration() != null) { - newAction.setTimeoutInMillisecond(action.getActionConfiguration().getTimeoutInMillisecond()); + actionDTO.setTimeoutInMillisecond(action.getActionConfiguration().getTimeoutInMillisecond()); } - // If the executeOnLoad field isn't true, set it to true + // If the executeOnLoad field isn't true, set it to true at this point if (!Boolean.TRUE.equals(action.getExecuteOnLoad())) { - Action updateAction = new Action(); - updateAction.setExecuteOnLoad(true); + action.setExecuteOnLoad(true); - return actionService.update(action.getId(), updateAction) - .thenReturn(newAction); + return newActionService.updateUnpublishedAction(newAction.getId(), action) + .thenReturn(actionDTO); } - - return Mono.just(newAction); + return Mono.just(actionDTO); }) .collect(toSet()) @@ -208,8 +200,8 @@ public class LayoutActionServiceImpl implements LayoutActionService { } @Override - public Mono moveAction(ActionMoveDTO actionMoveDTO) { - Action action = actionMoveDTO.getAction(); + public Mono moveAction(ActionMoveDTO actionMoveDTO) { + ActionDTO action = actionMoveDTO.getAction(); String oldPageId = actionMoveDTO.getAction().getPageId(); @@ -222,30 +214,37 @@ public class LayoutActionServiceImpl implements LayoutActionService { * 3. Run updateLayout on the new page. * 4. Return the saved action. */ - return actionService - .update(action.getId(), action) + return newActionService + // 1. Update and save the action + .updateUnpublishedAction(action.getId(), action) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, actionMoveDTO.getAction().getId()))) - .flatMap(savedAction -> pageService - .findById(oldPageId, MANAGE_PAGES) + .flatMap(savedAction -> + // fetch the unpublished source page + newPageService + .findPageById(oldPageId, MANAGE_PAGES, false) .flatMap(page -> { if (page.getLayouts() == null) { return Mono.empty(); } + // 2. Run updateLayout on the old page return Flux.fromIterable(page.getLayouts()) .flatMap(layout -> updateLayout(oldPageId, layout.getId(), layout)) .collect(toSet()); }) - .then(pageService.findById(actionMoveDTO.getDestinationPageId(), MANAGE_PAGES)) + // fetch the unpublished destination page + .then(newPageService.findPageById(actionMoveDTO.getDestinationPageId(), MANAGE_PAGES, false)) .flatMap(page -> { if (page.getLayouts() == null) { return Mono.empty(); } + // 3. Run updateLayout on the new page. return Flux.fromIterable(page.getLayouts()) .flatMap(layout -> updateLayout(actionMoveDTO.getDestinationPageId(), layout.getId(), layout)) .collect(toSet()); }) + // 4. Return the saved action. .thenReturn(savedAction)); } @@ -275,12 +274,12 @@ public class LayoutActionServiceImpl implements LayoutActionService { if (!allowed) { return Mono.error(new AppsmithException(AppsmithError.NAME_CLASH_NOT_ALLOWED_IN_REFACTOR, oldName, newName)); } - return actionService - .findByNameAndPageId(oldName, pageId, READ_ACTIONS); + return newActionService + .findByUnpublishedNameAndPageId(oldName, pageId, MANAGE_ACTIONS); }) .flatMap(action -> { action.setName(newName); - return actionService.update(action.getId(), action); + return newActionService.updateUnpublishedAction(action.getId(), action); }) .then(refactorName(pageId, layoutId, oldName, newName)); } @@ -305,8 +304,9 @@ public class LayoutActionServiceImpl implements LayoutActionService { params.add(FieldName.PAGE_ID, pageId); } - Mono updatePageMono = pageService - .findById(pageId, MANAGE_PAGES) + Mono updatePageMono = newPageService + // fetch the unpublished page + .findPageById(pageId, MANAGE_PAGES, false) .flatMap(page -> { List layouts = page.getLayouts(); for (Layout layout : layouts) { @@ -328,20 +328,21 @@ public class LayoutActionServiceImpl implements LayoutActionService { } page.setLayouts(layouts); // Since the page has most probably changed, save the page and return. - return pageService.save(page); + return newPageService.saveUnpublishedPage(page); } } // If we have reached here, the layout was not found and the page should be returned as is. return Mono.just(page); }); - Mono> updateActionsMono = actionService - .findByPageId(pageId, AclPermission.MANAGE_ACTIONS) + Mono> updateActionsMono = newActionService + .findByPageIdAndViewMode(pageId, false, MANAGE_ACTIONS) /* * Assuming that the datasource should not be dependent on the widget and hence not going through the same * to look for replacement pattern. */ - .flatMap(action -> { + .flatMap(newAction -> { + ActionDTO action = newAction.getUnpublishedAction(); Boolean actionUpdateRequired = false; ActionConfiguration actionConfiguration = action.getActionConfiguration(); Set jsonPathKeys = action.getJsonPathKeys(); @@ -358,7 +359,7 @@ public class LayoutActionServiceImpl implements LayoutActionService { } if (!actionUpdateRequired || actionConfiguration == null) { - return Mono.just(action); + return Mono.just(newAction); } // if actionupdateRequired is true AND actionConfiguration is not null try { @@ -367,20 +368,20 @@ public class LayoutActionServiceImpl implements LayoutActionService { String newActionConfigurationAsString = matcher.replaceAll(newName); ActionConfiguration newActionConfiguration = objectMapper.readValue(newActionConfigurationAsString, ActionConfiguration.class); action.setActionConfiguration(newActionConfiguration); - action = actionService.extractAndSetJsonPathKeys(action); - return actionService.save(action); + newAction = newActionService.extractAndSetJsonPathKeys(newAction); + return newActionService.save(newAction); } catch (JsonProcessingException e) { log.debug("Exception caught during conversion between string and action configuration object ", e); - return Mono.just(action); + return Mono.just(newAction); } }) - .map(savedAction -> savedAction.getName()) + .map(savedAction -> savedAction.getUnpublishedAction().getName()) .collect(toSet()); return Mono.zip(updateActionsMono, updatePageMono) .flatMap(tuple -> { Set updatedActionNames = tuple.getT1(); - Page page = tuple.getT2(); + PageDTO page = tuple.getT2(); log.debug("Actions updated due to refactor name in page {} are : {}", pageId, updatedActionNames); List layouts = page.getLayouts(); for (Layout layout : layouts) { @@ -439,8 +440,8 @@ public class LayoutActionServiceImpl implements LayoutActionService { params.add(FieldName.PAGE_ID, pageId); } - Mono> actionNamesInPageMono = actionService - .get(params) + Mono> actionNamesInPageMono = newActionService + .getUnpublishedActions(params) .map(action -> action.getName()) .collect(toSet()); @@ -448,8 +449,9 @@ public class LayoutActionServiceImpl implements LayoutActionService { * TODO : Execute this check directly on the DB server. We can query array of arrays by: * https://stackoverflow.com/questions/12629692/querying-an-array-of-arrays-in-mongodb */ - Mono> widgetNamesMono = pageService - .findById(pageId, MANAGE_PAGES) + Mono> widgetNamesMono = newPageService + // fetch the unpublished page + .findPageById(pageId, MANAGE_PAGES, false) .flatMap(page -> { List layouts = page.getLayouts(); for (Layout layout : layouts) { @@ -487,10 +489,6 @@ public class LayoutActionServiceImpl implements LayoutActionService { } /** - * This function updates an existing action in the DB. We are completely overriding the base update function to - * ensure that we can populate the JsonPathKeys field in the ActionConfiguration based on any changes that may - * have happened in the action object. - *

* After updating the action, page layout needs to be updated to update the page load actions with the new json * path keys. *

@@ -504,39 +502,43 @@ public class LayoutActionServiceImpl implements LayoutActionService { * @return */ @Override - public Mono updateAction(String id, Action action) { - if (id == null) { - return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID)); - } + public Mono updateAction(String id, ActionDTO action) { + Mono updateUnpublishedAction = newActionService + .updateUnpublishedAction(id, action) + .cache(); - Mono dbActionMono = actionService.findById(id) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "action", id))); + // First update the action + return updateUnpublishedAction + // Now update the page layout for any on load changes that may have occured. + .flatMap(savedAction -> updatePageLayoutsGivenAction(savedAction.getPageId())) + // Return back the updated action. + .then(updateUnpublishedAction); - return dbActionMono - .map(dbAction -> { - copyNewFieldValuesIntoOldObject(action, dbAction); - return dbAction; - }) - .flatMap(actionService::validateAndSaveActionToRepository) - .flatMap(this::updatePageLayoutsGivenAction) - .flatMap(analyticsService::sendUpdateEvent); } @Override - public Mono setExecuteOnLoad(String id, Boolean isExecuteOnLoad) { - return actionService.findById(id, MANAGE_ACTIONS) + public Mono setExecuteOnLoad(String id, Boolean isExecuteOnLoad) { + return newActionService.findById(id, MANAGE_ACTIONS) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, id))) - .flatMap(action -> { + .flatMap(newAction -> { + ActionDTO action = newAction.getUnpublishedAction(); + action.setUserSetOnLoad(true); action.setExecuteOnLoad(isExecuteOnLoad); - return actionService.save(action); - }) - .flatMap(this::updatePageLayoutsGivenAction); + + newAction.setUnpublishedAction(action); + + return newActionService.save(newAction) + .flatMap(savedAction -> updatePageLayoutsGivenAction(savedAction.getUnpublishedAction().getPageId()) + .then(newActionService.generateActionByViewMode(savedAction, false))); + + }); } - private Mono updatePageLayoutsGivenAction(Action action) { - return Mono.justOrEmpty(action.getPageId()) - .flatMap(pageId -> pageService.findById(pageId, MANAGE_PAGES)) + private Mono updatePageLayoutsGivenAction(String pageId) { + return Mono.justOrEmpty(pageId) + // fetch the unpublished page + .flatMap(id -> newPageService.findPageById(id, MANAGE_PAGES, false)) .flatMapMany(page -> { if (page.getLayouts() == null) { return Mono.empty(); @@ -545,6 +547,6 @@ public class LayoutActionServiceImpl implements LayoutActionService { .flatMap(layout -> updateLayout(page.getId(), layout.getId(), layout)); }) .collectList() - .then(Mono.just(action)); + .then(Mono.just(pageId)); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutServiceImpl.java index aff67fb570..7c86fd1f8d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/LayoutServiceImpl.java @@ -3,7 +3,7 @@ package com.appsmith.server.services; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Layout; -import com.appsmith.server.domains.Page; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import lombok.extern.slf4j.Slf4j; @@ -14,7 +14,6 @@ import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; -import java.util.regex.Pattern; import static com.appsmith.server.acl.AclPermission.READ_PAGES; @@ -22,21 +21,11 @@ import static com.appsmith.server.acl.AclPermission.READ_PAGES; @Service public class LayoutServiceImpl implements LayoutService { - private final ApplicationPageService applicationPageService; - private final PageService pageService; - /* - * This pattern finds all the String which have been extracted from the mustache dynamic bindings. - * e.g. for the given JS function using action with name "fetchUsers" - * {{JSON.stringify(fetchUsers)}} - * This pattern should return ["JSON.stringify", "fetchUsers"] - */ - private final Pattern pattern = Pattern.compile("[a-zA-Z0-9._]+"); + private final NewPageService newPageService; @Autowired - public LayoutServiceImpl(ApplicationPageService applicationPageService, - PageService pageService) { - this.applicationPageService = applicationPageService; - this.pageService = pageService; + public LayoutServiceImpl(NewPageService newPageService) { + this.newPageService = newPageService; } @Override @@ -45,8 +34,9 @@ public class LayoutServiceImpl implements LayoutService { return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.PAGE_ID)); } - Mono pageMono = pageService - .findById(pageId, AclPermission.MANAGE_PAGES) + // fetch the unpublished page + Mono pageMono = newPageService + .findPageById(pageId, AclPermission.MANAGE_PAGES, false) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.PAGE_ID))); return pageMono @@ -62,13 +52,13 @@ public class LayoutServiceImpl implements LayoutService { page.setLayouts(layoutList); return page; }) - .flatMap(pageService::save) + .flatMap(newPageService::saveUnpublishedPage) .then(Mono.just(layout)); } @Override public Mono getLayout(String pageId, String layoutId, Boolean viewMode) { - return pageService.findByIdAndLayoutsId(pageId, layoutId, READ_PAGES) + return newPageService.findByIdAndLayoutsId(pageId, layoutId, READ_PAGES, viewMode) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.PAGE_ID + " or " + FieldName.LAYOUT_ID))) .map(page -> { List layoutList = page.getLayouts(); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionService.java new file mode 100644 index 0000000000..c2568e0540 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionService.java @@ -0,0 +1,57 @@ +package com.appsmith.server.services; + +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.dtos.ActionDTO; +import com.appsmith.server.dtos.ActionViewDTO; +import com.appsmith.server.dtos.ExecuteActionDTO; +import org.springframework.data.domain.Sort; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public interface NewActionService extends CrudService { + + Mono generateActionByViewMode(NewAction newAction, Boolean viewMode); + + Mono createAction(ActionDTO action); + + NewAction extractAndSetJsonPathKeys(NewAction newAction); + + Mono updateUnpublishedAction(String id, ActionDTO action); + + Mono executeAction(ExecuteActionDTO executeActionDTO); + + T variableSubstitution(T configuration, Map replaceParamsMap); + + Mono findByUnpublishedNameAndPageId(String name, String pageId, AclPermission permission); + + Flux findUnpublishedOnLoadActionsInPage(Set names, String pageId); + + Mono findById(String id); + + Mono findById(String id, AclPermission aclPermission); + + Flux findByPageId(String pageId, AclPermission permission); + + Flux findByPageIdAndViewMode(String pageId, Boolean viewMode, AclPermission permission); + + Flux findAllByApplicationIdAndViewMode(String applicationId, Boolean viewMode, AclPermission permission, Sort sort); + + Flux getActionsForViewMode(String applicationId); + + Mono deleteUnpublishedAction(String id); + + Flux getUnpublishedActions(MultiValueMap params); + + Mono save(NewAction action); + + Flux saveAll(List actions); + + Flux findByPageId(String pageId); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java new file mode 100644 index 0000000000..9ca3b80d95 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java @@ -0,0 +1,831 @@ +package com.appsmith.server.services; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.PaginationField; +import com.appsmith.external.models.PaginationType; +import com.appsmith.external.models.Param; +import com.appsmith.external.models.Policy; +import com.appsmith.external.models.Property; +import com.appsmith.external.models.Provider; +import com.appsmith.external.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.pluginExceptions.StaleConnectionException; +import com.appsmith.external.plugins.PluginExecutor; +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.acl.PolicyGenerator; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Action; +import com.appsmith.server.domains.ActionProvider; +import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.domains.Page; +import com.appsmith.server.domains.Plugin; +import com.appsmith.server.domains.PluginType; +import com.appsmith.server.domains.QNewAction; +import com.appsmith.server.dtos.ActionDTO; +import com.appsmith.server.dtos.ActionViewDTO; +import com.appsmith.server.dtos.ExecuteActionDTO; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.MustacheHelper; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.repositories.NewActionRepository; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.ArrayUtils; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +import javax.lang.model.SourceVersion; +import javax.validation.Validator; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +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.READ_ACTIONS; +import static com.appsmith.server.acl.AclPermission.READ_PAGES; +import static com.appsmith.server.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; +import static com.appsmith.server.repositories.BaseAppsmithRepositoryImpl.fieldName; +import static java.lang.Boolean.TRUE; + +@Service +@Slf4j +public class NewActionServiceImpl extends BaseService implements NewActionService { + + private final NewActionRepository repository; + private final DatasourceService datasourceService; + private final PluginService pluginService; + private final DatasourceContextService datasourceContextService; + private final PluginExecutorHelper pluginExecutorHelper; + private final MarketplaceService marketplaceService; + private final PolicyGenerator policyGenerator; + private final NewPageService newPageService; + + public NewActionServiceImpl(Scheduler scheduler, + Validator validator, + MongoConverter mongoConverter, + ReactiveMongoTemplate reactiveMongoTemplate, + NewActionRepository repository, + AnalyticsService analyticsService, + DatasourceService datasourceService, + PluginService pluginService, + DatasourceContextService datasourceContextService, + PluginExecutorHelper pluginExecutorHelper, + MarketplaceService marketplaceService, + PolicyGenerator policyGenerator, + NewPageService newPageService) { + super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService); + this.repository = repository; + this.datasourceService = datasourceService; + this.pluginService = pluginService; + this.datasourceContextService = datasourceContextService; + this.pluginExecutorHelper = pluginExecutorHelper; + this.marketplaceService = marketplaceService; + this.policyGenerator = policyGenerator; + this.newPageService = newPageService; + } + + private Boolean validateActionName(String name) { + boolean isValidName = SourceVersion.isName(name); + String pattern = "^((?=[A-Za-z0-9_])(?![\\\\-]).)*$"; + boolean doesPatternMatch = name.matches(pattern); + return (isValidName && doesPatternMatch); + } + + private void setCommonFieldsFromNewActionIntoAction(NewAction newAction, ActionDTO action) { + + // Set the fields from NewAction into Action + action.setOrganizationId(newAction.getOrganizationId()); + action.setPluginType(newAction.getPluginType()); + action.setPluginId(newAction.getPluginId()); + action.setTemplateId(newAction.getTemplateId()); + action.setProviderId(newAction.getProviderId()); + action.setDocumentation(newAction.getDocumentation()); + + action.setId(newAction.getId()); + action.setUserPermissions(newAction.getUserPermissions()); + action.setPolicies(newAction.getPolicies()); + } + + private void setCommonFieldsFromActionDTOIntoNewAction(ActionDTO action, NewAction newAction) { + // Set the fields from NewAction into Action + newAction.setOrganizationId(action.getOrganizationId()); + newAction.setPluginType(action.getPluginType()); + newAction.setPluginId(action.getPluginId()); + newAction.setTemplateId(action.getTemplateId()); + newAction.setProviderId(action.getProviderId()); + newAction.setDocumentation(action.getDocumentation()); + newAction.setApplicationId(action.getApplicationId()); + } + + @Override + public Mono generateActionByViewMode(NewAction newAction, Boolean viewMode) { + ActionDTO action = null; + + if (TRUE.equals(viewMode)) { + if (newAction.getPublishedAction() != null) { + action = newAction.getPublishedAction(); + } else { + // We are trying to fetch published action but it doesnt exist because the action hasn't been published yet + return Mono.empty(); + } + } else { + if (newAction.getUnpublishedAction() != null) { + action = newAction.getUnpublishedAction(); + } + } + + // Set the fields from NewAction into Action + setCommonFieldsFromNewActionIntoAction(newAction, action); + + return Mono.just(action); + } + + private void generateAndSetActionPolicies(NewPage page, NewAction action) { + Set documentPolicies = policyGenerator.getAllChildPolicies(page.getPolicies(), Page.class, Action.class); + action.setPolicies(documentPolicies); + } + + @Override + public Mono createAction(ActionDTO action) { + if (action.getId() != null) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "id")); + } + + if (action.getPageId() == null || action.getPageId().isBlank()) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.PAGE_ID)); + } + + NewAction newAction = new NewAction(); + newAction.setPublishedAction(new ActionDTO()); + newAction.getPublishedAction().setDatasource(new Datasource()); + + return newPageService + .findById(action.getPageId(), READ_PAGES) + .switchIfEmpty(Mono.error( + new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "page", action.getPageId()))) + .flatMap(page -> { + + // Inherit the action policies from the page. + generateAndSetActionPolicies(page, newAction); + + setCommonFieldsFromActionDTOIntoNewAction(action, newAction); + + // Set the application id in the main domain + newAction.setApplicationId(page.getApplicationId()); + + // If the datasource is embedded, check for organizationId and set it in action + if (action.getDatasource() != null && + action.getDatasource().getId() == null) { + Datasource datasource = action.getDatasource(); + if (datasource.getOrganizationId() == null) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORGANIZATION_ID)); + } + newAction.setOrganizationId(datasource.getOrganizationId()); + } + + newAction.setUnpublishedAction(action); + + return Mono.just(newAction); + }) + .flatMap(this::validateAndSaveActionToRepository); + } + + private Mono validateAndSaveActionToRepository(NewAction newAction) { + ActionDTO action = newAction.getUnpublishedAction(); + + //Default the validity to true and invalids to be an empty set. + Set invalids = new HashSet<>(); + action.setIsValid(true); + + if (action.getName() == null || action.getName().trim().isEmpty()) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.NAME)); + } + + if (action.getPageId() == null || action.getPageId().isBlank()) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.PAGE_ID)); + } + + if (!validateActionName(action.getName())) { + action.setIsValid(false); + invalids.add(AppsmithError.INVALID_ACTION_NAME.getMessage()); + } + + if (action.getActionConfiguration() == null) { + action.setIsValid(false); + invalids.add(AppsmithError.NO_CONFIGURATION_FOUND_IN_ACTION.getMessage()); + } + + if (action.getDatasource() == null || action.getDatasource().getIsAutoGenerated()) { + if (action.getPluginType() != PluginType.JS) { + // This action isn't of type JS functions which requires that the pluginType be set by the client. Hence, + // datasource is very much required for such an action. + action.setIsValid(false); + invalids.add(AppsmithError.DATASOURCE_NOT_GIVEN.getMessage()); + } + action.setInvalids(invalids); + return super.create(newAction) + .flatMap(savedAction -> generateActionByViewMode(savedAction, false)); + } + + Mono datasourceMono; + if (action.getDatasource().getId() == null) { + datasourceMono = Mono.just(action.getDatasource()) + .flatMap(datasourceService::validateDatasource); + } else { + //Data source already exists. Find the same. + datasourceMono = datasourceService.findById(action.getDatasource().getId(), MANAGE_DATASOURCES) + .switchIfEmpty(Mono.defer(() -> { + action.setIsValid(false); + invalids.add(AppsmithError.NO_RESOURCE_FOUND.getMessage(FieldName.DATASOURCE, action.getDatasource().getId())); + return Mono.just(action.getDatasource()); + })) + .map(datasource -> { + // datasource is found. Update the action. + newAction.setOrganizationId(datasource.getOrganizationId()); + return datasource; + }); + } + + Mono pluginMono = datasourceMono.flatMap(datasource -> { + if (datasource.getPluginId() == null) { + return Mono.error(new AppsmithException(AppsmithError.PLUGIN_ID_NOT_GIVEN)); + } + return pluginService.findById(datasource.getPluginId()) + .switchIfEmpty(Mono.defer(() -> { + action.setIsValid(false); + invalids.add(AppsmithError.NO_RESOURCE_FOUND.getMessage(FieldName.PLUGIN, datasource.getPluginId())); + return Mono.just(new Plugin()); + })); + }); + + return pluginMono + .zipWith(datasourceMono) + //Set plugin in the action before saving. + .map(tuple -> { + Plugin plugin = tuple.getT1(); + Datasource datasource = tuple.getT2(); + action.setDatasource(datasource); + action.setInvalids(invalids); + newAction.setUnpublishedAction(action); + newAction.setPluginType(plugin.getType()); + newAction.setPluginId(plugin.getId()); + return newAction; + }).map(act -> extractAndSetJsonPathKeys(act)) + .map(updatedAction -> { + // In case of external datasource (not embedded) instead of storing the entire datasource + // again inside the action, instead replace it with just the datasource ID. This is so that + // datasource data is not duplicated across actions and datasource. + ActionDTO unpublishedAction = updatedAction.getUnpublishedAction(); + if (unpublishedAction.getDatasource().getId() != null) { + Datasource datasource = new Datasource(); + datasource.setId(unpublishedAction.getDatasource().getId()); + datasource.setPluginId(updatedAction.getPluginId()); + unpublishedAction.setDatasource(datasource); + updatedAction.setUnpublishedAction(unpublishedAction); + } + return updatedAction; + }) + .flatMap(super::create) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.REPOSITORY_SAVE_FAILED))) + .flatMap(this::setTransientFieldsInUnpublishedAction); + } + + /** + * This function extracts all the mustache template keys (as per the regex) and returns them to the calling fxn + * This set of keys is stored separately in the field `jsonPathKeys` in the action object. The client + * uses the set `jsonPathKeys` to simplify it's value substitution. + * + * @param actionConfiguration + * @return + */ + private Set extractKeysFromAction(ActionConfiguration actionConfiguration) { + if (actionConfiguration == null) { + return new HashSet<>(); + } + + return MustacheHelper.extractMustacheKeysFromFields(actionConfiguration); + } + + /** + * This function extracts the mustache keys and sets them in the field jsonPathKeys in the action object + * + * @param newAction + * @return + */ + @Override + public NewAction extractAndSetJsonPathKeys(NewAction newAction) { + ActionDTO action = newAction.getUnpublishedAction(); + Set actionKeys = extractKeysFromAction(action.getActionConfiguration()); + Set datasourceKeys = datasourceService.extractKeysFromDatasource(action.getDatasource()); + Set keys = new HashSet<>() {{ + addAll(actionKeys); + addAll(datasourceKeys); + }}; + action.setJsonPathKeys(keys); + + return newAction; + } + + private Mono setTransientFieldsInUnpublishedAction(NewAction newAction) { + ActionDTO action = newAction.getUnpublishedAction(); + + // In case of an action which was imported from a 3P API, fill in the extra information of the provider required by the front end UI. + Mono providerUpdateMono; + if ((action.getTemplateId() != null) && (action.getProviderId() != null)) { + + providerUpdateMono = marketplaceService + .getProviderById(action.getProviderId()) + .switchIfEmpty(Mono.just(new Provider())) + .map(provider -> { + ActionProvider actionProvider = new ActionProvider(); + actionProvider.setName(provider.getName()); + actionProvider.setCredentialSteps(provider.getCredentialSteps()); + actionProvider.setDescription(provider.getDescription()); + actionProvider.setImageUrl(provider.getImageUrl()); + actionProvider.setUrl(provider.getUrl()); + + action.setProvider(actionProvider); + return action; + }); + } else { + providerUpdateMono = Mono.just(action); + } + + return providerUpdateMono + .map(actionDTO -> { + newAction.setUnpublishedAction(actionDTO); + return newAction; + }) + .flatMap(action1 -> generateActionByViewMode(action1, false)); + } + + @Override + public Mono updateUnpublishedAction(String id, ActionDTO action) { + + if (id == null) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ID)); + } + + NewAction newAction = new NewAction(); + newAction.setUnpublishedAction(action); + + Mono updatedActionMono = repository.findById(id, MANAGE_ACTIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, id))) + .map(dbAction -> { + copyNewFieldValuesIntoOldObject(action, dbAction.getUnpublishedAction()); + return dbAction; + }) + .cache(); + + Mono savedUpdatedActionMono = updatedActionMono + .flatMap(this::validateAndSaveActionToRepository) + .cache(); + + Mono analyticsUpdateMono = updatedActionMono + .flatMap(analyticsService::sendUpdateEvent); + + // First Update the Action + return savedUpdatedActionMono + // Now send the update event to analytics service + .then(analyticsUpdateMono) + // Now return the updated action back. + .then(savedUpdatedActionMono); + } + + @Override + public Mono executeAction(ExecuteActionDTO executeActionDTO) { + + // 1. Validate input parameters which are required for mustache replacements + List params = executeActionDTO.getParams(); + if (!CollectionUtils.isEmpty(params)) { + for (Param param : params) { + // In case the parameter values turn out to be null, set it to empty string instead to allow the + // the execution to go through no matter what. + if (!StringUtils.isEmpty(param.getKey()) && param.getValue() == null) { + param.setValue(""); + } + } + } + + String actionId = executeActionDTO.getActionId(); + // 2. Fetch the action from the DB and check if it can be executed + Mono actionMono = repository.findById(actionId, EXECUTE_ACTIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId))) + .flatMap(dbAction -> { + ActionDTO action; + if (TRUE.equals(executeActionDTO.getViewMode())) { + action = dbAction.getPublishedAction(); + // If the action has not been published, return error + if (action == null) { + return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId)); + } + } else { + action = dbAction.getUnpublishedAction(); + } + + // Now check for erroneous situations which would deter the execution of the action : + + // Error out with in case of an invalid action + if (Boolean.FALSE.equals(action.getIsValid())) { + return Mono.error(new AppsmithException( + AppsmithError.INVALID_ACTION, + action.getName(), + actionId, + ArrayUtils.toString(action.getInvalids().toArray()) + )); + } + + // Error out in case of JS Plugin (this is currently client side execution only) + if (dbAction.getPluginType() == PluginType.JS) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + return Mono.just(action); + }) + .cache(); + + // 3. Instantiate the implementation class based on the query type + + Mono datasourceMono = actionMono + .flatMap(action -> { + // Global datasource requires us to fetch the datasource from DB. + if (action.getDatasource() != null && action.getDatasource().getId() != null) { + return datasourceService.findById(action.getDatasource().getId(), EXECUTE_DATASOURCES) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, + FieldName.DATASOURCE, + action.getDatasource().getId()))); + } + // This is a nested datasource. Return as is. + return Mono.just(action.getDatasource()); + }) + .cache(); + + Mono pluginMono = datasourceMono + .flatMap(datasource -> { + // For embedded datasources, validate the datasource for each execution + if (datasource.getId() == null) { + return datasourceService.validateDatasource(datasource); + } + + // The external datasources have already been validated. No need to validate again. + return Mono.just(datasource); + }) + .flatMap(datasource -> { + Set invalids = datasource.getInvalids(); + if (!CollectionUtils.isEmpty(invalids)) { + log.error("Unable to execute actionId: {} because it's datasource is not valid. Cause: {}", + actionId, ArrayUtils.toString(invalids)); + return Mono.error(new AppsmithException(AppsmithError.INVALID_DATASOURCE, ArrayUtils.toString(invalids))); + } + return pluginService.findById(datasource.getPluginId()); + }) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PLUGIN))); + + Mono pluginExecutorMono = pluginExecutorHelper.getPluginExecutor(pluginMono); + + // 4. Execute the query + Mono actionExecutionResultMono = Mono + .zip( + actionMono, + datasourceMono, + pluginExecutorMono + ) + .flatMap(tuple -> { + final ActionDTO action = tuple.getT1(); + final Datasource datasource = tuple.getT2(); + final PluginExecutor pluginExecutor = tuple.getT3(); + + + DatasourceConfiguration datasourceConfiguration = datasource.getDatasourceConfiguration(); + ActionConfiguration actionConfiguration = action.getActionConfiguration(); + + prepareConfigurationsForExecution(action, datasource, executeActionDTO, actionConfiguration, datasourceConfiguration); + + Integer timeoutDuration = actionConfiguration.getTimeoutInMillisecond(); + + log.debug("Execute Action called in Page {}, for action id : {} action name : {}, {}, {}", + action.getPageId(), actionId, action.getName(), datasourceConfiguration, + actionConfiguration); + + Mono executionMono = Mono.just(datasource) + .flatMap(datasourceContextService::getDatasourceContext) + // Now that we have the context (connection details), execute the action. + .flatMap( + resourceContext -> pluginExecutor.execute( + resourceContext.getConnection(), + datasourceConfiguration, + actionConfiguration + ) + ); + + return executionMono + .onErrorResume(StaleConnectionException.class, error -> { + log.info("Looks like the connection is stale. Retrying with a fresh context."); + return datasourceContextService + .deleteDatasourceContext(datasource.getId()) + .then(executionMono); + }) + .timeout(Duration.ofMillis(timeoutDuration)) + .onErrorMap( + StaleConnectionException.class, + error -> new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Secondary stale connection error." + ) + ) + .onErrorResume(e -> { + log.debug("In the action execution error mode.", e); + ActionExecutionResult result = new ActionExecutionResult(); + result.setBody(e.getMessage()); + result.setIsExecutionSuccess(false); + // Set the status code for Appsmith plugin errors + if (e instanceof AppsmithPluginException) { + result.setStatusCode(((AppsmithPluginException) e).getAppErrorCode().toString()); + } else { + result.setStatusCode(AppsmithPluginError.PLUGIN_ERROR.getAppErrorCode().toString()); + } + return Mono.just(result); + }); + }); + + return actionExecutionResultMono + .onErrorResume(AppsmithException.class, error -> { + ActionExecutionResult result = new ActionExecutionResult(); + result.setIsExecutionSuccess(false); + result.setStatusCode(error.getAppErrorCode().toString()); + result.setBody(error.getMessage()); + return Mono.just(result); + }); + } + + private void prepareConfigurationsForExecution(ActionDTO action, + Datasource datasource, + ExecuteActionDTO executeActionDTO, + ActionConfiguration actionConfiguration, + DatasourceConfiguration datasourceConfiguration) { + DatasourceConfiguration datasourceConfigurationTemp; + ActionConfiguration actionConfigurationTemp; + + //Do variable substitution + //Do this only if params have been provided in the execute command + if (executeActionDTO.getParams() != null && !executeActionDTO.getParams().isEmpty()) { + Map replaceParamsMap = executeActionDTO + .getParams() + .stream() + .collect(Collectors.toMap( + // Trimming here for good measure. If the keys have space on either side, + // Mustache won't be able to find the key. + // We also add a backslash before every double-quote or backslash character + // because we apply the template replacing in a JSON-stringified version of + // these properties, where these two characters are escaped. + p -> p.getKey().trim(), // .replaceAll("[\"\n\\\\]", "\\\\$0"), + Param::getValue, + // In case of a conflict, we pick the older value + (oldValue, newValue) -> oldValue) + ); + + datasourceConfigurationTemp = variableSubstitution(datasource.getDatasourceConfiguration(), replaceParamsMap); + actionConfigurationTemp = variableSubstitution(action.getActionConfiguration(), replaceParamsMap); + } else { + datasourceConfigurationTemp = datasource.getDatasourceConfiguration(); + actionConfigurationTemp = action.getActionConfiguration(); + } + + // If the action is paginated, update the configurations to update the correct URL. + if (action.getActionConfiguration() != null && + action.getActionConfiguration().getPaginationType() != null && + PaginationType.URL.equals(action.getActionConfiguration().getPaginationType()) && + executeActionDTO.getPaginationField() != null) { + datasourceConfiguration = updateDatasourceConfigurationForPagination(actionConfigurationTemp, datasourceConfigurationTemp, executeActionDTO.getPaginationField()); + actionConfiguration = updateActionConfigurationForPagination(actionConfigurationTemp, executeActionDTO.getPaginationField()); + } else { + datasourceConfiguration = datasourceConfigurationTemp; + actionConfiguration = actionConfigurationTemp; + } + + // Filter out any empty headers + if (actionConfiguration.getHeaders() != null && !actionConfiguration.getHeaders().isEmpty()) { + List headerList = actionConfiguration.getHeaders().stream() + .filter(header -> !StringUtils.isEmpty(header.getKey())) + .collect(Collectors.toList()); + actionConfiguration.setHeaders(headerList); + } + } + + private ActionConfiguration updateActionConfigurationForPagination(ActionConfiguration actionConfiguration, + PaginationField paginationField) { + if (PaginationField.NEXT.equals(paginationField) || PaginationField.PREV.equals(paginationField)) { + actionConfiguration.setPath(""); + actionConfiguration.setQueryParameters(null); + } + return actionConfiguration; + } + + private DatasourceConfiguration updateDatasourceConfigurationForPagination(ActionConfiguration actionConfiguration, + DatasourceConfiguration datasourceConfiguration, + PaginationField paginationField) { + if (PaginationField.NEXT.equals(paginationField)) { + datasourceConfiguration.setUrl(URLDecoder.decode(actionConfiguration.getNext(), StandardCharsets.UTF_8)); + } else if (PaginationField.PREV.equals(paginationField)) { + datasourceConfiguration.setUrl(actionConfiguration.getPrev()); + } + return datasourceConfiguration; + } + + /** + * This function replaces the variables in the Object with the actual params + */ + @Override + public T variableSubstitution(T configuration, Map replaceParamsMap) { + return MustacheHelper.renderFieldValues(configuration, replaceParamsMap); + } + + @Override + public Mono findByUnpublishedNameAndPageId(String name, String pageId, AclPermission permission) { + return repository.findByUnpublishedNameAndPageId(name, pageId, permission) + .flatMap(action -> generateActionByViewMode(action, false)); + } + + /** + * Given a list of names of actions and pageId, find all the actions matching this criteria of name, pageId, http + * method 'GET' (for API actions only) or have isExecuteOnLoad be true. + * + * @param names Set of Action names. The returned list of actions will be a subset of the actioned named in this set. + * @param pageId Id of the Page within which to look for Actions. + * @return A Flux of Actions that are identified to be executed on page-load. + */ + @Override + public Flux findUnpublishedOnLoadActionsInPage(Set names, String pageId) { + final Flux getApiActions = repository + .findUnpublishedActionsForRestApiOnLoad(names, + pageId, "GET", false, MANAGE_ACTIONS); + + final Flux explicitOnLoadActions = repository + .findUnpublishedActionsByNameInAndPageIdAndExecuteOnLoadTrue(names, pageId, MANAGE_ACTIONS); + + return getApiActions.concatWith(explicitOnLoadActions); + } + + @Override + public Mono findById(String id) { + return repository.findById(id); + } + + @Override + public Mono findById(String id, AclPermission aclPermission) { + return repository.findById(id, aclPermission); + } + + @Override + public Flux findByPageId(String pageId, AclPermission permission) { + return repository.findByPageId(pageId, permission); + } + + @Override + public Flux findByPageIdAndViewMode(String pageId, Boolean viewMode, AclPermission permission) { + return repository.findByPageIdAndViewMode(pageId, viewMode, permission); + } + + @Override + public Flux findAllByApplicationIdAndViewMode(String applicationId, Boolean viewMode, AclPermission permission, Sort sort) { + return repository.findByApplicationId(applicationId, permission, sort) + // In case of view mode being true, filter out all the actions which haven't been published + .flatMap(action -> { + if (Boolean.TRUE.equals(viewMode)) { + // In case we are trying to fetch published actions but this action has not been published, do not return + if (action.getPublishedAction() == null) { + return Mono.empty(); + } + } + // No need to handle the edge case of unpublished action not being present. This is not possible because + // every created action starts from an unpublishedAction state. + + return Mono.just(action); + }); + } + + @Override + public Flux getActionsForViewMode(String applicationId) { + Sort sort = Sort.by(fieldName(QNewAction.newAction.publishedAction) + "." + fieldName(QNewAction.newAction.publishedAction.name)); + + if (applicationId == null || applicationId.isEmpty()) { + return Flux.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.APPLICATION_ID)); + } + + // fetch the published actions by applicationId + return findAllByApplicationIdAndViewMode(applicationId, true, EXECUTE_ACTIONS, sort) + .map(action -> { + ActionViewDTO actionViewDTO = new ActionViewDTO(); + actionViewDTO.setId(action.getId()); + actionViewDTO.setName(action.getPublishedAction().getName()); + actionViewDTO.setPageId(action.getPublishedAction().getPageId()); + actionViewDTO.setConfirmBeforeExecute(action.getPublishedAction().getConfirmBeforeExecute()); + if (action.getPublishedAction().getJsonPathKeys() != null && !action.getPublishedAction().getJsonPathKeys().isEmpty()) { + Set jsonPathKeys; + jsonPathKeys = new HashSet<>(); + jsonPathKeys.addAll(action.getPublishedAction().getJsonPathKeys()); + actionViewDTO.setJsonPathKeys(jsonPathKeys); + } + if (action.getPublishedAction().getActionConfiguration() != null) { + actionViewDTO.setTimeoutInMillisecond(action.getPublishedAction().getActionConfiguration().getTimeoutInMillisecond()); + } + return actionViewDTO; + }); + } + + @Override + public Mono deleteUnpublishedAction(String id) { + Mono actionMono = repository.findById(id, MANAGE_ACTIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, id))); + return actionMono + .flatMap(toDelete -> { + + Mono newActionMono; + + // Using the name field to determine if the action was ever published. In case of never published + // action, publishedAction would exist with empty datasource and default fields. + if (toDelete.getPublishedAction() != null && toDelete.getPublishedAction().getName() != null) { + toDelete.getUnpublishedAction().setDeletedAt(Instant.now()); + newActionMono = repository.save(toDelete); + } else { + // This action was never published. This can be safely deleted from the db + newActionMono = repository.delete(toDelete).thenReturn(toDelete); + } + + return newActionMono; + }) + .flatMap(analyticsService::sendDeleteEvent) + .flatMap(updatedAction -> generateActionByViewMode(updatedAction, false)); + } + + @Override + public Flux getUnpublishedActions(MultiValueMap params) { + String name = null; + List pageIds = new ArrayList<>(); + Sort sort = Sort.by(FieldName.NAME); + + if (params.getFirst(FieldName.NAME) != null) { + name = params.getFirst(FieldName.NAME); + } + + if (params.getFirst(FieldName.PAGE_ID) != null) { + pageIds.add(params.getFirst(FieldName.PAGE_ID)); + } + + if (params.getFirst(FieldName.APPLICATION_ID) != null) { + // Fetch unpublished pages because GET actions is only called during edit mode. For view mode, different + // function call is made which takes care of returning only the essential fields of an action + return repository + .findByApplicationIdAndViewMode(params.getFirst(FieldName.APPLICATION_ID), false, READ_ACTIONS) + .flatMap(this::setTransientFieldsInUnpublishedAction); + } + return repository.findAllActionsByNameAndPageIdsAndViewMode(name, pageIds, false, READ_ACTIONS, sort) + .flatMap(this::setTransientFieldsInUnpublishedAction); + } + + @Override + public Mono save(NewAction action) { + return repository.save(action); + } + + @Override + public Flux saveAll(List actions) { + return repository.saveAll(actions); + } + + @Override + public Flux findByPageId(String pageId) { + return repository.findByPageId(pageId); + } + + @Override + public Mono delete(String id) { + Mono actionMono = repository.findById(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); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageService.java new file mode 100644 index 0000000000..a937ea81a6 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageService.java @@ -0,0 +1,56 @@ +package com.appsmith.server.services; + +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.dtos.ApplicationPagesDTO; +import com.appsmith.server.dtos.PageDTO; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +public interface NewPageService extends CrudService { + + Mono getPageByViewMode(NewPage newPage, Boolean viewMode); + + Mono findById(String pageId, AclPermission aclPermission); + + Mono findPageById(String pageId, AclPermission aclPermission, Boolean view); + + Flux findByApplicationId(String applicationId, AclPermission permission, Boolean view); + + Flux findNewPagesByApplicationId(String applicationId, AclPermission permission); + + Mono saveUnpublishedPage(PageDTO page); + + Mono createDefault(PageDTO object); + + Mono findByIdAndLayoutsId(String pageId, String layoutId, AclPermission aclPermission, Boolean view); + + Mono findByNameAndViewMode(String name, AclPermission permission, Boolean view); + + Mono deleteAll(); + + Mono findNamesByApplicationIdAndViewMode(String applicationId, Boolean view); + + Layout createDefaultLayout(); + + Mono findNamesByApplicationNameAndViewMode(String applicationName, Boolean view); + + Mono findByNameAndApplicationIdAndViewMode(String name, String applicationId, AclPermission permission, Boolean view); + + Mono> archivePagesByApplicationId(String applicationId, AclPermission permission); + + Mono> findAllPageIdsInApplication(String applicationId, AclPermission permission, Boolean view); + + Mono updatePage(String id, PageDTO page); + + Mono save(NewPage page); + + Mono archive(NewPage page); + + Mono archiveById(String id); + + Flux saveAll(List pages); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageServiceImpl.java new file mode 100644 index 0000000000..c45c4bf54d --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewPageServiceImpl.java @@ -0,0 +1,284 @@ +package com.appsmith.server.services; + +import com.appsmith.server.acl.AclPermission; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.dtos.ApplicationPagesDTO; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.dtos.PageNameIdDTO; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.repositories.NewPageRepository; +import lombok.extern.slf4j.Slf4j; +import net.minidev.json.JSONObject; +import net.minidev.json.parser.JSONParser; +import net.minidev.json.parser.ParseException; +import org.bson.types.ObjectId; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +import javax.validation.Validator; +import java.util.List; +import java.util.Set; + +import static com.appsmith.server.helpers.BeanCopyUtils.copyNewFieldValuesIntoOldObject; + +@Service +@Slf4j +public class NewPageServiceImpl extends BaseService implements NewPageService { + + private final ApplicationService applicationService; + + @Autowired + public NewPageServiceImpl(Scheduler scheduler, + Validator validator, + MongoConverter mongoConverter, + ReactiveMongoTemplate reactiveMongoTemplate, + NewPageRepository repository, + AnalyticsService analyticsService, + ApplicationService applicationService) { + super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService); + this.applicationService = applicationService; + } + + @Override + public Mono getPageByViewMode(NewPage newPage, Boolean viewMode) { + + PageDTO page = null; + if (Boolean.TRUE.equals(viewMode)) { + if (newPage.getPublishedPage() != null) { + page = newPage.getPublishedPage(); + page.setName(newPage.getPublishedPage().getName()); + + } else { + // We are trying to fetch published page but it doesnt exist because the page hasn't been published yet + return Mono.empty(); + } + } else { + if (newPage.getUnpublishedPage() != null) { + page = newPage.getUnpublishedPage(); + page.setName(newPage.getUnpublishedPage().getName()); + } + } + + if (page != null) { + page.setId(newPage.getId()); + page.setApplicationId(newPage.getApplicationId()); + page.setUserPermissions(newPage.getUserPermissions()); + page.setPolicies(newPage.getPolicies()); + return Mono.just(page); + } + + // We shouldn't reach here. + return Mono.empty(); + + } + + @Override + public Mono findById(String pageId, AclPermission aclPermission) { + return repository.findById(pageId, aclPermission); + } + + @Override + public Mono findPageById(String pageId, AclPermission aclPermission, Boolean view) { + return this.findById(pageId, aclPermission) + .flatMap(page -> getPageByViewMode(page, view)); + } + + @Override + public Flux findByApplicationId(String applicationId, AclPermission permission, Boolean view) { + return findNewPagesByApplicationId(applicationId, permission) + .flatMap(page -> getPageByViewMode(page, view)); + } + + @Override + public Mono saveUnpublishedPage(PageDTO page) { + + return findById(page.getId(), AclPermission.MANAGE_PAGES) + .flatMap(newPage -> { + newPage.setUnpublishedPage(page); + return repository.save(newPage); + }) + .flatMap(savedPage -> getPageByViewMode(savedPage, false)); + } + + @Override + public Mono createDefault(PageDTO object) { + NewPage newPage = new NewPage(); + newPage.setUnpublishedPage(object); + + newPage.setApplicationId(object.getApplicationId()); + newPage.setPolicies(object.getPolicies()); + return super.create(newPage) + .flatMap(page -> getPageByViewMode(page, false)); + } + + @Override + public Mono findByIdAndLayoutsId(String pageId, String layoutId, AclPermission aclPermission, Boolean view) { + return repository.findByIdAndLayoutsIdAndViewMode(pageId, layoutId, aclPermission, view) + .flatMap(page -> getPageByViewMode(page, view)); + } + + @Override + public Mono findByNameAndViewMode(String name, AclPermission permission, Boolean view) { + return repository.findByNameAndViewMode(name, permission, view) + .flatMap(page -> getPageByViewMode(page, view)); + } + + @Override + public Layout createDefaultLayout() { + Layout layout = new Layout(); + String id = new ObjectId().toString(); + layout.setId(id); + try { + layout.setDsl((JSONObject) new JSONParser(JSONParser.MODE_PERMISSIVE).parse(FieldName.DEFAULT_PAGE_LAYOUT)); + layout.setWidgetNames(Set.of(FieldName.DEFAULT_WIDGET_NAME)); + } catch (ParseException e) { + log.error("Unable to set the default page layout for generated id: {}", id); + } + return layout; + } + + @Override + public Mono deleteAll() { + return repository.deleteAll(); + } + + @Override + public Mono findNamesByApplicationIdAndViewMode(String applicationId, Boolean view) { + Mono applicationMono = applicationService.findById(applicationId, AclPermission.READ_APPLICATIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE + "by application id", applicationId))) + .cache(); + + Mono> pagesListMono = applicationMono + .flatMapMany(application -> findNamesByApplication(application, view)) + .collectList(); + + return Mono.zip(applicationMono, pagesListMono) + .map(tuple -> { + Application application = tuple.getT1(); + List nameIdDTOList = tuple.getT2(); + ApplicationPagesDTO applicationPagesDTO = new ApplicationPagesDTO(); + applicationPagesDTO.setOrganizationId(application.getOrganizationId()); + applicationPagesDTO.setPages(nameIdDTOList); + return applicationPagesDTO; + }); + } + + @Override + public Mono findNamesByApplicationNameAndViewMode(String applicationName, Boolean view) { + Mono applicationMono = applicationService.findByName(applicationName, AclPermission.READ_APPLICATIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.NAME, applicationName))) + .cache(); + + Mono> pagesListMono = applicationMono + .flatMapMany(application -> findNamesByApplication(application, view)) + .collectList(); + + return Mono.zip(applicationMono, pagesListMono) + .map(tuple -> { + Application application = tuple.getT1(); + List nameIdDTOList = tuple.getT2(); + ApplicationPagesDTO applicationPagesDTO = new ApplicationPagesDTO(); + applicationPagesDTO.setOrganizationId(application.getOrganizationId()); + applicationPagesDTO.setPages(nameIdDTOList); + return applicationPagesDTO; + }); + } + + private Flux findNamesByApplication(Application application, Boolean viewMode) { + List pages = application.getPages(); + return findByApplicationId(application.getId(), AclPermission.READ_PAGES, viewMode) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.PAGE + "by application name", application.getName()))) + .map(page -> { + PageNameIdDTO pageNameIdDTO = new PageNameIdDTO(); + pageNameIdDTO.setId(page.getId()); + pageNameIdDTO.setName(page.getName()); + for (ApplicationPage applicationPage : pages) { + if (applicationPage.getId().equals(page.getId())) { + pageNameIdDTO.setIsDefault(applicationPage.getIsDefault()); + } + } + return pageNameIdDTO; + }); + } + + @Override + public Mono findByNameAndApplicationIdAndViewMode(String name, String applicationId, AclPermission permission, Boolean view) { + return repository.findByNameAndApplicationIdAndViewMode(name, applicationId, permission, view) + .flatMap(page -> getPageByViewMode(page, view)); + } + + @Override + public Flux findNewPagesByApplicationId(String applicationId, AclPermission permission) { + return repository.findByApplicationId(applicationId, permission); + } + + @Override + public Mono> archivePagesByApplicationId(String applicationId, AclPermission permission) { + return findNewPagesByApplicationId(applicationId, permission) + .flatMap(repository::archive) + .collectList(); + } + + @Override + public Mono> findAllPageIdsInApplication(String applicationId, AclPermission aclPermission, Boolean view) { + return findNewPagesByApplicationId(applicationId, aclPermission) + .flatMap(newPage -> { + if (Boolean.TRUE.equals(view)) { + if (newPage.getPublishedPage().getDeletedAt() != null) { + return Mono.just(newPage.getId()); + } + } else { + if (newPage.getUnpublishedPage().getDeletedAt() != null) { + return Mono.just(newPage.getId()); + } + } + // Looks like the page has been deleted in the `view` mode. Don't return the id for this page. + return Mono.empty(); + }) + .collectList(); + } + + @Override + public Mono updatePage(String id, PageDTO page) { + NewPage newPage = new NewPage(); + newPage.setUnpublishedPage(page); + + return repository.findById(id, AclPermission.MANAGE_PAGES) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.PAGE, id))) + .flatMap(dbPage -> { + copyNewFieldValuesIntoOldObject(page, dbPage.getUnpublishedPage()); + return this.update(id, dbPage); + }) + .flatMap(savedPage -> getPageByViewMode(savedPage, false)); + } + + @Override + public Mono save(NewPage page) { + return repository.save(page); + } + + @Override + public Mono archive(NewPage page) { + return repository.archive(page); + } + + @Override + public Mono archiveById(String id) { + return repository.archiveById(id); + } + + @Override + public Flux saveAll(List pages) { + return repository.saveAll(pages); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PostmanImporterService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PostmanImporterService.java index 5ef592cf1e..ee4b7835f5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PostmanImporterService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PostmanImporterService.java @@ -5,8 +5,8 @@ import com.appsmith.external.models.ApiTemplate; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Property; import com.appsmith.external.models.TemplateCollection; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Datasource; +import com.appsmith.server.dtos.ActionDTO; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Service; @@ -19,8 +19,8 @@ import java.util.List; @Slf4j public class PostmanImporterService extends BaseApiImporter { @Override - public Mono importAction(Object input, String pageId, String name, String orgId) { - Action action = new Action(); + public Mono importAction(Object input, String pageId, String name, String orgId) { + ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); Datasource datasource = new Datasource(); DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java index 58003ed1b1..583c60a34a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java @@ -7,6 +7,8 @@ import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; @@ -170,16 +172,17 @@ public class UserOrganizationServiceImpl implements UserOrganizationService { // Update the underlying application/page/action Flux updatedDatasourcesFlux = policyUtils.updateWithNewPoliciesToDatasourcesByOrgId(updatedOrganization.getId(), datasourcePolicyMap, true); - Flux updatedApplicationsFlux = policyUtils.updateWithNewPoliciesToApplicationsByOrgId(updatedOrganization.getId(), applicationPolicyMap, true); - Flux updatedPagesFlux = updatedApplicationsFlux + Flux updatedApplicationsFlux = policyUtils.updateWithNewPoliciesToApplicationsByOrgId(updatedOrganization.getId(), applicationPolicyMap, true) + .cache(); + Flux updatedPagesFlux = updatedApplicationsFlux .flatMap(application -> policyUtils.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, true)); - Flux updatedActionsFlux = updatedPagesFlux - .flatMap(page -> policyUtils.updateWithPagePermissionsToAllItsActions(page.getId(), actionPolicyMap, true)); + Flux updatedActionsFlux = updatedApplicationsFlux + .flatMap(application -> policyUtils.updateWithPagePermissionsToAllItsActions(application.getId(), actionPolicyMap, true)); - return Mono.zip(updatedDatasourcesFlux.collectList(), updatedActionsFlux.collectList(), Mono.just(updatedOrganization)) + return Mono.zip(updatedDatasourcesFlux.collectList(), updatedPagesFlux.collectList(), updatedActionsFlux.collectList(), Mono.just(updatedOrganization)) .flatMap(tuple -> { //By now all the datasources/applications/pages/actions have been updated. Just save the organization now - Organization updatedOrgBeforeSave = tuple.getT3(); + Organization updatedOrgBeforeSave = tuple.getT4(); return organizationRepository.save(updatedOrgBeforeSave); }); } @@ -233,16 +236,17 @@ public class UserOrganizationServiceImpl implements UserOrganizationService { // Update the underlying application/page/action Flux updatedDatasourcesFlux = policyUtils.updateWithNewPoliciesToDatasourcesByOrgId(updatedOrganization.getId(), datasourcePolicyMap, false); - Flux updatedApplicationsFlux = policyUtils.updateWithNewPoliciesToApplicationsByOrgId(updatedOrganization.getId(), applicationPolicyMap, false); - Flux updatedPagesFlux = updatedApplicationsFlux + Flux updatedApplicationsFlux = policyUtils.updateWithNewPoliciesToApplicationsByOrgId(updatedOrganization.getId(), applicationPolicyMap, false) + .cache(); + Flux updatedPagesFlux = updatedApplicationsFlux .flatMap(application -> policyUtils.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, false)); - Flux updatedActionsFlux = updatedPagesFlux - .flatMap(page -> policyUtils.updateWithPagePermissionsToAllItsActions(page.getId(), actionPolicyMap, false)); + Flux updatedActionsFlux = updatedApplicationsFlux + .flatMap(application -> policyUtils.updateWithPagePermissionsToAllItsActions(application.getId(), actionPolicyMap, false)); - return Mono.zip(updatedDatasourcesFlux.collectList(), updatedActionsFlux.collectList(), Mono.just(updatedOrganization)) + return Mono.zip(updatedDatasourcesFlux.collectList(), updatedPagesFlux.collectList(), updatedActionsFlux.collectList(), Mono.just(updatedOrganization)) .flatMap(tuple -> { //By now all the datasources/applications/pages/actions have been updated. Just save the organization now - Organization updatedOrgBeforeSave = tuple.getT3(); + Organization updatedOrgBeforeSave = tuple.getT4(); return organizationRepository.save(updatedOrgBeforeSave); }); } @@ -369,13 +373,15 @@ public class UserOrganizationServiceImpl implements UserOrganizationService { // Update the underlying application/page/action Flux updatedDatasourcesFlux = policyUtils.updateWithNewPoliciesToDatasourcesByOrgId(updatedOrganization.getId(), datasourcePolicyMap, true); - Flux updatedApplicationsFlux = policyUtils.updateWithNewPoliciesToApplicationsByOrgId(updatedOrganization.getId(), applicationPolicyMap, true); - Flux updatedPagesFlux = updatedApplicationsFlux + Flux updatedApplicationsFlux = policyUtils.updateWithNewPoliciesToApplicationsByOrgId(updatedOrganization.getId(), applicationPolicyMap, true) + .cache(); + Flux updatedPagesFlux = updatedApplicationsFlux .flatMap(application -> policyUtils.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, true)); - Flux updatedActionsFlux = updatedPagesFlux - .flatMap(page -> policyUtils.updateWithPagePermissionsToAllItsActions(page.getId(), actionPolicyMap, true)); - return Mono.when(updatedDatasourcesFlux.collectList(), updatedActionsFlux.collectList()) + Flux updatedActionsFlux = updatedApplicationsFlux + .flatMap(application -> policyUtils.updateWithPagePermissionsToAllItsActions(application.getId(), actionPolicyMap, true)); + + return Mono.when(updatedDatasourcesFlux.collectList(), updatedPagesFlux.collectList(), updatedActionsFlux.collectList()) //By now all the datasources/applications/pages/actions have been updated. Just save the organization now .then(organizationRepository.save(updatedOrganization)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java index da8b20b502..8a5df6b3fe 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java @@ -1,26 +1,26 @@ package com.appsmith.server.solutions; import com.appsmith.external.models.BaseDomain; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Organization; -import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.DslActionDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; -import com.appsmith.server.repositories.ActionRepository; import com.appsmith.server.repositories.DatasourceRepository; +import com.appsmith.server.repositories.NewPageRepository; import com.appsmith.server.repositories.OrganizationRepository; -import com.appsmith.server.repositories.PageRepository; -import com.appsmith.server.services.ActionService; import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.services.ConfigService; import com.appsmith.server.services.DatasourceContextService; import com.appsmith.server.services.DatasourceService; +import com.appsmith.server.services.NewActionService; import com.appsmith.server.services.OrganizationService; import com.appsmith.server.services.SessionUserService; import com.appsmith.server.services.UserService; @@ -46,15 +46,14 @@ public class ExamplesOrganizationCloner { private final OrganizationService organizationService; private final OrganizationRepository organizationRepository; private final DatasourceService datasourceService; - private final PageRepository pageRepository; - private final ActionService actionService; private final DatasourceRepository datasourceRepository; - private final ActionRepository actionRepository; private final ConfigService configService; private final SessionUserService sessionUserService; private final UserService userService; private final ApplicationPageService applicationPageService; private final DatasourceContextService datasourceContextService; + private final NewPageRepository newPageRepository; + private final NewActionService newActionService; public Mono cloneExamplesOrganization() { return sessionUserService @@ -162,7 +161,8 @@ public class ExamplesOrganizationCloner { */ private Mono cloneApplications(String fromOrganizationId, String toOrganizationId, Flux applicationsFlux) { final Mono> cloneDatasourcesMono = cloneDatasources(fromOrganizationId, toOrganizationId).cache(); - final List clonedPages = new ArrayList<>(); + final List clonedPages = new ArrayList<>(); + final List newApplicationIds = new ArrayList<>(); return applicationsFlux .flatMap(application -> { @@ -174,7 +174,7 @@ public class ExamplesOrganizationCloner { .findFirst() .orElse(""); - return doCloneApplication(application) + return doOnlyCloneApplicationObjectWithoutItsDependenciesAndReturnPages(application, newApplicationIds) .flatMap(page -> Mono.zip( Mono.just(page), @@ -183,41 +183,49 @@ public class ExamplesOrganizationCloner { ); }) .flatMap(tuple -> { - final Page page = tuple.getT1(); + final NewPage newPage = tuple.getT1(); final boolean isDefault = tuple.getT2(); - final String templatePageId = page.getId(); + final String templatePageId = newPage.getId(); + + makePristine(newPage); + PageDTO page = newPage.getUnpublishedPage(); - makePristine(page); if (page.getLayouts() != null) { for (final Layout layout : page.getLayouts()) { layout.setId(new ObjectId().toString()); } } + page.setApplicationId(newPage.getApplicationId()); + return applicationPageService .createPage(page) .flatMap(savedPage -> isDefault ? applicationPageService.makePageDefault(savedPage).thenReturn(savedPage) : Mono.just(savedPage)) + .flatMap(savedPage -> newPageRepository.findById(savedPage.getId())) .flatMapMany(savedPage -> { clonedPages.add(savedPage); - return actionRepository + return newActionService .findByPageId(templatePageId) - .map(action -> { - log.info("Preparing action for cloning {} {}.", action.getName(), action.getId()); + .map(newAction -> { + ActionDTO action = newAction.getUnpublishedAction(); + log.info("Preparing action for cloning {} {}.", action.getName(), newAction.getId()); action.setPageId(savedPage.getId()); - return action; + return newAction; }); }); }) - .flatMap(action -> { - final String originalActionId = action.getId(); + .flatMap(newAction -> { + final String originalActionId = newAction.getId(); log.info("Creating clone of action {}", originalActionId); - makePristine(action); - action.setOrganizationId(toOrganizationId); + makePristine(newAction); + newAction.setOrganizationId(toOrganizationId); + ActionDTO action = newAction.getUnpublishedAction(); action.setCollectionId(null); - Mono actionMono = Mono.just(action); + + Mono actionMono = Mono.just(action); final Datasource datasourceInsideAction = action.getDatasource(); if (datasourceInsideAction != null) { if (datasourceInsideAction.getId() != null) { @@ -231,81 +239,91 @@ public class ExamplesOrganizationCloner { } } return actionMono - .flatMap(actionService::create) - .map(Action::getId) + .flatMap(newActionService::createAction) + .map(ActionDTO::getId) .zipWith(Mono.just(originalActionId)); }) // This call to `collectMap` will wait for all actions in all pages to have been processed, and so the // `clonedPages` list will also contain all pages cloned. .collectMap(Tuple2::getT2, Tuple2::getT1) - .flatMapMany(actionIdsMap -> { - final List> pageSaveMonos = new ArrayList<>(); - - for (final Page page : clonedPages) { - if (page.getLayouts() == null) { - continue; - } - - boolean shouldSave = false; - - for (final Layout layout : page.getLayouts()) { - if (layout.getLayoutOnLoadActions() != null) { - for (final Set actionSet : layout.getLayoutOnLoadActions()) { - for (final DslActionDTO actionDTO : actionSet) { - if (actionIdsMap.containsKey(actionDTO.getId())) { - actionDTO.setId(actionIdsMap.get(actionDTO.getId())); - shouldSave = true; - } else { - log.error( - "Couldn't find cloned action ID for layoutOnLoadAction {} in page {}", - actionDTO.getId(), - page.getId() - ); - } - } - } - } - if (layout.getPublishedLayoutOnLoadActions() != null) { - for (final Set actionSet : layout.getPublishedLayoutOnLoadActions()) { - for (final DslActionDTO actionDTO : actionSet) { - if (actionIdsMap.containsKey(actionDTO.getId())) { - actionDTO.setId(actionIdsMap.get(actionDTO.getId())); - shouldSave = true; - } else { - log.error( - "Couldn't find cloned action ID for publishedLayoutOnLoadAction {} in page {}", - actionDTO.getId(), - page.getId() - ); - } - } - } - } - } - - if (shouldSave) { - pageSaveMonos.add(pageRepository.save(page)); - } - } - - return Flux.concat(pageSaveMonos); - }) + .flatMapMany(actionIdsMap -> updateActionIdsInClonedPages(clonedPages, actionIdsMap)) .then(cloneDatasourcesMono) // Run the datasource cloning mono if it isn't already done. + // Now publish all the example applications which have been cloned to ensure that there is a + // view mode for the newly created user. + .then(Mono.just(newApplicationIds)) + .flatMapMany(applicationIds -> Flux.fromIterable(applicationIds)) + .flatMap(appId -> applicationPageService.publish(appId)) + .collectList() .then(); } - private Flux doCloneApplication(Application application) { + private Flux updateActionIdsInClonedPages(List clonedPages, Map actionIdsMap) { + final List> pageSaveMonos = new ArrayList<>(); + + for (final NewPage page : clonedPages) { + // If there are no unpublished layouts, there would be no published layouts either. + // Move on to the next page. + if (page.getUnpublishedPage().getLayouts() == null) { + continue; + } + + boolean shouldSave = false; + + for (final Layout layout : page.getUnpublishedPage().getLayouts()) { + if (layout.getLayoutOnLoadActions() != null) { + shouldSave = updateOnLoadActionsWithNewActionIds(actionIdsMap, page.getId(), shouldSave, layout); + } + } + + if (shouldSave) { + pageSaveMonos.add(newPageRepository.save(page)); + } + } + + return Flux.concat(pageSaveMonos); + } + + private boolean updateOnLoadActionsWithNewActionIds(Map actionIdsMap, String pageId, boolean shouldSave, Layout layout) { + for (final Set actionSet : layout.getLayoutOnLoadActions()) { + for (final DslActionDTO actionDTO : actionSet) { + if (actionIdsMap.containsKey(actionDTO.getId())) { + actionDTO.setId(actionIdsMap.get(actionDTO.getId())); + shouldSave = true; + } else { + log.error( + "Couldn't find cloned action ID for publishedLayoutOnLoadAction {} in page {}", + actionDTO.getId(), + pageId + ); + } + } + } + return shouldSave; + } + + /** + * This function simply creates a clone of the Application object without cloning its children (page and actions) + * Once the new application object is created, it adds the new application's id into the list applicationIds + * + * @param application : Application to be cloned + * @param applicationIds : List where the cloned new application's id would be stored + * @return + */ + private Flux doOnlyCloneApplicationObjectWithoutItsDependenciesAndReturnPages(Application application, List applicationIds) { final String templateApplicationId = application.getId(); return applicationPageService .cloneExampleApplication(application) .flatMapMany( - savedApplication -> pageRepository - .findByApplicationId(templateApplicationId) - .map(page -> { - log.info("Preparing page for cloning {} {}.", page.getName(), page.getId()); - page.setApplicationId(savedApplication.getId()); - return page; - }) + savedApplication -> { + applicationIds.add(savedApplication.getId()); + return newPageRepository + .findByApplicationId(templateApplicationId) + .map(newPage -> { + log.info("Preparing page for cloning {} {}.", newPage.getUnpublishedPage().getName(), newPage.getId()); + newPage.setApplicationId(savedApplication.getId()); + return newPage; + }); + } ); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PingScheduledTask.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PingScheduledTask.java index 0d9be480d7..4062f07cd7 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PingScheduledTask.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/PingScheduledTask.java @@ -80,9 +80,9 @@ public class PingScheduledTask { .header("Authorization", "Basic QjJaM3hXRThXdDRwYnZOWDRORnJPNWZ3VXdnYWtFbk06") .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(Map.of( - "userId", instanceId, + "userId", ipAddress, "context", Map.of("ip", ipAddress), - "properties", Map.of("ip", ipAddress), + "properties", Map.of("instanceId", instanceId), "event", "Instance Active" ))) .retrieve() diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MockPluginExecutor.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MockPluginExecutor.java index 6cd9e9f871..f2f758be0e 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MockPluginExecutor.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/MockPluginExecutor.java @@ -14,8 +14,19 @@ public class MockPluginExecutor implements PluginExecutor { @Override public Mono execute(Object connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) { + if (actionConfiguration == null) { + return Mono.error(new Exception("ActionConfiguration is null")); + } + if (datasourceConfiguration == null) { + return Mono.error(new Exception("DatasourceConfiguration is null")); + } System.out.println("In the execute"); - return null; + + ActionExecutionResult actionExecutionResult = new ActionExecutionResult(); + actionExecutionResult.setBody(""); + actionExecutionResult.setIsExecutionSuccess(true); + actionExecutionResult.setStatusCode("200"); + return Mono.just(actionExecutionResult); } @Override diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java index 40494b44b0..23057324e8 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionServiceTest.java @@ -2,6 +2,7 @@ package com.appsmith.server.services; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Policy; import com.appsmith.external.models.Property; import com.appsmith.external.pluginExceptions.AppsmithPluginError; @@ -10,17 +11,17 @@ import com.appsmith.external.pluginExceptions.StaleConnectionException; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Layout; import com.appsmith.server.domains.Organization; -import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ActionMoveDTO; import com.appsmith.server.dtos.ActionViewDTO; import com.appsmith.server.dtos.ExecuteActionDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.MockPluginExecutor; @@ -53,6 +54,7 @@ import java.util.UUID; import static com.appsmith.external.constants.ActionConstants.DEFAULT_ACTION_EXECUTION_TIMEOUT_MS; import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; +import static com.appsmith.server.acl.AclPermission.READ_PAGES; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) @@ -61,13 +63,13 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class ActionServiceTest { @Autowired - ActionService actionService; + NewActionService newActionService; @Autowired ApplicationPageService applicationPageService; @Autowired - PageService pageService; + NewPageService newPageService; @Autowired UserService userService; @@ -96,18 +98,23 @@ public class ActionServiceTest { @Autowired LayoutService layoutService; + @Autowired + DatasourceService datasourceService; + Application testApp = null; - Page testPage = null; + PageDTO testPage = null; Datasource datasource; + String orgId; + @Before @WithUserDetails(value = "api_user") public void setup() { User apiUser = userService.findByEmail("api_user").block(); - String orgId = apiUser.getOrganizationIds().iterator().next(); + orgId = apiUser.getOrganizationIds().iterator().next(); Organization organization = organizationService.getById(orgId).block(); if (testApp == null && testPage == null) { @@ -123,7 +130,7 @@ public class ActionServiceTest { layout.setPublishedDsl(dsl); layoutService.createLayout(pageId, layout).block(); - testPage = pageService.getById(pageId).block(); + testPage = newPageService.findPageById(pageId, READ_PAGES, false).block(); } Organization testOrg = organizationRepository.findByName("Another Test Organization", AclPermission.READ_ORGANIZATIONS).block(); @@ -156,7 +163,7 @@ public class ActionServiceTest { .users(Set.of("api_user")) .build(); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("validAction"); action.setPageId(testPage.getId()); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -164,7 +171,9 @@ public class ActionServiceTest { action.setActionConfiguration(actionConfiguration); action.setDatasource(datasource); - Mono actionMono = actionService.create(action); + Mono actionMono = newActionService.createAction(action) + .flatMap(createdAction -> newActionService.findById(createdAction.getId(), READ_ACTIONS)) + .flatMap(newAction -> newActionService.generateActionByViewMode(newAction, false)); StepVerifier .create(actionMono) @@ -181,12 +190,12 @@ public class ActionServiceTest { public void validMoveAction() { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - Page newPage = new Page(); + PageDTO newPage = new PageDTO(); newPage.setName("Destination Page"); newPage.setApplicationId(testApp.getId()); - Page destinationPage = applicationPageService.createPage(newPage).block(); + PageDTO destinationPage = applicationPageService.createPage(newPage).block(); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("validAction"); action.setPageId(testPage.getId()); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -194,9 +203,9 @@ public class ActionServiceTest { action.setActionConfiguration(actionConfiguration); action.setDatasource(datasource); - Mono createActionMono = actionService.create(action).cache(); + Mono createActionMono = newActionService.createAction(action).cache(); - Mono movedActionMono = createActionMono + Mono movedActionMono = createActionMono .flatMap(savedAction -> { ActionMoveDTO actionMoveDTO = new ActionMoveDTO(); actionMoveDTO.setAction(savedAction); @@ -207,8 +216,8 @@ public class ActionServiceTest { StepVerifier .create(Mono.zip(createActionMono, movedActionMono)) .assertNext(tuple -> { - Action originalAction = tuple.getT1(); - Action movedAction = tuple.getT2(); + ActionDTO originalAction = tuple.getT1(); + ActionDTO movedAction = tuple.getT2(); assertThat(movedAction.getId()).isEqualTo(originalAction.getId()); assertThat(movedAction.getName()).isEqualTo(originalAction.getName()); @@ -225,15 +234,15 @@ public class ActionServiceTest { public void createValidActionWithJustName() { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("randomActionName"); action.setPageId(testPage.getId()); ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setHttpMethod(HttpMethod.GET); action.setActionConfiguration(actionConfiguration); action.setDatasource(datasource); - Mono actionMono = Mono.just(action) - .flatMap(actionService::create); + Mono actionMono = Mono.just(action) + .flatMap(newActionService::createAction); StepVerifier .create(actionMono) .assertNext(createdAction -> { @@ -249,12 +258,12 @@ public class ActionServiceTest { public void createValidActionNullActionConfiguration() { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("randomActionName2"); action.setPageId(testPage.getId()); action.setDatasource(datasource); - Mono actionMono = Mono.just(action) - .flatMap(actionService::create); + Mono actionMono = Mono.just(action) + .flatMap(newActionService::createAction); StepVerifier .create(actionMono) .assertNext(createdAction -> { @@ -269,14 +278,14 @@ public class ActionServiceTest { @Test @WithUserDetails(value = "api_user") public void invalidCreateActionNullName() { - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setPageId(testPage.getId()); ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setHttpMethod(HttpMethod.GET); action.setActionConfiguration(actionConfiguration); action.setDatasource(datasource); - Mono actionMono = Mono.just(action) - .flatMap(actionService::create); + Mono actionMono = Mono.just(action) + .flatMap(newActionService::createAction); StepVerifier .create(actionMono) .expectErrorMatches(throwable -> throwable instanceof AppsmithException && @@ -287,13 +296,13 @@ public class ActionServiceTest { @Test @WithUserDetails(value = "api_user") public void invalidCreateActionNullPageId() { - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("randomActionName"); ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setHttpMethod(HttpMethod.GET); action.setActionConfiguration(actionConfiguration); - Mono actionMono = Mono.just(action) - .flatMap(actionService::create); + Mono actionMono = Mono.just(action) + .flatMap(newActionService::createAction); StepVerifier .create(actionMono) .expectErrorMatches(throwable -> throwable instanceof AppsmithException && @@ -304,14 +313,14 @@ public class ActionServiceTest { @Test @WithUserDetails(value = "api_user") public void invalidCreateActionInvalidPageId() { - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("randomActionName3"); action.setPageId("invalid page id here"); ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setHttpMethod(HttpMethod.GET); action.setActionConfiguration(actionConfiguration); - Mono actionMono = Mono.just(action) - .flatMap(actionService::create); + Mono actionMono = Mono.just(action) + .flatMap(newActionService::createAction); StepVerifier .create(actionMono) .expectErrorMatches(throwable -> throwable instanceof AppsmithException && @@ -355,11 +364,11 @@ public class ActionServiceTest { " \"name\": \"propertyPane\"\n" + "}"; - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setActionConfiguration(new ActionConfiguration()); action.getActionConfiguration().setBody("{{Input.text}}"); - Action renderedAction = actionService.variableSubstitution(action, Map.of("Input.text", json)); + ActionDTO renderedAction = newActionService.variableSubstitution(action, Map.of("Input.text", json)); assertThat(renderedAction).isNotNull(); assertThat(renderedAction.getActionConfiguration().getBody()).isEqualTo(json); } @@ -367,11 +376,11 @@ public class ActionServiceTest { @Test @WithUserDetails(value = "api_user") public void testVariableSubstitutionWithNewline() { - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setActionConfiguration(new ActionConfiguration()); action.getActionConfiguration().setBody("{{Input.text}}"); - Action renderedAction = actionService.variableSubstitution(action, Map.of("Input.text", "name\nvalue")); + ActionDTO renderedAction = newActionService.variableSubstitution(action, Map.of("Input.text", "name\nvalue")); assertThat(renderedAction).isNotNull(); assertThat(renderedAction.getActionConfiguration().getBody()).isEqualTo("name\nvalue"); } @@ -379,21 +388,28 @@ public class ActionServiceTest { @Test @WithUserDetails(value = "api_user") public void testActionExecute() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + ActionExecutionResult mockResult = new ActionExecutionResult(); mockResult.setIsExecutionSuccess(true); mockResult.setBody("response-body"); mockResult.setStatusCode("200"); mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - Action action = new Action(); + ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setHttpMethod(HttpMethod.POST); actionConfiguration.setBody("random-request-body"); actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecute"); + action.setDatasource(datasource); + ActionDTO createdAction = newActionService.createAction(action).block(); ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setAction(action); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult); } @@ -401,20 +417,27 @@ public class ActionServiceTest { @Test @WithUserDetails(value = "api_user") public void testActionExecuteNullRequestBody() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + ActionExecutionResult mockResult = new ActionExecutionResult(); mockResult.setIsExecutionSuccess(true); mockResult.setBody("response-body"); mockResult.setStatusCode("200"); mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); - Action action = new Action(); + ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setHttpMethod(HttpMethod.GET); actionConfiguration.setHeaders(List.of(new Property("random-header-key", "random-header-value"))); action.setActionConfiguration(actionConfiguration); + action.setName("testActionExecuteNullRequestBody"); + action.setPageId(testPage.getId()); + action.setDatasource(datasource); + ActionDTO createdAction = newActionService.createAction(action).block(); ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setAction(action); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult); } @@ -422,17 +445,24 @@ public class ActionServiceTest { @Test @WithUserDetails(value = "api_user") public void testActionExecuteDbQuery() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + ActionExecutionResult mockResult = new ActionExecutionResult(); mockResult.setIsExecutionSuccess(true); mockResult.setBody("response-body"); - Action action = new Action(); + ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setBody("select * from users"); action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecuteDbQuery"); + action.setDatasource(datasource); + ActionDTO createdAction = newActionService.createAction(action).block(); ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setAction(action); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); executeAndAssertAction(executeActionDTO, actionConfiguration, mockResult); } @@ -440,27 +470,34 @@ public class ActionServiceTest { @Test @WithUserDetails(value = "api_user") public void testActionExecuteErrorResponse() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); + ActionExecutionResult mockResult = new ActionExecutionResult(); mockResult.setIsExecutionSuccess(true); mockResult.setBody("response-body"); - Action action = new Action(); + ActionDTO action = new ActionDTO(); ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setHeaders(List.of( new Property("random-header-key", "random-header-value"), new Property("", "") )); action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("testActionExecuteErrorResponse"); + action.setDatasource(datasource); + ActionDTO createdAction = newActionService.createAction(action).block(); ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setAction(action); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); AppsmithPluginException pluginException = new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR); Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.error(pluginException)); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - Mono executionResultMono = actionService.executeAction(executeActionDTO); + Mono executionResultMono = newActionService.executeAction(executeActionDTO); StepVerifier.create(executionResultMono) .assertNext(result -> { @@ -476,7 +513,7 @@ public class ActionServiceTest { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); String key = "bodyMustacheKey"; - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("actionInViewMode"); action.setPageId(testPage.getId()); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -485,7 +522,7 @@ public class ActionServiceTest { action.setActionConfiguration(actionConfiguration); action.setDatasource(datasource); - Action action1 = new Action(); + ActionDTO action1 = new ActionDTO(); action1.setName("actionInViewModeWithoutMustacheKey"); action1.setPageId(testPage.getId()); ActionConfiguration actionConfiguration1 = new ActionConfiguration(); @@ -494,15 +531,16 @@ public class ActionServiceTest { action1.setActionConfiguration(actionConfiguration1); action1.setDatasource(datasource); - Action action2 = new Action(); + ActionDTO action2 = new ActionDTO(); action2.setName("actionInViewModeWithoutActionConfiguration"); action2.setPageId(testPage.getId()); action2.setDatasource(datasource); - Mono> actionsListMono = actionService.create(action) - .then(actionService.create(action1)) - .then(actionService.create(action2)) - .then(actionService.getActionsForViewMode(testApp.getId()).collectList()); + Mono> actionsListMono = newActionService.createAction(action) + .then(newActionService.createAction(action1)) + .then(newActionService.createAction(action2)) + .then(applicationPageService.publish(testPage.getApplicationId())) + .then(newActionService.getActionsForViewMode(testApp.getId()).collectList()); StepVerifier .create(actionsListMono) @@ -538,21 +576,27 @@ public class ActionServiceTest { mockResult.setIsExecutionSuccess(true); mockResult.setBody("response-body"); - Action action = new Action(); - ActionConfiguration actionConfiguration = new ActionConfiguration(); - actionConfiguration.setBody("select * from users"); - action.setActionConfiguration(actionConfiguration); - - ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); - executeActionDTO.setAction(action); - Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(pluginExecutor)); Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())) .thenThrow(new StaleConnectionException()) .thenReturn(Mono.just(mockResult)); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - Mono actionExecutionResultMono = actionService.executeAction(executeActionDTO); + + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setBody("select * from users"); + action.setActionConfiguration(actionConfiguration); + action.setPageId(testPage.getId()); + action.setName("checkRecoveryFromStaleConnections"); + action.setDatasource(datasource); + ActionDTO createdAction = newActionService.createAction(action).block(); + + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(createdAction.getId()); + executeActionDTO.setViewMode(false); + + Mono actionExecutionResultMono = newActionService.executeAction(executeActionDTO); StepVerifier.create(actionExecutionResultMono) .assertNext(result -> { @@ -579,7 +623,7 @@ public class ActionServiceTest { Mockito.when(pluginExecutor.execute(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(Mono.just(mockResult)); Mockito.when(pluginExecutor.datasourceCreate(Mockito.any())).thenReturn(Mono.empty()); - Mono actionExecutionResultMono = actionService.executeAction(executeActionDTO); + Mono actionExecutionResultMono = newActionService.executeAction(executeActionDTO); return actionExecutionResultMono; } @@ -588,7 +632,7 @@ public class ActionServiceTest { public void getActionInViewMode() { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("view-mode-action-test"); action.setPageId(testPage.getId()); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -597,9 +641,11 @@ public class ActionServiceTest { action.setActionConfiguration(actionConfiguration); action.setDatasource(datasource); - Mono createActionMono = actionService.create(action); + Mono createActionMono = newActionService.createAction(action); Mono> actionViewModeListMono = createActionMono - .then(actionService.getActionsForViewMode(testApp.getId()).collectList()); + // Publish the application before fetching the action in view mode + .then(applicationPageService.publish(testApp.getId())) + .then(newActionService.getActionsForViewMode(testApp.getId()).collectList()); StepVerifier.create(actionViewModeListMono) .assertNext(actions -> { @@ -613,4 +659,46 @@ public class ActionServiceTest { }) .verifyComplete(); } + + @Test + @WithUserDetails(value = "api_user") + public void executeActionWithExternalDatasource() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + + Datasource externalDatasource = new Datasource(); + externalDatasource.setName("Default Database"); + externalDatasource.setOrganizationId(orgId); + Plugin installed_plugin = pluginRepository.findByPackageName("installed-plugin").block(); + externalDatasource.setPluginId(installed_plugin.getId()); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setUrl("some url here"); + externalDatasource.setDatasourceConfiguration(datasourceConfiguration); + Datasource savedDs = datasourceService.create(externalDatasource).block(); + + ActionDTO action = new ActionDTO(); + action.setName("actionWithExternalDatasource"); + action.setPageId(testPage.getId()); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(savedDs); + + + Mono resultMono = newActionService.createAction(action) + .flatMap(savedAction -> { + ExecuteActionDTO executeActionDTO = new ExecuteActionDTO(); + executeActionDTO.setActionId(savedAction.getId()); + executeActionDTO.setViewMode(false); + return newActionService.executeAction(executeActionDTO); + }); + + + StepVerifier + .create(resultMono) + .assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.getStatusCode()).isEqualTo("200"); + }) + .verifyComplete(); + } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java index 6b45d93dd6..443069440f 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ApplicationServiceTest.java @@ -4,21 +4,25 @@ import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Policy; import com.appsmith.server.constants.FieldName; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Organization; -import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.ApplicationAccessDTO; import com.appsmith.server.dtos.OrganizationApplicationsDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.dtos.UserHomepageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.MockPluginExecutor; import com.appsmith.server.helpers.PluginExecutorHelper; -import com.appsmith.server.repositories.PageRepository; +import com.appsmith.server.repositories.NewPageRepository; import com.appsmith.server.solutions.ApplicationFetcher; import lombok.extern.slf4j.Slf4j; import org.junit.Before; @@ -38,6 +42,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -65,18 +70,12 @@ public class ApplicationServiceTest { @Autowired ApplicationPageService applicationPageService; - @Autowired - PageService pageService; - @Autowired UserService userService; @Autowired OrganizationService organizationService; - @Autowired - PageRepository pageRepository; - @Autowired DatasourceService datasourceService; @@ -84,7 +83,7 @@ public class ApplicationServiceTest { PluginService pluginService; @Autowired - ActionService actionService; + NewActionService newActionService; @MockBean PluginExecutorHelper pluginExecutorHelper; @@ -92,6 +91,12 @@ public class ApplicationServiceTest { @Autowired ApplicationFetcher applicationFetcher; + @Autowired + NewPageService newPageService; + + @Autowired + NewPageRepository newPageRepository; + String orgId; @Before @@ -147,9 +152,10 @@ public class ApplicationServiceTest { public void defaultPageCreateOnCreateApplicationTest() { Application testApplication = new Application(); testApplication.setName("ApplicationServiceTest TestAppForTestingPage"); - Flux pagesFlux = applicationPageService + Flux pagesFlux = applicationPageService .createApplication(testApplication, orgId) - .flatMapMany(application -> pageService.findByApplicationId(application.getId(), READ_PAGES)); + // Fetch the unpublished pages by applicationId + .flatMapMany(application -> newPageService.findByApplicationId(application.getId(), READ_PAGES, false)); Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue()) .users(Set.of("api_user")) @@ -403,17 +409,17 @@ public class ApplicationServiceTest { .changeViewAccess(createdApplication.getId(), applicationAccessDTO) .cache(); - Mono pageMono = publicAppMono + Mono pageMono = publicAppMono .flatMap(app -> { String pageId = app.getPages().get(0).getId(); - return pageRepository.findById(pageId); + return newPageService.findPageById(pageId, READ_PAGES, false); }); StepVerifier .create(Mono.zip(publicAppMono, pageMono)) .assertNext(tuple -> { Application publicApp = tuple.getT1(); - Page page = tuple.getT2(); + PageDTO page = tuple.getT2(); assertThat(publicApp.getIsPublic()).isTrue(); assertThat(publicApp.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy)); @@ -458,17 +464,17 @@ public class ApplicationServiceTest { }) .cache(); - Mono pageMono = privateAppMono + Mono pageMono = privateAppMono .flatMap(app -> { String pageId = app.getPages().get(0).getId(); - return pageRepository.findById(pageId); + return newPageService.findPageById(pageId, READ_PAGES, false); }); StepVerifier .create(Mono.zip(privateAppMono, pageMono)) .assertNext(tuple -> { Application app = tuple.getT1(); - Page page = tuple.getT2(); + PageDTO page = tuple.getT2(); assertThat(app.getIsPublic()).isFalse(); assertThat(app.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy)); @@ -522,7 +528,7 @@ public class ApplicationServiceTest { Datasource savedDatasource = datasourceService.create(datasource).block(); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("Public App Test action"); action.setPageId(pageId); action.setDatasource(savedDatasource); @@ -530,7 +536,7 @@ public class ApplicationServiceTest { actionConfiguration.setHttpMethod(HttpMethod.GET); action.setActionConfiguration(actionConfiguration); - Action savedAction = actionService.create(action).block(); + ActionDTO savedAction = newActionService.createAction(action).block(); ApplicationAccessDTO applicationAccessDTO = new ApplicationAccessDTO(); applicationAccessDTO.setPublicAccess(true); @@ -542,14 +548,14 @@ public class ApplicationServiceTest { Mono datasourceMono = publicAppMono .then(datasourceService.findById(savedDatasource.getId())); - Mono actionMono = publicAppMono - .then(actionService.findById(savedAction.getId())); + Mono actionMono = publicAppMono + .then(newActionService.findById(savedAction.getId())); StepVerifier .create(Mono.zip(datasourceMono, actionMono)) .assertNext(tuple -> { Datasource datasource1 = tuple.getT1(); - Action action1 = tuple.getT2(); + NewAction action1 = tuple.getT2(); // Check that the datasource used in the app contains public execute permission assertThat(datasource1.getPolicies()).containsAll(Set.of(manageDatasourcePolicy, readDatasourcePolicy, executeDatasourcePolicy)); @@ -581,9 +587,9 @@ public class ApplicationServiceTest { .users(Set.of("api_user")) .build(); - Mono> pageListMono = applicationMono + Mono> pageListMono = applicationMono .flatMapMany(application -> Flux.fromIterable(application.getPages())) - .flatMap(applicationPage -> pageRepository.findById(applicationPage.getId())) + .flatMap(applicationPage -> newPageService.findPageById(applicationPage.getId(), READ_PAGES, false)) .collectList(); Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue()) @@ -597,7 +603,7 @@ public class ApplicationServiceTest { .create(Mono.zip(applicationMono, pageListMono)) .assertNext(tuple -> { Application application = tuple.getT1(); - List pageList = tuple.getT2(); + List pageList = tuple.getT2(); assertThat(application).isNotNull(); assertThat(application.isAppIsExample()).isFalse(); assertThat(application.getId()).isNotNull(); @@ -606,7 +612,7 @@ public class ApplicationServiceTest { assertThat(application.getOrganizationId().equals(orgId)); assertThat(pageList).isNotEmpty(); - for (Page page : pageList) { + for (PageDTO page : pageList) { assertThat(page.getPolicies()).containsAll(Set.of(managePagePolicy, readPagePolicy)); assertThat(page.getApplicationId()).isEqualTo(application.getId()); } @@ -615,19 +621,19 @@ public class ApplicationServiceTest { // verify that Pages are cloned - Mono> testPageListMono = testApplicationMono + Mono> testPageListMono = testApplicationMono .flatMapMany(application -> Flux.fromIterable(application.getPages())) - .flatMap(applicationPage -> pageRepository.findById(applicationPage.getId())) + .flatMap(applicationPage -> newPageRepository.findById(applicationPage.getId())) .collectList(); Mono> pageIdListMono = pageListMono .flatMapMany(Flux::fromIterable) - .map(Page::getId) + .map(PageDTO::getId) .collectList(); Mono> testPageIdListMono = testPageListMono .flatMapMany(Flux::fromIterable) - .map(Page::getId) + .map(NewPage::getId) .collectList(); StepVerifier @@ -644,12 +650,12 @@ public class ApplicationServiceTest { Mono> pageNameListMono = pageListMono .flatMapMany(Flux::fromIterable) - .map(Page::getName) + .map(PageDTO::getName) .collectList(); Mono> testPageNameListMono = testPageListMono .flatMapMany(Flux::fromIterable) - .map(Page::getName) + .map(newPage -> newPage.getUnpublishedPage().getName()) .collectList(); StepVerifier @@ -663,4 +669,184 @@ public class ApplicationServiceTest { .verifyComplete(); } + @Test + @WithUserDetails(value = "api_user") + public void basicPublishApplicationTest() { + Application testApplication = new Application(); + String appName = "ApplicationServiceTest Publish Application"; + testApplication.setName(appName); + Mono applicationMono = applicationPageService.createApplication(testApplication, orgId) + .flatMap(application -> applicationPageService.publish(application.getId())) + .then(applicationService.findByName(appName, MANAGE_APPLICATIONS)) + .cache(); + + Mono> applicationPagesMono = applicationMono + .map(application -> application.getPages()) + .flatMapMany(Flux::fromIterable) + .flatMap(applicationPage -> newPageService.findById(applicationPage.getId(), READ_PAGES)) + .collectList(); + + StepVerifier + .create(Mono.zip(applicationMono, applicationPagesMono)) + .assertNext(tuple -> { + Application application = tuple.getT1(); + List pages = tuple.getT2(); + + assertThat(application).isNotNull(); + assertThat(application.isAppIsExample()).isFalse(); + assertThat(application.getId()).isNotNull(); + assertThat(application.getName().equals(appName)); + assertThat(application.getPages().size()).isEqualTo(1); + assertThat(application.getPublishedPages().size()).isEqualTo(1); + + assertThat(pages.size()).isEqualTo(1); + NewPage newPage = pages.get(0); + assertThat(newPage.getUnpublishedPage().getName()).isEqualTo(newPage.getPublishedPage().getName()); + assertThat(newPage.getUnpublishedPage().getLayouts().get(0).getId()).isEqualTo(newPage.getPublishedPage().getLayouts().get(0).getId()); + assertThat(newPage.getUnpublishedPage().getLayouts().get(0).getDsl()).isEqualTo(newPage.getPublishedPage().getLayouts().get(0).getDsl()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void deleteUnpublishedPageFromApplication() { + Application testApplication = new Application(); + String appName = "ApplicationServiceTest Publish Application Delete Page"; + testApplication.setName(appName); + Mono applicationMono = applicationPageService.createApplication(testApplication, orgId) + .flatMap(application -> { + PageDTO page = new PageDTO(); + page.setName("New Page"); + page.setApplicationId(application.getId()); + Layout defaultLayout = newPageService.createDefaultLayout(); + List layouts = new ArrayList<>(); + layouts.add(defaultLayout); + page.setLayouts(layouts); + return applicationPageService.createPage(page); + }) + .flatMap(page -> applicationPageService.publish(page.getApplicationId())) + .then(applicationService.findByName(appName, MANAGE_APPLICATIONS)) + .cache(); + + PageDTO newPage = applicationMono + .flatMap(application -> newPageService + .findByNameAndApplicationIdAndViewMode("New Page", application.getId(), READ_PAGES, false) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "page"))) + .flatMap(page -> applicationPageService.deleteUnpublishedPage(page.getId()))).block(); + + ApplicationPage applicationPage = new ApplicationPage(); + applicationPage.setId(newPage.getId()); + applicationPage.setIsDefault(false); + + StepVerifier + .create(applicationService.findById(newPage.getApplicationId(), MANAGE_APPLICATIONS)) + .assertNext(editedApplication -> { + + List publishedPages = editedApplication.getPublishedPages(); + assertThat(publishedPages).size().isEqualTo(2); + assertThat(publishedPages).containsAnyOf(applicationPage); + + List editedApplicationPages = editedApplication.getPages(); + assertThat(editedApplicationPages.size()).isEqualTo(1); + assertThat(editedApplicationPages).doesNotContain(applicationPage); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void changeDefaultPageForAPublishedApplication() { + Application testApplication = new Application(); + String appName = "ApplicationServiceTest Publish Application Change Default Page"; + testApplication.setName(appName); + Mono applicationMono = applicationPageService.createApplication(testApplication, orgId) + .flatMap(application -> { + PageDTO page = new PageDTO(); + page.setName("New Page"); + page.setApplicationId(application.getId()); + Layout defaultLayout = newPageService.createDefaultLayout(); + List layouts = new ArrayList<>(); + layouts.add(defaultLayout); + page.setLayouts(layouts); + return applicationPageService.createPage(page); + }) + .flatMap(page -> applicationPageService.publish(page.getApplicationId())) + .then(applicationService.findByName(appName, MANAGE_APPLICATIONS)) + .cache(); + + PageDTO newPage = applicationMono + .flatMap(application -> newPageService + .findByNameAndApplicationIdAndViewMode("New Page", application.getId(), READ_PAGES, false) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "unpublishedEditedPage")))).block(); + + Mono updatedDefaultPageApplicationMono = applicationMono + .flatMap(application -> applicationPageService.makePageDefault(application.getId(), newPage.getId())); + + ApplicationPage unpublishedEditedPage = new ApplicationPage(); + unpublishedEditedPage.setId(newPage.getId()); + unpublishedEditedPage.setIsDefault(true); + + ApplicationPage publishedEditedPage = new ApplicationPage(); + publishedEditedPage.setId(newPage.getId()); + publishedEditedPage.setIsDefault(false); + + StepVerifier + .create(updatedDefaultPageApplicationMono) + .assertNext(editedApplication -> { + + List publishedPages = editedApplication.getPublishedPages(); + assertThat(publishedPages).size().isEqualTo(2); + assertThat(publishedPages).containsAnyOf(publishedEditedPage); + + List editedApplicationPages = editedApplication.getPages(); + assertThat(editedApplicationPages.size()).isEqualTo(2); + assertThat(editedApplicationPages).containsAnyOf(unpublishedEditedPage); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void getApplicationInViewMode() { + Application testApplication = new Application(); + String appName = "ApplicationServiceTest Get Application In View Mode"; + testApplication.setName(appName); + Mono applicationMono = applicationPageService.createApplication(testApplication, orgId) + .flatMap(application -> { + PageDTO page = new PageDTO(); + page.setName("New Page"); + page.setApplicationId(application.getId()); + Layout defaultLayout = newPageService.createDefaultLayout(); + List layouts = new ArrayList<>(); + layouts.add(defaultLayout); + page.setLayouts(layouts); + return applicationPageService.createPage(page); + }) + .flatMap(page -> applicationPageService.publish(page.getApplicationId())) + .then(applicationService.findByName(appName, MANAGE_APPLICATIONS)) + .cache(); + + PageDTO newPage = applicationMono + .flatMap(application -> newPageService + .findByNameAndApplicationIdAndViewMode("New Page", application.getId(), READ_PAGES, false) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "page"))) + .flatMap(page -> applicationPageService.deleteUnpublishedPage(page.getId()))).block(); + + Mono viewModeApplicationMono = applicationMono + .flatMap(application -> applicationService.getApplicationInViewMode(application.getId())); + + ApplicationPage applicationPage = new ApplicationPage(); + applicationPage.setId(newPage.getId()); + applicationPage.setIsDefault(false); + + StepVerifier + .create(viewModeApplicationMono) + .assertNext(viewApplication -> { + List editedApplicationPages = viewApplication.getPages(); + assertThat(editedApplicationPages.size()).isEqualTo(2); + assertThat(editedApplicationPages).containsAnyOf(applicationPage); + }) + .verifyComplete(); + } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CurlImporterServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CurlImporterServiceTest.java index 1e8e77967c..3e3443232a 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CurlImporterServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CurlImporterServiceTest.java @@ -4,10 +4,10 @@ import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.Property; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.acl.AclPermission; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; -import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithException; import lombok.extern.slf4j.Slf4j; import org.junit.Before; @@ -47,7 +47,7 @@ public class CurlImporterServiceTest { ApplicationPageService applicationPageService; @Autowired - PageService pageService; + NewPageService newPageService; @Autowired UserService userService; @@ -110,10 +110,10 @@ public class CurlImporterServiceTest { app.setName("curlTest App"); Application application = applicationPageService.createApplication(app, orgId).block(); - Page page = pageService.findById(application.getPages().get(0).getId(), AclPermission.MANAGE_PAGES).block(); + PageDTO page = newPageService.findPageById(application.getPages().get(0).getId(), AclPermission.MANAGE_PAGES, false).block(); String command = "curl -X GET http://localhost:8080/api/v1/actions?name=something -H 'Accept: */*' -H 'Accept-Encoding: gzip, deflate' -H 'Authorization: Basic YXBpX3VzZXI6OHVBQDsmbUI6Y252Tn57Iw==' -H 'Cache-Control: no-cache' -H 'Connection: keep-alive' -H 'Content-Type: application/json' -H 'Cookie: SESSION=97c5def4-4f72-45aa-96fe-e8a9f5ade0b5,SESSION=97c5def4-4f72-45aa-96fe-e8a9f5ade0b5; SESSION=' -H 'Host: localhost:8080' -H 'Postman-Token: 16e4b6bc-2c7a-4ab1-a127-bca382dfc0f0,a6655daa-db07-4c5e-aca3-3fd505bd230d' -H 'User-Agent: PostmanRuntime/7.20.1' -H 'cache-control: no-cache' -d '{someJson}'"; - Mono action = curlImporterService.importAction(command, page.getId(), "actionName", orgId); + Mono action = curlImporterService.importAction(command, page.getId(), "actionName", orgId); StepVerifier .create(action) .assertNext(action1 -> { @@ -133,7 +133,7 @@ public class CurlImporterServiceTest { @Test public void urlInSingleQuotes() throws AppsmithException { String command = "curl --location --request POST 'http://localhost:8080/scrap/api?slugifiedName=Freshdesk&ownerName=volodimir.kudriachenko'"; - Action action = curlImporterService.curlToAction(command); + ActionDTO action = curlImporterService.curlToAction(command); assertThat(action).isNotNull(); assertThat(action.getDatasource()).isNotNull(); @@ -151,7 +151,7 @@ public class CurlImporterServiceTest { @Test public void missingMethod() throws AppsmithException { String command = "curl http://localhost:8080/scrap/api"; - Action action = curlImporterService.curlToAction(command); + ActionDTO action = curlImporterService.curlToAction(command); assertThat(action).isNotNull(); assertThat(action.getDatasource()).isNotNull(); @@ -172,7 +172,7 @@ public class CurlImporterServiceTest { " -H \"Content-Type: application/json\" \\\n" + " \"http://piper.net\""; - Action action = curlImporterService.curlToAction(command); + ActionDTO action = curlImporterService.curlToAction(command); assertThat(action).isNotNull(); assertThat(action.getDatasource()).isNotNull(); @@ -189,7 +189,7 @@ public class CurlImporterServiceTest { @Test public void testUrlEncodedData() throws AppsmithException { - Action action = curlImporterService.curlToAction( + ActionDTO action = curlImporterService.curlToAction( "curl --data-urlencode '=all of this exactly, but url encoded ' http://loc" ); assertMethod(action, HttpMethod.POST); @@ -225,7 +225,7 @@ public class CurlImporterServiceTest { @Test public void chromeCurlCommands1() throws AppsmithException { - Action action = curlImporterService.curlToAction( + ActionDTO action = curlImporterService.curlToAction( "curl 'http://localhost:3000/applications/5ea054c531cc0f7a61af0cbe/pages/5ea054c531cc0f7a61af0cc0/edit/api' \\\n" + " -H 'Connection: keep-alive' \\\n" + " -H 'Cache-Control: max-age=0' \\\n" + @@ -290,7 +290,7 @@ public class CurlImporterServiceTest { @Test public void firefoxCurlCommands1() throws AppsmithException { - final Action action = curlImporterService.curlToAction("curl 'http://localhost:8080/api/v1/actions?applicationId=5ea054c531cc0f7a61af0cbe' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:75.0) Gecko/20100101 Firefox/75.0' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Origin: http://localhost:3000' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Referer: http://localhost:3000/' -H 'Cookie: SESSION=69b4b392-03b6-4e0a-a889-49ca4b8e267e'"); + final ActionDTO action = curlImporterService.curlToAction("curl 'http://localhost:8080/api/v1/actions?applicationId=5ea054c531cc0f7a61af0cbe' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:75.0) Gecko/20100101 Firefox/75.0' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Origin: http://localhost:3000' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Referer: http://localhost:3000/' -H 'Cookie: SESSION=69b4b392-03b6-4e0a-a889-49ca4b8e267e'"); assertMethod(action, HttpMethod.GET); assertUrl(action, "http://localhost:8080"); assertPath(action, "/api/v1/actions"); @@ -309,7 +309,7 @@ public class CurlImporterServiceTest { @Test public void postmanExportCommands1() throws AppsmithException { - final Action action = curlImporterService.curlToAction( + final ActionDTO action = curlImporterService.curlToAction( "curl --location --request PUT 'https://release-api.appsmith.com/api/v1/users/5d81feb218e1c8217d20e13f' \\\n" + "--header 'Content-Type: application/json' \\\n" + "--header 'Authorization: Basic abcdefghijklmnop==' \\\n" + @@ -333,7 +333,7 @@ public class CurlImporterServiceTest { @Test public void postmanCreateDatasource() throws AppsmithException { - final Action action = curlImporterService.curlToAction( + final ActionDTO action = curlImporterService.curlToAction( "curl --location --request POST 'https://release-api.appsmith.com/api/v1/datasources' \\\n" + "--header 'Content-Type: application/json' \\\n" + "--header 'Cookie: SESSION=61ee9df5-3cab-400c-831b-9533218d8f9f' \\\n" + @@ -375,7 +375,7 @@ public class CurlImporterServiceTest { @Test public void postmanCreateProvider() throws AppsmithException { - final Action action = curlImporterService.curlToAction( + final ActionDTO action = curlImporterService.curlToAction( "curl --location --request POST 'https://release-api.appsmith.com/api/v1/providers' \\\n" + "--header 'Cookie: SESSION=61ee9df5-3cab-400c-831b-9533218d8f9f' \\\n" + "--header 'Content-Type: application/json' \\\n" + @@ -445,7 +445,7 @@ public class CurlImporterServiceTest { public void parseCurlJsTestsPart1() throws AppsmithException { // Tests adapted from . - Action action = curlImporterService.curlToAction("curl http://api.sloths.com"); + ActionDTO action = curlImporterService.curlToAction("curl http://api.sloths.com"); assertMethod(action, HttpMethod.GET); assertUrl(action, "http://api.sloths.com"); assertEmptyHeaders(action); @@ -484,7 +484,7 @@ public class CurlImporterServiceTest { @Test public void parseCurlJsTestsPart2() throws AppsmithException { - Action action = curlImporterService.curlToAction("curl -d \"foo=bar\" https://api.sloths.com"); + ActionDTO action = curlImporterService.curlToAction("curl -d \"foo=bar\" https://api.sloths.com"); assertMethod(action, HttpMethod.POST); assertUrl(action, "https://api.sloths.com"); assertEmptyPath(action); @@ -499,7 +499,7 @@ public class CurlImporterServiceTest { assertMethod(action, HttpMethod.POST); assertUrl(action, "https://api.sloths.com"); assertEmptyPath(action); - assertHeaders(action, new Property("Content-Type", "application/x-www-form-urlencoded")); + assertHeaders(action, new Property("Content-Type", "application/x-www-form-urlencoded")); assertEmptyBody(action); assertBodyFormData( action, @@ -561,7 +561,7 @@ public class CurlImporterServiceTest { @Test public void parseWithoutProtocol() throws AppsmithException { - Action action = curlImporterService.curlToAction("curl api.sloths.com"); + ActionDTO action = curlImporterService.curlToAction("curl api.sloths.com"); assertMethod(action, HttpMethod.GET); assertUrl(action, "http://api.sloths.com"); assertEmptyPath(action); @@ -571,7 +571,7 @@ public class CurlImporterServiceTest { @Test public void parseWithDashedUrlArgument() throws AppsmithException { - Action action = curlImporterService.curlToAction("curl --url http://api.sloths.com"); + ActionDTO action = curlImporterService.curlToAction("curl --url http://api.sloths.com"); assertMethod(action, HttpMethod.GET); assertUrl(action, "http://api.sloths.com"); assertEmptyPath(action); @@ -581,7 +581,7 @@ public class CurlImporterServiceTest { @Test public void parseWithDashedUrlArgument2() throws AppsmithException { - Action action = curlImporterService.curlToAction("curl -X POST -d '{\"name\":\"test\",\"salary\":\"123\",\"age\":\"23\"}' --url http://dummy.restapiexample.com/api/v1/create"); + ActionDTO action = curlImporterService.curlToAction("curl -X POST -d '{\"name\":\"test\",\"salary\":\"123\",\"age\":\"23\"}' --url http://dummy.restapiexample.com/api/v1/create"); assertMethod(action, HttpMethod.POST); assertUrl(action, "http://dummy.restapiexample.com"); assertPath(action, "/api/v1/create"); @@ -595,7 +595,7 @@ public class CurlImporterServiceTest { @Test public void parseWithJson() throws AppsmithException { - Action action = curlImporterService.curlToAction("curl -X POST -H'Content-Type: application/json' -d '{\"name\":\"test\",\"salary\":\"123\",\"age\":\"23\"}' --url http://dummy.restapiexample.com/api/v1/create"); + ActionDTO action = curlImporterService.curlToAction("curl -X POST -H'Content-Type: application/json' -d '{\"name\":\"test\",\"salary\":\"123\",\"age\":\"23\"}' --url http://dummy.restapiexample.com/api/v1/create"); assertMethod(action, HttpMethod.POST); assertUrl(action, "http://dummy.restapiexample.com"); assertPath(action, "/api/v1/create"); @@ -605,7 +605,7 @@ public class CurlImporterServiceTest { @Test public void parseWithSpacedHeader() throws AppsmithException { - Action action = curlImporterService.curlToAction("curl -H \"Accept:application/json\" http://httpbin.org/get"); + ActionDTO action = curlImporterService.curlToAction("curl -H \"Accept:application/json\" http://httpbin.org/get"); assertMethod(action, HttpMethod.GET); assertUrl(action, "http://httpbin.org"); assertPath(action, "/get"); @@ -615,7 +615,7 @@ public class CurlImporterServiceTest { @Test public void parseCurlCommand1() throws AppsmithException { - Action action = curlImporterService.curlToAction("curl -i -H \"Accept: application/json\" -H \"Content-Type: application/json\" -X POST -d '{\"name\":\"test\",\"salary\":\"123\",\"age\":\"23\"}' --url http://dummy.restapiexample.com/api/v1/create"); + ActionDTO action = curlImporterService.curlToAction("curl -i -H \"Accept: application/json\" -H \"Content-Type: application/json\" -X POST -d '{\"name\":\"test\",\"salary\":\"123\",\"age\":\"23\"}' --url http://dummy.restapiexample.com/api/v1/create"); assertMethod(action, HttpMethod.POST); assertUrl(action, "http://dummy.restapiexample.com"); assertPath(action, "/api/v1/create"); @@ -625,7 +625,7 @@ public class CurlImporterServiceTest { @Test public void parseMultipleData() throws AppsmithException { - Action action = curlImporterService.curlToAction("curl https://api.stripe.com/v1/refunds -d payment_intent=pi_Aabcxyz01aDfoo -d amount=1000"); + ActionDTO action = curlImporterService.curlToAction("curl https://api.stripe.com/v1/refunds -d payment_intent=pi_Aabcxyz01aDfoo -d amount=1000"); assertMethod(action, HttpMethod.POST); assertUrl(action, "https://api.stripe.com"); assertPath(action, "/v1/refunds"); @@ -643,7 +643,7 @@ public class CurlImporterServiceTest { public void importInvalidCurlCommand() { String command = "invalid curl command here"; - Mono actionMono = curlImporterService.importAction(command, "pageId", "actionName", orgId); + Mono actionMono = curlImporterService.importAction(command, "pageId", "actionName", orgId); StepVerifier .create(actionMono) @@ -651,43 +651,43 @@ public class CurlImporterServiceTest { } // Assertion utilities for working with Action assertions. - private static void assertMethod(Action action, HttpMethod method) { + private static void assertMethod(ActionDTO action, HttpMethod method) { assertThat(action.getActionConfiguration().getHttpMethod()).isEqualByComparingTo(method); } - private static void assertUrl(Action action, String url) { + private static void assertUrl(ActionDTO action, String url) { assertThat(action.getDatasource().getDatasourceConfiguration().getUrl()).isEqualTo(url); } - private static void assertEmptyPath(Action action) { + private static void assertEmptyPath(ActionDTO action) { assertThat(action.getActionConfiguration().getPath()).isNullOrEmpty(); } - private static void assertPath(Action action, String path) { + private static void assertPath(ActionDTO action, String path) { assertThat(action.getActionConfiguration().getPath()).isEqualTo(path); } - private static void assertQueryParams(Action action, Property... params) { + private static void assertQueryParams(ActionDTO action, Property... params) { assertThat(action.getActionConfiguration().getQueryParameters()).containsExactly(params); } - private static void assertEmptyHeaders(Action action) { + private static void assertEmptyHeaders(ActionDTO action) { assertThat(action.getActionConfiguration().getHeaders()).isNullOrEmpty(); } - private static void assertHeaders(Action action, Property... headers) { + private static void assertHeaders(ActionDTO action, Property... headers) { assertThat(action.getActionConfiguration().getHeaders()).containsExactlyInAnyOrder(headers); } - private static void assertEmptyBody(Action action) { + private static void assertEmptyBody(ActionDTO action) { assertThat(action.getActionConfiguration().getBody()).isNullOrEmpty(); } - private static void assertBody(Action action, String body) { + private static void assertBody(ActionDTO action, String body) { assertThat(action.getActionConfiguration().getBody()).isEqualTo(body); } - private static void assertBodyFormData(Action action, Property... params) { + private static void assertBodyFormData(ActionDTO action, Property... params) { assertThat(action.getActionConfiguration().getBodyFormData()).containsExactly(params); } 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 7ecb196982..517ad8962b 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 @@ -11,12 +11,12 @@ import com.appsmith.external.models.SSLDetails; import com.appsmith.external.models.UploadedFile; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Organization; -import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; +import com.appsmith.server.dtos.ActionDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.MockPluginExecutor; @@ -63,7 +63,7 @@ public class DatasourceServiceTest { OrganizationRepository organizationRepository; @Autowired - ActionService actionService; + NewActionService newActionService; @Autowired ApplicationPageService applicationPageService; @@ -385,7 +385,7 @@ public class DatasourceServiceTest { datasourceService.create(datasource), applicationPageService.createApplication(application, organization.getId()) .flatMap(application1 -> { - final Page page = new Page(); + final PageDTO page = new PageDTO(); page.setName("test page 1"); page.setApplicationId(application1.getId()); page.setPolicies(Set.of(Policy.builder() @@ -399,9 +399,9 @@ public class DatasourceServiceTest { }) .flatMap(objects -> { final Datasource datasource = objects.getT3(); - final Page page = objects.getT4(); + final PageDTO page = objects.getT4(); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("validAction"); action.setOrganizationId(objects.getT1().getId()); action.setPluginId(objects.getT2().getId()); @@ -411,7 +411,7 @@ public class DatasourceServiceTest { action.setActionConfiguration(actionConfiguration); action.setDatasource(datasource); - return actionService.create(action).thenReturn(datasource); + return newActionService.createAction(action).thenReturn(datasource); }) .flatMap(datasource -> datasourceService.delete(datasource.getId())); @@ -551,10 +551,7 @@ public class DatasourceServiceTest { 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:`." - ); + assertThat(createdDatasource.getInvalids()).isEmpty(); Policy manageDatasourcePolicy = Policy.builder().permission(MANAGE_DATASOURCES.getValue()) .users(Set.of("api_user")) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutActionServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutActionServiceTest.java index ae0c7a8f2e..4322b9c2e7 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutActionServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutActionServiceTest.java @@ -2,14 +2,17 @@ package com.appsmith.server.services; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.server.acl.AclPermission; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewAction; import com.appsmith.server.domains.Organization; -import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; +import com.appsmith.server.dtos.DslActionDTO; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.dtos.RefactorNameDTO; import com.appsmith.server.helpers.MockPluginExecutor; import com.appsmith.server.helpers.PluginExecutorHelper; import com.appsmith.server.repositories.OrganizationRepository; @@ -34,6 +37,7 @@ import reactor.test.StepVerifier; import java.util.Map; import java.util.UUID; +import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; import static com.appsmith.server.acl.AclPermission.READ_PAGES; import static org.assertj.core.api.Assertions.assertThat; @@ -43,14 +47,11 @@ import static org.assertj.core.api.Assertions.assertThat; @DirtiesContext public class LayoutActionServiceTest { @Autowired - ActionService actionService; + NewActionService newActionService; @Autowired ApplicationPageService applicationPageService; - @Autowired - PageService pageService; - @Autowired UserService userService; @@ -72,9 +73,12 @@ public class LayoutActionServiceTest { @Autowired LayoutService layoutService; + @Autowired + NewPageService newPageService; + Application testApp = null; - Page testPage = null; + PageDTO testPage = null; Datasource datasource; @@ -94,7 +98,7 @@ public class LayoutActionServiceTest { final String pageId = testApp.getPages().get(0).getId(); - testPage = pageService.getById(pageId).block(); + testPage = newPageService.findPageById(pageId, READ_PAGES, false).block(); Layout layout = testPage.getLayouts().get(0); JSONObject dsl = new JSONObject(Map.of("text", "{{ query1.data }}")); @@ -102,7 +106,7 @@ public class LayoutActionServiceTest { layout.setPublishedDsl(dsl); layoutActionService.updateLayout(pageId, layout.getId(), layout).block(); - testPage = pageService.getById(pageId).block(); + testPage = newPageService.findPageById(pageId, READ_PAGES, false).block(); } Organization testOrg = organizationRepository.findByName("Another Test Organization", AclPermission.READ_ORGANIZATIONS).block(); @@ -128,7 +132,7 @@ public class LayoutActionServiceTest { public void updateActionUpdatesLayout() { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - Action action = new Action(); + ActionDTO action = new ActionDTO(); action.setName("query1"); action.setPageId(testPage.getId()); ActionConfiguration actionConfiguration = new ActionConfiguration(); @@ -136,16 +140,17 @@ public class LayoutActionServiceTest { action.setActionConfiguration(actionConfiguration); action.setDatasource(datasource); - Mono resultMono = actionService - .create(action) + Mono resultMono = newActionService + .createAction(action) .flatMap(savedAction -> { - Action updates = new Action(); + ActionDTO updates = new ActionDTO(); updates.setExecuteOnLoad(true); updates.setPolicies(null); updates.setUserPermissions(null); return layoutActionService.updateAction(savedAction.getId(), updates); }) - .flatMap(savedAction -> pageService.findById(testPage.getId(), READ_PAGES)); + // fetch the unpublished page + .flatMap(savedAction -> newPageService.findPageById(testPage.getId(), READ_PAGES, false)); StepVerifier .create(resultMono) @@ -156,4 +161,53 @@ public class LayoutActionServiceTest { }) .verifyComplete(); } + + @Test + @WithUserDetails(value = "api_user") + public void refactorActionName() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + + ActionDTO action = new ActionDTO(); + action.setName("beforeNameChange"); + action.setPageId(testPage.getId()); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(datasource); + + JSONObject dsl = new JSONObject(Map.of("widgetName", "firstWidget", "mustacheProp", "{{ beforeNameChange.data }}")); + Layout layout = testPage.getLayouts().get(0); + layout.setDsl(dsl); + layout.setPublishedDsl(dsl); + + ActionDTO createdAction = newActionService.createAction(action).block(); + + Layout firstLayout = layoutActionService.updateLayout(testPage.getId(), layout.getId(), layout).block(); + + + RefactorNameDTO refactorNameDTO = new RefactorNameDTO(); + refactorNameDTO.setPageId(testPage.getId()); + refactorNameDTO.setLayoutId(firstLayout.getId()); + refactorNameDTO.setOldName("beforeNameChange"); + refactorNameDTO.setNewName("PostNameChange"); + + Layout postNameChangeLayout = layoutActionService.refactorActionName(refactorNameDTO).block(); + + Mono postNameChangeActionMono = newActionService.findById(createdAction.getId(), READ_ACTIONS); + + StepVerifier + .create(postNameChangeActionMono) + .assertNext(updatedAction -> { + + assertThat(updatedAction.getUnpublishedAction().getName()).isEqualTo("PostNameChange"); + + DslActionDTO actionDTO = postNameChangeLayout.getLayoutOnLoadActions().get(0).iterator().next(); + assertThat(actionDTO.getName()).isEqualTo("PostNameChange"); + + JSONObject newDsl = new JSONObject(Map.of("widgetName", "firstWidget", "mustacheProp", "{{ PostNameChange.data }}")); + assertThat(postNameChangeLayout.getDsl()).isEqualTo(newDsl); + }) + .verifyComplete(); + + } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutServiceTest.java index 03b26fd91f..a0d7a323c2 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutServiceTest.java @@ -4,14 +4,14 @@ import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.constants.FieldName; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Layout; -import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.DslActionDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.MockPluginExecutor; @@ -69,11 +69,14 @@ public class LayoutServiceTest { OrganizationService organizationService; @Autowired - ActionService actionService; + NewActionService newActionService; @Autowired PluginRepository pluginRepository; + @Autowired + NewPageService newPageService; + @MockBean PluginExecutorHelper pluginExecutorHelper; @@ -102,7 +105,7 @@ public class LayoutServiceTest { } private void purgeAllPages() { - pageService.deleteAll(); + newPageService.deleteAll(); } @Test @@ -133,14 +136,14 @@ public class LayoutServiceTest { @Test @WithUserDetails(value = "api_user") public void createValidLayout() { - Page testPage = new Page(); + PageDTO testPage = new PageDTO(); testPage.setName("createLayoutPageName"); Application application = new Application(); application.setName("createValidLayout-Test-Application"); Mono applicationMono = applicationPageService.createApplication(application, orgId); - Mono pageMono = applicationMono + Mono pageMono = applicationMono .switchIfEmpty(Mono.error(new Exception("No application found"))) .map(app -> { testPage.setApplicationId(app.getId()); @@ -166,16 +169,15 @@ public class LayoutServiceTest { .verifyComplete(); } - private Mono createPage(Application app, Page page) { - Mono pageMono = pageService - .findByName(page.getName(), AclPermission.READ_PAGES) + private Mono createPage(Application app, PageDTO page) { + return newPageService + .findByNameAndViewMode(page.getName(), AclPermission.READ_PAGES, false) .switchIfEmpty(applicationPageService.createApplication(app, orgId) .map(application -> { page.setApplicationId(application.getId()); return page; }) .flatMap(applicationPageService::createPage)); - return pageMono; } @Test @@ -186,7 +188,7 @@ public class LayoutServiceTest { obj.put("key", "value"); testLayout.setDsl(obj); - Page testPage = new Page(); + PageDTO testPage = new PageDTO(); testPage.setName("LayoutServiceTest updateLayoutInvalidPageId"); Layout updateLayout = new Layout(); @@ -196,7 +198,7 @@ public class LayoutServiceTest { Application app = new Application(); app.setName("newApplication-updateLayoutInvalidPageId-Test"); - Page page = createPage(app, testPage).block(); + PageDTO page = createPage(app, testPage).block(); Layout startLayout = layoutService.createLayout(page.getId(), testLayout).block(); @@ -223,19 +225,19 @@ public class LayoutServiceTest { obj1.put("key1", "value-updated"); updateLayout.setDsl(obj); - Page testPage = new Page(); + PageDTO testPage = new PageDTO(); testPage.setName("LayoutServiceTest updateLayoutValidPageId"); Application app = new Application(); - app.setName("newApplication-updateLayoutValidPageId-Test"); + app.setName("newApplication-updateLayoutValidPageId-TestApplication"); - Mono pageMono = createPage(app, testPage).cache(); + Mono pageMono = createPage(app, testPage).cache(); Mono startLayoutMono = pageMono.flatMap(page -> layoutService.createLayout(page.getId(), testLayout)); Mono updatedLayoutMono = Mono.zip(pageMono, startLayoutMono) .flatMap(tuple -> { - Page page = tuple.getT1(); + PageDTO page = tuple.getT1(); Layout startLayout = tuple.getT2(); return layoutActionService.updateLayout(page.getId(), startLayout.getId(), updateLayout); }); @@ -255,28 +257,35 @@ public class LayoutServiceTest { public void getActionsExecuteOnLoad() { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - Mono testMono = pageService - .findByName("validPageName", AclPermission.READ_PAGES) - .flatMap(page1 -> { - List> monos = new ArrayList<>(); + PageDTO testPage = new PageDTO(); + testPage.setName("ActionsExecuteOnLoad Test Page"); - Action action = new Action(); + Application app = new Application(); + app.setName("newApplication-updateLayoutValidPageId-Test"); + + Mono pageMono = createPage(app, testPage).cache(); + + Mono testMono = pageMono + .flatMap(page1 -> { + List> monos = new ArrayList<>(); + + ActionDTO action = new ActionDTO(); action.setName("aGetAction"); action.setActionConfiguration(new ActionConfiguration()); action.getActionConfiguration().setHttpMethod(HttpMethod.GET); action.setPageId(page1.getId()); action.setDatasource(datasource); - monos.add(actionService.create(action)); + monos.add(newActionService.createAction(action)); - action = new Action(); + action = new ActionDTO(); action.setName("aPostAction"); action.setActionConfiguration(new ActionConfiguration()); action.getActionConfiguration().setHttpMethod(HttpMethod.POST); action.setPageId(page1.getId()); action.setDatasource(datasource); - monos.add(actionService.create(action)); + monos.add(newActionService.createAction(action)); - action = new Action(); + action = new ActionDTO(); action.setName("aPostActionWithAutoExec"); action.setActionConfiguration(new ActionConfiguration()); action.getActionConfiguration().setHttpMethod(HttpMethod.POST); @@ -286,32 +295,32 @@ public class LayoutServiceTest { action.setPageId(page1.getId()); action.setExecuteOnLoad(true); action.setDatasource(datasource); - monos.add(actionService.create(action)); + monos.add(newActionService.createAction(action)); - action = new Action(); + action = new ActionDTO(); action.setName("aPostSecondaryAction"); action.setActionConfiguration(new ActionConfiguration()); action.getActionConfiguration().setHttpMethod(HttpMethod.POST); action.setPageId(page1.getId()); action.setDatasource(datasource); - monos.add(actionService.create(action)); + monos.add(newActionService.createAction(action)); - action = new Action(); + action = new ActionDTO(); action.setName("aPostTertiaryAction"); action.setActionConfiguration(new ActionConfiguration()); action.getActionConfiguration().setHttpMethod(HttpMethod.POST); action.setPageId(page1.getId()); action.setExecuteOnLoad(true); action.setDatasource(datasource); - monos.add(actionService.create(action)); + monos.add(newActionService.createAction(action)); - action = new Action(); + action = new ActionDTO(); action.setName("aDeleteAction"); action.setActionConfiguration(new ActionConfiguration()); action.getActionConfiguration().setHttpMethod(HttpMethod.DELETE); action.setPageId(page1.getId()); action.setDatasource(datasource); - monos.add(actionService.create(action)); + monos.add(newActionService.createAction(action)); return Mono.zip(monos, objects -> page1); }) @@ -326,7 +335,7 @@ public class LayoutServiceTest { return layoutService.createLayout(page1.getId(), layout); }) .flatMap(tuple2 -> { - final Page page1 = tuple2.getT1(); + final PageDTO page1 = tuple2.getT1(); final Layout layout = tuple2.getT2(); Layout newLayout = new Layout(); @@ -361,6 +370,6 @@ public class LayoutServiceTest { @After public void purgePages() { - pageService.deleteAll(); + newPageService.deleteAll(); } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java index 076f6c6b99..c8ecef61c7 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PageServiceTest.java @@ -1,12 +1,21 @@ package com.appsmith.server.services; +import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.Policy; +import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; -import com.appsmith.server.domains.Page; +import com.appsmith.server.domains.Datasource; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.Plugin; import com.appsmith.server.domains.User; +import com.appsmith.server.dtos.ActionDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.MockPluginExecutor; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.repositories.PluginRepository; import lombok.extern.slf4j.Slf4j; import net.minidev.json.parser.JSONParser; import net.minidev.json.parser.ParseException; @@ -14,18 +23,22 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.util.List; import java.util.Set; import java.util.UUID; import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; +import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; import static com.appsmith.server.acl.AclPermission.READ_PAGES; import static org.assertj.core.api.Assertions.assertThat; @@ -34,9 +47,6 @@ import static org.assertj.core.api.Assertions.assertThat; @Slf4j @DirtiesContext public class PageServiceTest { - @Autowired - PageService pageService; - @Autowired ApplicationPageService applicationPageService; @@ -49,10 +59,27 @@ public class PageServiceTest { @Autowired ApplicationService applicationService; + @Autowired + NewPageService newPageService; + + @Autowired + NewActionService newActionService; + + @Autowired + PluginRepository pluginRepository; + + @MockBean + PluginExecutorHelper pluginExecutorHelper; + + @MockBean + PluginExecutor pluginExecutor; + Application application = null; String applicationId = null; + String orgId; + @Before @WithUserDetails(value = "api_user") public void setup() { @@ -63,7 +90,7 @@ public class PageServiceTest { public void setupTestApplication() { if (application == null) { User apiUser = userService.findByEmail("api_user").block(); - String orgId = apiUser.getOrganizationIds().iterator().next(); + orgId = apiUser.getOrganizationIds().iterator().next(); Application newApp = new Application(); newApp.setName(UUID.randomUUID().toString()); @@ -75,8 +102,8 @@ public class PageServiceTest { @Test @WithUserDetails(value = "api_user") public void createPageWithNullName() { - Page page = new Page(); - Mono pageMono = Mono.just(page) + PageDTO page = new PageDTO(); + Mono pageMono = Mono.just(page) .flatMap(applicationPageService::createPage); StepVerifier .create(pageMono) @@ -88,9 +115,9 @@ public class PageServiceTest { @Test @WithUserDetails(value = "api_user") public void createPageWithNullApplication() { - Page page = new Page(); + PageDTO page = new PageDTO(); page.setName("Page without application"); - Mono pageMono = Mono.just(page) + Mono pageMono = Mono.just(page) .flatMap(applicationPageService::createPage); StepVerifier .create(pageMono) @@ -109,12 +136,12 @@ public class PageServiceTest { .users(Set.of("api_user")) .build(); - Page testPage = new Page(); + PageDTO testPage = new PageDTO(); testPage.setName("PageServiceTest TestApp"); setupTestApplication(); testPage.setApplicationId(application.getId()); - Mono pageMono = applicationPageService.createPage(testPage); + Mono pageMono = applicationPageService.createPage(testPage); Object parsedJson = new JSONParser(JSONParser.MODE_PERMISSIVE).parse(FieldName.DEFAULT_PAGE_LAYOUT); StepVerifier @@ -144,17 +171,17 @@ public class PageServiceTest { .users(Set.of("api_user")) .build(); - Page testPage = new Page(); + PageDTO testPage = new PageDTO(); testPage.setName("Before Page Name Change"); setupTestApplication(); testPage.setApplicationId(application.getId()); - Mono pageMono = applicationPageService.createPage(testPage) + Mono pageMono = applicationPageService.createPage(testPage) .flatMap(page -> { - Page newPage = new Page(); + PageDTO newPage = new PageDTO(); newPage.setId(page.getId()); newPage.setName("New Page Name"); - return pageService.update(page.getId(), newPage); + return newPageService.updatePage(page.getId(), newPage); }); StepVerifier @@ -175,6 +202,8 @@ public class PageServiceTest { @Test @WithUserDetails(value = "api_user") public void clonePage() throws ParseException { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + Policy managePagePolicy = Policy.builder().permission(MANAGE_PAGES.getValue()) .users(Set.of("api_user")) .build(); @@ -182,18 +211,41 @@ public class PageServiceTest { .users(Set.of("api_user")) .build(); - Page testPage = new Page(); + PageDTO testPage = new PageDTO(); testPage.setName("PageServiceTest CloneTest Source"); setupTestApplication(); testPage.setApplicationId(application.getId()); - Mono pageMono = applicationPageService.createPage(testPage) - .flatMap(page -> applicationPageService.clonePage(page.getId())); + ActionDTO action = new ActionDTO(); + action.setName("Page Action"); + action.setActionConfiguration(new ActionConfiguration()); + Datasource datasource = new Datasource(); + datasource.setOrganizationId(orgId); + datasource.setName("datasource test name for page test"); + Plugin installed_plugin = pluginRepository.findByPackageName("installed-plugin").block(); + datasource.setPluginId(installed_plugin.getId()); + action.setDatasource(datasource); + + Mono pageMono = applicationPageService.createPage(testPage) + .flatMap(page -> { + action.setPageId(page.getId()); + return newActionService.createAction(action) + .thenReturn(page); + }) + .flatMap(page -> applicationPageService.clonePage(page.getId())) + .cache(); + + + Mono> actionsMono = pageMono + // fetch the actions by new cloned page id. + .flatMapMany(page -> newActionService.findByPageId(page.getId(), READ_ACTIONS)) + .collectList(); Object parsedJson = new JSONParser(JSONParser.MODE_PERMISSIVE).parse(FieldName.DEFAULT_PAGE_LAYOUT); StepVerifier - .create(pageMono) - .assertNext(page -> { + .create(Mono.zip(pageMono, actionsMono)) + .assertNext(tuple -> { + PageDTO page = tuple.getT1(); assertThat(page).isNotNull(); assertThat(page.getId()).isNotNull(); assertThat("PageServiceTest CloneTest Source Copy".equals(page.getName())); @@ -205,6 +257,11 @@ public class PageServiceTest { assertThat(page.getLayouts().get(0).getDsl()).isEqualTo(parsedJson); assertThat(page.getLayouts().get(0).getWidgetNames()).isNotEmpty(); assertThat(page.getLayouts().get(0).getPublishedDsl()).isNullOrEmpty(); + + // Confirm that the page action got copied as well + List actions = tuple.getT2(); + assertThat(actions.size()).isEqualTo(1); + assertThat(actions.get(0).getUnpublishedAction().getName()).isEqualTo("Page Action"); }) .verifyComplete(); } @@ -212,7 +269,7 @@ public class PageServiceTest { @After public void purgeAllPages() { - pageService.deleteAll(); + newPageService.deleteAll(); } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java index c5e0ee3caf..46a8c1ea06 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java @@ -5,27 +5,29 @@ import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Property; import com.appsmith.server.constants.FieldName; -import com.appsmith.server.domains.Action; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationPage; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Organization; -import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; +import com.appsmith.server.dtos.ActionDTO; import com.appsmith.server.dtos.DslActionDTO; +import com.appsmith.server.dtos.PageDTO; import com.appsmith.server.helpers.MockPluginExecutor; import com.appsmith.server.helpers.PluginExecutorHelper; import com.appsmith.server.repositories.PluginRepository; import com.appsmith.server.services.ActionCollectionService; -import com.appsmith.server.services.ActionService; import com.appsmith.server.services.ApplicationPageService; import com.appsmith.server.services.ApplicationService; import com.appsmith.server.services.DatasourceService; import com.appsmith.server.services.EncryptionService; import com.appsmith.server.services.LayoutActionService; +import com.appsmith.server.services.NewActionService; +import com.appsmith.server.services.NewPageService; import com.appsmith.server.services.OrganizationService; -import com.appsmith.server.services.PageService; import com.appsmith.server.services.SessionUserService; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; @@ -87,10 +89,7 @@ public class ExamplesOrganizationClonerTests { private SessionUserService sessionUserService; @Autowired - private ActionService actionService; - - @Autowired - private PageService pageService; + private NewActionService newActionService; @Autowired private ActionCollectionService actionCollectionService; @@ -110,13 +109,16 @@ public class ExamplesOrganizationClonerTests { @Autowired private MongoTemplate mongoTemplate; + @Autowired + private NewPageService newPageService; + private Plugin installedPlugin; private static class OrganizationData { Organization organization; List applications = new ArrayList<>(); List datasources = new ArrayList<>(); - List actions = new ArrayList<>(); + List actions = new ArrayList<>(); } public Mono loadOrganizationData(Organization organization) { @@ -246,7 +248,7 @@ public class ExamplesOrganizationClonerTests { .zip( applicationPageService.createApplication(app1), applicationPageService.createApplication(app2).flatMap(application -> { - final Page newPage = new Page(); + final PageDTO newPage = new PageDTO(); newPage.setName("The New Page"); newPage.setApplicationId(application.getId()); return applicationPageService.createPage(newPage).thenReturn(application); @@ -521,7 +523,7 @@ public class ExamplesOrganizationClonerTests { final String pageId1 = app.getPages().get(0).getId(); final Datasource ds1Again = tuple1.getT3(); - final Page newPage = new Page(); + final PageDTO newPage = new PageDTO(); newPage.setName("A New Page"); newPage.setApplicationId(app.getId()); newPage.setLayouts(new ArrayList<>()); @@ -534,7 +536,7 @@ public class ExamplesOrganizationClonerTests { layout.setLayoutOnLoadActions(List.of(dslActionDTOS)); newPage.getLayouts().add(layout); - final Action newPageAction = new Action(); + final ActionDTO newPageAction = new ActionDTO(); newPageAction.setName("newPageAction"); newPageAction.setOrganizationId(organization.getId()); newPageAction.setDatasource(ds1Again); @@ -542,14 +544,14 @@ public class ExamplesOrganizationClonerTests { newPageAction.setActionConfiguration(new ActionConfiguration()); newPageAction.getActionConfiguration().setHttpMethod(HttpMethod.GET); - final Action action1 = new Action(); + final ActionDTO action1 = new ActionDTO(); action1.setName("action1"); action1.setPageId(pageId1); action1.setOrganizationId(organization.getId()); action1.setDatasource(ds1Again); action1.setPluginId(installedPlugin.getId()); - final Action action2 = new Action(); + final ActionDTO action2 = new ActionDTO(); action2.setPageId(pageId1); action2.setName("action2"); action2.setOrganizationId(organization.getId()); @@ -560,14 +562,14 @@ public class ExamplesOrganizationClonerTests { final String pageId2 = app2Again.getPages().get(0).getId(); final Datasource ds2Again = tuple1.getT4(); - final Action action3 = new Action(); + final ActionDTO action3 = new ActionDTO(); action3.setName("action3"); action3.setPageId(pageId2); action3.setOrganizationId(organization.getId()); action3.setDatasource(ds2Again); action3.setPluginId(installedPlugin.getId()); - final Action action4 = new Action(); + final ActionDTO action4 = new ActionDTO(); action4.setPageId(pageId2); action4.setName("action4"); action4.setOrganizationId(organization.getId()); @@ -581,7 +583,7 @@ public class ExamplesOrganizationClonerTests { return applicationPageService.addPageToApplication(app, page, false) .then(actionCollectionService.createAction(newPageAction)) .flatMap(savedAction -> layoutActionService.updateAction(savedAction.getId(), savedAction)) - .then(pageService.findById(page.getId(), READ_PAGES)); + .then(newPageService.findPageById(page.getId(), READ_PAGES, false)); }) .map(tuple2 -> { log.info("Created action and added page to app {}", tuple2); @@ -620,10 +622,11 @@ public class ExamplesOrganizationClonerTests { final Application firstApplication = data.applications.stream().filter(app -> app.getName().equals("first application")).findFirst().orElse(null); assert firstApplication != null; assertThat(firstApplication.getPages().stream().filter(ApplicationPage::isDefault).count()).isEqualTo(1); - final Page newPage = mongoTemplate.findOne(Query.query(Criteria.where("applicationId").is(firstApplication.getId()).and("name").is("A New Page")), Page.class); + final NewPage newPage = mongoTemplate.findOne(Query.query(Criteria.where("applicationId").is(firstApplication.getId()).and("unpublishedPage.name").is("A New Page")), NewPage.class); assert newPage != null; - final String actionId = newPage.getLayouts().get(0).getLayoutOnLoadActions().get(0).iterator().next().getId(); - final Action newPageAction = mongoTemplate.findOne(Query.query(Criteria.where("id").is(actionId)), Action.class); + log.debug("new page is : {}", newPage.toString()); + final String actionId = newPage.getUnpublishedPage().getLayouts().get(0).getLayoutOnLoadActions().get(0).iterator().next().getId(); + final NewAction newPageAction = mongoTemplate.findOne(Query.query(Criteria.where("id").is(actionId)), NewAction.class); assert newPageAction != null; assertThat(newPageAction.getOrganizationId()).isEqualTo(data.organization.getId()); @@ -634,7 +637,7 @@ public class ExamplesOrganizationClonerTests { ); assertThat(data.actions).hasSize(5); - assertThat(map(data.actions, Action::getName)).containsExactlyInAnyOrder( + assertThat(getUnpublishedActionName(data.actions)).containsExactlyInAnyOrder( "newPageAction", "action1", "action2", @@ -645,15 +648,24 @@ public class ExamplesOrganizationClonerTests { .verifyComplete(); } + private List getUnpublishedActionName(List actions) { + List names = new ArrayList<>(); + for (ActionDTO action : actions) { + names.add(action.getName()); + } + return names; + } + private List map(List list, Function fn) { return list.stream().map(fn).collect(Collectors.toList()); } - private Flux getActionsInOrganization(Organization organization) { + private Flux getActionsInOrganization(Organization organization) { return applicationService .findByOrganizationId(organization.getId(), READ_APPLICATIONS) - .flatMap(application -> pageService.findByApplicationId(application.getId(), READ_PAGES)) - .flatMap(page -> actionService.get(new LinkedMultiValueMap<>( + // fetch the unpublished pages + .flatMap(application -> newPageService.findByApplicationId(application.getId(), READ_PAGES, false)) + .flatMap(page -> newActionService.getUnpublishedActions(new LinkedMultiValueMap<>( Map.of(FieldName.PAGE_ID, Collections.singletonList(page.getId()))))); } } 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 index 552086caa2..685e3ab36a 100644 Binary files a/app/server/appsmith-server/src/test/resources/test_assets/OrganizationServiceTest/my_organization_logo.png and b/app/server/appsmith-server/src/test/resources/test_assets/OrganizationServiceTest/my_organization_logo.png differ diff --git a/contributions/ClientSetup.md b/contributions/ClientSetup.md index 91719f74d2..505967703b 100644 --- a/contributions/ClientSetup.md +++ b/contributions/ClientSetup.md @@ -7,7 +7,7 @@ On your development machine, please ensure that: 1. You have `docker` installed in your system. If not, please visit: [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/) 2. You have `mkcert` installed. Please visit: [https://github.com/FiloSottile/mkcert#installation](https://github.com/FiloSottile/mkcert#installation) for details. For `mkcert` to work with Firefox you may require the `nss` utility to be installed. Details are in the link above. -3. You have `envsubst` installed. use `brew install gettext` on macOS. Linux machines usually have this installed. +3. You have `envsubst` installed. use `brew install gettext` on MacOS. Linux machines usually have this installed. 4. You have cloned the repo in your local machine. ### Create local HTTPS certificates: @@ -27,8 +27,15 @@ This command will create 2 files in the `docker/` directory: ```bash echo "127.0.0.1 dev.appsmith.com" | sudo tee -a /etc/hosts ``` +Note: +- Please be careful when copying the above string as space between the ip and the string goes missing sometimes. +- Please check that the string has been copied properly by running +``` +cat /etc/hosts | grep appsmith +``` +3. Run cmd: `cp .env.example .env` -3. Run the script `start-https.sh` in order to start the nginx container that will proxy the frontend code on your local system. +4. Run the script `start-https.sh` in order to start the nginx container that will proxy the frontend code on your local system. ```bash cd app/client ./start-https.sh @@ -44,13 +51,21 @@ cd app/client ### Steps to build & run the code: 1. Run `yarn` + +Note: +- On Ubuntu Linux platform, please run the following cmd before step 2 below: +``` +echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p +``` 2. Run `yarn start` ๐ŸŽ‰ Your Appsmith client is now running on https://dev.appsmith.com. This URL must be opened with https and not have the port 3000 in it -Your client is pointing to the cloud staging server https://release-api.appsmith.com +#### Note: +- By default your client app points to the local api server - `http://host.docker.internal:8080` for MacOS or `http://localhost:8080` for Linux. Your page will load with errors if you don't have the api server running on your local system. To setup the api server on your local system please follow the instructions [here](https://github.com/appsmithorg/appsmith/blob/release/contributions/ServerSetup.md) +- In case you are unable to setup the api server on your local system, you can also use Appsmith's api server: https://release-api.appsmith.com/. How to make this change is described in [this section](#if-you-would-like-to-hit-a-different-appsmith-server). #### If yarn start throws mismatch node version error This error occurs because the node version is not compatible with the app environment. In this case Node version manager can be used which allows multiple @@ -59,11 +74,12 @@ node versions to be used in different projects. Check below for installation and 2. In the root of the project, run `nvm use 10.16.3` or `fnm use 10.16.3`. #### If you would like to hit a different Appsmith server: -- Change the API endpoint in the Nginx configuration files (`app/client/docker/templates/nginx-linux.conf.template` or `app/client/docker/templates/nginx-mac.conf.template`). +- Change the API endpoint in the Nginx configuration files (`app/client/docker/templates/nginx-mac.conf.template` on MacOS or `app/client/docker/templates/nginx-linux.conf.template` on Linux). By default it points to your local instance i.e. `http://host.docker.internal:8080` for MacOS or `http://localhost:8080` for Linux. You need to replace all the occurances of the default ip with your preferred ip. - Run `start-https.sh` script again. - Run -```bash -REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start +``` +yarn +yarn start ``` @@ -74,3 +90,7 @@ REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start 3. Generate the certificates manually via `mkcert`. Check the command in `start-https-server.sh` file. 4. Change the value of the certificate location for keys `ssl_certificate` & `ssl_certificate_key` to the place where these certificates were generated. 5. If you ran `./start-https`, but containers failed to start (you have to check with `docker ps` since it fails silently). Some Linux distros (`Ubuntu` for example) have installed and running `apache2` webserver on port `80`. This can result in `Address already in use` error (you can check with `docker logs wildcard-nginx`). Simple solution for this is simply turning it off temporarily with `sudo systemctl stop apache2`. After that just run `./start-https` again. + + +## Need Assistance +- If you are unable to resolve any issue while doing the setup, please initiate a Github discussion or join our [discord server](https://discord.com/invite/rBTTVJp) diff --git a/contributions/ServerSetup.md b/contributions/ServerSetup.md index 70b4df933b..b54062ae01 100644 --- a/contributions/ServerSetup.md +++ b/contributions/ServerSetup.md @@ -37,9 +37,22 @@ This command creates a `.env` file in the `app/server` folder. All run scripts p ``` ./build.sh ``` - This command will create a `dist` folder which contains the final packaged jar along with multiple jars for the binaries for plugins as well. +Note: +- If you want to skip tests, you can pass `-DskipTests` flag to the build cmd. +- On Ubuntu Linux environment docker needs root privilege, hence ./build.sh script needs to be run with root privilege as well. +- On Ubuntu Linux environment, the script may not be able to read .env file, so it is advised that you run the cmd like: +``` +sudo APPSMITH_MONGODB_URI="mongodb://localhost:27017/appsmith" APPSMITH_REDIS_URL="redis://127.0.0.1:6379" APPSMITH_MAIL_ENABLED=false APPSMITH_ENCRYPTION_PASSWORD=abcd APPSMITH_ENCRYPTION_SALT=abcd ./build.sh +``` +- If the volume containing docker's data root path (macOS: `~/Library/Containers/com.docker.docker/Data/vms/0/`, Ubuntu: `/var/lib/docker/`) has less than 2 GB of free space, then the script may fail with the following error: +``` +Check failed: Docker environment should have more than 2GB free disk space. +``` +There are two ways to resolve this issue: (1) free up more space (2) change docker's data root path. + + 6. Start the Java server by running ``` @@ -50,7 +63,7 @@ By default, the server will start on port 8080. 7. When the server starts, it automatically runs migrations on MongoDB and will populate it with some initial required data. -8. You can check the status of the server by hitting the endpoint: [http://localhost:8080](http://localhost:8080) on your browser. By default you should see a blank page. +8. You can check the status of the server by hitting the endpoint: [http://localhost:8080](http://localhost:8080) on your browser. By default you should see an HTTP 401 error. ## Setting up a local MongoDB @@ -75,3 +88,7 @@ docker run -p 127.0.0.1:6379:6379 --name appsmith-redis redis ``` When using this command, the value of `APPSMITH_REDIS_URI` should be set to `redis://localhost:6379`. + +## Need Assistance +- If you are unable to resolve any issue while doing the setup, please initiate a Github discussion or send an email to support@appsmith.com. We'll be happy to help you. +- In case you notice any discrepancy, please raise an issue on Github and/or send an email to support@appsmith.com. diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md new file mode 100644 index 0000000000..9479788e65 --- /dev/null +++ b/deploy/k8s/README.md @@ -0,0 +1,126 @@ +--- +description: Appsmith stands for speed and getting started with Appsmith is just as fast. +--- + +# Getting started + +You can begin using appsmith via our cloud instance or by deploying appsmith yourself + +* [Using Appsmith Cloud](quick-start.md#appsmith-cloud) **\(recommended\):** Create a new application with just one click +* [Using Docker](quick-start.md#docker): Deploy anywhere using docker + +## Appsmith Cloud + +The fastest way to get started with appsmith is using our cloud-hosted version. It's as easy as + +1. [Create an Account](https://app.appsmith.com/user/signup) +2. [Start Building](core-concepts/building-the-ui/) + +## Prerequisites +* Ensure `kubectl` is installed and configured to connect to your cluster + * Install kubeclt: [kubernetes.io/vi/docs/tasks/tools/install-kubectl/](https://kubernetes.io/vi/docs/tasks/tools/install-kubectl/) + * Minikube: [Setup Kubectl](https://minikube.sigs.k8s.io/docs/handbook/kubectl/) + * Google Cloud Kubernetes: [Configuring cluster access for kubectl +](https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl) + * Aws EKS: [Create a kubeconfig for Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html) + + * Microk8s: [Working with kubectl](https://microk8s.io/docs/working-with-kubectl) +* Kubernetes NGINX Ingress Controller must be enable on your cluster by default. Please make sure that you install the right version for your cluster + * Minikube: [Set up Ingress on Minikube with the NGINX Ingress Controller](https://kubernetes.io/docs/tasks/access-application-cluster/ingress-minikube/) + * Google Cloud Kubernetes: [Ingress with NGINX controller on Google Kubernetes Engine](https://kubernetes.github.io/ingress-nginx/deploy/) + * AWS EKS: [Install NGINX Controller for AWS EKS](https://kubernetes.github.io/ingress-nginx/deploy/#network-load-balancer-nlb) + * Microk8s: [Add on: Ingress](https://microk8s.io/docs/addon-ingress) +* Script tested on Minikube with Kubernetes v1.18.0 + +## Kubernetes + +Appsmith also comes with an installation script that will help you configure Appsmith & deploy your app on Kubernetes cluster. + + +1. Fetch the **install.k8s.sh** script on the system you want to deploy appsmith + +```bash +# Downloads install.sh +curl -O https://raw.githubusercontent.com/appsmithorg/appsmith/master/deploy/k8s/install.k8s.sh +``` + +2. Make the script executable + +```bash +chmod +x install.k8s.sh +``` + +3. Run the script. + +```bash +./install.k8s.sh +``` + +4. Check if all the pods are running correctly. + +```bash +kubectl get pods + +#Output should look like this +NAME READY STATUS RESTARTS AGE +appsmith-editor-cbf5956c4-2zxlz 1/1 Running 0 4m26s +appsmith-internal-server-d5d555dbc-qddmb. 1/1 Running 2 4m22s +imago-1602817200-g28b2 1/1 Running 0 4m39s +mongo-statefulset-0 1/1 Running 0 4m13s +redis-statefulset-0 1/1 Running 0 4m00s +``` + +5. Custom Appsmith's Configuration + * After you successfully run the script, all the configuration files have been downloaded and & stored into `` + * If you want to update your app settings (ex: database host). Go to the `/config-template`, update the corresponding value in the configmap file, then restart the pods. + * Below steps will help you update database hostname of your application: + * Open file `appsmith-configmap.yaml` in `/config-template` folder + * Update the value of variable `APPSMITH_MONGODB_URI` to your database host name + * Run commands: +``` +kubectl apply -f appsmith-configmap.yaml +kubectl scale deployment appsmith-internal-server --replicas=0 +kubectl scale deployment appsmith-internal-server --replicas=1 +``` + +{% hint style="success" %} +* You can access the running application on the **Ingress Endpoint** if you not chose to provide custom domain for your application . +``` +kubectl get ingress +NAME CLASS HOSTS ADDRESS PORTS AGE +appsmith-ingress * XXX.XXX.XX.XXX 80 2m +``` +* You may need to wait 2-3 minutes before accessing the application to allow application start (depends on your cluster). +{% endhint %} + + + + +### Custom Domains + +To host Appsmith on a custom domain, you can contact your domain registrar and update your DNS records. Most domain registrars have documentation on how you can do this yourself. + +* [GoDaddy](https://in.godaddy.com/help/create-a-subdomain-4080) +* [Amazon Route 53](https://aws.amazon.com/premiumsupport/knowledge-center/create-subdomain-route-53/) +* [Digital Ocean](https://www.digitalocean.com/docs/networking/dns/how-to/add-subdomain/) +* [NameCheap](https://www.namecheap.com/support/knowledgebase/article.aspx/9776/2237/how-to-create-a-subdomain-for-my-domain) +* [Domain.com](https://www.domain.com/help/article/domain-management-how-to-update-subdomains) + +{% hint style="warning" %} +* During the setup of Ingress Controller on your cloud. You will need to map your custom domain with the External IP of the controller before running the installation script +* Below is an example how to achieve the External IP of NGINX Ingress Controller +``` +โžœ kubectl get svc -n ingress-nginx +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +ingress-nginx-controller LoadBalancer XX.XXX.X.XX XX.XX.XX.XXX 80:XXXXX/TCP,443:XXXXX/TCP 17h +ingress-nginx-controller-admission ClusterIP XX.XXX.X.XX 443/TCP 17h +``` +{% endhint %} + + +## Troubleshooting + +If at any time you encounter an error during the installation process, reach out to **support@appsmith.com** or join our [Discord Server](https://discord.com/invite/rBTTVJp) + +If you know the error and would like to reinstall Appsmith, simply delete the installation folder and the templates folder and execute the script again + diff --git a/deploy/k8s/install.k8s.sh b/deploy/k8s/install.k8s.sh new file mode 100755 index 0000000000..18ac86e125 --- /dev/null +++ b/deploy/k8s/install.k8s.sh @@ -0,0 +1,563 @@ +#!/bin/bash + +set -o errexit + +is_command_present() { + 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_k8s_setup() { + echo "Checking your k8s setup status" + if ! is_command_present kubectl; then + echo "Please install kubectl on your machine" + exit 1 + else + + if ! is_command_present jq; then + install_jq + fi + clusters=`kubectl config view -o json | jq -r '."current-context"'` + if [[ ! -n $clusters ]]; then + echo "Please setup a k8s cluster & config kubectl to connect to it" + exit 1 + fi + k8s_minor_version=`kubectl version --short -o json | jq ."serverVersion.minor" | sed 's/[^0-9]*//g'` + if [[ $k8s_minor_version < 18 ]]; then + echo "+++++++++++ ERROR ++++++++++++++++++++++" + echo "Appsmith deployments require Kubernetes >= v1.18. Found version: v1.$k8s_minor_version" + echo "+++++++++++ ++++++++++++++++++++++++++++" + exit 1 + fi; + fi +} + +install_jq(){ + if [ $package_manager == "brew" ]; then + brew install jq + elif [ $package_manager == "yum" ]; then + yum_cmd="sudo yum --assumeyes --quiet" + $yum_cmd install jq + else + apt_cmd="sudo apt-get --yes --quiet" + $apt_cmd update + $apt_cmd install jq + fi +} + + + +check_os() { + if is_mac; then + package_manager="brew" + desired_os=1 + os="Mac" + return + fi + + os_name="$(cat /etc/*-release | awk -F= '$1 == "NAME" { gsub(/"/, ""); print $2; exit }')" + + case "$os_name" in + Ubuntu*) + desired_os=1 + os="ubuntu" + package_manager="apt-get" + ;; + Debian*) + desired_os=1 + os="debian" + package_manager="apt-get" + ;; + Red\ Hat*) + desired_os=1 + os="red hat" + package_manager="yum" + ;; + CentOS*) + desired_os=1 + os="centos" + package_manager="yum" + ;; + *) + desired_os=0 + os="Not Found" + esac +} + +overwrite_file() { + local relative_path="$1" + local template_file="$2" + local full_path="$install_dir/$relative_path" + echo "Copy $template_file to $full_path" + + if [[ -f $full_path ]] && ! confirm y "File $relative_path already exists. Would you like to replace it?"; then + rm -f "$template_file" + else + mv -f "$template_file" "$full_path" + fi +} + +# This function prompts the user for an input for a non-empty Mongo root password. +read_mongo_password() { + 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 -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 -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 -rp 'Set the mongo root user: ' mongo_root_user + done +} + +urlencode() { + # urlencode + local old_lc_collate="$LC_COLLATE" + LC_COLLATE=C + + local length="${#1}" + for (( i = 0; i < length; i++ )); do + local c="${1:i:1}" + case $c in + [a-zA-Z0-9.~_-]) printf "$c" ;; + *) printf '%%%02X' "'$c" ;; + esac + done + + 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 -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 ]] +} + + +echo_contact_support() { + echo "Please contact with your OS details and version${1:-.}" +} + +bye() { # Prints a friendly good bye message and exits the script. + set +o errexit + echo "Please share your email to receive support with the installation" + read -rp 'Email: ' email + curl -s --location --request POST 'https://hook.integromat.com/dkwb6i52am93pi30ojeboktvj32iw0fa' \ + --header 'Content-Type: text/plain' \ + --data-raw '{ + "userId": "'"$APPSMITH_INSTALLATION_ID"'", + "event": "Installation Support", + "data": { + "os": "'"$os"'", + "email": "'"$email"'", + "platform": "k8s" + } + }' + echo -e "\nExiting for now. Bye! \U1F44B\n" + exit 1 +} +download_template_file() { + templates_dir="$(mktemp -d)" + template_endpoint="https://raw.githubusercontent.com/appsmithorg/appsmith/master" + mkdir -p "$templates_dir" + ( + cd "$templates_dir" + curl --remote-name-all --silent --show-error -o appsmith-configmap.yaml.sh \ + "$template_endpoint/deploy/k8s/scripts/appsmith-configmap.yaml.sh" + curl --remote-name-all --silent --show-error -o appsmith-ingress.yaml.sh \ + "$template_endpoint/deploy/k8s/scripts/appsmith-ingress.yaml.sh" + curl --remote-name-all --silent --show-error -o encryption-configmap.yaml.sh \ + "$template_endpoint/deploy/k8s/scripts/encryption-configmap.yaml.sh" + curl --remote-name-all --silent --show-error -o mongo-configmap.yaml.sh \ + "$template_endpoint/deploy/k8s/scripts/mongo-configmap.yaml.sh" + curl --remote-name-all --silent --show-error -o nginx-configmap.yaml \ + "$template_endpoint/deploy/k8s/scripts/nginx-configmap.yaml" + if [[ "$ssl_enable" == "true" ]]; then + curl --remote-name-all --silent --show-error -o issuer-template.yaml.sh\ + "$template_endpoint/deploy/k8s/scripts/issuer-template.yaml.sh" + fi + ) + ( + cd "$install_dir" + curl --remote-name-all --silent --show-error -o backend-template.yaml \ + "$template_endpoint/deploy/k8s/templates/backend-template.yaml" + + curl --remote-name-all --silent --show-error -o frontend-template.yaml \ + "$template_endpoint/deploy/k8s/templates/frontend-template.yaml" + if [[ "$fresh_installation" == "true" ]]; then + curl --remote-name-all --silent --show-error -o mongo-template.yaml \ + "$template_endpoint/deploy/k8s/templates/mongo-template.yaml" + fi + + curl --remote-name-all --silent --show-error -o redis-template.yaml \ + "$template_endpoint/deploy/k8s/templates/redis-template.yaml" + curl --remote-name-all --silent --show-error -o imago-template.yaml\ + "$template_endpoint/deploy/k8s/templates/imago-template.yaml" + ) +} + + +deploy_app() { + kubectl apply -f "$install_dir/config-template" + kubectl apply -f "$install_dir" +} + +install_certmanager() { + cert_manager_ns=`kubectl get namespace cert-manager --no-headers --output=go-template={{.metadata.name}} --ignore-not-found` + if [ -z "${cert_manager_ns}" ]; then + echo "Installing Cert-manager"; + # cert-manager installation document: https://cert-manager.io/docs/installation/kubernetes/ + kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.0.3/cert-manager.yaml + sleep 30; # Wait 30s for cert-manger ready + else + echo "Cert-manager already install" + fi +} + +wait_for_application_start() { + local timeout=$1 + address=$custom_domain + if [[ "$ssl_enable" == "true" ]]; then + protocol="https" + else + protocol="http" + fi + # The while loop is important because for-loops don't work for dynamic values + while [[ $timeout -gt 0 ]]; do + if [[ $address == "" || $address == null ]]; then + address=`kubectl get ingress appsmith-ingress -o json | jq -r '.status.loadBalancer.ingress[0].ip'` + fi + status_code="$(curl -s -o /dev/null -w "%{http_code}" $protocol://$address/api/v1 || true)" + if [[ status_code -eq 401 ]]; then + break + else + echo -ne "Waiting for all containers to start. This check will timeout in $timeout seconds...\r\c" + fi + ((timeout--)) + sleep 1 + done + + echo "" +} + +echo -e "\U1F44B Thank you for trying out Appsmith! " +echo "" + + +# Checking OS and assigning package manager +desired_os=0 +os="" +echo -e "\U1F575 Detecting your OS" +check_os +APPSMITH_INSTALLATION_ID=$(curl -s 'https://api64.ipify.org') + +# Run bye if failure happens +trap bye EXIT + +curl -s --location --request POST 'https://hook.integromat.com/dkwb6i52am93pi30ojeboktvj32iw0fa' \ +--header 'Content-Type: text/plain' \ +--data-raw '{ + "userId": "'"$APPSMITH_INSTALLATION_ID"'", + "event": "Installation Started", + "data": { + "os": "'"$os"'", + "platform": "k8s" + } +}' + +if [[ $desired_os -eq 0 ]];then + echo "" + echo "This script is currently meant to install Appsmith on Mac OS X | Ubuntu machines." + 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 [[ $EUID -eq 0 ]]; then + echo "+++++++++++ ERROR ++++++++++++++++++++++" + echo "Please do not run this script as root/sudo." + echo "++++++++++++++++++++++++++++++++++++++++" + echo_contact_support + bye +fi + +# Check for kubernetes setup +check_k8s_setup + +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 +echo "Installing Appsmith to '$install_dir'." +mkdir -p "$install_dir" +echo "" + + +if confirm y "Is this a fresh installation?"; then + fresh_installation="true" + echo "Appsmith needs to create a MongoDB instance." + mongo_protocol="mongodb://" + mongo_host="mongo-service" + 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 + fresh_installation="false" + read -rp 'Enter your current mongo db protocol (mongodb:// || mongodb+srv://): ' mongo_protocol + 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 + echo "" + 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") + +encryptionEnv="$install_dir/config-template/encryption-configmap.yaml" +if test -f "$encryptionEnv"; then + echo "CAUTION : This isn't your first time installing appsmith. Encryption password and salt already exist. Do you want to override this? NOTE: Overwriting the existing salt and password would lead to you losing access to sensitive information encrypted using the same" + 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 -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" + elif [[ $overwrite_encryption -eq 3 ]];then + setup_encryption="true" + auto_generate_encryption="false" + fi +else + setup_encryption="true" +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 -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 "" + +if confirm n "Do you have a custom domain that you would like to link? (Only for cloud installations)"; then + read -rp 'Enter the domain or subdomain on which you want to host appsmith (example.com / app.example.com): ' custom_domain + curl -s --location --request POST 'https://hook.integromat.com/dkwb6i52am93pi30ojeboktvj32iw0fa' \ + --header 'Content-Type: text/plain' \ + --data-raw '{ + "userId": "'"$APPSMITH_INSTALLATION_ID"'", + "event": "Installation Custom Domain", + "data": { + "os": "'"$os"'", + "platform": "k8s" + } + }' + echo "" + echo "+++++++++++ IMPORTANT PLEASE READ ++++++++++++++++++++++" + echo "Please update your DNS records with your domain registrar" + echo "You can read more about this in our Documentation" + echo "https://docs.appsmith.com/v/v1.1/quick-start#custom-domains" + echo "+++++++++++++++++++++++++++++++++++++++++++++++" + echo "" + echo "Would you like to provision an SSL certificate for your custom domain / subdomain?" + if confirm y '(Your DNS records must be updated for us to proceed)'; then + ssl_enable="true" + fi + + read -rp 'Enter email address to create SSL certificate: (Optional, but strongly recommended): ' user_email + + 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 + issuer_server="https://acme-staging-v02.api.letsencrypt.org/directory" + else + issuer_server="https://acme-v02.api.letsencrypt.org/directory" + fi +fi + +echo "" +echo "Downloading the configuration templates..." +download_template_file + +echo "" +echo "Generating the configuration files from the templates" + +cd "$templates_dir" + + +mkdir -p "$install_dir/config-template" + +bash "$templates_dir/appsmith-configmap.yaml.sh" "$mongo_protocol" "$mongo_host" "$encoded_mongo_root_user" "$encoded_mongo_root_password" "$mongo_database" > appsmith-configmap.yaml +if [[ "$setup_encryption" == "true" ]]; then + bash "$templates_dir/encryption-configmap.yaml.sh" "$user_encryption_password" "$user_encryption_salt" > encryption-configmap.yaml + overwrite_file "config-template" "encryption-configmap.yaml" +fi + +if [[ -n $custom_domain ]]; then + bash "$templates_dir/appsmith-ingress.yaml.sh" "$custom_domain" "$ssl_enable"> ingress-template.yaml +else + bash "$templates_dir/appsmith-ingress.yaml.sh" "" "$ssl_enable" > ingress-template.yaml +fi + +if [[ "$ssl_enable" == "true" ]]; then + echo "$user_email" + echo "$issuer_server" + bash "$templates_dir/issuer-template.yaml.sh" "$user_email" "$issuer_server" > issuer-template.yaml + overwrite_file "" "issuer-template.yaml" +fi + +overwrite_file "config-template" "nginx-configmap.yaml" +overwrite_file "config-template" "appsmith-configmap.yaml" +overwrite_file "" "ingress-template.yaml" + + + +if [[ "$fresh_installation" == "true" ]]; then + bash "$templates_dir/mongo-configmap.yaml.sh" "$mongo_root_user" "$mongo_root_password" "$mongo_database" > mongo-configmap.yaml + overwrite_file "config-template" "mongo-configmap.yaml" +fi + + + +echo "" +echo "Deploy Appmisth on your cluster" +echo "" +if [[ "$ssl_enable" == "true" ]]; then + install_certmanager +else + echo "No domain found. Skipping generation of SSL certificate." +fi +echo "" +deploy_app + +wait_for_application_start 60 + +echo "" +if [[ $status_code -ne 401 ]]; then + echo "+++++++++++ ERROR ++++++++++++++++++++++" + echo "The containers didn't seem to start correctly. Please run the following command to check pods that may have errored out:" + echo "" + echo -e "kubectl get pods" + echo "For troubleshooting help, please reach out to us via our Discord server: https://discord.com/invite/rBTTVJp" + echo "++++++++++++++++++++++++++++++++++++++++" + echo "" + echo "Please share your email to receive help with the installation" + read -rp 'Email: ' email + curl -s --location --request POST 'https://hook.integromat.com/dkwb6i52am93pi30ojeboktvj32iw0fa' \ + --header 'Content-Type: text/plain' \ + --data-raw '{ + "userId": "'"$APPSMITH_INSTALLATION_ID"'", + "event": "Installation Support", + "data": { + "os": "'"$os"'", + "email": "'"$email"'", + "platform": "k8s" + } + }' +else + curl -s --location --request POST 'https://hook.integromat.com/dkwb6i52am93pi30ojeboktvj32iw0fa' \ + --header 'Content-Type: text/plain' \ + --data-raw '{ + "userId": "'"$APPSMITH_INSTALLATION_ID"'", + "event": "Installation Success", + "data": { + "os": "'"$os"'", + "platform": "k8s" + } + }' + echo "+++++++++++ SUCCESS ++++++++++++++++++++++++++++++" + echo "Your installation is complete!" + echo "" + if [[ -z $custom_domain ]]; then + echo "Your application is running on '$protocol://$address'." + else + echo "Your application is running on '$protocol://$custom_domain'." + fi + echo "" + echo "+++++++++++++++++++++++++++++++++++++++++++++++++" + echo "" + echo "Need help Getting Started?" + echo "Join our Discord server https://discord.com/invite/rBTTVJp" + echo "Please share your email to receive support & updates about appsmith!" + read -rp 'Email: ' email + curl -s --location --request POST 'https://hook.integromat.com/dkwb6i52am93pi30ojeboktvj32iw0fa' \ + --header 'Content-Type: text/plain' \ + --data-raw '{ + "userId": "'"$APPSMITH_INSTALLATION_ID"'", + "event": "Identify Successful Installation", + "data": { + "os": "'"$os"'", + "email": "'"$email"'", + "platform": "k8s" + } + }' +fi + +echo -e "\nPeace out \U1F596\n" diff --git a/deploy/k8s/scripts/appsmith-configmap.yaml.sh b/deploy/k8s/scripts/appsmith-configmap.yaml.sh new file mode 100644 index 0000000000..bca2ed6928 --- /dev/null +++ b/deploy/k8s/scripts/appsmith-configmap.yaml.sh @@ -0,0 +1,31 @@ +set -o nounset + +mongo_protocol="$1" +mongo_host="$2" +encoded_mongo_root_user="$3" +encoded_mongo_root_password="$4" +mongo_db="$5" + +cat<