diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 2ecc33909f..6dd1ec69e7 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: | @@ -229,6 +229,6 @@ jobs: if: success() && github.ref == 'refs/heads/master' run: | docker build -t appsmith/appsmith-editor-ee:${GITHUB_SHA} . - docker build -t appsmith/appsmith-editor-ee:latest . + docker build -t appsmith/appsmith-editor-ee:nightly . echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker push appsmith/appsmith-editor-ee \ No newline at end of file diff --git a/.github/workflows/github-release.yml b/.github/workflows/github-release.yml new file mode 100644 index 0000000000..e5a7985322 --- /dev/null +++ b/.github/workflows/github-release.yml @@ -0,0 +1,149 @@ +name: Appsmith Github Release Workflow + +on: + push: + # 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}} . + + # 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 + + 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}} . + + # 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 + + create-release: + needs: + - build-server + - build-client + runs-on: ubuntu-latest + + 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_prerelease + 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 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: ${{steps.get_prerelease.outputs.status}} + \ No newline at end of file diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 534cae6487..68ab8f47bb 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-ee:${GITHUB_SHA} . - docker build -t appsmith/appsmith-server-ee:latest . + docker build -t appsmith/appsmith-server-ee:nightly . echo ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} | docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} --password-stdin docker push appsmith/appsmith-server-ee 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_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 6bec492f77..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,6 +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.WaitAutoSave(); cy.RunAPI(); cy.ResponseStatusCheck(testdata.successStatusCode); cy.get(apiwidget.createApiOnSideBar) 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); 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/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/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[][] = []; 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, + ); + } } }); }; 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()))))); + } +} diff --git a/deploy/install.sh b/deploy/install.sh index a0c1f47ce2..16bb2b5097 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 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 "++++++++++++++++++++++++++++++++++++++++++++++++" + exit fi fi