Merging community release

This commit is contained in:
Arpit Mohan 2020-07-28 19:27:12 +05:30
commit 9c0b2a4e33
22 changed files with 918 additions and 91 deletions

View File

@ -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

149
.github/workflows/github-release.yml vendored Normal file
View File

@ -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}}

View File

@ -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

View File

@ -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);

View File

@ -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");

View File

@ -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)

View File

@ -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);
});
});
});

View File

@ -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);

View File

@ -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 }) => {

View File

@ -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" }}
/>
) : (
<FormIcons.DELETE_ICON
height={20}
width={20}
color={Colors["CADET_BLUE"]}
onClick={() => props.fields.remove(index)}
style={{ alignSelf: "center" }}
/>

View File

@ -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),

View File

@ -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<any>[][] = [];

View File

@ -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,
);
}
}
});
};

View File

@ -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";

View File

@ -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;
}

View File

@ -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<Applicat
Flux<Application> findByOrganizationId(String orgId, AclPermission permission);
Flux<Application> findByMultipleOrganizationIds(Set<String> orgIds, AclPermission permission);
Mono<UpdateResult> addPageToApplication(Application application, Page page, boolean isDefault);
}

View File

@ -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<UpdateResult> 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
);
}
}

View File

@ -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<Page> createPage(Page page);
Mono<Application> addPageToApplication(Mono<Application> applicationMono, Page page, Boolean isDefault);
Mono<UpdateResult> addPageToApplication(Application application, Page page, Boolean isDefault);
Mono<Page> getPage(String pageId, Boolean viewMode);
@ -19,5 +20,7 @@ public interface ApplicationPageService {
Mono<Application> makePageDefault(String applicationId, String pageId);
Mono<Application> cloneApplication(Application application);
Mono<Application> deleteApplication(String id);
}

View File

@ -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<Page> 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<Application> addPageToApplication(Mono<Application> applicationMono, Page page, Boolean isDefault) {
return applicationMono
.map(application -> {
List<ApplicationPage> applicationPages = application.getPages();
if (applicationPages == null) {
applicationPages = new ArrayList<>();
@Override
public Mono<UpdateResult> 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<Page> 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<Application> 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<User> userMono = sessionUserService.getCurrentUser().cache();
Mono<Application> applicationWithPoliciesMono = userMono
.flatMap(user -> {
Mono<Organization> 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<Policy> policySet = org.getPolicies().stream()
.filter(policy ->
policy.getPermission().equals(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) ||
policy.getPermission().equals(ORGANIZATION_READ_APPLICATIONS.getValue())
).collect(Collectors.toSet());
Set<Policy> 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<Policy> policySet = application.getPolicies().stream()
.filter(policy -> policy.getPermission().equals(MANAGE_APPLICATIONS.getValue())

View File

@ -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<Organization> cloneOrganizationForUser(String templateOrganizationId, User user) {
public Mono<Organization> 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<String, Datasource> 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<Action> 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<Page> 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)

View File

@ -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<Application> applications = new ArrayList<>();
List<Datasource> datasources = new ArrayList<>();
List<Action> actions = new ArrayList<>();
}
public Mono<OrganizationData> 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<OrganizationData> 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<OrganizationData> 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<OrganizationData> 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<OrganizationData> 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<OrganizationData> 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<OrganizationData> 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<OrganizationData> 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 <InType, OutType> List<OutType> map(List<InType> list, Function<InType, OutType> fn) {
return list.stream().map(fn).collect(Collectors.toList());
}
private Flux<Action> getActionsInOrganization(Organization organization) {
return applicationService
.findByOrganizationId(organization.getId(), READ_APPLICATIONS)
.flatMap(application -> pageService.findByApplicationId(application.getId(), READ_PAGES))
.flatMap(page -> actionService.get(new LinkedMultiValueMap<String, String>(
Map.of(FieldName.PAGE_ID, Collections.singletonList(page.getId())))));
}
}

View File

@ -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