From 015c56e1a64e5861afdd82456000b2a21f3f2b99 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Mon, 27 Jul 2020 11:52:18 +0530 Subject: [PATCH 01/15] Github action to push to Docker Hub and create Github release (#169) * Adding a github action to push to Docker Hub and create Github release when a tag is created on master branch. * Extracting the tag from the github ref and only tagging the Docker image with the relevant tag. --- .github/workflows/client.yml | 2 +- .github/workflows/github-release.yml | 123 +++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/github-release.yml diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index b5b5c1e0b4..778d4b7c2b 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -37,7 +37,7 @@ jobs: env: cache-name: cache-yarn-dependencies with: - # maven dependencies are stored in `~/.m2` on Linux/macOS + # npm dependencies are stored in `~/.npm` on Linux/macOS path: ~/.npm key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} restore-keys: | diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 0000000000..5a1d77124f --- /dev/null +++ b/.github/workflows/github-release.yml @@ -0,0 +1,123 @@ +name: Appsmith Github Release Workflow + +on: + push: + branches: master + # Only trigger if a tag has been created and pushed to this branch + tags: + - 'v*' + +jobs: + build-client: + runs-on: ubuntu-latest + defaults: + run: + working-directory: app/client + + steps: + # Checkout the code + - uses: actions/checkout@v2 + + - name: Use Node.js 10.16.3 + uses: actions/setup-node@v1 + with: + node-version: '10.16.3' + + # Retrieve npm dependencies from cache. After a successful run, these dependencies are cached again + - name: Cache npm dependencies + uses: actions/cache@v2 + env: + cache-name: cache-yarn-dependencies + with: + # npm dependencies are stored in `~/.m2` on Linux/macOS + path: ~/.npm + key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.OS }}-node- + ${{ runner.OS }}- + + # Install all the dependencies + - name: Install dependencies + run: yarn install + + - name: Set the build environment based on the branch + id: vars + run: | + REACT_APP_ENVIRONMENT="PRODUCTION" + echo ::set-output name=REACT_APP_ENVIRONMENT::${REACT_APP_ENVIRONMENT} + + - name: Create the bundle + run: REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} yarn build + + - name: Get the version + id: get_version + run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + + # Build Docker image and push to Docker Hub + - name: Push production image to Docker Hub with commit tag + run: | + docker build -t appsmith/appsmith-editor:${{steps.get_version.outputs.tag}} . + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin + docker push appsmith/appsmith-editor + + build-server: + runs-on: ubuntu-latest + defaults: + run: + working-directory: app/server + + steps: + # Checkout the code + - uses: actions/checkout@v2 + + # Setup Java + - name: Set up JDK 1.11 + uses: actions/setup-java@v1 + with: + java-version: 1.11 + + # Retrieve maven dependencies from cache. After a successful run, these dependencies are cached again + - name: Cache maven dependencies + uses: actions/cache@v2 + env: + cache-name: cache-maven-dependencies + with: + # maven dependencies are stored in `~/.m2` on Linux/macOS + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + # Build the code + - name: Build without running any tests + run: mvn -B package -DskipTests + + - name: Get the version + id: get_version + run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + + # Build Docker image and push to Docker Hub + - name: Push image to Docker Hub + run: | + docker build -t appsmith/appsmith-server:${{steps.get_version.outputs.tag}} . + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin + docker push appsmith/appsmith-server + + create-release: + needs: + - build-server + - build-client + runs-on: ubuntu-latest + + steps: + # Creating the release on Github + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + \ No newline at end of file From 74756a25b1f8a198d6d604f7a9c0e69f70c09466 Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Mon, 27 Jul 2020 12:25:45 +0530 Subject: [PATCH 02/15] Fix/deploy script (#170) * Added information to setup domain * Fixed incorrect var reference * Updated documentation message * Updated env template * updated template * removed debug echo * Updated Script * Updated Text * Removed option to connect to external mongo for fresh installs Exit script of docker desktop is not installed Co-authored-by: Nikhil Nandagopal --- deploy/install.sh | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/deploy/install.sh b/deploy/install.sh index a0c1f47ce2..8da6a2e683 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -116,18 +116,15 @@ read -p 'Installation Directory [appsmith]: ' install_dir install_dir=${install_dir:-appsmith} mkdir -p $PWD/$install_dir install_dir=$PWD/$install_dir -echo "Appsmith needs a mongodb instance to run" -echo "1) Automatically setup mongo db on this instance (recommended)" -echo "2) Connect to an external mongo db" -read -p 'Enter option number [1]: ' mongo_option -mongo_option=${mongo_option:-1} +read -p 'Is this a fresh installation? [Y/n]' fresh_install +fresh_install=${fresh_install:-Y} echo "" -if [[ $mongo_option -eq 2 ]];then - read -p 'Enter your mongo db host: ' mongo_host - read -p 'Enter the mongo root user: ' mongo_root_user - read -sp 'Enter the mongo password: ' mongo_root_password - read -p 'Enter your mongo database name: ' mongo_database +if [ $fresh_install == "N" -o $fresh_install == "n" -o $fresh_install == "no" -o $fresh_install == "No" ];then + read -p 'Enter your current mongo db host: ' mongo_host + read -p 'Enter your current mongo root user: ' mongo_root_user + read -sp 'Enter your current mongo password: ' mongo_root_password + read -p 'Enter your current mongo database name: ' mongo_database # It is possible that this isn't the first installation. echo "" read -p 'Do you have any existing data in the database?[Y/n]: ' existing_encrypted_data @@ -138,7 +135,8 @@ if [[ $mongo_option -eq 2 ]];then else auto_generate_encryption="false" fi -elif [[ $mongo_option -eq 1 ]];then +elif [ $fresh_install == "Y" -o $fresh_install == "y" -o $fresh_install == "yes" -o $fresh_install == "Yes" ];then + echo "Appsmith needs to configure a mongo db to run" mongo_host="mongo" mongo_database="appsmith" read -p 'Set the mongo root user: ' mongo_root_user @@ -186,11 +184,11 @@ echo "" read -p 'Would you like to host appsmith on a custom domain / subdomain? [Y/n]: ' setup_domain setup_domain=${setup_domain:-Y} if [ $setup_domain == "Y" -o $setup_domain == "y" -o $setup_domain == "yes" -o $setup_domain == "Yes" ];then - 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?" read -p '(Your DNS records must be updated for us to provision SSL) [Y/n]: ' setup_ssl setup_ssl=${setup_ssl:-Y} @@ -199,7 +197,7 @@ else fi if [ $setup_ssl == "Y" -o $setup_ssl == "y" -o $setup_ssl == "yes" -o $setup_ssl == "Yes" ];then - read -p 'Enter your domain / subdomain name (example.com / app.example.com): ' custom_domain + read -p 'Enter the domain or subdomain on which you want to host appsmith (example.com / app.example.com): ' custom_domain fi NGINX_SSL_CMNT="" @@ -222,8 +220,11 @@ if ! is_command_present docker ;then if [ $package_manager == "apt-get" -o $package_manager == "yum" ];then install_docker else - echo "Please follow below link to Install Docker Desktop on Mac:" + echo "+++++++++++ IMPORTANT ++++++++++++++++++++++" + echo "Docker Desktop must be installed to proceed." echo "https://docs.docker.com/docker-for-mac/install/" + echo "++++++++++++++++++++++++++++++++++++++++++++" + exit fi fi From f120fb128173ac52dc41b525c2d45ae171d53ff2 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Mon, 27 Jul 2020 12:31:00 +0530 Subject: [PATCH 03/15] Fixing the yml file for Github Action (#171) --- .github/workflows/github-release.yml | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 5a1d77124f..4f34d1f8fb 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -37,28 +37,28 @@ jobs: ${{ runner.OS }}- # Install all the dependencies - - name: Install dependencies - run: yarn install + - name: Install dependencies + run: yarn install - - name: Set the build environment based on the branch - id: vars - run: | - REACT_APP_ENVIRONMENT="PRODUCTION" - echo ::set-output name=REACT_APP_ENVIRONMENT::${REACT_APP_ENVIRONMENT} + - name: Set the build environment based on the branch + id: vars + run: | + REACT_APP_ENVIRONMENT="PRODUCTION" + echo ::set-output name=REACT_APP_ENVIRONMENT::${REACT_APP_ENVIRONMENT} - - name: Create the bundle - run: REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} yarn build + - name: Create the bundle + run: REACT_APP_ENVIRONMENT=${{steps.vars.outputs.REACT_APP_ENVIRONMENT}} yarn build - - name: Get the version - id: get_version - run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + - name: Get the version + id: get_version + run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - # Build Docker image and push to Docker Hub - - name: Push production image to Docker Hub with commit tag - run: | - docker build -t appsmith/appsmith-editor:${{steps.get_version.outputs.tag}} . - echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin - docker push appsmith/appsmith-editor + # Build Docker image and push to Docker Hub + - name: Push production image to Docker Hub with commit tag + run: | + docker build -t appsmith/appsmith-editor:${{steps.get_version.outputs.tag}} . + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin + docker push appsmith/appsmith-editor build-server: runs-on: ubuntu-latest From 45163c5f99536b2d43c2c580f929804fc599669f Mon Sep 17 00:00:00 2001 From: Nikhil Nandagopal Date: Mon, 27 Jul 2020 12:52:53 +0530 Subject: [PATCH 04/15] Updated installation script (#172) * Added information to setup domain * Fixed incorrect var reference * Updated documentation message * Updated env template * updated template * removed debug echo * Updated Script * Updated Text * Removed option to connect to external mongo for fresh installs Exit script of docker desktop is not installed * Updated docker installation explanation Co-authored-by: Nikhil Nandagopal --- deploy/install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/install.sh b/deploy/install.sh index 8da6a2e683..16bb2b5097 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -220,10 +220,10 @@ if ! is_command_present docker ;then if [ $package_manager == "apt-get" -o $package_manager == "yum" ];then install_docker else - echo "+++++++++++ IMPORTANT ++++++++++++++++++++++" - echo "Docker Desktop must be installed to proceed." + echo "+++++++++++ IMPORTANT READ ++++++++++++++++++++++" + echo "Docker Desktop must be installed manually on Mac OS to proceed. Docker will be installed automatically on Ubuntu / Redhat / Cent OS" echo "https://docs.docker.com/docker-for-mac/install/" - echo "++++++++++++++++++++++++++++++++++++++++++++" + echo "++++++++++++++++++++++++++++++++++++++++++++++++" exit fi fi From adee90aee974c862278c5602478111d660fcac9f Mon Sep 17 00:00:00 2001 From: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> Date: Mon, 27 Jul 2020 16:44:06 +0530 Subject: [PATCH 05/15] assertion type changed (#174) Co-authored-by: Nandan Anantharamu --- .../Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js | 1 - .../Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js | 2 +- .../integration/Smoke_TestSuite/Binding/TextTable.js | 11 ++++------- app/client/cypress/support/commands.js | 1 + 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js index 724ca5f43b..fe47e05be6 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js @@ -24,7 +24,6 @@ describe("Test curl import flow", function() { cy.ResponseStatusCheck("200 OK"); cy.get(ApiEditor.formActionButtons).should("be.visible"); cy.get(ApiEditor.ApiDeleteBtn).click(); - cy.get(ApiEditor.ApiDeleteBtn).should("be.disabled"); cy.wait("@deleteAction"); cy.get("@deleteAction").then(response => { cy.expect(response.response.body.responseMeta.success).to.eq(true); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js index 6bec492f77..9311ff7005 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js @@ -9,7 +9,7 @@ describe("API Panel Test Functionality", function() { cy.CreateAPI("FirstAPI"); cy.log("Creation of FirstAPI Action successful"); cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods); - cy.RunAPI(); + cy.SaveAndRunAPI(); cy.ResponseStatusCheck(testdata.successStatusCode); cy.get(apiwidget.createApiOnSideBar) .first() diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js index 0974234b8f..51140fb9cf 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js @@ -65,7 +65,7 @@ describe("Text-Table Binding Functionality", function() { .find(".tr") .then(listing => { const listingCount = listing.length.toString(); - cy.get(commonlocators.TextInside).should("have.text", listingCount); + cy.get(commonlocators.TextInside).contains(listingCount); cy.EvaluateDataType("string"); cy.EvaluateCurrentValue(listingCount); cy.PublishtheApp(); @@ -73,10 +73,7 @@ describe("Text-Table Binding Functionality", function() { .find(".tr") .then(listing => { const listingCountP = listing.length.toString(); - cy.get(commonlocators.TextInside).should( - "have.text", - listingCountP, - ); + cy.get(commonlocators.TextInside).contains(listingCountP); }); }); }); @@ -96,14 +93,14 @@ describe("Text-Table Binding Functionality", function() { */ cy.readTabledata("1", "2").then(tabData => { const tabValue = `\"${tabData}\"`; - cy.get(commonlocators.TextInside).should("have.text", tabValue); + cy.get(commonlocators.TextInside).contains(tabValue); cy.EvaluateDataType("string"); cy.EvaluateCurrentValue(tabValue); cy.PublishtheApp(); cy.isSelectRow(1); cy.readTabledataPublish("1", "2").then(tabDataP => { const tabValueP = `\"${tabDataP}\"`; - cy.get(commonlocators.TextInside).should("have.text", tabValueP); + cy.get(commonlocators.TextInside).contains(tabValueP); }); }); }); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 3d73fb60c9..65cac3a16d 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -691,6 +691,7 @@ Cypress.Commands.add("EvaluateDataType", dataType => { }); Cypress.Commands.add("EvaluateCurrentValue", currentValue => { + cy.wait(2000); cy.get(commonlocators.evaluatedCurrentValue) .should("be.visible") .contains(currentValue); From 502733315b420aeb85fb1fc30489407bf8c2da0d Mon Sep 17 00:00:00 2001 From: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com> Date: Tue, 28 Jul 2020 10:45:26 +0530 Subject: [PATCH 06/15] Fix key value array not being able to set initial value (#150) * Fix key value array not being able to set initial value. Now we allow multiple values to initialise for array input --- .../formControls/KeyValueArrayControl.tsx | 4 +++- app/client/src/sagas/DatasourcesSagas.ts | 23 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/client/src/components/formControls/KeyValueArrayControl.tsx b/app/client/src/components/formControls/KeyValueArrayControl.tsx index dbfa6c1f1d..715db3633d 100644 --- a/app/client/src/components/formControls/KeyValueArrayControl.tsx +++ b/app/client/src/components/formControls/KeyValueArrayControl.tsx @@ -10,6 +10,7 @@ import DynamicTextField from "components/editorComponents/form/fields/DynamicTex import FormLabel from "components/editorComponents/FormLabel"; import { InputType } from "widgets/InputWidget"; import HelperTooltip from "components/editorComponents/HelperTooltip"; +import { Colors } from "constants/Colors"; const FormRowWithLabel = styled.div` display: flex; @@ -105,13 +106,14 @@ const KeyValueRow = (props: Props & WrappedFieldArrayProps) => { onClick={() => props.fields.push({ key: "", value: "" }) } - color={"#A3B3BF"} + color={Colors["CADET_BLUE"]} style={{ alignSelf: "center" }} /> ) : ( props.fields.remove(index)} style={{ alignSelf: "center" }} /> diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index c0f1e47375..b7ea666e1e 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -232,11 +232,24 @@ function* createDatasourceFromFormSaga( } if (subSection.initialValue) { - _.set( - initialValues, - subSection.configProperty, - subSection.initialValue, - ); + if (subSection.controlType === "KEYVALUE_ARRAY") { + subSection.initialValue.forEach( + (initialValue: string | number, index: number) => { + const configProperty = subSection.configProperty.replace( + "*", + index, + ); + + _.set(initialValues, configProperty, initialValue); + }, + ); + } else { + _.set( + initialValues, + subSection.configProperty, + subSection.initialValue, + ); + } } }); }; From 5028a98d45f1167021c6037ecfa81aedaa66bb2d Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 28 Jul 2020 14:17:16 +0530 Subject: [PATCH 07/15] Correcting github release action yml file (#177) --- .github/workflows/github-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 4f34d1f8fb..cfc374fe97 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -21,7 +21,7 @@ jobs: - name: Use Node.js 10.16.3 uses: actions/setup-node@v1 with: - node-version: '10.16.3' + node-version: '10.16.3' # Retrieve npm dependencies from cache. After a successful run, these dependencies are cached again - name: Cache npm dependencies From 7b142ba8b82f83a2951033cb27fb9f83db56aa7d Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 28 Jul 2020 14:26:51 +0530 Subject: [PATCH 08/15] Fixing the github yml (#178) --- .github/workflows/github-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index cfc374fe97..e4958b3c4e 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -63,7 +63,7 @@ jobs: build-server: runs-on: ubuntu-latest defaults: - run: + run: working-directory: app/server steps: From 55bb744a3b9eac5d51d67d27ee2a9249c1e3ba90 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 28 Jul 2020 14:55:16 +0530 Subject: [PATCH 09/15] Adding condition to run only on master branch for specific tag push (#179) --- .github/workflows/github-release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index e4958b3c4e..265ee560cb 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -2,13 +2,13 @@ name: Appsmith Github Release Workflow on: push: - branches: master # Only trigger if a tag has been created and pushed to this branch tags: - - 'v*' + - v* jobs: build-client: + if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest defaults: run: @@ -61,6 +61,7 @@ jobs: docker push appsmith/appsmith-editor build-server: + if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest defaults: run: @@ -103,6 +104,7 @@ jobs: docker push appsmith/appsmith-server create-release: + if: github.ref == 'refs/heads/master' needs: - build-server - build-client From f3d88bc8b80e7df07e02070f8c22622f6ca4253e Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 28 Jul 2020 15:04:13 +0530 Subject: [PATCH 10/15] Removing erroneous condition on master branch (#180) --- .github/workflows/github-release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 265ee560cb..8fc3683f37 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -8,7 +8,6 @@ on: jobs: build-client: - if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest defaults: run: @@ -61,7 +60,6 @@ jobs: docker push appsmith/appsmith-editor build-server: - if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest defaults: run: @@ -104,7 +102,6 @@ jobs: docker push appsmith/appsmith-server create-release: - if: github.ref == 'refs/heads/master' needs: - build-server - build-client From 01a3d4682fcb2b695c6063a132512173f33e5e9d Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Tue, 28 Jul 2020 16:11:51 +0530 Subject: [PATCH 11/15] Fix action batching issue (#181) --- .../Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js | 3 +++ .../Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js | 3 ++- app/client/src/actions/actionActions.ts | 4 ++-- app/client/src/sagas/ActionSagas.ts | 3 +-- app/client/src/sagas/BatchSagas.tsx | 6 +++++- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js index d755ed2d9e..a5a9c85f6f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_All_Verb_spec.js @@ -110,6 +110,7 @@ describe("API Panel Test Functionality", function() { cy.CreateAPI(apiname); cy.log("Creation of API Action successful"); cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods); + cy.WaitAutoSave(); cy.RunAPI(); cy.ResponseStatusCheck(testdata.successStatusCode); cy.log("Response code check successful"); @@ -135,6 +136,7 @@ describe("API Panel Test Functionality", function() { cy.CreateAPI("ThirdAPI"); cy.log("Creation of API Action successful"); cy.enterDatasourceAndPath(testdata.baseUrl, testdata.queryAndValue); + cy.WaitAutoSave(); cy.RunAPI(); cy.ResponseStatusCheck("200 OK"); cy.log("Response code check successful"); @@ -153,6 +155,7 @@ describe("API Panel Test Functionality", function() { testdata.queryKey, testdata.queryValue, ); + cy.WaitAutoSave(); cy.RunAPI(); cy.ResponseStatusCheck("5000"); cy.log("Response code check successful"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js index 9311ff7005..7629cee44e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js @@ -9,7 +9,8 @@ describe("API Panel Test Functionality", function() { cy.CreateAPI("FirstAPI"); cy.log("Creation of FirstAPI Action successful"); cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods); - cy.SaveAndRunAPI(); + cy.WaitAutoSave(); + cy.RunAPI(); cy.ResponseStatusCheck(testdata.successStatusCode); cy.get(apiwidget.createApiOnSideBar) .first() diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index 531538fd58..0696e1d3d1 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -68,10 +68,10 @@ export const runAction = (id: string, paginationField?: PaginationField) => { }; export const updateAction = (payload: { id: string }) => { - return { + return batchAction({ type: ReduxActionTypes.UPDATE_ACTION_INIT, payload, - }; + }); }; export const updateActionSuccess = (payload: { data: Action }) => { diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 0665c7e69e..e1c2231df8 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -10,7 +10,6 @@ import { select, takeEvery, takeLatest, - debounce, } from "redux-saga/effects"; import ActionAPI, { ActionCreateUpdateResponse, Property } from "api/ActionAPI"; import _ from "lodash"; @@ -481,7 +480,7 @@ export function* watchActionSagas() { fetchActionsForViewModeSaga, ), takeEvery(ReduxActionTypes.CREATE_ACTION_INIT, createActionSaga), - debounce(500, ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga), + takeLatest(ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga), takeLatest(ReduxActionTypes.DELETE_ACTION_INIT, deleteActionSaga), takeLatest(ReduxActionTypes.SAVE_API_NAME, saveApiNameSaga), takeLatest(ReduxActionTypes.MOVE_ACTION_INIT, moveActionSaga), diff --git a/app/client/src/sagas/BatchSagas.tsx b/app/client/src/sagas/BatchSagas.tsx index 8afcc5379a..effb2100e7 100644 --- a/app/client/src/sagas/BatchSagas.tsx +++ b/app/client/src/sagas/BatchSagas.tsx @@ -25,9 +25,13 @@ const BATCH_PRIORITY = { needsSaga: true, }, [ReduxActionTypes.UPDATE_ACTION_PROPERTY]: { - priority: 1, + priority: 0, needsSaga: false, }, + [ReduxActionTypes.UPDATE_ACTION_INIT]: { + priority: 1, + needsSaga: true, + }, }; const batches: ReduxAction[][] = []; From 0bff7ca165f9291714626eb02e87533ff3d79058 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 28 Jul 2020 17:33:05 +0530 Subject: [PATCH 12/15] Adding nightly builds that will be generated on each push to master (#183) The latest tag will only be generated when a non-beta Github release is created. --- .github/workflows/client.yml | 2 +- .github/workflows/github-release.yml | 12 ++++++++++++ .github/workflows/server.yml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 778d4b7c2b..de83060742 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -229,6 +229,6 @@ jobs: if: success() && github.ref == 'refs/heads/master' run: | docker build -t appsmith/appsmith-editor:${GITHUB_SHA} . - docker build -t appsmith/appsmith-editor:latest . + docker build -t appsmith/appsmith-editor:nightly . echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker push appsmith/appsmith-editor \ No newline at end of file diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 8fc3683f37..66d53b2030 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -56,6 +56,12 @@ jobs: - name: Push production image to Docker Hub with commit tag run: | docker build -t appsmith/appsmith-editor:${{steps.get_version.outputs.tag}} . + + # Only build & tag with latest if the tag doesn't contain beta + if [[ ! ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then + docker build -t appsmith/appsmith-editor:latest . + fi + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker push appsmith/appsmith-editor @@ -98,6 +104,12 @@ jobs: - name: Push image to Docker Hub run: | docker build -t appsmith/appsmith-server:${{steps.get_version.outputs.tag}} . + + # Only build & tag with latest if the tag doesn't contain beta + if [[ ! ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then + docker build -t appsmith/appsmith-server:latest . + fi + echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker push appsmith/appsmith-server diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index bae8bd7317..d523607c7d 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -79,6 +79,6 @@ jobs: if: success() && github.ref == 'refs/heads/master' run: | docker build -t appsmith/appsmith-server:${GITHUB_SHA} . - docker build -t appsmith/appsmith-server:latest . + docker build -t appsmith/appsmith-server:nightly . echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker push appsmith/appsmith-server From 66bf23106dff1c9a93cc20ede76809170c092452 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 28 Jul 2020 17:54:06 +0530 Subject: [PATCH 13/15] Fix cloning fails in some cases for organizations (#159) * Fix cloning fails in some cases for organizations Cloning currently fails in cases like the following: - Application with no pages - Pages with no actions - Pages with more than one action * Remove debug naming of cloned datasources * Add test for organization cloning * Add more tests for organization cloning * Fix potential race condition in adding pages to an application * Move db update call to add page to application, into repository * Use `getIdCriteria` to query for document's _id --- .../appsmith/server/constants/FieldName.java | 1 + .../server/domains/ApplicationPage.java | 3 + .../CustomApplicationRepository.java | 4 + .../CustomApplicationRepositoryImpl.java | 16 + .../services/ApplicationPageService.java | 5 +- .../services/ApplicationPageServiceImpl.java | 96 ++- .../solutions/ExamplesOrganizationCloner.java | 85 ++- .../ExamplesOrganizationClonerTests.java | 556 ++++++++++++++++++ 8 files changed, 712 insertions(+), 54 deletions(-) create mode 100644 app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java index a23f3a4911..fe535ffcaa 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/FieldName.java @@ -24,6 +24,7 @@ public class FieldName { public static String PROVIDER_ID = "providerId"; public static String CATEGORY = "category"; public static String PAGE = "page"; + public static String PAGES = "pages"; public static String SIZE = "size"; public static String ROLE = "role"; public static String DEFAULT_WIDGET_NAME = "MainContainer"; 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 ae0bc9f8b9..f6a6d65194 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,5 +1,6 @@ package com.appsmith.server.domains; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -9,9 +10,11 @@ import lombok.ToString; @Setter @ToString @NoArgsConstructor +@AllArgsConstructor public class ApplicationPage { String id; Boolean isDefault; + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepository.java index 5ac9c66a8b..8469b02ed5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepository.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomApplicationRepository.java @@ -2,6 +2,8 @@ package com.appsmith.server.repositories; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.Page; +import com.mongodb.client.result.UpdateResult; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -16,4 +18,6 @@ public interface CustomApplicationRepository extends AppsmithRepository findByOrganizationId(String orgId, AclPermission permission); Flux findByMultipleOrganizationIds(Set orgIds, AclPermission permission); + + Mono addPageToApplication(Application application, Page page, boolean isDefault); } 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 8222ea2ca0..c9b41f8bad 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 @@ -2,14 +2,20 @@ package com.appsmith.server.repositories; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.PolicyGenerator; +import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.Page; import com.appsmith.server.domains.QApplication; +import com.mongodb.client.result.UpdateResult; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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.data.mongodb.core.query.Update; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -64,4 +70,14 @@ public class CustomApplicationRepositoryImpl extends BaseAppsmithRepositoryImpl< return queryAll(List.of(orgIdsCriteria), permission); } + @Override + public Mono addPageToApplication(Application application, Page page, boolean isDefault) { + final ApplicationPage applicationPage = new ApplicationPage(page.getId(), isDefault); + return mongoOperations.updateFirst( + Query.query(getIdCriteria(application.getId())), + new Update().addToSet(FieldName.PAGES, applicationPage), + Application.class + ); + } + } 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 6f4185bfcb..537a575a26 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 @@ -2,12 +2,13 @@ package com.appsmith.server.services; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Page; +import com.mongodb.client.result.UpdateResult; import reactor.core.publisher.Mono; public interface ApplicationPageService { Mono createPage(Page page); - Mono addPageToApplication(Mono applicationMono, Page page, Boolean isDefault); + Mono addPageToApplication(Application application, Page page, Boolean isDefault); Mono getPage(String pageId, Boolean viewMode); @@ -19,5 +20,7 @@ public interface ApplicationPageService { Mono makePageDefault(String applicationId, String pageId); + Mono cloneApplication(Application application); + Mono deleteApplication(String id); } 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 b223d7c8a1..348db488e3 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 @@ -13,11 +13,15 @@ import com.appsmith.server.domains.Page; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.repositories.ApplicationRepository; +import com.mongodb.client.result.UpdateResult; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -40,18 +44,22 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { private final AnalyticsService analyticsService; private final PolicyGenerator policyGenerator; + private final ApplicationRepository applicationRepository; + public ApplicationPageServiceImpl(ApplicationService applicationService, PageService pageService, SessionUserService sessionUserService, OrganizationService organizationService, AnalyticsService analyticsService, - PolicyGenerator policyGenerator) { + PolicyGenerator policyGenerator, + ApplicationRepository applicationRepository) { this.applicationService = applicationService; this.pageService = pageService; this.sessionUserService = sessionUserService; this.organizationService = organizationService; this.analyticsService = analyticsService; this.policyGenerator = policyGenerator; + this.applicationRepository = applicationRepository; } public Mono createPage(Page page) { @@ -87,34 +95,31 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { return pageMono .flatMap(pageService::createDefault) //After the page has been saved, update the application (save the page id inside the application) - .flatMap(savedPage -> - addPageToApplication(applicationMono, savedPage, false) - .thenReturn(savedPage)); + .zipWith(applicationMono) + .flatMap(tuple -> { + final Page savedPage = tuple.getT1(); + final Application application = tuple.getT2(); + return addPageToApplication(application, savedPage, false) + .thenReturn(savedPage); + }); } /** - * This function is called during page create in Page Service. It adds the newly created - * page to its ApplicationPages list. + * This function is called during page create in Page Service. It adds the given page to its ApplicationPages list. + * Note: It is assumed here that `application` is already checked for the MANAGE_APPLICATIONS policy. * - * @param applicationMono - * @param page - * @return Updated application + * @param application Application to which the page will be added. Should have an `id` already. + * @param page Page to be added to the application. Should have an `id` already. + * @return UpdateResult object with details on how many documents have been updated, which should be 0 or 1. */ - public Mono addPageToApplication(Mono applicationMono, Page page, Boolean isDefault) { - return applicationMono - .map(application -> { - List applicationPages = application.getPages(); - if (applicationPages == null) { - applicationPages = new ArrayList<>(); + @Override + public Mono addPageToApplication(Application application, Page page, Boolean isDefault) { + return applicationRepository.addPageToApplication(application, page, isDefault) + .doOnSuccess(result -> { + if (result.getModifiedCount() != 1) { + log.error("Add page to application didn't update anything, probably because application wasn't found."); } - ApplicationPage applicationPage = new ApplicationPage(); - applicationPage.setId(page.getId()); - applicationPage.setIsDefault(isDefault); - applicationPages.add(applicationPage); - application.setPages(applicationPages); - return application; - }) - .flatMap(applicationService::save); + }); } public Mono getPage(String pageId, Boolean viewMode) { @@ -246,10 +251,53 @@ public class ApplicationPageServiceImpl implements ApplicationPageService { return pageService .createDefault(page) - .flatMap(savedPage -> addPageToApplication(Mono.just(savedApplication), savedPage, true)); + .flatMap(savedPage -> addPageToApplication(savedApplication, savedPage, true)) + .then(applicationService.findById(savedApplication.getId(), READ_APPLICATIONS)); }); } + @Override + public Mono cloneApplication(Application application) { + if (!StringUtils.hasText(application.getName())) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.NAME)); + } + + String orgId = application.getOrganizationId(); + if (!StringUtils.hasText(orgId)) { + return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.ORGANIZATION_ID)); + } + + // Clean the object so that it will be saved as a new application for the currently signed in user. + application.setId(null); + application.setPolicies(new HashSet<>()); + application.setPages(new ArrayList<>()); + + Mono userMono = sessionUserService.getCurrentUser().cache(); + Mono applicationWithPoliciesMono = userMono + .flatMap(user -> { + Mono orgMono = organizationService.findById(orgId, ORGANIZATION_MANAGE_APPLICATIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, orgId))); + + return orgMono.map(org -> { + application.setOrganizationId(org.getId()); + // At the organization level, filter out all the application specific policies and apply them + // to the new application that we are creating. + Set policySet = org.getPolicies().stream() + .filter(policy -> + policy.getPermission().equals(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) || + policy.getPermission().equals(ORGANIZATION_READ_APPLICATIONS.getValue()) + ).collect(Collectors.toSet()); + + Set documentPolicies = policyGenerator.getAllChildPolicies(policySet, Organization.class, Application.class); + application.setPolicies(documentPolicies); + return application; + }); + }); + + return applicationWithPoliciesMono + .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()) 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 c3aa9d3e46..ee62513f7b 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 @@ -3,8 +3,10 @@ package com.appsmith.server.solutions; import com.appsmith.external.models.BaseDomain; 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.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -94,7 +96,8 @@ public class ExamplesOrganizationCloner { * @param user The user who will own the new cloned organization. * @return Publishes the newly created organization. */ - private Mono cloneOrganizationForUser(String templateOrganizationId, User user) { + public Mono cloneOrganizationForUser(String templateOrganizationId, User user) { + log.info("Cloning organization id {}", templateOrganizationId); return organizationRepository .findById(templateOrganizationId) .doOnSuccess(organization -> { @@ -144,46 +147,70 @@ public class ExamplesOrganizationCloner { .findByOrganizationIdAndIsPublicTrue(fromOrganizationId) .flatMap(application -> { final String templateApplicationId = application.getId(); - makePristine(application); application.setOrganizationId(toOrganizationId); - if (!CollectionUtils.isEmpty(application.getPages())) { - application.getPages().clear(); - } - return Flux.combineLatest( - pageRepository.findByApplicationId(templateApplicationId), - applicationPageService.createApplication(application).cache(), - (page, savedApplication) -> { - log.info("Cloned application {} into new application {}", templateApplicationId, savedApplication.getId()); - page.setApplicationId(savedApplication.getId()); - return page; - } - ); + return doCloneApplication(application, templateApplicationId); }) .flatMap(page -> { final String templatePageId = page.getId(); makePristine(page); - return Flux.combineLatest( - actionRepository.findByPageId(templatePageId), - applicationPageService.createPage(page).cache(), - (action, savedPage) -> { - action.setPageId(savedPage.getId()); - return action; - } - ); + return applicationPageService + .createPage(page) + .flatMap(page1 -> { + log.info("Cloned into new page {}", page1); + return applicationRepository.findById(page.getApplicationId()) + .map(application -> { + log.info("Application after page got cloned: {}", application); + return page1; + }); + }) + .flatMapMany( + savedPage -> actionRepository + .findByPageId(templatePageId) + .map(action -> { + log.info("Preparing action for cloning {} {}.", action.getName(), action.getId()); + action.setPageId(savedPage.getId()); + return action; + }) + ); }) - .zipWith(cloneDatasourcesMono) - .flatMap(tuple -> { - final Action action = tuple.getT1(); - final Map newDatasourcesByTemplateId = tuple.getT2(); + .flatMap(action -> { + log.info("Creating clone of action {}", action.getId()); makePristine(action); action.setOrganizationId(toOrganizationId); action.setCollectionId(null); - action.setDatasource(newDatasourcesByTemplateId.get(action.getDatasource().getId())); - return actionService.create(action); + Mono actionMono = Mono.just(action); + final Datasource datasourceInsideAction = action.getDatasource(); + if (datasourceInsideAction != null) { + if (datasourceInsideAction.getId() != null) { + actionMono = cloneDatasourcesMono + .map(newDatasourcesByTemplateId -> { + action.setDatasource(newDatasourcesByTemplateId.get(datasourceInsideAction.getId())); + return action; + }); + } else { + datasourceInsideAction.setOrganizationId(toOrganizationId); + } + } + return actionMono.flatMap(actionService::create); }) + .then(cloneDatasourcesMono) // Run the datasource cloning mono if it isn't already done. .then(); } + private Flux doCloneApplication(Application application, String templateApplicationId) { + return applicationPageService + .cloneApplication(application) + .flatMapMany( + savedApplication -> pageRepository + .findByApplicationId(templateApplicationId) + .map(page -> { + log.info("Preparing page for cloning {} {}.", page.getName(), page.getId()); + page.setApplicationId(savedApplication.getId()); + return page; + }) + ); + } + /** * Clone all the datasources (except deleted ones) from one organization to another. Publishes a map where the keys * are IDs of datasources that were copied (source IDs), and the values are the cloned datasource objects which @@ -202,7 +229,7 @@ public class ExamplesOrganizationCloner { } makePristine(datasource); datasource.setOrganizationId(toOrganizationId); - datasource.setName(datasource.getName() + " cloned " + Math.random()); + datasource.setName(datasource.getName()); return Mono.zip( Mono.just(templateDatasourceId), datasourceService.create(datasource) 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 new file mode 100644 index 0000000000..89f9613ccb --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java @@ -0,0 +1,556 @@ +package com.appsmith.server.solutions; + +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.Datasource; +import com.appsmith.server.domains.Organization; +import com.appsmith.server.domains.Page; +import com.appsmith.server.domains.Plugin; +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.OrganizationService; +import com.appsmith.server.services.PageService; +import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.services.UserService; +import lombok.extern.slf4j.Slf4j; +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 org.springframework.util.LinkedMultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; +import static com.appsmith.server.acl.AclPermission.READ_DATASOURCES; +import static com.appsmith.server.acl.AclPermission.READ_PAGES; +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@RunWith(SpringRunner.class) +@SpringBootTest +@DirtiesContext +public class ExamplesOrganizationClonerTests { + + @Autowired + UserService userService; + + @Autowired + private ExamplesOrganizationCloner examplesOrganizationCloner; + + @Autowired + private ApplicationService applicationService; + + @Autowired + private DatasourceService datasourceService; + + @Autowired + private OrganizationService organizationService; + + @Autowired + private ApplicationPageService applicationPageService; + + @Autowired + private SessionUserService sessionUserService; + + @Autowired + private ActionService actionService; + + @Autowired + private PageService pageService; + + @Autowired + private ActionCollectionService actionCollectionService; + + @Autowired + private PluginRepository pluginRepository; + + @MockBean + private PluginExecutorHelper pluginExecutorHelper; + + private Plugin installedPlugin; + + private static class OrganizationData { + Organization organization; + List applications = new ArrayList<>(); + List datasources = new ArrayList<>(); + List actions = new ArrayList<>(); + } + + public Mono loadOrganizationData(Organization organization) { + final OrganizationData data = new OrganizationData(); + data.organization = organization; + + return Mono + .when( + applicationService + .findByOrganizationId(organization.getId(), READ_APPLICATIONS) + .map(data.applications::add), + datasourceService + .findAllByOrganizationId(organization.getId(), READ_DATASOURCES) + .map(data.datasources::add), + getActionsInOrganization(organization) + .map(data.actions::add) + ) + .thenReturn(data); + } + + @Before + public void setup() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + installedPlugin = pluginRepository.findByPackageName("installed-plugin").block(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneEmptyOrganization() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization"); + final Mono resultMono = organizationService.create(newOrganization) + .zipWith(sessionUserService.getCurrentUser()) + .flatMap(tuple -> + examplesOrganizationCloner.cloneOrganizationForUser(tuple.getT1().getId(), tuple.getT2())) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).isEmpty(); + assertThat(data.datasources).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithItsContents() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + Application app1 = new Application(); + app1.setName("1 - public app"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + Application app2 = new Application(); + app2.setOrganizationId(organization.getId()); + app2.setName("2 - private app"); + + return Mono.when( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).hasSize(1); + assertThat(map(data.applications, Application::getName)).containsExactly("1 - public app"); + assertThat(data.applications.get(0).getPages()).hasSize(1); + + assertThat(data.datasources).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithOnlyPublicApplications() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + Application app1 = new Application(); + app1.setName("1 - public app more"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + Application app2 = new Application(); + app2.setOrganizationId(organization.getId()); + app2.setName("2 - another public app more"); + app2.setIsPublic(true); + + return Mono.zip( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2).flatMap(application -> { + final Page newPage = new Page(); + newPage.setName("The New Page"); + newPage.setApplicationId(application.getId()); + return applicationPageService.createPage(newPage); + }) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).hasSize(2); + assertThat(map(data.applications, Application::getName)).containsExactlyInAnyOrder( + "1 - public app more", + "2 - another public app more" + ); + + for (final Application app : data.applications) { + if ("2 - another public app more".equals(app.getName())) { + assertThat(app.getPages()).hasSize(2); + } else { + assertThat(app.getPages()).hasSize(1); + } + } + + assertThat(data.datasources).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithOnlyPrivateApplications() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + Application app1 = new Application(); + app1.setName("1 - private app more"); + app1.setOrganizationId(organization.getId()); + + Application app2 = new Application(); + app2.setOrganizationId(organization.getId()); + app2.setName("2 - another private app more"); + + return Mono.when( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).isEmpty(); + assertThat(data.datasources).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithOnlyDatasources() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + final Datasource ds1 = new Datasource(); + ds1.setName("datasource 1"); + ds1.setOrganizationId(organization.getId()); + final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + ds1.setDatasourceConfiguration(datasourceConfiguration); + datasourceConfiguration.setUrl("http://httpbin.org/get"); + datasourceConfiguration.setHeaders(List.of( + new Property("X-Answer", "42") + )); + + final Datasource ds2 = new Datasource(); + ds2.setName("datasource 2"); + ds2.setOrganizationId(organization.getId()); + + return Mono.when( + datasourceService.create(ds1), + datasourceService.create(ds2) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.datasources).hasSize(2); + assertThat(map(data.datasources, Datasource::getName)).containsExactlyInAnyOrder( + "datasource 1", + "datasource 2" + ); + + final Datasource ds1 = data.datasources.stream() + .filter(datasource -> "datasource 1".equals(datasource.getName())) + .findFirst() + .orElseThrow(); + assertThat(ds1.getDatasourceConfiguration().getUrl()).isEqualTo("http://httpbin.org/get"); + assertThat(ds1.getDatasourceConfiguration().getHeaders()).containsOnly( + new Property("X-Answer", "42") + ); + + assertThat(data.applications).isEmpty(); + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithDatasourcesAndApplications() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + final Application app1 = new Application(); + app1.setName("first application"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + final Application app2 = new Application(); + app2.setName("second application"); + app2.setOrganizationId(organization.getId()); + app2.setIsPublic(true); + + final Datasource ds1 = new Datasource(); + ds1.setName("datasource 1"); + ds1.setOrganizationId(organization.getId()); + + final Datasource ds2 = new Datasource(); + ds2.setName("datasource 2"); + ds2.setOrganizationId(organization.getId()); + + return Mono.when( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2), + datasourceService.create(ds1), + datasourceService.create(ds2) + ).then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).hasSize(2); + assertThat(map(data.applications, Application::getName)).containsExactlyInAnyOrder( + "first application", + "second application" + ); + + assertThat(data.datasources).hasSize(2); + assertThat(map(data.datasources, Datasource::getName)).containsExactlyInAnyOrder( + "datasource 1", + "datasource 2" + ); + + assertThat(data.actions).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void cloneOrganizationWithDatasourcesAndApplicationsAndActions() { + Organization newOrganization = new Organization(); + newOrganization.setName("Template Organization 2"); + final Mono resultMono = Mono + .zip( + organizationService.create(newOrganization), + sessionUserService.getCurrentUser() + ) + .flatMap(tuple -> { + final Organization organization = tuple.getT1(); + + final Application app1 = new Application(); + app1.setName("first application"); + app1.setOrganizationId(organization.getId()); + app1.setIsPublic(true); + + final Application app2 = new Application(); + app2.setName("second application"); + app2.setOrganizationId(organization.getId()); + app2.setIsPublic(true); + + final Datasource ds1 = new Datasource(); + ds1.setName("datasource 1"); + ds1.setOrganizationId(organization.getId()); + ds1.setPluginId(installedPlugin.getId()); + + final Datasource ds2 = new Datasource(); + ds2.setName("datasource 2"); + ds2.setOrganizationId(organization.getId()); + ds2.setPluginId(installedPlugin.getId()); + + return Mono + .zip( + applicationPageService.createApplication(app1), + applicationPageService.createApplication(app2), + datasourceService.create(ds1), + datasourceService.create(ds2) + ) + .flatMap(tuple1 -> { + final Application app = tuple1.getT1(); + final String pageId1 = app.getPages().get(0).getId(); + final Datasource ds1Again = tuple1.getT3(); + + final Action action1 = new Action(); + action1.setName("action1"); + action1.setPageId(pageId1); + action1.setOrganizationId(organization.getId()); + action1.setDatasource(ds1Again); + action1.setPluginId(installedPlugin.getId()); + + final Action action2 = new Action(); + action2.setPageId(pageId1); + action2.setName("action2"); + action2.setOrganizationId(organization.getId()); + action2.setDatasource(ds1Again); + action2.setPluginId(installedPlugin.getId()); + + final Application app2Again = tuple1.getT2(); + final String pageId2 = app2Again.getPages().get(0).getId(); + final Datasource ds2Again = tuple1.getT4(); + + final Action action3 = new Action(); + action3.setName("action3"); + action3.setPageId(pageId2); + action3.setOrganizationId(organization.getId()); + action3.setDatasource(ds2Again); + action3.setPluginId(installedPlugin.getId()); + + final Action action4 = new Action(); + action4.setPageId(pageId2); + action4.setName("action4"); + action4.setOrganizationId(organization.getId()); + action4.setDatasource(ds2Again); + action4.setPluginId(installedPlugin.getId()); + + return Mono.when( + actionCollectionService.createAction(action1), + actionCollectionService.createAction(action2), + actionCollectionService.createAction(action3), + actionCollectionService.createAction(action4) + ); + }) + .then(examplesOrganizationCloner.cloneOrganizationForUser(organization.getId(), tuple.getT2())); + }) + .flatMap(this::loadOrganizationData); + + StepVerifier.create(resultMono) + .assertNext(data -> { + assertThat(data.organization).isNotNull(); + assertThat(data.organization.getId()).isNotNull(); + assertThat(data.organization.getName()).isEqualTo("api_user's Examples"); + assertThat(data.organization.getPolicies()).isNotEmpty(); + + assertThat(data.applications).hasSize(2); + assertThat(map(data.applications, Application::getName)).containsExactlyInAnyOrder( + "first application", + "second application" + ); + + assertThat(data.datasources).hasSize(2); + assertThat(map(data.datasources, Datasource::getName)).containsExactlyInAnyOrder( + "datasource 1", + "datasource 2" + ); + + assertThat(data.actions).hasSize(4); + assertThat(map(data.actions, Action::getName)).containsExactlyInAnyOrder( + "action1", + "action2", + "action3", + "action4" + ); + }) + .verifyComplete(); + } + + private List map(List list, Function fn) { + return list.stream().map(fn).collect(Collectors.toList()); + } + + 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( + Map.of(FieldName.PAGE_ID, Collections.singletonList(page.getId()))))); + } +} From 7eda0f02016a5599b6d1712b9f0ad617d71299b8 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 28 Jul 2020 18:21:40 +0530 Subject: [PATCH 14/15] Making the pre-release tag configurable based on the tag (#184) --- .github/workflows/github-release.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index 66d53b2030..e36d834dae 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -121,6 +121,21 @@ jobs: steps: # Creating the release on Github + - name: Get the version + id: get_version + run: echo ::set-output name=tag::${GITHUB_REF#refs/tags/} + + # If the tag has the string "beta", then mark the Github release as a pre-release + - name: Get the version + id: get_release + run: | + STATUS=false + if [[ ! ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then + STATUS=true + fi + + echo ::set-output name=status::${STATUS} + - name: Create Release id: create_release uses: actions/create-release@v1 @@ -130,5 +145,5 @@ jobs: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false - prerelease: false + prerelease: ${{steps.get_release.outputs.status}} \ No newline at end of file From ad410639441fb4b57764c792719f8eb7fe3e2420 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Tue, 28 Jul 2020 18:52:59 +0530 Subject: [PATCH 15/15] Fixing bug for beta releases (#185) --- .github/workflows/github-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml index e36d834dae..e5a7985322 100644 --- a/.github/workflows/github-release.yml +++ b/.github/workflows/github-release.yml @@ -127,10 +127,10 @@ jobs: # If the tag has the string "beta", then mark the Github release as a pre-release - name: Get the version - id: get_release + id: get_prerelease run: | STATUS=false - if [[ ! ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then + if [[ ${{steps.get_version.outputs.tag}} == *"beta"* ]]; then STATUS=true fi @@ -145,5 +145,5 @@ jobs: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false - prerelease: ${{steps.get_release.outputs.status}} + prerelease: ${{steps.get_prerelease.outputs.status}} \ No newline at end of file